Skip to content

Commit 1e0b4be

Browse files
committed
GROOVY-11985: Static method override on trait implementer ignored when called via this in trait body
1 parent 0a04376 commit 1e0b4be

2 files changed

Lines changed: 161 additions & 3 deletions

File tree

src/main/java/org/codehaus/groovy/transform/trait/TraitReceiverTransformer.java

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -273,9 +273,20 @@ private Expression transformMethodCallOnThis(final MethodCallExpression call) {
273273
// GROOVY-7213, GROOVY-7214, GROOVY-8282, GROOVY-8859, GROOVY-10106, GROOVY-10312
274274
MethodNode methodNode = findConcreteMethod(traitClass, call.getMethodAsString());
275275
if (methodNode != null) {
276-
// this.m(x) --> (this or T$Trait$Helper).m($self or $static$self or (Class)$self.getClass(), x)
277-
Expression selfClassOrObject = methodNode.isStatic() && !ClassHelper.isClassType(weaved.getOriginType()) ? castX(ClassHelper.CLASS_Type.getPlainNodeReference(), callX(weaved, "getClass")) : weaved;
278-
MethodCallExpression newCall = callX(!inClosure ? thisExpr : classX(traitHelper), method, createArgumentList(selfClassOrObject, arguments));
276+
MethodCallExpression newCall;
277+
if (methodNode.isStatic() && !methodNode.isPrivate() && !inClosure) {
278+
// GROOVY-11985: dispatch unqualified/this-qualified calls to
279+
// public trait statics through the implementing class so an
280+
// override declared on the implementer is visible from trait code.
281+
Expression implClass = ClassHelper.isClassType(weaved.getOriginType()) ? varX(weaved) : castX(ClassHelper.CLASS_Type.getPlainNodeReference(), callX(varX(weaved), "getClass"));
282+
newCall = callX(implClass, method, transform(arguments));
283+
newCall.setImplicitThis(false);
284+
newCall.putNodeMetaData(TraitASTTransformation.DO_DYNAMIC, methodNode.getReturnType());
285+
} else {
286+
// this.m(x) --> (this or T$Trait$Helper).m($self or $static$self or (Class)$self.getClass(), x)
287+
Expression selfClassOrObject = methodNode.isStatic() && !ClassHelper.isClassType(weaved.getOriginType()) ? castX(ClassHelper.CLASS_Type.getPlainNodeReference(), callX(weaved, "getClass")) : weaved;
288+
newCall = callX(!inClosure ? thisExpr : classX(traitHelper), method, createArgumentList(selfClassOrObject, arguments));
289+
}
279290
newCall.setGenericsTypes(call.getGenericsTypes());
280291
newCall.setSpreadSafe(call.isSpreadSafe());
281292
newCall.setSourcePosition(call);
Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
package org.codehaus.groovy.transform.traitx
20+
21+
import groovy.test.GroovyAssert
22+
import groovy.transform.CompileStatic
23+
import org.junit.jupiter.api.Test
24+
25+
final class Groovy11985 {
26+
27+
// Unqualified and `this.`-qualified public static calls inside a trait
28+
// method must dispatch through the implementing class so an override on
29+
// the implementer is visible from trait code.
30+
31+
@Test
32+
void testStaticOverrideVisibleFromTraitThisCall() {
33+
GroovyAssert.assertScript '''
34+
trait Validateable {
35+
static boolean defaultNullable() { false }
36+
static boolean defaultNullableSeenByTrait() { this.defaultNullable() }
37+
}
38+
class MyNullableValidateable implements Validateable {
39+
static boolean defaultNullable() { true }
40+
}
41+
class DefaultValidateable implements Validateable {}
42+
assert MyNullableValidateable.defaultNullable() == true
43+
assert MyNullableValidateable.defaultNullableSeenByTrait() == true
44+
assert DefaultValidateable.defaultNullable() == false
45+
assert DefaultValidateable.defaultNullableSeenByTrait() == false
46+
'''
47+
}
48+
49+
@Test
50+
void testStaticOverrideVisibleFromTraitUnqualifiedCall() {
51+
GroovyAssert.assertScript '''
52+
trait Validateable {
53+
static boolean defaultNullable() { false }
54+
static boolean defaultNullableUnqualified() { defaultNullable() }
55+
}
56+
class MyNullableValidateable implements Validateable {
57+
static boolean defaultNullable() { true }
58+
}
59+
class DefaultValidateable implements Validateable {}
60+
assert MyNullableValidateable.defaultNullableUnqualified() == true
61+
assert DefaultValidateable.defaultNullableUnqualified() == false
62+
'''
63+
}
64+
65+
@Test
66+
void testStaticOverrideVisibleFromInstanceMethod() {
67+
GroovyAssert.assertScript '''
68+
trait T {
69+
static String which() { 'trait' }
70+
String greet() { which() }
71+
}
72+
class C implements T {
73+
static String which() { 'class' }
74+
}
75+
class D implements T {}
76+
assert new C().greet() == 'class'
77+
assert new D().greet() == 'trait'
78+
'''
79+
}
80+
81+
@Test
82+
void testStaticOverrideUnderCompileStatic() {
83+
GroovyAssert.assertScript '''
84+
import groovy.transform.CompileStatic
85+
@CompileStatic
86+
trait Validateable {
87+
static boolean defaultNullable() { false }
88+
static boolean defaultNullableSeenByTrait() { this.defaultNullable() }
89+
static boolean defaultNullableUnqualified() { defaultNullable() }
90+
}
91+
@CompileStatic
92+
class MyNullableValidateable implements Validateable {
93+
static boolean defaultNullable() { true }
94+
}
95+
@CompileStatic
96+
class DefaultValidateable implements Validateable {}
97+
assert MyNullableValidateable.defaultNullableSeenByTrait() == true
98+
assert MyNullableValidateable.defaultNullableUnqualified() == true
99+
assert DefaultValidateable.defaultNullableSeenByTrait() == false
100+
assert DefaultValidateable.defaultNullableUnqualified() == false
101+
'''
102+
}
103+
104+
@Test
105+
void testOverloadResolutionStillWorks() {
106+
GroovyAssert.assertScript '''
107+
trait T {
108+
static String foo() { 'no-arg' }
109+
static String foo(int n) { "int=$n" }
110+
static String bar() { foo() + ' / ' + foo(42) }
111+
}
112+
class C implements T {}
113+
assert C.bar() == 'no-arg / int=42'
114+
'''
115+
}
116+
117+
@Test
118+
void testSuperTraitPublicStaticIsPolymorphic() {
119+
GroovyAssert.assertScript '''
120+
trait Base { static String hello() { 'base' } }
121+
trait Mid extends Base { static String greet() { hello() } }
122+
class C implements Mid {}
123+
class D implements Mid { static String hello() { 'override' } }
124+
assert C.greet() == 'base'
125+
assert D.greet() == 'override'
126+
'''
127+
}
128+
129+
@Test
130+
void testPrivateStaticStillRoutesToHelper() {
131+
// Private statics are not composed onto the implementer, so they must
132+
// continue to dispatch directly to the helper. The override on the
133+
// implementer (if any) is a different method and intentionally not
134+
// visible from trait code.
135+
GroovyAssert.assertScript '''
136+
trait T {
137+
boolean passes
138+
void audit() {
139+
if (checkCondition()) { passes = true }
140+
}
141+
private static boolean checkCondition() { true }
142+
}
143+
class C implements T {}
144+
def c = new C(); c.audit(); assert c.passes
145+
'''
146+
}
147+
}

0 commit comments

Comments
 (0)