Skip to content

Commit 8d792b4

Browse files
committed
make @Anchored default flip spike
1 parent e4a9bb8 commit 8d792b4

8 files changed

Lines changed: 400 additions & 542 deletions

File tree

src/main/java/groovy/transform/Anchored.java

Lines changed: 0 additions & 70 deletions
This file was deleted.
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
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 public trait <code>static</code> method as <em>virtual</em>:
29+
* trait-body calls to this method dispatch through the implementing class
30+
* at runtime, so a same-signature static method declared on the
31+
* implementing class overrides the trait's default.
32+
*
33+
* <p>This is the opt-in counterpart to the default declarer-bound
34+
* dispatch for trait static methods (where trait-body calls always
35+
* invoke the trait's own copy regardless of any same-named static on
36+
* the implementer). The opt-in restores the per-implementer override
37+
* pattern used by Grails' {@code Validateable.defaultNullable()} and
38+
* similar framework hooks.
39+
*
40+
* <p>The marker is <em>per-callee</em>, not per-caller: it changes how
41+
* trait code invokes the annotated method, regardless of which method
42+
* inside the trait does the calling.
43+
*
44+
* <p>Valid only on public, non-abstract <code>static</code> trait
45+
* methods. Applying it to an instance method, a private method, an
46+
* abstract method, or anything outside a trait is a compile-time
47+
* error. {@code @Virtual} and {@link groovy.transform.Anchored} are
48+
* mutually exclusive — interface promotion (the {@code @Anchored}
49+
* effect) requires declarer-bound dispatch and is incompatible with
50+
* virtual dispatch.
51+
*
52+
* @since 6.0.0
53+
*/
54+
@Documented
55+
@Retention(RetentionPolicy.RUNTIME)
56+
@Target(ElementType.METHOD)
57+
public @interface Virtual {
58+
}

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

