diff --git a/src/main/java/groovy/transform/Anchored.java b/src/main/java/groovy/transform/Anchored.java
deleted file mode 100644
index 1666117b027..00000000000
--- a/src/main/java/groovy/transform/Anchored.java
+++ /dev/null
@@ -1,70 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements. See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership. The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied. See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-package groovy.transform;
-
-import java.lang.annotation.Documented;
-import java.lang.annotation.ElementType;
-import java.lang.annotation.Retention;
-import java.lang.annotation.RetentionPolicy;
-import java.lang.annotation.Target;
-
-/**
- * Marks a trait method as anchored to the trait — dispatch is fixed
- * to the trait's own definition rather than routed through the implementing
- * class.
- *
- *
Applied to a trait static method, this opts out of the
- * default per-implementer override dispatch (the Groovy distinctive that the
- * Grails Validateable.defaultNullable() pattern depends on) and
- * substitutes the JVM interface-static dispatch model: the implementer's
- * same-signature static is an independent method, not an override; the trait
- * body always calls the trait's own copy.
- *
- *
By default the marker also promotes the annotated method onto the
- * generated trait interface as a JVM-native public static. That makes
- * external Trait.m() calls and from-trait T.m()
- * calls both resolve at the JVM level rather than throwing
- * MissingMethodException. The {@link #inInterface()} attribute
- * is provided as an opt-out for the narrow case where dispatch should be
- * trait-anchored but the method should not be published on the
- * interface (e.g. a soft-deprecated trait-internal helper that needs to
- * remain publicly callable for backward compatibility but should not gain a
- * fresh Java-visible API surface).
- *
- *
Use this marker for trait statics that are part of the trait's published
- * contract — guaranteed-invariant utilities the trait author does not want
- * implementers to redefine. Continue to use plain static when
- * the trait offers a default that implementers may legitimately override.
- *
- * @since 6.0.0
- */
-@Documented
-@Retention(RetentionPolicy.RUNTIME)
-@Target(ElementType.METHOD)
-public @interface Anchored {
- /**
- * Whether the annotated method should also be promoted onto the generated
- * trait interface. Default true: the marker bundles
- * declarer-bound dispatch with external interface visibility (the
- * coherent JVM interface-static model). Set to false for the
- * narrow opt-out described above — dispatch stays trait-anchored but the
- * method is not exposed on the interface.
- */
- boolean inInterface() default true;
-}
diff --git a/src/main/java/groovy/transform/Virtual.java b/src/main/java/groovy/transform/Virtual.java
new file mode 100644
index 00000000000..e81b8924c18
--- /dev/null
+++ b/src/main/java/groovy/transform/Virtual.java
@@ -0,0 +1,59 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package groovy.transform;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * Marks a public trait static method as virtual:
+ * trait-body calls to this method dispatch through the implementing class
+ * at runtime, so a same-signature static method declared on the
+ * implementing class overrides the trait's default.
+ *
+ *
This is the opt-in counterpart to the default declarer-bound
+ * dispatch for trait static methods (where trait-body calls always
+ * invoke the trait's own copy regardless of any same-named static on
+ * the implementer). The opt-in restores the per-implementer override
+ * pattern used by Grails' {@code Validateable.defaultNullable()} and
+ * similar framework hooks.
+ *
+ *
The marker is per-callee, not per-caller: it changes how
+ * trait code invokes the annotated method, regardless of which method
+ * inside the trait does the calling.
+ *
+ *
Valid only on public, non-abstract static trait
+ * methods. Applying it to an instance method, a private method, an
+ * abstract method, or anything outside a trait is a compile-time
+ * error. Unlike plain trait statics, an {@code @Virtual} method is
+ * not promoted to a JVM-native interface static on the trait
+ * interface — interface statics are declarer-bound by JVM rule and
+ * incompatible with virtual dispatch — so external callers reach the
+ * method through the implementer rather than via {@code Trait.m()}.
+ *
+ * @since 6.0.0
+ */
+@Documented
+@Retention(RetentionPolicy.RUNTIME)
+@Target(ElementType.METHOD)
+public @interface Virtual {
+}
diff --git a/src/main/java/org/codehaus/groovy/transform/trait/TraitASTTransformation.java b/src/main/java/org/codehaus/groovy/transform/trait/TraitASTTransformation.java
index 15cc9a8be19..f9b4e19db2e 100644
--- a/src/main/java/org/codehaus/groovy/transform/trait/TraitASTTransformation.java
+++ b/src/main/java/org/codehaus/groovy/transform/trait/TraitASTTransformation.java
@@ -116,8 +116,8 @@
@GroovyASTTransformation(phase = CompilePhase.SEMANTIC_ANALYSIS)
public class TraitASTTransformation extends AbstractASTTransformation implements CompilationUnitAware {
- /** Marker annotation type for {@code @Anchored} trait static methods. */
- private static final ClassNode ANCHORED_TYPE = ClassHelper.make(groovy.transform.Anchored.class);
+ /** Marker annotation type for {@code @Virtual} trait static methods. */
+ private static final ClassNode VIRTUAL_TYPE = ClassHelper.make(groovy.transform.Virtual.class);
/**
* Metadata key that marks trait-generated calls requiring dynamic dispatch.
@@ -268,18 +268,17 @@ private void createHelperClasses(final ClassNode cNode) {
processField(field, initializer, staticInitializer, fieldHelper, helper, staticFieldHelper, cNode, fieldNames);
}
- // Reject misapplied @Anchored markers before we waste effort
+ // Reject misapplied @Virtual markers before we waste effort
// processing them. Errors are registered against the source unit but
// processing continues so that multiple violations can be reported
// in a single compilation.
- validateAnchoredAnnotations(cNode);
+ validateVirtualAnnotations(cNode);
- // Identify @Anchored public statics whose bodies the main loop will
- // emit on the helper. Captured up front so the main loop carries no
- // @Anchored-specific branching and so the interface forwarders can be
- // installed in a single post-processing step after the originals are
- // removed from the trait interface.
- List anchoredOnInterface = collectAnchoredOnInterface(cNode);
+ // Identify public static trait methods whose bodies the main loop
+ // will emit on the helper; each gets a JVM-native interface static
+ // method promoted onto the trait interface in a post-processing
+ // step after the originals are removed from the trait interface.
+ List interfaceStatics = collectInterfaceStatics(cNode);
// add methods
List nonPublicAPIMethods = new ArrayList<>();
@@ -310,12 +309,13 @@ private void createHelperClasses(final ClassNode cNode) {
}
// Install a public-static method on the trait interface for each
- // @Anchored callee identified above. The forwarder delegates to the
- // helper so external `Trait.m()` and from-trait `T.m()` calls resolve
- // at the JVM level. Done after the removal step so the original
- // static method is no longer on cNode when the forwarder is added.
- for (MethodNode anchored : anchoredOnInterface) {
- cNode.addMethod(createAnchoredInterfaceForwarder(cNode, helper, anchored));
+ // public static trait method identified above. The forwarder
+ // delegates to the helper so external `Trait.m()` and from-trait
+ // `T.m()` calls resolve at the JVM level. Done after the removal
+ // step so the original static method is no longer on cNode when
+ // the forwarder is added.
+ for (MethodNode m : interfaceStatics) {
+ cNode.addMethod(createInterfaceStaticForwarder(cNode, helper, m));
}
// copy statements from static and instance init blocks
@@ -651,15 +651,15 @@ private void processField(final FieldNode field, final MethodNode initializer, f
}
/**
- * Reports a compile error for any {@code @Anchored} annotation that is
+ * Reports a compile error for any {@code @Virtual} annotation that is
* applied to something other than a public static non-abstract trait
* method. Without this check the misapplied annotation would be silently
* ignored, leaving the user with no signal that the marker had no effect.
*/
- private void validateAnchoredAnnotations(final ClassNode traitClass) {
+ private void validateVirtualAnnotations(final ClassNode traitClass) {
for (MethodNode methodNode : traitClass.getMethods()) {
- List annotations = methodNode.getAnnotations(ANCHORED_TYPE);
- if (annotations.isEmpty()) continue;
+ List virtualAnns = methodNode.getAnnotations(VIRTUAL_TYPE);
+ if (virtualAnns.isEmpty()) continue;
String issue;
if (!methodNode.isStatic()) {
issue = "is not static";
@@ -670,47 +670,38 @@ private void validateAnchoredAnnotations(final ClassNode traitClass) {
} else {
continue; // valid
}
- AnnotationNode anchored = annotations.get(0);
+ AnnotationNode at = virtualAnns.get(0);
sourceUnit.addError(new SyntaxException(
- "@Anchored can only be applied to public static trait methods; "
+ "@Virtual can only be applied to public static trait methods; "
+ traitClass.getName() + "#" + methodNode.getTypeDescriptor() + " " + issue,
- anchored.getLineNumber(), anchored.getColumnNumber()));
+ at.getLineNumber(), at.getColumnNumber()));
}
}
/**
- * Returns the public {@code static} trait methods whose {@code @Anchored}
- * marker requests interface promotion (i.e. {@code inInterface=true}, the
- * default). The returned list snapshots the trait's method set so the
- * caller can iterate the methods without being affected by later
- * mutations to {@code traitClass.getMethods()}.
+ * Returns the public {@code static} non-abstract trait methods that
+ * should be promoted onto the generated trait interface as JVM-native
+ * interface static methods. Excludes {@code @Virtual} methods, whose
+ * dispatch path requires the helper-based dynamic-dispatch mechanism;
+ * promoting them onto the interface as direct static methods would
+ * make {@code @CompileStatic} callers bind to the trait's copy
+ * statically and bypass the virtual-dispatch path entirely.
+ *
+ * The returned list snapshots the trait's method set so the caller
+ * can iterate without being affected by later mutations to
+ * {@code traitClass.getMethods()}.
*/
- private static List collectAnchoredOnInterface(final ClassNode traitClass) {
+ private static List collectInterfaceStatics(final ClassNode traitClass) {
List result = new ArrayList<>();
for (MethodNode methodNode : traitClass.getMethods()) {
if (methodNode.isStatic() && !methodNode.isPrivate() && !methodNode.isAbstract()
- && isAnchoredOnInterface(methodNode)) {
+ && methodNode.getAnnotations(VIRTUAL_TYPE).isEmpty()) {
result.add(methodNode);
}
}
return result;
}
- /**
- * Returns {@code true} if the method is annotated with {@code @Anchored}
- * and the {@code inInterface} attribute is true (the default).
- */
- private static boolean isAnchoredOnInterface(final MethodNode methodNode) {
- List anns = methodNode.getAnnotations(ANCHORED_TYPE);
- if (anns.isEmpty()) return false;
- Expression member = anns.get(0).getMember("inInterface");
- if (member instanceof ConstantExpression
- && Boolean.FALSE.equals(((ConstantExpression) member).getValue())) {
- return false;
- }
- return true;
- }
-
/**
* Builds a public-static method on the trait interface that delegates to
* the corresponding helper method.
@@ -719,9 +710,9 @@ private static boolean isAnchoredOnInterface(final MethodNode methodNode) {
* preserving generics, exceptions and parameter list of the original
* trait static. The trait class itself is passed as the synthetic
* {@code $self} receiver expected by the helper, consistent with the
- * declarer-bound dispatch model that {@code @Anchored} selects.
+ * declarer-bound dispatch model of trait static methods.
*/
- private static MethodNode createAnchoredInterfaceForwarder(final ClassNode traitClass, final ClassNode helper, final MethodNode original) {
+ private static MethodNode createInterfaceStaticForwarder(final ClassNode traitClass, final ClassNode helper, final MethodNode original) {
Parameter[] params = original.getParameters();
Expression[] callArgs = new Expression[params.length + 1];
callArgs[0] = classX(traitClass);
diff --git a/src/main/java/org/codehaus/groovy/transform/trait/TraitReceiverTransformer.java b/src/main/java/org/codehaus/groovy/transform/trait/TraitReceiverTransformer.java
index 0f7aa58aeac..b338ff631f8 100644
--- a/src/main/java/org/codehaus/groovy/transform/trait/TraitReceiverTransformer.java
+++ b/src/main/java/org/codehaus/groovy/transform/trait/TraitReceiverTransformer.java
@@ -67,7 +67,7 @@
*/
class TraitReceiverTransformer extends ClassCodeExpressionTransformer {
- private static final ClassNode ANCHORED_TYPE = ClassHelper.make(groovy.transform.Anchored.class);
+ private static final ClassNode VIRTUAL_TYPE = ClassHelper.make(groovy.transform.Virtual.class);
private final VariableExpression weaved;
private final SourceUnit unit;
@@ -359,23 +359,22 @@ private Expression transformMethodCallOnThis(final MethodCallExpression call) {
MethodNode methodNode = findConcreteMethod(traitClass, call.getMethodAsString());
if (methodNode != null) {
MethodCallExpression newCall;
- boolean anchored = !methodNode.getAnnotations(ANCHORED_TYPE).isEmpty();
- if (methodNode.isStatic() && !methodNode.isPrivate() && !anchored && !inClosure) {
- // GROOVY-11985: dispatch unqualified/this-qualified calls to
- // public, non-@Anchored trait statics through the
- // implementing class so an override declared on the
- // implementer is visible from trait code. Annotating the
- // trait static with @Anchored opts out of this override
- // path and keeps dispatch declarer-bound through the trait
- // helper (Java/interface-static flavour); the matching
- // interface promotion is performed in TraitASTTransformation.
+ boolean virtual = !methodNode.getAnnotations(VIRTUAL_TYPE).isEmpty();
+ if (methodNode.isStatic() && !methodNode.isPrivate() && virtual && !inClosure) {
+ // Default dispatch for trait static methods is
+ // declarer-bound; per-implementer override visibility
+ // is opt-in via `@Virtual`. Annotating a public trait
+ // static with @Virtual emits the dynamic-dispatch
+ // path so the implementer's override (if any) is
+ // visible from trait code.
Expression implClass = ClassHelper.isClassType(weaved.getOriginType()) ? varX(weaved) : castX(ClassHelper.CLASS_Type.getPlainNodeReference(), callX(varX(weaved), "getClass"));
newCall = callX(implClass, method, transform(arguments));
newCall.setImplicitThis(false);
newCall.putNodeMetaData(TraitASTTransformation.DO_DYNAMIC, methodNode.getReturnType());
} else {
// this.m(x) --> (this or T$Trait$Helper).m($self or $static$self or (Class)$self.getClass(), x)
- // Reached for: private static, @Anchored static, instance method, or any call inside a closure.
+ // Reached for: plain (non-@Virtual) static, private static,
+ // instance method, or any call inside a closure.
Expression selfClassOrObject = methodNode.isStatic() && !ClassHelper.isClassType(weaved.getOriginType()) ? castX(ClassHelper.CLASS_Type.getPlainNodeReference(), callX(weaved, "getClass")) : weaved;
newCall = callX(!inClosure ? thisExpr : classX(traitHelper), method, createArgumentList(selfClassOrObject, arguments));
}
diff --git a/src/test/groovy/org/codehaus/groovy/transform/traitx/Groovy11985.groovy b/src/test/groovy/org/codehaus/groovy/transform/traitx/Groovy11985.groovy
index 8f95ddcd690..5a4628f07c8 100644
--- a/src/test/groovy/org/codehaus/groovy/transform/traitx/Groovy11985.groovy
+++ b/src/test/groovy/org/codehaus/groovy/transform/traitx/Groovy11985.groovy
@@ -31,8 +31,9 @@ final class Groovy11985 {
@Test
void testStaticOverrideVisibleFromTraitThisCall() {
GroovyAssert.assertScript '''
+ import groovy.transform.Virtual
trait Validateable {
- static boolean defaultNullable() { false }
+ @Virtual static boolean defaultNullable() { false }
static boolean defaultNullableSeenByTrait() { this.defaultNullable() }
}
class MyNullableValidateable implements Validateable {
@@ -49,8 +50,9 @@ final class Groovy11985 {
@Test
void testStaticOverrideVisibleFromTraitUnqualifiedCall() {
GroovyAssert.assertScript '''
+ import groovy.transform.Virtual
trait Validateable {
- static boolean defaultNullable() { false }
+ @Virtual static boolean defaultNullable() { false }
static boolean defaultNullableUnqualified() { defaultNullable() }
}
class MyNullableValidateable implements Validateable {
@@ -65,8 +67,9 @@ final class Groovy11985 {
@Test
void testStaticOverrideVisibleFromInstanceMethod() {
GroovyAssert.assertScript '''
+ import groovy.transform.Virtual
trait T {
- static String which() { 'trait' }
+ @Virtual static String which() { 'trait' }
String greet() { which() }
}
class C implements T {
@@ -82,9 +85,10 @@ final class Groovy11985 {
void testStaticOverrideUnderCompileStatic() {
GroovyAssert.assertScript '''
import groovy.transform.CompileStatic
+ import groovy.transform.Virtual
@CompileStatic
trait Validateable {
- static boolean defaultNullable() { false }
+ @Virtual static boolean defaultNullable() { false }
static boolean defaultNullableSeenByTrait() { this.defaultNullable() }
static boolean defaultNullableUnqualified() { defaultNullable() }
}
@@ -117,7 +121,8 @@ final class Groovy11985 {
@Test
void testSuperTraitPublicStaticIsPolymorphic() {
GroovyAssert.assertScript '''
- trait Base { static String hello() { 'base' } }
+ import groovy.transform.Virtual
+ trait Base { @Virtual static String hello() { 'base' } }
trait Mid extends Base { static String greet() { hello() } }
class C implements Mid {}
class D implements Mid { static String hello() { 'override' } }
diff --git a/src/test/groovy/org/codehaus/groovy/transform/traitx/Groovy12093.groovy b/src/test/groovy/org/codehaus/groovy/transform/traitx/Groovy12093.groovy
deleted file mode 100644
index 275b6c93fdd..00000000000
--- a/src/test/groovy/org/codehaus/groovy/transform/traitx/Groovy12093.groovy
+++ /dev/null
@@ -1,270 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements. See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership. The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied. See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-package org.codehaus.groovy.transform.traitx
-
-import org.codehaus.groovy.control.MultipleCompilationErrorsException
-import org.junit.Test
-
-import static groovy.test.GroovyAssert.assertScript
-import static groovy.test.GroovyAssert.shouldFail
-
-/**
- * Exercises the {@code @Anchored} marker on trait static methods (GROOVY-12093).
- *
- * The marker selects declarer-bound dispatch (Java/interface-static
- * flavour) in place of the default override-flavour dispatch, and (by
- * default) also promotes the annotated method onto the generated trait
- * interface.
- *
- *
- * plain static m() -> override flavour (dispatch via
- * implementer; Grails Validateable
- * canary; Groovy 4.0.32 + post-#2529)
- * {@literal @}Anchored static m() -> declarer-bound dispatch + on the
- * generated trait interface
- * {@literal @}Anchored(inInterface=false)
- * static m() -> declarer-bound dispatch only, not
- * published on the interface
- *
- *
- * Dispatch is implemented in {@code TraitReceiverTransformer}; interface
- * promotion is implemented in {@code TraitASTTransformation}.
- */
-final class Groovy12093 {
-
- // Basic-case coverage lives in TraitStaticDispatchMatrix:
- // * plain-static override visible to trait body → matrix row 1
- // * @Anchored makes dispatch trait-anchored → matrix row 1a
- // * @Anchored from-trait T.m() → matrix row 7a
- // * @Anchored external Trait.m() → matrix row 8a
- // * @Anchored(inInterface=false) opt-out → matrix row 13
- // This class covers the @Anchored-specific tests that the matrix
- // doesn't carry: per-callee keying, @CompileStatic variants, the
- // inInterface=false STC compile-error case, and validation that
- // @Anchored is rejected on non-public-static targets.
-
- /**
- * The marker is keyed on the callee, not the caller — same
- * discipline as the existing private-static escape.
- */
- @Test
- void anchored_isKeyedOnCallee() {
- assertScript '''
- import groovy.transform.Anchored
-
- trait V {
- static boolean overridable() { false }
- @Anchored
- static boolean anchored() { false }
- // Same caller, different callees: result depends on each
- // callee's own marker, not the caller's.
- static List bothSeen() { [this.overridable(), this.anchored()] }
- }
- class Over implements V {
- static boolean overridable() { true }
- static boolean anchored() { true }
- }
- assert Over.bothSeen() == [true, false]
- '''
- }
-
- /** Plain {@code static} inside {@code @CompileStatic} still routes via the implementer. */
- @Test
- void plainStatic_compileStatic_overrideVisible() {
- assertScript '''
- @groovy.transform.CompileStatic
- trait V {
- static String name() { 'trait' }
- static String seen() { this.name() }
- }
- @groovy.transform.CompileStatic
- class Impl implements V { static String name() { 'impl' } }
-
- assert Impl.seen() == 'impl'
- '''
- }
-
- /** {@code @Anchored} inside {@code @CompileStatic} stays trait-anchored. */
- @Test
- void anchored_compileStatic_traitAnchored() {
- assertScript '''
- import groovy.transform.Anchored
-
- @groovy.transform.CompileStatic
- trait V {
- @Anchored
- static String name() { 'trait' }
- static String seen() { this.name() }
- }
- @groovy.transform.CompileStatic
- class Impl implements V { static String name() { 'impl' } }
-
- assert Impl.seen() == 'trait'
- '''
- }
-
- /**
- * STC sanity check: external {@code V.name()} from a {@code @CompileStatic}
- * caller type-checks and runs — the interface static is visible to the
- * static type checker, not just to dynamic dispatch.
- */
- @Test
- void anchored_compileStatic_externalTraitDotM_typeChecks() {
- assertScript '''
- import groovy.transform.Anchored
- import groovy.transform.CompileStatic
-
- trait V {
- @Anchored
- static String name() { 'trait' }
- }
- class Impl implements V { }
-
- @CompileStatic
- static String callExternally() {
- V.name() // must type-check against the interface static
- }
-
- assert callExternally() == 'trait'
- '''
- }
-
- /**
- * STC sanity check: from-trait {@code V.m()} from inside a
- * {@code @CompileStatic} trait body also type-checks against the
- * interface static.
- */
- @Test
- void anchored_compileStatic_fromTraitT_DotM_typeChecks() {
- assertScript '''
- import groovy.transform.Anchored
-
- @groovy.transform.CompileStatic
- trait V {
- @Anchored
- static String name() { 'trait' }
- static String forced() { V.name() } // trait-qualified, under @CS
- }
- @groovy.transform.CompileStatic
- class Over implements V { static String name() { 'over' } }
-
- assert Over.forced() == 'trait'
- '''
- }
-
- /**
- * STC sanity check: with the {@code inInterface=false} opt-out, external
- * {@code V.name()} from a {@code @CompileStatic} caller should fail to
- * compile — no interface static to bind against.
- */
- @Test
- void anchored_inInterfaceFalse_compileStatic_externalCall_isCompileError() {
- shouldFail(MultipleCompilationErrorsException) {
- new GroovyShell().evaluate '''
- import groovy.transform.Anchored
- import groovy.transform.CompileStatic
-
- trait V {
- @Anchored(inInterface=false)
- static String name() { 'trait' }
- }
- class Impl implements V { }
-
- @CompileStatic
- static String callExternally() {
- V.name() // not on the interface — should be a compile error under @CS
- }
- callExternally()
- '''
- }
- }
-
- /**
- * The generated interface forwarder for an {@code @Anchored} static should
- * carry the original method's runtime-retention annotations (e.g.
- * {@code @Deprecated}), matching the forwarders {@code TraitComposer} creates
- * for ordinary trait methods.
- */
- @Test
- void anchored_propagatesRuntimeAnnotationsToInterfaceForwarder() {
- assertScript '''
- import groovy.transform.Anchored
- import java.lang.reflect.Modifier
-
- trait T {
- @Deprecated
- @Anchored
- static String m() { 'T' }
- }
-
- def m = T.getDeclaredMethod('m')
- assert Modifier.isStatic(m.modifiers)
- assert m.isAnnotationPresent(Deprecated)
- '''
- }
-
- // ---- Validation: @Anchored must target a public static (non-abstract) trait method ----
-
- /** {@code @Anchored} on an instance method must be a compile error. */
- @Test
- void anchored_onInstanceMethod_isCompileError() {
- def err = shouldFail '''
- import groovy.transform.Anchored
- trait V {
- @Anchored
- def m() { 'V' } // instance, not static
- }
- new Object()
- '''
- assert err.message.contains('@Anchored can only be applied to public static trait methods')
- assert err.message.contains('is not static')
- }
-
- /** {@code @Anchored} on a private static method must be a compile error. */
- @Test
- void anchored_onPrivateStatic_isCompileError() {
- def err = shouldFail '''
- import groovy.transform.Anchored
- trait V {
- @Anchored
- private static m() { 'V' }
- }
- new Object()
- '''
- assert err.message.contains('@Anchored can only be applied to public static trait methods')
- assert err.message.contains('is private')
- }
-
- /** {@code @Anchored} on an abstract trait method (which is implicitly
- * non-static — Groovy rejects {@code abstract static}) surfaces the same
- * "not static" diagnostic. */
- @Test
- void anchored_onAbstractInstanceMethod_isCompileError() {
- def err = shouldFail '''
- import groovy.transform.Anchored
- trait V {
- @Anchored
- abstract m()
- }
- new Object()
- '''
- assert err.message.contains('@Anchored can only be applied to public static trait methods')
- assert err.message.contains('is not static')
- }
-}
diff --git a/src/test/groovy/org/codehaus/groovy/transform/traitx/TraitStaticDispatchMatrix.groovy b/src/test/groovy/org/codehaus/groovy/transform/traitx/TraitStaticDispatchMatrix.groovy
index 9272734a36d..bb5d9063a55 100644
--- a/src/test/groovy/org/codehaus/groovy/transform/traitx/TraitStaticDispatchMatrix.groovy
+++ b/src/test/groovy/org/codehaus/groovy/transform/traitx/TraitStaticDispatchMatrix.groovy
@@ -22,25 +22,16 @@
// 12 design rows from GEP-22-progression-analysis.html (with row 11
// expanded to rows 11/11c/11esc covering the trait-shadows-superclass
// quadrant + the T.super.m() escape; row 9 paired with row 9acc covering
-// the static-field non-goal and its accessor workaround), and four
-// @Anchored-marker rows characterising the GROOVY-12093 proposal.
+// the static-field non-goal and its accessor workaround).
//
// Run via gradle: `./gradlew :test --tests '*.TraitStaticDispatchMatrix'`
//
-// Two kinds of test:
-// * Matrix tests assert OBSERVED behaviour. They pass on every
-// currently-shipping Groovy that delivers the spec behaviour and turn
-// RED on releases that deviate (e.g. rows 1/10/12 on Groovy 5.0.0–
-// 5.0.6 / 6.0.0-alpha-1, which carry the GROOVY-8854 regression
-// corrected by GROOVY-11985 in 5.0.7 / 6.0.0-alpha-2). That red is
-// the intended bug-detector signal, not a defect in the test.
-// * The four @Anchored rows (1a/7a/8a/13) assert the GROOVY-12093
-// behaviour now delivered by `groovy.transform.Anchored` (added in
-// this change); each references the annotation directly.
-//
-// Verdicts encoded here are PROPOSED PENDING TEAM REVIEW where the
-// underlying design point is not yet ratified (the four @Anchored rows
-// in particular).
+// Matrix tests assert OBSERVED behaviour. They pass on every
+// currently-shipping Groovy that delivers the spec behaviour and turn
+// RED on releases that deviate (e.g. rows 1/10/12 on Groovy 5.0.0–
+// 5.0.6 / 6.0.0-alpha-1, which carry the GROOVY-8854 regression
+// corrected by GROOVY-11985 in 5.0.7 / 6.0.0-alpha-2). That red is
+// the intended bug-detector signal, not a defect in the test.
package org.codehaus.groovy.transform.traitx
@@ -141,9 +132,13 @@ class TraitStaticDispatchMatrix {
// this row's polymorphic answer is the trait-machinery distinctive.
@Test
void row01_publicStatic_overrideSeenByTrait() {
+ // Per-implementer override visibility is opt-in via @Virtual.
+ // Plain `static` is declarer-bound; @Virtual makes the trait body's
+ // dispatch route through the implementing class.
def r = ev '''
+ import groovy.transform.Virtual
trait V {
- static boolean defaultNullable() { false }
+ @Virtual static boolean defaultNullable() { false }
static boolean seenThis() { this.defaultNullable() }
static boolean seenUnqualified() { defaultNullable() }
}
@@ -154,29 +149,8 @@ class TraitStaticDispatchMatrix {
'''
assert r.direct == true // direct call: override always wins
assert r.defThis == false // row 1': no override -> trait default
- assert r.overThis == true : "row1 this. : override must be visible to trait body, got ${r.overThis} — on 5.0.0–5.0.6 / 6.0.0-alpha-1 this is the GROOVY-8854 regression (fixed in 5.0.7 / 6.0.0-alpha-2 by GROOVY-11985)"
- assert r.overUnq == true : "row1 unqual: override must be visible to trait body, got ${r.overUnq}"
- }
-
- // ---- Row 1a — @Anchored static, trait body sees trait's own copy ----
- // The dispatch half of the @Anchored marker. With the annotation,
- // `this.m()`/`m()` in the trait body should dispatch to the trait's own
- // copy regardless of any implementer override (the JVM/interface-static
- // model — Eric's "static implies final" use case). Unmet on every
- // shipping Groovy (annotation does not exist there); met on the spike.
- @Test
- void row01a_anchored_dispatchIsTraitAnchored() {
- def r = ev '''
- import groovy.transform.Anchored
- trait V {
- @Anchored
- static boolean defaultNullable() { false }
- static boolean seen() { this.defaultNullable() }
- }
- class Over implements V { static boolean defaultNullable() { true } }
- Over.seen()
- '''
- assert r == false : "row1a @Anchored: trait body should always see the trait's own copy (got ${r})"
+ assert r.overThis == true : "row1 this. : @Virtual override must be visible to trait body, got ${r.overThis}"
+ assert r.overUnq == true : "row1 unqual: @Virtual override must be visible to trait body, got ${r.overUnq}"
}
// ---- Row 2 — same as #1 but the call is inside a closure ----
@@ -259,75 +233,31 @@ class TraitStaticDispatchMatrix {
assert r.c == 'class' && r.d == 'trait'
}
- // ---- Row 7 — T.m() trait-qualified inside trait body ----
- // Throws MissingMethodException on every version (the trait interface
- // carries no statics — GEP-22 § Static members, item 9). The desired
- // "force trait default" escape this could represent is provided by
- // @Anchored via interface promotion — see row 7a.
+ // ---- Row 7 — T.m() trait-qualified from inside trait body ----
+ // Resolves at the JVM level via the trait interface's static method
+ // (public trait statics are promoted onto the trait interface). The
+ // trait's own copy wins; any same-named static on the implementer is
+ // an independent method, not an override (declarer-bound dispatch).
@Test
- void row07_traitQualified_observed() {
- def outcome
- try {
- outcome = ev '''
- trait T {
- static String who() { 'T' }
- static String viaTrait() { T.who() }
- }
- class C implements T { static String who() { 'C' } }
- C.viaTrait()
- '''
- } catch (Throwable t) {
- outcome = "THREW:${t.class.simpleName}"
- }
- println "row07 observed on Groovy ${MAJOR}: ${outcome} (T.m() from trait body throws on every version; @Anchored fixes it — see row 7a)"
- assert outcome == 'THREW:MissingMethodException' : "row7 expected MissingMethodException: ${outcome}"
- }
-
- // ---- Row 7a — @Anchored: T.m() from trait body resolves via interface static ----
- // Interface promotion side-effect. With @Anchored, the trait static is
- // also added to the trait interface, so the "force trait default" escape
- // T.m() actually resolves (row 7 above throws on every shipping version).
- @Test
- void row07a_anchored_traitQualified_resolvesViaInterfaceStatic() {
+ void row07_traitQualified_resolvesViaInterfaceStatic() {
def r = ev '''
- import groovy.transform.Anchored
trait T {
- @Anchored
static String who() { 'T' }
static String viaTrait() { T.who() }
}
class C implements T { static String who() { 'C' } }
C.viaTrait()
'''
- assert r == 'T' : "row7a @Anchored: T.m() from trait body should resolve via interface static (got ${r})"
+ assert r == 'T' : "row7: T.m() from trait body must resolve to trait's own copy via interface static (got ${r})"
}
- // ---- Row 8 — external Impl.m() works; external T.m() unsupported ----
+ // ---- Row 8 — external Impl.m() works AND external Trait.m() works ----
+ // Public trait statics are JVM-native interface statics, so both
+ // forms of external access resolve cleanly.
@Test
void row08_externalAccess() {
assert ev('trait T { static String foo() { "ok" } }\nclass C implements T {}\nC.foo()') == 'ok'
- GroovyAssert.shouldFail { // no statics on the trait interface
- ev 'trait T { static String foo() { "x" } }\nT.foo()'
- }
- }
-
- // ---- Row 8a — @Anchored: external Trait.m() resolves via interface static ----
- // Interface promotion main effect. Jochen's "the static method is not added
- // to the interface. That should be a bug." With @Anchored, external
- // T.m() resolves to the JVM-native interface static — the row 8 "external
- // T.m() unsupported" limitation no longer applies for marker-bearing methods.
- @Test
- void row08a_anchored_externalTraitDotM_works() {
- def r = ev '''
- import groovy.transform.Anchored
- trait T {
- @Anchored
- static String foo() { 'ok' }
- }
- class C implements T {}
- T.foo()
- '''
- assert r == 'ok' : "row8a @Anchored: external T.m() should resolve via interface static (got ${r})"
+ assert ev('trait T { static String foo() { "ok" } }\nT.foo()') == 'ok'
}
// ---- Row 9 — static FIELD override NOT seen by trait (documented non-goal) ----
@@ -384,14 +314,20 @@ class TraitStaticDispatchMatrix {
// as row 1, same fix in 5.0.7 / 6.0.0-alpha-2 via GROOVY-11985.
@Test
void row10_subclassAnchorsOnDirectImplementer() {
+ // m2 needs @Virtual for A's override to be visible to T's m1
+ // (per-implementer override visibility is opt-in).
def r = ev '''
- trait T { static String m2() { 'T.m2' }; static String m1() { m2() } }
+ import groovy.transform.Virtual
+ trait T {
+ @Virtual static String m2() { 'T.m2' }
+ static String m1() { m2() }
+ }
class A implements T { static String m2() { 'A.m2' } }
class B extends A { static String m2() { 'B.m2' } }
[ a: A.m1(), b: B.m1(), bDirect: B.m2() ]
'''
assert r.bDirect == 'B.m2'
- assert r.a == 'A.m2' : "row10 a: A's override must be visible to A.m1(), got ${r.a} — see row 1 note on the GROOVY-8854 regression"
+ assert r.a == 'A.m2' : "row10 a: A's override must be visible to A.m1() (m2 is @Virtual), got ${r.a}"
assert r.b == 'A.m2' : "row10 b: B.m1() must anchor on A (the direct implementer), got ${r.b}"
assert r.b != 'B.m2' : "row10 load-bearing: B's own m2 must never win — dispatch anchors on direct implementer A, not the receiver class"
}
@@ -463,9 +399,15 @@ class TraitStaticDispatchMatrix {
// post-fix outcome so any further change is surfaced for discussion.
@Test
void row12_overloadUnderCompileStatic_observed() {
+ // Overload dispatch through the implementer requires @Virtual;
+ // without it the trait's m(X1) is declarer-bound and always wins.
def out = ev '''
+ import groovy.transform.Virtual
@groovy.transform.CompileStatic
- trait T { static int m(X1 x){ 2 }; static int call2(X1 x){ m(x) } }
+ trait T {
+ @Virtual static int m(X1 x){ 2 }
+ static int call2(X1 x){ m(x) }
+ }
@groovy.transform.CompileStatic
class A implements T { static int m(X2 x){ 1 } }
class X1 {}
@@ -476,54 +418,13 @@ class TraitStaticDispatchMatrix {
assert out == '21' : "row12: expected '21' (post-GROOVY-11985), got '${out}' — on 5.0.0–5.0.6 / 6.0.0-alpha-1 this returns '22' (the regression boundary)"
}
- // ---- Row 13 — @Anchored(inInterface=false) opt-out ----
- // The narrow escape: dispatch stays trait-anchored (like row 1a) but the
- // interface static is suppressed, so external T.m() continues to fail
- // (the row 8 behaviour persists for this method). Useful for soft-
- // deprecated trait-internal helpers that need to stay publicly callable
- // via the existing Impl.m() forwarder but should not gain a fresh
- // Java-visible interface API surface.
- @Test
- void row13_anchored_inInterfaceFalse_optsOutOfInterfacePromotion() {
- // Part 1: trait-body dispatch is still trait-anchored
- def r = ev '''
- import groovy.transform.Anchored
- trait V {
- @Anchored(inInterface=false)
- static String name() { 'trait' }
- static String seen() { this.name() }
- }
- class Impl implements V { static String name() { 'impl' } }
- Impl.seen()
- '''
- assert r == 'trait' : "row13 inInterface=false: dispatch should still be trait-anchored (got ${r})"
-
- // Part 2: external V.name() must still throw — no interface static generated
- boolean externalThrows = false
- try {
- ev '''
- import groovy.transform.Anchored
- trait V {
- @Anchored(inInterface=false)
- static String name() { 'trait' }
- }
- class Impl implements V {}
- V.name()
- '''
- } catch (Throwable ignored) {
- externalThrows = true
- }
- assert externalThrows : "row13 inInterface=false: external V.name() should still fail (no interface static)"
- }
-
- // ---- Row 14 — T.this.* qualifier in trait code (GROOVY-12104) ----
- // `T.this.m()` inside trait code is rejected at compile time. Without
- // the reject, the call previously generated invalid bytecode
- // (VerifyError on 4.x: "Class not assignable to Closure";
- // ClassCastException at runtime on 5.x/6.x). See Groovy12104.groovy
- // for focused coverage (instance variants + field-access form +
- // regression-guards for explicit other-class qualifiers). This row is
- // the matrix-level anchor.
+ // ---- Row 14 — T.this.* qualifier in trait code (proposed compile error) ----
+ // Latent bug surfaced during the GROOVY-12093 alternatives discussion.
+ // `T.this.m()` inside trait code currently produces invalid bytecode
+ // (VerifyError on 4.x: "Class not assignable to Closure"; ClassCastException
+ // at runtime on 5.x/6.x). GEP-22 § "this, super, and stackable traits"
+ // item 4 proposes rejecting the syntax at compile time. NYI until the
+ // fix lands; flips red on a build that implements the rejection.
@Test
void row14_T_this_qualifier_inTrait_isCompileError() {
GroovyAssert.shouldFail(MultipleCompilationErrorsException) {
diff --git a/src/test/groovy/org/codehaus/groovy/transform/traitx/VirtualAnnotationTest.groovy b/src/test/groovy/org/codehaus/groovy/transform/traitx/VirtualAnnotationTest.groovy
new file mode 100644
index 00000000000..97612e9f21d
--- /dev/null
+++ b/src/test/groovy/org/codehaus/groovy/transform/traitx/VirtualAnnotationTest.groovy
@@ -0,0 +1,243 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.codehaus.groovy.transform.traitx
+
+import org.codehaus.groovy.control.MultipleCompilationErrorsException
+import org.junit.Test
+
+import static groovy.test.GroovyAssert.assertScript
+import static groovy.test.GroovyAssert.shouldFail
+
+/**
+ * Exercises the {@code @groovy.transform.Virtual} marker on trait static
+ * methods.
+ *
+ * Public trait static methods are promoted onto the generated trait
+ * interface as JVM-native interface statics and dispatched declarer-bound
+ * by default. Per-implementer override visibility is opt-in via
+ * {@code @Virtual}:
+ *
+ *
+ * plain static m() -> declarer-bound + on the interface (default)
+ * {@literal @}Virtual static m() -> virtual via implementer
+ *
+ */
+final class VirtualAnnotationTest {
+
+ // ============ Default (no annotations) — declarer-bound ============
+
+ /** Plain {@code static}: trait body sees the trait's own copy, not the impl override. */
+ @Test
+ void plainStatic_isDeclarerBound_byDefault() {
+ assertScript '''
+ trait V {
+ static boolean defaultNullable() { false }
+ static boolean seen() { this.defaultNullable() }
+ }
+ class Over implements V { static boolean defaultNullable() { true } }
+
+ // Direct call: implementer's same-named static is independent.
+ assert Over.defaultNullable() == true
+
+ // Trait body: now sees only the trait's own copy (declarer-bound).
+ assert Over.seen() == false
+ '''
+ }
+
+ // ============ @Virtual — virtual dispatch via implementer ============
+
+ /** {@code @Virtual}: trait body dispatches through the implementer. */
+ @Test
+ void virtualStatic_overrideVisibleToTraitBody() {
+ assertScript '''
+ import groovy.transform.Virtual
+
+ trait V {
+ @Virtual
+ static boolean defaultNullable() { false }
+ static boolean seen() { this.defaultNullable() } // trait-body call
+ }
+ class Over implements V { static boolean defaultNullable() { true } }
+ class Def implements V { }
+
+ // Direct call: override always wins
+ assert Over.defaultNullable() == true
+
+ // Trait body sees the implementer's override
+ assert Over.seen() == true
+ assert Def.seen() == false // no override → trait default
+ '''
+ }
+
+ /**
+ * Worked example: a framework trait declares a {@code @Virtual} static
+ * default that user classes override by declaring a same-signature
+ * static. Trait-internal logic that consumes the value sees the
+ * implementer's override.
+ */
+ @Test
+ void virtual_overridableStaticDefault_workedExample() {
+ assertScript '''
+ import groovy.transform.Virtual
+
+ trait Validateable {
+ @Virtual
+ static boolean defaultNullable() { false }
+
+ // Trait-internal method that consumes the (potentially overridden) value
+ static boolean defaultNullableSeenByTrait() {
+ this.defaultNullable()
+ }
+ }
+
+ class MyNullableCommand implements Validateable {
+ static boolean defaultNullable() { true }
+ }
+ class MyOtherCommand implements Validateable { /* uses default */ }
+
+ assert MyNullableCommand.defaultNullable() == true
+ assert MyNullableCommand.defaultNullableSeenByTrait() == true // override visible
+ assert MyOtherCommand.defaultNullableSeenByTrait() == false // trait default
+ '''
+ }
+
+ /** {@code @Virtual} is keyed on the callee, not the caller — same discipline as private static. */
+ @Test
+ void virtual_isKeyedOnCallee() {
+ assertScript '''
+ import groovy.transform.Virtual
+
+ trait V {
+ static boolean fixed() { false } // plain → declarer-bound
+ @Virtual
+ static boolean overridable() { false } // virtual
+ static List bothSeen() {
+ [this.fixed(), this.overridable()]
+ }
+ }
+ class Over implements V {
+ static boolean fixed() { true }
+ static boolean overridable() { true }
+ }
+ assert Over.bothSeen() == [false, true] // fixed stays trait's, virtual sees override
+ '''
+ }
+
+ /** {@code @Virtual} under @CompileStatic also dispatches virtually. */
+ @Test
+ void virtualStatic_under_CompileStatic() {
+ assertScript '''
+ import groovy.transform.Virtual
+
+ @groovy.transform.CompileStatic
+ trait V {
+ @Virtual
+ static String name() { 'trait' }
+ static String seen() { this.name() }
+ }
+ @groovy.transform.CompileStatic
+ class Impl implements V { static String name() { 'impl' } }
+
+ assert Impl.seen() == 'impl'
+ '''
+ }
+
+ // ============ Validation ============
+
+ /** {@code @Virtual} on an instance method → compile error. */
+ @Test
+ void virtual_onInstanceMethod_isCompileError() {
+ def err = shouldFail(MultipleCompilationErrorsException) {
+ assertScript '''
+ import groovy.transform.Virtual
+ trait V {
+ @Virtual
+ def m() { 'V' } // instance, not static
+ }
+ new Object()
+ '''
+ }
+ assert err.message.contains('@Virtual can only be applied to public static trait methods')
+ assert err.message.contains('is not static')
+ }
+
+ /** {@code @Virtual} on a private static → compile error. */
+ @Test
+ void virtual_onPrivateStatic_isCompileError() {
+ def err = shouldFail(MultipleCompilationErrorsException) {
+ assertScript '''
+ import groovy.transform.Virtual
+ trait V {
+ @Virtual
+ private static m() { 'V' }
+ }
+ new Object()
+ '''
+ }
+ assert err.message.contains('@Virtual can only be applied to public static trait methods')
+ assert err.message.contains('is private')
+ }
+
+
+ // ============ Closure asymmetry (unchanged from current spec) ============
+
+ /**
+ * Closure carve-out: a static call inside a closure body inside a trait method
+ * is helper-bound, even when the callee is {@code @Virtual}. This is the
+ * long-standing closure-context asymmetry; the {@code @Virtual} marker
+ * does not extend into closure scope.
+ */
+ @Test
+ void virtual_insideClosure_stillHelperBound() {
+ assertScript '''
+ import groovy.transform.Virtual
+
+ trait V {
+ @Virtual
+ static boolean defaultNullable() { false }
+ static boolean seenInClosure() {
+ [1].collect { this.defaultNullable() }[0] // closure body
+ }
+ }
+ class Over implements V { static boolean defaultNullable() { true } }
+
+ // The closure carve-out keeps the call helper-bound even with @Virtual.
+ // Same asymmetry as the current spec's row 2; documented as deliberate.
+ assert Over.seenInClosure() == false
+ '''
+ }
+
+ // ============ Instance method dispatch is unaffected (regression guard) ============
+
+ /** Instance methods dispatch polymorphically — the {@code @Virtual} marker is for trait statics only. */
+ @Test
+ void instanceMethods_dispatchUnchanged() {
+ assertScript '''
+ trait V {
+ def which() { 'trait' }
+ def greet() { this.which() }
+ }
+ class C implements V { def which() { 'class' } }
+ class D implements V { }
+
+ assert new C().greet() == 'class' // override wins (standard polymorphism)
+ assert new D().greet() == 'trait' // trait default
+ '''
+ }
+}