Skip to content

Commit e22b200

Browse files
committed
GROOVY-12093: Static method override on trait implementer ignored when called via this in trait body (cont'd)
Backport to GROOVY_5_0_X. Preserves existing 5.x-vs-master differences in the trait transforms (the createHelperClasses refactor, javadoc and pattern- matching differences are master-only and left untouched). The TraitStaticDispatchMatrix test remains master-only for now; Groovy12093's comment is adjusted accordingly and @Anchored is marked @SInCE 5.0.7.
1 parent 40debfa commit e22b200

4 files changed

Lines changed: 475 additions & 3 deletions

File tree

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
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 groovy.transform;
20+
21+
import java.lang.annotation.Documented;
22+
import java.lang.annotation.ElementType;
23+
import java.lang.annotation.Retention;
24+
import java.lang.annotation.RetentionPolicy;
25+
import java.lang.annotation.Target;
26+
27+
/**
28+
* Marks a trait method as <em>anchored to the trait</em> — dispatch is fixed
29+
* to the trait's own definition rather than routed through the implementing
30+
* class.
31+
*
32+
* <p>Applied to a trait <code>static</code> method, this opts out of the
33+
* default per-implementer override dispatch (the Groovy distinctive that the
34+
* Grails <code>Validateable.defaultNullable()</code> pattern depends on) and
35+
* substitutes the JVM interface-static dispatch model: the implementer's
36+
* same-signature static is an independent method, not an override; the trait
37+
* body always calls the trait's own copy.
38+
*
39+
* <p>By default the marker also promotes the annotated method onto the
40+
* generated trait interface as a JVM-native public static. That makes
41+
* external <code>Trait.m()</code> calls and from-trait <code>T.m()</code>
42+
* calls both resolve at the JVM level rather than throwing
43+
* <code>MissingMethodException</code>. The {@link #inInterface()} attribute
44+
* is provided as an opt-out for the narrow case where dispatch should be
45+
* trait-anchored but the method should <em>not</em> be published on the
46+
* interface (e.g. a soft-deprecated trait-internal helper that needs to
47+
* remain publicly callable for backward compatibility but should not gain a
48+
* fresh Java-visible API surface).
49+
*
50+
* <p>Use this marker for trait statics that are part of the trait's published
51+
* contract — guaranteed-invariant utilities the trait author does not want
52+
* implementers to redefine. Continue to use plain <code>static</code> when
53+
* the trait offers a default that implementers may legitimately override.
54+
*
55+
* @since 5.0.7
56+
*/
57+
@Documented
58+
@Retention(RetentionPolicy.RUNTIME)
59+
@Target(ElementType.METHOD)
60+
public @interface Anchored {
61+
/**
62+
* Whether the annotated method should also be promoted onto the generated
63+
* trait interface. Default <code>true</code>: the marker bundles
64+
* declarer-bound dispatch with external interface visibility (the
65+
* coherent JVM interface-static model). Set to <code>false</code> for the
66+
* narrow opt-out described above — dispatch stays trait-anchored but the
67+
* method is not exposed on the interface.
68+
*/
69+
boolean inInterface() default true;
70+
}

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

Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
import org.codehaus.groovy.ast.AnnotatedNode;
2828
import org.codehaus.groovy.ast.AnnotationNode;
2929
import org.codehaus.groovy.ast.ClassCodeExpressionTransformer;
30+
import org.codehaus.groovy.ast.ClassHelper;
3031
import org.codehaus.groovy.ast.ClassNode;
3132
import org.codehaus.groovy.ast.FieldNode;
3233
import org.codehaus.groovy.ast.GenericsType;
@@ -36,6 +37,7 @@
3637
import org.codehaus.groovy.ast.Parameter;
3738
import org.codehaus.groovy.ast.PropertyNode;
3839
import org.codehaus.groovy.ast.expr.BinaryExpression;
40+
import org.codehaus.groovy.ast.expr.ConstantExpression;
3941
import org.codehaus.groovy.ast.expr.Expression;
4042
import org.codehaus.groovy.ast.expr.MethodCallExpression;
4143
import org.codehaus.groovy.ast.expr.SpreadExpression;
@@ -115,6 +117,9 @@
115117
@GroovyASTTransformation(phase = CompilePhase.SEMANTIC_ANALYSIS)
116118
public class TraitASTTransformation extends AbstractASTTransformation implements CompilationUnitAware {
117119

120+
/** Marker annotation type for {@code @Anchored} trait static methods. */
121+
private static final ClassNode ANCHORED_TYPE = ClassHelper.make(groovy.transform.Anchored.class);
122+
118123
public static final String DO_DYNAMIC = TraitReceiverTransformer.class + ".doDynamic";
119124
public static final String POST_TYPECHECKING_REPLACEMENT = TraitReceiverTransformer.class + ".replacement";
120125

@@ -246,6 +251,19 @@ private ClassNode createHelperClass(final ClassNode cNode) {
246251
processField(field, initializer, staticInitializer, fieldHelper, helper, staticFieldHelper, cNode, fieldNames);
247252
}
248253

254+
// Reject misapplied @Anchored markers before we waste effort
255+
// processing them. Errors are registered against the source unit but
256+
// processing continues so that multiple violations can be reported
257+
// in a single compilation.
258+
validateAnchoredAnnotations(cNode);
259+
260+
// Identify @Anchored public statics whose bodies the main loop will
261+
// emit on the helper. Captured up front so the main loop carries no
262+
// @Anchored-specific branching and so the interface forwarders can be
263+
// installed in a single post-processing step after the originals are
264+
// removed from the trait interface.
265+
List<MethodNode> anchoredOnInterface = collectAnchoredOnInterface(cNode);
266+
249267
// add methods
250268
List<MethodNode> nonPublicAPIMethods = new ArrayList<>();
251269
List<Statement> staticInitStatements = null;
@@ -274,6 +292,15 @@ private ClassNode createHelperClass(final ClassNode cNode) {
274292
cNode.removeMethod(privateMethod);
275293
}
276294

295+
// Install a public-static method on the trait interface for each
296+
// @Anchored callee identified above. The forwarder delegates to the
297+
// helper so external `Trait.m()` and from-trait `T.m()` calls resolve
298+
// at the JVM level. Done after the removal step so the original
299+
// static method is no longer on cNode when the forwarder is added.
300+
for (MethodNode anchored : anchoredOnInterface) {
301+
cNode.addMethod(createAnchoredInterfaceForwarder(cNode, helper, anchored));
302+
}
303+
277304
// copy statements from static and instance init blocks
278305
if (staticInitStatements != null) {
279306
BlockStatement toBlock = getBlockStatement(staticInitializer, staticInitializer.getCode());
@@ -617,6 +644,106 @@ private void processField(final FieldNode field, final MethodNode initializer, f
617644
fieldHelper.addField(dummyField);
618645
}
619646

647+
/**
648+
* Reports a compile error for any {@code @Anchored} annotation that is
649+
* applied to something other than a public static non-abstract trait
650+
* method. Without this check the misapplied annotation would be silently
651+
* ignored, leaving the user with no signal that the marker had no effect.
652+
*/
653+
private void validateAnchoredAnnotations(final ClassNode traitClass) {
654+
for (MethodNode methodNode : traitClass.getMethods()) {
655+
List<AnnotationNode> annotations = methodNode.getAnnotations(ANCHORED_TYPE);
656+
if (annotations.isEmpty()) continue;
657+
String issue;
658+
if (!methodNode.isStatic()) {
659+
issue = "is not static";
660+
} else if (methodNode.isPrivate()) {
661+
issue = "is private";
662+
} else if (methodNode.isAbstract()) {
663+
issue = "is abstract";
664+
} else {
665+
continue; // valid
666+
}
667+
AnnotationNode anchored = annotations.get(0);
668+
sourceUnit.addError(new SyntaxException(
669+
"@Anchored can only be applied to public static trait methods; "
670+
+ traitClass.getName() + "#" + methodNode.getTypeDescriptor() + " " + issue,
671+
anchored.getLineNumber(), anchored.getColumnNumber()));
672+
}
673+
}
674+
675+
/**
676+
* Returns the public {@code static} trait methods whose {@code @Anchored}
677+
* marker requests interface promotion (i.e. {@code inInterface=true}, the
678+
* default). The returned list snapshots the trait's method set so the
679+
* caller can iterate the methods without being affected by later
680+
* mutations to {@code traitClass.getMethods()}.
681+
*/
682+
private static List<MethodNode> collectAnchoredOnInterface(final ClassNode traitClass) {
683+
List<MethodNode> result = new ArrayList<>();
684+
for (MethodNode methodNode : traitClass.getMethods()) {
685+
if (methodNode.isStatic() && !methodNode.isPrivate() && !methodNode.isAbstract()
686+
&& isAnchoredOnInterface(methodNode)) {
687+
result.add(methodNode);
688+
}
689+
}
690+
return result;
691+
}
692+
693+
/**
694+
* Returns {@code true} if the method is annotated with {@code @Anchored}
695+
* and the {@code inInterface} attribute is true (the default).
696+
*/
697+
private static boolean isAnchoredOnInterface(final MethodNode methodNode) {
698+
List<AnnotationNode> anns = methodNode.getAnnotations(ANCHORED_TYPE);
699+
if (anns.isEmpty()) return false;
700+
Expression member = anns.get(0).getMember("inInterface");
701+
if (member instanceof ConstantExpression
702+
&& Boolean.FALSE.equals(((ConstantExpression) member).getValue())) {
703+
return false;
704+
}
705+
return true;
706+
}
707+
708+
/**
709+
* Builds a public-static method on the trait interface that delegates to
710+
* the corresponding helper method.
711+
*
712+
* <p>Emits {@code public static R m(args) { return T$Trait$Helper.m(T.class, args); }},
713+
* preserving generics, exceptions and parameter list of the original
714+
* trait static. The trait class itself is passed as the synthetic
715+
* {@code $self} receiver expected by the helper, consistent with the
716+
* declarer-bound dispatch model that {@code @Anchored} selects.
717+
*/
718+
private static MethodNode createAnchoredInterfaceForwarder(final ClassNode traitClass, final ClassNode helper, final MethodNode original) {
719+
Parameter[] params = original.getParameters();
720+
Expression[] callArgs = new Expression[params.length + 1];
721+
callArgs[0] = classX(traitClass);
722+
for (int i = 0; i < params.length; i++) {
723+
callArgs[i + 1] = varX(params[i]);
724+
}
725+
MethodCallExpression call = callX(classX(helper), original.getName(), args(callArgs));
726+
Statement body = VOID_TYPE.equals(original.getReturnType()) ? stmt(call) : returnS(call);
727+
MethodNode forwarder = new MethodNode(
728+
original.getName(),
729+
ACC_PUBLIC | ACC_STATIC,
730+
original.getReturnType(),
731+
params,
732+
original.getExceptions(),
733+
body);
734+
forwarder.setGenericsTypes(original.getGenericsTypes());
735+
forwarder.setSynthetic(true);
736+
forwarder.setSourcePosition(original);
737+
// Carry over the trait method's RUNTIME/CLASS-retention annotations (e.g.
738+
// @Deprecated) so the promoted interface static behaves like the original,
739+
// consistent with the forwarders generated by TraitComposer. The helper
740+
// filters out SOURCE-retention/transform markers and closure-member ones.
741+
List<AnnotationNode> copied = new ArrayList<>(), notCopied = new ArrayList<>();
742+
GeneralUtils.copyAnnotatedNodeAnnotations(original, copied, notCopied);
743+
forwarder.addAnnotations(copied);
744+
return forwarder;
745+
}
746+
620747
private MethodNode processMethod(final ClassNode traitClass, final ClassNode traitHelperClass, final MethodNode methodNode, final ClassNode fieldHelper, final Collection<String> knownFields) {
621748
boolean isAbstractMethod = methodNode.isAbstract();
622749
boolean isPrivateMethod = methodNode.isPrivate();

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

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,8 @@
6666
*/
6767
class TraitReceiverTransformer extends ClassCodeExpressionTransformer {
6868

69+
private static final ClassNode ANCHORED_TYPE = ClassHelper.make(groovy.transform.Anchored.class);
70+
6971
private final VariableExpression weaved;
7072
private final SourceUnit unit;
7173
private final ClassNode traitClass;
@@ -278,16 +280,23 @@ private Expression transformMethodCallOnThis(final MethodCallExpression call) {
278280
MethodNode methodNode = findConcreteMethod(traitClass, call.getMethodAsString());
279281
if (methodNode != null) {
280282
MethodCallExpression newCall;
281-
if (methodNode.isStatic() && !methodNode.isPrivate() && !inClosure) {
283+
boolean anchored = !methodNode.getAnnotations(ANCHORED_TYPE).isEmpty();
284+
if (methodNode.isStatic() && !methodNode.isPrivate() && !anchored && !inClosure) {
282285
// GROOVY-11985: dispatch unqualified/this-qualified calls to
283-
// public trait statics through the implementing class so an
284-
// override declared on the implementer is visible from trait code.
286+
// public, non-@Anchored trait statics through the
287+
// implementing class so an override declared on the
288+
// implementer is visible from trait code. Annotating the
289+
// trait static with @Anchored opts out of this override
290+
// path and keeps dispatch declarer-bound through the trait
291+
// helper (Java/interface-static flavour); the matching
292+
// interface promotion is performed in TraitASTTransformation.
285293
Expression implClass = ClassHelper.isClassType(weaved.getOriginType()) ? varX(weaved) : castX(ClassHelper.CLASS_Type.getPlainNodeReference(), callX(varX(weaved), "getClass"));
286294
newCall = callX(implClass, method, transform(arguments));
287295
newCall.setImplicitThis(false);
288296
newCall.putNodeMetaData(TraitASTTransformation.DO_DYNAMIC, methodNode.getReturnType());
289297
} else {
290298
// this.m(x) --> (this or T$Trait$Helper).m($self or $static$self or (Class)$self.getClass(), x)
299+
// Reached for: private static, @Anchored static, instance method, or any call inside a closure.
291300
Expression selfClassOrObject = methodNode.isStatic() && !ClassHelper.isClassType(weaved.getOriginType()) ? castX(ClassHelper.CLASS_Type.getPlainNodeReference(), callX(weaved, "getClass")) : weaved;
292301
newCall = callX(!inClosure ? thisExpr : classX(traitHelper), method, createArgumentList(selfClassOrObject, arguments));
293302
}

0 commit comments

Comments
 (0)