Lines changed: 38 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -116,8 +116,8 @@
116116
@GroovyASTTransformation(phase = CompilePhase.SEMANTIC_ANALYSIS)
117117
public class TraitASTTransformation extends AbstractASTTransformation implements CompilationUnitAware {
118118

119-
/** Marker annotation type for {@code @Anchored} trait static methods. */
120-
private static final ClassNode ANCHORED_TYPE = ClassHelper.make(groovy.transform.Anchored.class);
119+
/** Marker annotation type for {@code @Virtual} trait static methods. */
120+
private static final ClassNode VIRTUAL_TYPE = ClassHelper.make(groovy.transform.Virtual.class);
121121

122122
/**
123123
* Metadata key that marks trait-generated calls requiring dynamic dispatch.
@@ -268,18 +268,17 @@ private void createHelperClasses(final ClassNode cNode) {
268268
processField(field, initializer, staticInitializer, fieldHelper, helper, staticFieldHelper, cNode, fieldNames);
269269
}
270270

271-
// Reject misapplied @Anchored markers before we waste effort
271+
// Reject misapplied @Virtual markers before we waste effort
272272
// processing them. Errors are registered against the source unit but
273273
// processing continues so that multiple violations can be reported
274274
// in a single compilation.
275-
validateAnchoredAnnotations(cNode);
275+
validateVirtualAnnotations(cNode);
276276

277-
// Identify @Anchored public statics whose bodies the main loop will
278-
// emit on the helper. Captured up front so the main loop carries no
279-
// @Anchored-specific branching and so the interface forwarders can be
280-
// installed in a single post-processing step after the originals are
281-
// removed from the trait interface.
282-
List<MethodNode> anchoredOnInterface = collectAnchoredOnInterface(cNode);
277+
// Identify public static trait methods whose bodies the main loop
278+
// will emit on the helper; each gets a JVM-native interface static
279+
// method promoted onto the trait interface in a post-processing
280+
// step after the originals are removed from the trait interface.
281+
List<MethodNode> interfaceStatics = collectInterfaceStatics(cNode);
283282

284283
// add methods
285284
List<MethodNode> nonPublicAPIMethods = new ArrayList<>();
@@ -310,12 +309,13 @@ private void createHelperClasses(final ClassNode cNode) {
310309
}
311310

312311
// Install a public-static method on the trait interface for each
313-
// @Anchored callee identified above. The forwarder delegates to the
314-
// helper so external `Trait.m()` and from-trait `T.m()` calls resolve
315-
// at the JVM level. Done after the removal step so the original
316-
// static method is no longer on cNode when the forwarder is added.
317-
for (MethodNode anchored : anchoredOnInterface) {
318-
cNode.addMethod(createAnchoredInterfaceForwarder(cNode, helper, anchored));
312+
// public static trait method identified above. The forwarder
313+
// delegates to the helper so external `Trait.m()` and from-trait
314+
// `T.m()` calls resolve at the JVM level. Done after the removal
315+
// step so the original static method is no longer on cNode when
316+
// the forwarder is added.
317+
for (MethodNode m : interfaceStatics) {
318+
cNode.addMethod(createInterfaceStaticForwarder(cNode, helper, m));
319319
}
320320

321321
// copy statements from static and instance init blocks
@@ -651,15 +651,15 @@ private void processField(final FieldNode field, final MethodNode initializer, f
651651
}
652652

653653
/**
654-
* Reports a compile error for any {@code @Anchored} annotation that is
654+
* Reports a compile error for any {@code @Virtual} annotation that is
655655
* applied to something other than a public static non-abstract trait
656656
* method. Without this check the misapplied annotation would be silently
657657
* ignored, leaving the user with no signal that the marker had no effect.
658658
*/
659-
private void validateAnchoredAnnotations(final ClassNode traitClass) {
659+
private void validateVirtualAnnotations(final ClassNode traitClass) {
660660
for (MethodNode methodNode : traitClass.getMethods()) {
661-
List<AnnotationNode> annotations = methodNode.getAnnotations(ANCHORED_TYPE);
662-
if (annotations.isEmpty()) continue;
661+
List<AnnotationNode> virtualAnns = methodNode.getAnnotations(VIRTUAL_TYPE);
662+
if (virtualAnns.isEmpty()) continue;
663663
String issue;
664664
if (!methodNode.isStatic()) {
665665
issue = "is not static";
@@ -670,47 +670,38 @@ private void validateAnchoredAnnotations(final ClassNode traitClass) {
670670
} else {
671671
continue; // valid
672672
}
673-
AnnotationNode anchored = annotations.get(0);
673+
AnnotationNode at = virtualAnns.get(0);
674674
sourceUnit.addError(new SyntaxException(
675-
"@Anchored can only be applied to public static trait methods; "
675+
"@Virtual can only be applied to public static trait methods; "
676676
+ traitClass.getName() + "#" + methodNode.getTypeDescriptor() + " " + issue,
677-
anchored.getLineNumber(), anchored.getColumnNumber()));
677+
at.getLineNumber(), at.getColumnNumber()));
678678
}
679679
}
680680

681681
/**
682-
* Returns the public {@code static} trait methods whose {@code @Anchored}
683-
* marker requests interface promotion (i.e. {@code inInterface=true}, the
684-
* default). The returned list snapshots the trait's method set so the
685-
* caller can iterate the methods without being affected by later
686-
* mutations to {@code traitClass.getMethods()}.
682+
* Returns the public {@code static} non-abstract trait methods that
683+
* should be promoted onto the generated trait interface as JVM-native
684+
* interface static methods. Excludes {@code @Virtual} methods, whose
685+
* dispatch path requires the helper-based dynamic-dispatch mechanism;
686+
* promoting them onto the interface as direct static methods would
687+
* make {@code @CompileStatic} callers bind to the trait's copy
688+
* statically and bypass the virtual-dispatch path entirely.
689+
*
690+
* <p>The returned list snapshots the trait's method set so the caller
691+
* can iterate without being affected by later mutations to
692+
* {@code traitClass.getMethods()}.
687693
*/
688-
private static List<MethodNode> collectAnchoredOnInterface(final ClassNode traitClass) {
694+
private static List<MethodNode> collectInterfaceStatics(final ClassNode traitClass) {
689695
List<MethodNode> result = new ArrayList<>();
690696
for (MethodNode methodNode : traitClass.getMethods()) {
691697
if (methodNode.isStatic() && !methodNode.isPrivate() && !methodNode.isAbstract()
692-
&& isAnchoredOnInterface(methodNode)) {
698+
&& methodNode.getAnnotations(VIRTUAL_TYPE).isEmpty()) {
693699
result.add(methodNode);
694700
}
695701
}
696702
return result;
697703
}
698704

699-
/**
700-
* Returns {@code true} if the method is annotated with {@code @Anchored}
701-
* and the {@code inInterface} attribute is true (the default).
702-
*/
703-
private static boolean isAnchoredOnInterface(final MethodNode methodNode) {
704-
List<AnnotationNode> anns = methodNode.getAnnotations(ANCHORED_TYPE);
705-
if (anns.isEmpty()) return false;
706-
Expression member = anns.get(0).getMember("inInterface");
707-
if (member instanceof ConstantExpression
708-
&& Boolean.FALSE.equals(((ConstantExpression) member).getValue())) {
709-
return false;
710-
}
711-
return true;
712-
}
713-
714705
/**
715706
* Builds a public-static method on the trait interface that delegates to
716707
* the corresponding helper method.
@@ -719,9 +710,9 @@ private static boolean isAnchoredOnInterface(final MethodNode methodNode) {
719710
* preserving generics, exceptions and parameter list of the original
720711
* trait static. The trait class itself is passed as the synthetic
721712
* {@code $self} receiver expected by the helper, consistent with the
722-
* declarer-bound dispatch model that {@code @Anchored} selects.
713+
* declarer-bound dispatch model of trait static methods.
723714
*/
724-
private static MethodNode createAnchoredInterfaceForwarder(final ClassNode traitClass, final ClassNode helper, final MethodNode original) {
715+
private static MethodNode createInterfaceStaticForwarder(final ClassNode traitClass, final ClassNode helper, final MethodNode original) {
725716
Parameter[] params = original.getParameters();
726717
Expression[] callArgs = new Expression[params.length + 1];
727718
callArgs[0] = classX(traitClass);

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

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

69-
private static final ClassNode ANCHORED_TYPE = ClassHelper.make(groovy.transform.Anchored.class);
69+
private static final ClassNode VIRTUAL_TYPE = ClassHelper.make(groovy.transform.Virtual.class);
7070

7171
private final VariableExpression weaved;
7272
private final SourceUnit unit;
@@ -297,23 +297,22 @@ private Expression transformMethodCallOnThis(final MethodCallExpression call) {
297297
MethodNode methodNode = findConcreteMethod(traitClass, call.getMethodAsString());
298298
if (methodNode != null) {
299299
MethodCallExpression newCall;
300-
boolean anchored = !methodNode.getAnnotations(ANCHORED_TYPE).isEmpty();
301-
if (methodNode.isStatic() && !methodNode.isPrivate() && !anchored && !inClosure) {
302-
// GROOVY-11985: dispatch unqualified/this-qualified calls to
303-
// public, non-@Anchored trait statics through the
304-
// implementing class so an override declared on the
305-
// implementer is visible from trait code. Annotating the
306-
// trait static with @Anchored opts out of this override
307-
// path and keeps dispatch declarer-bound through the trait
308-
// helper (Java/interface-static flavour); the matching
309-
// interface promotion is performed in TraitASTTransformation.
300+
boolean virtual = !methodNode.getAnnotations(VIRTUAL_TYPE).isEmpty();
301+
if (methodNode.isStatic() && !methodNode.isPrivate() && virtual && !inClosure) {
302+
// Default dispatch for trait static methods is
303+
// declarer-bound; per-implementer override visibility
304+
// is opt-in via `@Virtual`. Annotating a public trait
305+
// static with @Virtual emits the dynamic-dispatch
306+
// path so the implementer's override (if any) is
307+
// visible from trait code.
310308
Expression implClass = ClassHelper.isClassType(weaved.getOriginType()) ? varX(weaved) : castX(ClassHelper.CLASS_Type.getPlainNodeReference(), callX(varX(weaved), "getClass"));
311309
newCall = callX(implClass, method, transform(arguments));
312310
newCall.setImplicitThis(false);
313311
newCall.putNodeMetaData(TraitASTTransformation.DO_DYNAMIC, methodNode.getReturnType());
314312
} else {
315313
// this.m(x) --> (this or T$Trait$Helper).m($self or $static$self or (Class)$self.getClass(), x)
316-
// Reached for: private static, @Anchored static, instance method, or any call inside a closure.
314+
// Reached for: plain (non-@Virtual) static, private static,
315+
// instance method, or any call inside a closure.
317316
Expression selfClassOrObject = methodNode.isStatic() && !ClassHelper.isClassType(weaved.getOriginType()) ? castX(ClassHelper.CLASS_Type.getPlainNodeReference(), callX(weaved, "getClass")) : weaved;
318317
newCall = callX(!inClosure ? thisExpr : classX(traitHelper), method, createArgumentList(selfClassOrObject, arguments));
319318
}

src/test/groovy/org/codehaus/groovy/transform/traitx/Groovy11985.groovy

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -31,8 +31,9 @@ final class Groovy11985 {
3131
@Test
3232
void testStaticOverrideVisibleFromTraitThisCall() {
3333
GroovyAssert.assertScript '''
34+
import groovy.transform.Virtual
3435
trait Validateable {
35-
static boolean defaultNullable() { false }
36+
@Virtual static boolean defaultNullable() { false }
3637
static boolean defaultNullableSeenByTrait() { this.defaultNullable() }
3738
}
3839
class MyNullableValidateable implements Validateable {
@@ -49,8 +50,9 @@ final class Groovy11985 {
4950
@Test
5051
void testStaticOverrideVisibleFromTraitUnqualifiedCall() {
5152
GroovyAssert.assertScript '''
53+
import groovy.transform.Virtual
5254
trait Validateable {
53-
static boolean defaultNullable() { false }
55+
@Virtual static boolean defaultNullable() { false }
5456
static boolean defaultNullableUnqualified() { defaultNullable() }
5557
}
5658
class MyNullableValidateable implements Validateable {
@@ -65,8 +67,9 @@ final class Groovy11985 {
6567
@Test
6668
void testStaticOverrideVisibleFromInstanceMethod() {
6769
GroovyAssert.assertScript '''
70+
import groovy.transform.Virtual
6871
trait T {
69-
static String which() { 'trait' }
72+
@Virtual static String which() { 'trait' }
7073
String greet() { which() }
7174
}
7275
class C implements T {
@@ -82,9 +85,10 @@ final class Groovy11985 {
8285
void testStaticOverrideUnderCompileStatic() {
8386
GroovyAssert.assertScript '''
8487
import groovy.transform.CompileStatic
88+
import groovy.transform.Virtual
8589
@CompileStatic
8690
trait Validateable {
87-
static boolean defaultNullable() { false }
91+
@Virtual static boolean defaultNullable() { false }
8892
static boolean defaultNullableSeenByTrait() { this.defaultNullable() }
8993
static boolean defaultNullableUnqualified() { defaultNullable() }
9094
}
@@ -117,7 +121,8 @@ final class Groovy11985 {
117121
@Test
118122
void testSuperTraitPublicStaticIsPolymorphic() {
119123
GroovyAssert.assertScript '''
120-
trait Base { static String hello() { 'base' } }
124+
import groovy.transform.Virtual
125+
trait Base { @Virtual static String hello() { 'base' } }
121126
trait Mid extends Base { static String greet() { hello() } }
122127
class C implements Mid {}
123128
class D implements Mid { static String hello() { 'override' } }

0 commit comments

Comments
 (0)