This is an automated email from the ASF dual-hosted git repository.
emilles pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/groovy.git
The following commit(s) were added to refs/heads/master by this push:
new 3a43a99eac GROOVY-11820: check category getter/setter declaring class
distance
3a43a99eac is described below
commit 3a43a99eac7a449656cfeb40673bc7b1655b7eb1
Author: Eric Milles <[email protected]>
AuthorDate: Sun Dec 28 12:38:17 2025 -0600
GROOVY-11820: check category getter/setter declaring class distance
---
src/main/java/groovy/lang/MetaClassImpl.java | 24 +-
src/spec/test/metaprogramming/CategoryTest.groovy | 32 +-
src/test/groovy/groovy/CategoryTest.groovy | 538 +++++++++++++---------
3 files changed, 334 insertions(+), 260 deletions(-)
diff --git a/src/main/java/groovy/lang/MetaClassImpl.java
b/src/main/java/groovy/lang/MetaClassImpl.java
index 21db1df992..767ad24801 100644
--- a/src/main/java/groovy/lang/MetaClassImpl.java
+++ b/src/main/java/groovy/lang/MetaClassImpl.java
@@ -2113,10 +2113,11 @@ public class MetaClassImpl implements MetaClass,
MutableMetaClass {
}
private Tuple2<MetaMethod, MetaProperty>
createMetaMethodAndMetaProperty(final Class<?> sender, final String name, final
boolean useSuper, final boolean isStatic) {
- MetaMethod method = null;
+ MetaMethod mm = null;
MetaProperty mp = getMetaProperty(sender, name, useSuper, isStatic);
+
if ((mp == null || mp instanceof CachedField) && !name.isEmpty() &&
isUpperCase(name.charAt(0)) && (name.length() < 2 ||
!isUpperCase(name.charAt(1))) && !"Class".equals(name) &&
!"MetaClass".equals(name)) {
- // GROOVY-9618 adjust because capitalised properties aren't stored
as meta bean props
+ // GROOVY-9618: adjust because capitalised properties aren't
stored as meta bean props
MetaProperty saved = mp;
mp = getMetaProperty(sender, BeanUtils.decapitalize(name),
useSuper, isStatic);
if (mp == null || (saved != null && mp instanceof CachedField)) {
@@ -2125,23 +2126,23 @@ public class MetaClassImpl implements MetaClass,
MutableMetaClass {
}
}
- if (mp instanceof MetaBeanProperty) {
- MetaBeanProperty mbp = (MetaBeanProperty) mp;
- method = mbp.getGetter();
+ if (mp instanceof MetaBeanProperty mbp) {
+ mm = mbp.getGetter();
mp = mbp.getField();
}
// check for a category method named like a getter
if (!useSuper && !isStatic &&
GroovyCategorySupport.hasCategoryInCurrentThread()) {
- String getterName =
GroovyCategorySupport.getPropertyCategoryGetterName(name);
+ var getterName =
GroovyCategorySupport.getPropertyCategoryGetterName(name);
if (getterName != null) {
MetaMethod categoryMethod = getCategoryMethodGetter(theClass,
getterName, false);
- if (categoryMethod != null)
- method = categoryMethod;
+ if (categoryMethod != null && (mm == null ||
Boolean.TRUE.equals(getMatchKindForCategory(mm, categoryMethod)))) { //
GROOVY-11820
+ mm = categoryMethod;
+ }
}
}
- return tuple(method, mp);
+ return tuple(mm, mp);
}
private static CategoryMethod getCategoryMethodMissing(final Class<?>
sender) {
@@ -2704,8 +2705,7 @@ public class MetaClassImpl implements MetaClass,
MutableMetaClass {
MetaProperty mp = getMetaProperty(sender, name, useSuper, isStatic);
MetaProperty field = null;
if (mp != null) {
- if (mp instanceof MetaBeanProperty) {
- MetaBeanProperty mbp = (MetaBeanProperty) mp;
+ if (mp instanceof MetaBeanProperty mbp) {
method = mbp.getSetter();
MetaProperty f = mbp.getField();
if (method != null || (f != null && !f.isFinal())) {
@@ -2722,7 +2722,7 @@ public class MetaClassImpl implements MetaClass,
MutableMetaClass {
var setterName =
GroovyCategorySupport.getPropertyCategorySetterName(name);
if (setterName != null) {
MetaMethod categoryMethod = getCategoryMethodSetter(theClass,
setterName, false);
- if (categoryMethod != null) {
+ if (categoryMethod != null && (method == null ||
Boolean.TRUE.equals(getMatchKindForCategory(method, categoryMethod)))) { //
GROOVY-11820
method = categoryMethod;
arguments = new Object[]{newValue};
}
diff --git a/src/spec/test/metaprogramming/CategoryTest.groovy
b/src/spec/test/metaprogramming/CategoryTest.groovy
index 1d528c6bbc..6a19dd106a 100644
--- a/src/spec/test/metaprogramming/CategoryTest.groovy
+++ b/src/spec/test/metaprogramming/CategoryTest.groovy
@@ -18,14 +18,17 @@
*/
package metaprogramming
-import groovy.test.GroovyTestCase
import groovy.time.TimeCategory
+import org.junit.jupiter.api.Test
-class CategoryTest extends GroovyTestCase {
+import static groovy.test.GroovyAssert.assertScript
+final class CategoryTest {
+
+ @Test
void testApplyTimeCategory() {
// tag::time_category[]
- use(TimeCategory) {
+ use(TimeCategory) {
println 1.minute.from.now // <1>
println 10.hours.ago
@@ -35,6 +38,7 @@ class CategoryTest extends GroovyTestCase {
// end::time_category[]
}
+ @Test
void testCategoryAnnotation() {
assertScript '''
// tag::time_category_anno[]
@@ -50,30 +54,10 @@ class CategoryTest extends GroovyTestCase {
}
}
- use (NumberCategory) {
+ use(NumberCategory) {
assert 42.meters.toString() == '42m'
}
// end::time_category_anno[]
'''
}
-
- // GROOVY-8433
- void testCategoryAnnotationAndAIC() {
- assertScript '''
- @Category(Number)
- class NumberCategory {
- def m() {
- String variable = 'works'
- new Object() { // "Cannot cast object '1' with class
'java.lang.Integer' to class 'NumberCategory'" due to implicit "this"
- String toString() { variable }
- }
- }
- }
-
- use (NumberCategory) {
- String result = 1.m()
- assert result == 'works'
- }
- '''
- }
}
diff --git a/src/test/groovy/groovy/CategoryTest.groovy
b/src/test/groovy/groovy/CategoryTest.groovy
index 0723180c13..9584918242 100644
--- a/src/test/groovy/groovy/CategoryTest.groovy
+++ b/src/test/groovy/groovy/CategoryTest.groovy
@@ -18,71 +18,95 @@
*/
package groovy
-import groovy.test.GroovyTestCase
+import org.junit.jupiter.api.Test
-final class CategoryTest extends GroovyTestCase {
+import static groovy.test.GroovyAssert.assertScript
+import static org.junit.jupiter.api.Assertions.assertThrows
- @Override
- protected void setUp() {
- def dummy = null
- CategoryTestPropertyCategory.setSomething(dummy, 'hello')
- CategoryTestHelperPropertyReplacer.setaProperty(dummy, 'anotherValue')
+final class CategoryTest {
+
+ static class StringCategory {
+ static String lower(String s) {
+ s.toLowerCase()
+ }
+ }
+
+ static class IntegerCategory {
+ static Integer inc(Integer i) {
+ i + 1
+ }
}
- void testCategories() {
- use (StringCategory) {
- assert "Sam".lower() == "sam";
- use (IntegerCategory.class) {
- assert "Sam".lower() == "sam";
- assert 1.inc() == 2;
+ @Test
+ void testNestingOfCategories() {
+ use(StringCategory) {
+ assert 'Sam'.lower() == 'sam'
+ use(IntegerCategory) {
+ assert 9.inc() == 10
+ assert 'Sam'.lower() == 'sam'
}
- shouldFail(MissingMethodException, { 1.inc() });
+ assertThrows(MissingMethodException) { 1.inc() }
}
- shouldFail(MissingMethodException, { "Sam".lower() });
+ assertThrows(MissingMethodException) { 'Sam'.lower() }
}
+ @Test
void testReturnValueWithUseClass() {
def returnValue = use(StringCategory) {
- "Green Eggs And Ham".lower()
+ 'Green Eggs And Ham'.lower()
}
- assert "green eggs and ham" == returnValue
+ assert returnValue == 'green eggs and ham'
}
- void testReturnValueWithUseList() {
+ @Test
+ void testReturnValueWithUseClassList() {
def returnValue = use([StringCategory, IntegerCategory]) {
- "Green Eggs And Ham".lower() + 5.inc()
+ 'Green Eggs And Ham'.lower() + 5.inc()
}
- assert "green eggs and ham6" == returnValue
+ assert returnValue == 'green eggs and ham6'
}
+
//--------------------------------------------------------------------------
+
+ @Test
void testCategoryDefinedProperties() {
- use(CategoryTestPropertyCategory) {
- assert getSomething() == "hello"
- assert something == "hello"
- something = "nihao"
- assert something == "nihao"
- }
+ assertScript '''
+ class CategoryTestPropertyCategory {
+ private static aVal = 'hello'
+ static getSomething(Object self) { return aVal }
+ static void setSomething(Object self, newValue) { aVal =
newValue }
+ }
- // test the new value again in a new block
- use(CategoryTestPropertyCategory) {
- assert something == "nihao"
- }
+ use(CategoryTestPropertyCategory) {
+ assert getSomething() == 'hello'
+ assert something == 'hello'
+ something = 'nihao'
+ assert something == 'nihao'
+ }
+ // test the new value again in a new block
+ use(CategoryTestPropertyCategory) {
+ assert something == 'nihao'
+ }
+ '''
}
// GROOVY-10133
+ @Test
void testCategoryDefinedProperties2() {
assertScript '''
class Cat {
static boolean isAbc(self) { true }
static boolean getAbc(self) { false }
}
- use (Cat) {
- assert abc // should select "isAbc()"
+
+ use(Cat) {
+ assert abc // should select isAbc()
}
'''
}
// GROOVY-5245
+ @Test
void testCategoryDefinedProperties3() {
assertScript '''
class Isser {
@@ -92,7 +116,8 @@ final class CategoryTest extends GroovyTestCase {
static boolean getWorking2(Isser b) { true }
static boolean isNotWorking(Isser b) { true }
}
- use (IsserCat) {
+
+ use(IsserCat) {
assert new Isser().working
assert new Isser().working2
assert new Isser().notWorking // MissingPropertyException
@@ -100,161 +125,225 @@ final class CategoryTest extends GroovyTestCase {
'''
}
- void testCategoryReplacedPropertyAccessMethod() {
- def cth = new CategoryTestHelper()
- cth.aProperty = "aValue"
- assert cth.aProperty == "aValue"
- use (CategoryTestHelperPropertyReplacer) {
- assert cth.aProperty == "anotherValue"
- cth.aProperty = "this is boring"
- assert cth.aProperty == "this is boring"
- }
- assert cth.aProperty == "aValue"
+ @Test
+ void testCategoryMethodReplacesPropertyAccessMethod() {
+ assertScript '''
+ class CategoryTestHelper {
+ def aProperty = 'aValue'
+ }
+ class CategoryTestHelperPropertyReplacer {
+ private static aVal = 'anotherValue'
+ static getaProperty(CategoryTestHelper self) { return aVal }
+ static void setaProperty(CategoryTestHelper self, newValue) {
aVal = newValue }
+ }
+
+ def cth = new CategoryTestHelper()
+ cth.aProperty = 'aValue'
+ assert cth.aProperty == 'aValue'
+ use(CategoryTestHelperPropertyReplacer) {
+ assert cth.aProperty == 'anotherValue'
+ cth.aProperty = 'this is boring'
+ assert cth.aProperty == 'this is boring'
+ }
+ assert cth.aProperty == 'aValue'
+ '''
+ }
+
+ // GROOVY-11820
+ @Test
+ void testCategoryMethodHiddenByPropertyAccessMethod() {
+ assertScript '''import java.lang.reflect.Field
+ class Pogo {
+ public float field
+ }
+
+ Field field = Pogo.fields.first()
+ assert field.getType() == float
+ assert field.type == float
+
+ @Category(Field)
+ class FieldCat {
+ def getType() { 'override' }
+ }
+
+ use(FieldCat) {
+ assert field.getType() == 'override'
+ assert field.type == 'override'
+ }
+
+ @Category(Object)
+ class ObjectCat {
+ def getType() { 'override' }
+ }
+
+ use(ObjectCat) { // class method is closer than category method
+ assert field.getType() == float
+ assert field.type == float
+ }
+ '''
}
+ @Test
void testCategoryHiddenByClassMethod() {
- assertScript """
- class A{}
- class B extends A{def m(){1}}
- class Category{ static m(A a) {2}}
- def b = new B()
- use (Category) {
- assert b.m() == 1
- }
- """
+ assertScript '''
+ class A {
+ }
+ class B extends A {
+ def m() { 1 }
+ }
+ class C {
+ static m(A a) { 2 }
+ }
+
+ def b = new B()
+ use(C) {
+ assert b.m() == 1
+ }
+ '''
}
+ @Test
void testCategoryOverridingClassMethod() {
- assertScript """
- class A {def m(){1}}
- class Category{ static m(A a) {2}}
- def a = new A()
- use (Category) {
- assert a.m() == 2
- }
- """
- assertScript """
- class A {def m(){1}}
- class B extends A{}
- class Category{ static m(A a) {2}}
- def a = new B()
- use (Category) {
- assert a.m() == 2
- }
- """
+ assertScript '''
+ class A {
+ def m() { 1 }
+ }
+ class C {
+ static m(A a) { 2 }
+ }
+
+ def a = new A()
+ use(C) {
+ assert a.m() == 2
+ }
+ '''
}
+ @Test
+ void testCategoryOverridingClassMethod2() {
+ assertScript '''
+ class A {
+ def m() { 1 }
+ }
+ class B extends A {
+ }
+ class C {
+ static m(A a) { 2 }
+ }
+
+ def a = new B()
+ use(C) {
+ assert a.m() == 2
+ }
+ '''
+ }
+
+ @Test
void testCategoryWithMixedOverriding() {
- assertScript """
- class A{def m(){0}}
- class B extends A{def m(){1}}
- class Category{ static m(A a) {2}}
- def b = new B()
- use (Category) {
- assert b.m() == 1
- }
- """
+ assertScript '''
+ class A {
+ def m() { 0 }
+ }
+ class B extends A {
+ def m() { 1 }
+ }
+ class C {
+ static m(A a) { 2 }
+ }
+
+ def b = new B()
+ use(C) {
+ assert b.m() == 1
+ }
+ '''
}
+ @Test
void testCategoryInheritance() {
- assertScript """
- public class Foo {
- static Object foo(Object obj) {
- "Foo.foo()"
- }
- }
-
- public class Bar extends Foo{
- static Object bar(Object obj) {
- "Bar.bar()"
- }
- }
+ assertScript '''
+ class Foo {
+ static Object foo(Object obj) {
+ 'Foo.foo()'
+ }
+ }
+ class Bar extends Foo {
+ static Object bar(Object obj) {
+ 'Bar.bar()'
+ }
+ }
- def obj = new Object()
+ def obj = new Object()
+ use(Foo) {
+ assert obj.foo() == 'Foo.foo()'
+ }
+ use(Bar) {
+ assert obj.bar() == 'Bar.bar()'
+ assert obj.foo() == 'Foo.foo()'
+ }
+ '''
+ }
- use(Foo){
- assert obj.foo() == "Foo.foo()"
- }
+ // GROOVY-8433
+ @Test
+ void testCategoryAnnotationAndAIC() {
+ assertScript '''
+ @Category(Number)
+ class NumberCategory {
+ def m() {
+ String variable = 'works'
+ new Object() { // "Cannot cast object '1' with class
'java.lang.Integer' to class 'NumberCategory'" due to implicit "this"
+ String toString() { variable }
+ }
+ }
+ }
- use(Bar){
- assert obj.bar() == "Bar.bar()"
- assert obj.foo() == "Foo.foo()"
- }
- """
+ use(NumberCategory) {
+ String result = 1.m()
+ assert result == 'works'
+ }
+ '''
}
+ // GROOVY-5248
+ @Test
void testNullReceiverChangeForPOJO() {
- // GROOVY-5248
// this test will call a method using a POJO while a category is active
// in call site caching this triggers the usage of POJOMetaClassSite,
// which was missing a null check for the receiver. The last foo call
// uses null to exactly check that path. I use multiple calls with
foo(1)
// before to ensure for example indy will do the right things as well,
// since indy may need more than one call here.
- assertScript """
- class Cat {
- public static findAll(Integer x, Closure cl) {1}
- }
-
- def foo(x) {
- x.findAll {}
- }
-
- use (Cat) {
- assert foo(1) == 1
- assert foo(1) == 1
- assert foo(1) == 1
- assert foo(null) == []
- assert foo(1) == 1
- assert foo(1) == 1
- assert foo(1) == 1
- }
- """
- }
-
- def foo(x){x.bar()}
-
- void testMethodHiding1() {
- def x = new X()
- assert foo(x) == 1
- use (XCat) {
- assert foo(x) == 2
- def t = Thread.start {assert foo(x)==1}
- t.join()
- }
- assert foo(x) == 1
- def t = Thread.start {use (XCat2){assert foo(x)==3}}
- t.join()
- assert foo(x) == 1
- }
+ assertScript '''
+ class C {
+ static findAll(Integer x, Closure c) { 1 }
+ }
+ def foo(x) {
+ x.findAll { -> }
+ }
- void testMethodHiding2() {
- def x = new X()
- assert foo(x) == 1
- use (XCat) {
- assert foo(x) == 2
- def t = Thread.start {use (XCat2){assert foo(x)==3}}
- t.join()
- assert foo(x) == 2
- t = Thread.start {assert foo(x)==1}
- t.join()
- }
- assert foo(x) == 1
- def t = Thread.start {use (XCat2){assert foo(x)==3}}
- t.join()
- assert foo(x) == 1
+ use(C) {
+ assert foo(1) == 1
+ assert foo(1) == 1
+ assert foo(1) == 1
+ assert foo() == []
+ assert foo(1) == 1
+ assert foo(1) == 1
+ assert foo(1) == 1
+ }
+ '''
}
+ @Test
void testCallToPrivateMethod1() {
assertScript '''
class A {
private foo() { 1 }
def baz() { foo() }
}
-
- class B extends A {}
-
- class C {}
+ class B extends A {
+ }
+ class C {
+ }
use(C) {
assert new B().baz() == 1
@@ -263,16 +352,17 @@ final class CategoryTest extends GroovyTestCase {
}
// GROOVY-6263
+ @Test
void testCallToPrivateMethod2() {
assertScript '''
class A {
private foo(a) { 1 }
def baz() { foo() }
}
-
- class B extends A {}
-
- class C {}
+ class B extends A {
+ }
+ class C {
+ }
use(C) {
assert new B().baz() == 1
@@ -281,13 +371,15 @@ final class CategoryTest extends GroovyTestCase {
}
// GROOVY-5453
+ @Test
void testOverloadedGetterMethod1() {
assertScript '''
- class Cat {
- static getFoo(String s) {'String'}
- static getFoo(CharSequence s) {'CharSequence'}
+ class C {
+ static getFoo(String s) { 'String' }
+ static getFoo(CharSequence s) { 'CharSequence' }
}
- use (Cat) {
+
+ use(C) {
assert 'abc'.getFoo() == 'String'
assert 'abc'.foo == 'String'
}
@@ -295,6 +387,7 @@ final class CategoryTest extends GroovyTestCase {
}
// GROOVY-10214
+ @Test
void testOverloadedGetterMethod2() {
assertScript '''
class Cat {
@@ -320,7 +413,8 @@ final class CategoryTest extends GroovyTestCase {
'Double'
}
}
- use (Cat) {
+
+ use(Cat) {
assert 123.foo == 'Integer'
assert 4.5d.foo == 'Double'
}
@@ -328,6 +422,7 @@ final class CategoryTest extends GroovyTestCase {
}
// GROOVY-10743
+ @Test
void testStaticMethodOnInterface() {
assertScript '''
use(java.util.stream.Stream) {
@@ -337,96 +432,91 @@ final class CategoryTest extends GroovyTestCase {
'''
}
- // GROOVY-3867
- void testPropertyMissing() {
- def x = new X()
+
//--------------------------------------------------------------------------
- shouldFail(MissingPropertyException) {
- assert x.baz != "works" // accessing x.baz should throw MPE
- }
+ static class X { def bar() {1} }
+ static class XCat { static bar(X x) {2} }
+ static class XCat2 { static bar(X x) {3} }
+ static class XCat3 { static methodMissing(X x, String name, args) {4} }
+ static class XCat4 { static propertyMissing(X x, String name) {'works'} }
- use(XCat4) {
- assert x.baz == "works"
- }
+ def foo(x) { x.bar() }
- shouldFail(MissingPropertyException) {
- assert x.baz != "works" // accessing x.baz should throw MPE
+ @Test
+ void testMethodHiding1() {
+ def x = new X()
+ assert foo(x) == 1
+ use(XCat) {
+ assert foo(x) == 2
+ def t = Thread.start {assert foo(x)==1}
+ t.join()
}
+ assert foo(x) == 1
+ def t = Thread.start {use(XCat2){assert foo(x)==3}}
+ t.join()
+ assert foo(x) == 1
}
- // GROOVY-10783
- void testPropertyMissing2() {
- assertScript '''\
- class X{ def bar(){1}}
- class XCat4{ static propertyMissing(X x, String name) {"works"}}
-
- def x = new X()
-
- use(XCat4) {
- assert x.baz == "works"
- }
- '''
+ @Test
+ void testMethodHiding2() {
+ def x = new X()
+ assert foo(x) == 1
+ use(XCat) {
+ assert foo(x) == 2
+ def t = Thread.start {use(XCat2){assert foo(x)==3}}
+ t.join()
+ assert foo(x) == 2
+ t = Thread.start {assert foo(x)==1}
+ t.join()
+ }
+ assert foo(x) == 1
+ def t = Thread.start {use(XCat2){assert foo(x)==3}}
+ t.join()
+ assert foo(x) == 1
}
// GROOVY-3867
+ @Test
void testMethodMissing() {
def x = new X()
assert foo(x) == 1
- use (XCat3) {
+ use(XCat3) {
assert foo(x) == 1 // regular foo() is not affected by
methodMissing in category
assert x.baz() == 4 // XCat3.methodMissing is called
}
assert foo(x) == 1
- def t = Thread.start {use (XCat3){assert x.baz()==4}}
+ def t = Thread.start {use(XCat3){assert x.baz()==4}}
t.join()
assert foo(x) == 1
- shouldFail(MissingMethodException) {
+ assertThrows(MissingMethodException) {
x.baz()
}
}
// GROOVY-3867
+ @Test
void testMethodMissingNoStatic() {
def x = new X()
- use (XCat3) {
+ use(XCat3) {
assert x.baz() == 4 // XCat3.methodMissing is called for instance
- shouldFail(MissingMethodException) {
+ assertThrows(MissingMethodException) {
assert X.baz() != 4 // XCat3.methodMissing should not be
called for static method of X
}
}
}
-}
-
-class X{ def bar(){1}}
-class XCat{ static bar(X x){2}}
-class XCat2{ static bar(X x){3}}
-class XCat3{ static methodMissing(X x, String name, args) {4}}
-class XCat4{ static propertyMissing(X x, String name) {"works"}}
-
-class StringCategory {
- static String lower(String string) {
- return string.toLowerCase();
- }
-}
-class IntegerCategory {
- static Integer inc(Integer i) {
- return i + 1;
+ // GROOVY-3867, GROOVY-10783
+ @Test
+ void testPropertyMissing() {
+ def x = new X()
+ assertThrows(MissingPropertyException) {
+ assert x.baz != 'works' // accessing x.baz should throw MPE
+ }
+ use(XCat4) {
+ assert x.baz == 'works'
+ }
+ assertThrows(MissingPropertyException) {
+ assert x.baz != 'works' // accessing x.baz should throw MPE
+ }
}
}
-
-class CategoryTestPropertyCategory {
- private static aVal = "hello"
- static getSomething(Object self) { return aVal }
- static void setSomething(Object self, newValue) { aVal = newValue }
-}
-
-class CategoryTestHelper {
- def aProperty = "aValue"
-}
-
-class CategoryTestHelperPropertyReplacer {
- private static aVal = "anotherValue"
- static getaProperty(CategoryTestHelper self) { return aVal }
- static void setaProperty(CategoryTestHelper self, newValue) { aVal =
newValue }
-}