You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@groovy.apache.org by pa...@apache.org on 2026/06/27 04:43:22 UTC
(groovy) branch GROOVY_5_0_X updated: GROOVY-12093 adjusted: out with @Anchored in with @Virtual
This is an automated email from the ASF dual-hosted git repository.
paulk-asert pushed a commit to branch GROOVY_5_0_X
in repository https://gitbox.apache.org/repos/asf/groovy.git
The following commit(s) were added to refs/heads/GROOVY_5_0_X by this push:
new 1ecbd77bdc GROOVY-12093 adjusted: out with @Anchored in with @Virtual
1ecbd77bdc is described below
commit 1ecbd77bdc4451fed9a189e3c64c51a8fc6e5c9d
Author: Paul King <pa...@asert.com.au>
AuthorDate: Sat Jun 27 11:50:48 2026 +1000
GROOVY-12093 adjusted: out with @Anchored in with @Virtual
Backport of master's post-review adjustment to the 5.0.x line. Replaces
the unreleased groovy.transform.Anchored marker (added in this branch's
GROOVY-12093 backport, never shipped) with groovy.transform.Virtual,
AND flips the default dispatch shape for public trait statics so the
two release lines stay aligned at the source level:
default declarer-bound (trait body sees its own copy)
@Virtual per-implementer override visible to trait body
On 5.x the default returns to the pre-existing 4.x shape; @Virtual is
the opt-in that restores the per-implementer override path used by
Grails' Validateable.defaultNullable() and similar framework hooks.
The annotated tests (Groovy11985 and matrix rows 1/10/12) add the
marker so they exercise the per-implementer path under the new model.
Out of scope for this backport (master-only on the 6.x line):
GROOVY-12104 (reject T.this.* qualifier inside trait code),
GROOVY-12105 (reject unqualified super.m() from a static trait method),
and the Groovy-6 default of promoting public trait statics to
JVM-native interface statics.
The matrix test loses its four @Anchored-marker rows (1a/7a/8a/13)
and its header references; rows 1/10/12 gain @Virtual on the relevant
trait static so they continue to assert per-implementer dispatch. A
new VirtualAnnotationTest covers both @Virtual's dispatch contract
(with and without the marker, dynamic and @CompileStatic) and the
validation that rejects misapplied placements at compile time.
---
src/main/java/groovy/transform/Anchored.java | 70 ------
src/main/java/groovy/transform/Virtual.java | 54 +++++
.../transform/trait/TraitASTTransformation.java | 111 +--------
.../transform/trait/TraitReceiverTransformer.java | 20 +-
.../groovy/transform/traitx/Groovy11985.groovy | 21 +-
.../groovy/transform/traitx/Groovy12093.groovy | 266 ---------------------
.../traitx/TraitStaticDispatchMatrix.groovy | 224 +++++------------
.../transform/traitx/VirtualAnnotationTest.groovy | 164 +++++++++++++
8 files changed, 312 insertions(+), 618 deletions(-)
diff --git a/src/main/java/groovy/transform/Anchored.java b/src/main/java/groovy/transform/Anchored.java
deleted file mode 100644
index 2f4b369c68..0000000000
--- 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 <em>anchored to the trait</em> — dispatch is fixed
- * to the trait's own definition rather than routed through the implementing
- * class.
- *
- * <p>Applied to a trait <code>static</code> method, this opts out of the
- * default per-implementer override dispatch (the Groovy distinctive that the
- * Grails <code>Validateable.defaultNullable()</code> 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.
- *
- * <p>By default the marker also promotes the annotated method onto the
- * generated trait interface as a JVM-native public static. That makes
- * external <code>Trait.m()</code> calls and from-trait <code>T.m()</code>
- * calls both resolve at the JVM level rather than throwing
- * <code>MissingMethodException</code>. The {@link #inInterface()} attribute
- * is provided as an opt-out for the narrow case where dispatch should be
- * trait-anchored but the method should <em>not</em> 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).
- *
- * <p>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 <code>static</code> when
- * the trait offers a default that implementers may legitimately override.
- *
- * @since 5.0.7
- */
-@Documented
-@Retention(RetentionPolicy.RUNTIME)
-@Target(ElementType.METHOD)
-public @interface Anchored {
- /**
- * Whether the annotated method should also be promoted onto the generated
- * trait interface. Default <code>true</code>: the marker bundles
- * declarer-bound dispatch with external interface visibility (the
- * coherent JVM interface-static model). Set to <code>false</code> 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 0000000000..a37bd9011d
--- /dev/null
+++ b/src/main/java/groovy/transform/Virtual.java
@@ -0,0 +1,54 @@
+/*
+ * 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 <code>static</code> method as <em>virtual</em>:
+ * 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.
+ *
+ * <p>This is the opt-in counterpart to the default declarer-bound
+ * dispatch for trait static methods, where trait-body calls 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.
+ *
+ * <p>The marker is <em>per-callee</em>, not per-caller: it changes how
+ * trait code invokes the annotated method, regardless of which method
+ * inside the trait does the calling.
+ *
+ * <p>Valid only on public, non-abstract <code>static</code> trait
+ * methods. Applying it to an instance method, a private method, an
+ * abstract method, or anything outside a trait is a compile-time error.
+ *
+ * @since 5.0.7
+ */
+@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 3ce40b577f..ce6bb59b2f 100644
--- a/src/main/java/org/codehaus/groovy/transform/trait/TraitASTTransformation.java
+++ b/src/main/java/org/codehaus/groovy/transform/trait/TraitASTTransformation.java
@@ -37,7 +37,6 @@ import org.codehaus.groovy.ast.MethodNode;
import org.codehaus.groovy.ast.Parameter;
import org.codehaus.groovy.ast.PropertyNode;
import org.codehaus.groovy.ast.expr.BinaryExpression;
-import org.codehaus.groovy.ast.expr.ConstantExpression;
import org.codehaus.groovy.ast.expr.Expression;
import org.codehaus.groovy.ast.expr.MethodCallExpression;
import org.codehaus.groovy.ast.expr.SpreadExpression;
@@ -117,8 +116,8 @@ import static org.objectweb.asm.Opcodes.ACC_SYNTHETIC;
@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);
public static final String DO_DYNAMIC = TraitReceiverTransformer.class + ".doDynamic";
public static final String POST_TYPECHECKING_REPLACEMENT = TraitReceiverTransformer.class + ".replacement";
@@ -251,18 +250,11 @@ public class TraitASTTransformation extends AbstractASTTransformation implements
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);
-
- // 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<MethodNode> anchoredOnInterface = collectAnchoredOnInterface(cNode);
+ validateVirtualAnnotations(cNode);
// add methods
List<MethodNode> nonPublicAPIMethods = new ArrayList<>();
@@ -292,15 +284,6 @@ public class TraitASTTransformation extends AbstractASTTransformation implements
cNode.removeMethod(privateMethod);
}
- // 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));
- }
-
// copy statements from static and instance init blocks
if (staticInitStatements != null) {
BlockStatement toBlock = getBlockStatement(staticInitializer, staticInitializer.getCode());
@@ -645,15 +628,15 @@ public class TraitASTTransformation extends AbstractASTTransformation implements
}
/**
- * 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<AnnotationNode> annotations = methodNode.getAnnotations(ANCHORED_TYPE);
- if (annotations.isEmpty()) continue;
+ List<AnnotationNode> virtualAnns = methodNode.getAnnotations(VIRTUAL_TYPE);
+ if (virtualAnns.isEmpty()) continue;
String issue;
if (!methodNode.isStatic()) {
issue = "is not static";
@@ -664,84 +647,12 @@ public class TraitASTTransformation extends AbstractASTTransformation implements
} else {
continue; // valid
}
- AnnotationNode anchored = annotations.get(0);
+ AnnotationNode virtual = 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()));
- }
- }
-
- /**
- * 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()}.
- */
- private static List<MethodNode> collectAnchoredOnInterface(final ClassNode traitClass) {
- List<MethodNode> result = new ArrayList<>();
- for (MethodNode methodNode : traitClass.getMethods()) {
- if (methodNode.isStatic() && !methodNode.isPrivate() && !methodNode.isAbstract()
- && isAnchoredOnInterface(methodNode)) {
- result.add(methodNode);
- }
+ virtual.getLineNumber(), virtual.getColumnNumber()));
}
- 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<AnnotationNode> 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.
- *
- * <p>Emits {@code public static R m(args) { return T$Trait$Helper.m(T.class, args); }},
- * 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.
- */
- private static MethodNode createAnchoredInterfaceForwarder(final ClassNode traitClass, final ClassNode helper, final MethodNode original) {
- Parameter[] params = original.getParameters();
- Expression[] callArgs = new Expression[params.length + 1];
- callArgs[0] = classX(traitClass);
- for (int i = 0; i < params.length; i++) {
- callArgs[i + 1] = varX(params[i]);
- }
- MethodCallExpression call = callX(classX(helper), original.getName(), args(callArgs));
- Statement body = VOID_TYPE.equals(original.getReturnType()) ? stmt(call) : returnS(call);
- MethodNode forwarder = new MethodNode(
- original.getName(),
- ACC_PUBLIC | ACC_STATIC,
- original.getReturnType(),
- params,
- original.getExceptions(),
- body);
- forwarder.setGenericsTypes(original.getGenericsTypes());
- forwarder.setSynthetic(true);
- forwarder.setSourcePosition(original);
- // Carry over the trait method's RUNTIME/CLASS-retention annotations (e.g.
- // @Deprecated) so the promoted interface static behaves like the original,
- // consistent with the forwarders generated by TraitComposer. The helper
- // filters out SOURCE-retention/transform markers and closure-member ones.
- List<AnnotationNode> copied = new ArrayList<>(), notCopied = new ArrayList<>();
- GeneralUtils.copyAnnotatedNodeAnnotations(original, copied, notCopied);
- forwarder.addAnnotations(copied);
- return forwarder;
}
private MethodNode processMethod(final ClassNode traitClass, final ClassNode traitHelperClass, final MethodNode methodNode, final ClassNode fieldHelper, final Collection<String> knownFields) {
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 9db8cf8d2d..cba6074f9a 100644
--- a/src/main/java/org/codehaus/groovy/transform/trait/TraitReceiverTransformer.java
+++ b/src/main/java/org/codehaus/groovy/transform/trait/TraitReceiverTransformer.java
@@ -66,7 +66,7 @@ import static org.codehaus.groovy.ast.tools.GeneralUtils.varX;
*/
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;
@@ -280,23 +280,21 @@ class TraitReceiverTransformer extends ClassCodeExpressionTransformer {
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
+ boolean virtual = !methodNode.getAnnotations(VIRTUAL_TYPE).isEmpty();
+ if (methodNode.isStatic() && !methodNode.isPrivate() && virtual && !inClosure) {
+ // Opt-in via @Virtual: dispatch unqualified/this-
+ // qualified calls to the trait static 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.
+ // implementer is visible from trait code. Plain
+ // (un-annotated) public trait statics use the
+ // declarer-bound helper path below instead.
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 8f95ddcd69..5116cfde8a 100644
--- a/src/test/groovy/org/codehaus/groovy/transform/traitx/Groovy11985.groovy
+++ b/src/test/groovy/org/codehaus/groovy/transform/traitx/Groovy11985.groovy
@@ -24,15 +24,16 @@ import org.junit.jupiter.api.Test
final class Groovy11985 {
- // Unqualified and `this.`-qualified public static calls inside a trait
- // method must dispatch through the implementing class so an override on
- // the implementer is visible from trait code.
+ // When a public trait static is marked @Virtual, unqualified and
+ // `this.`-qualified calls to it inside the trait body dispatch through
+ // the implementing class so an override on the implementer is visible.
@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 c2074ab6f3..0000000000
--- a/src/test/groovy/org/codehaus/groovy/transform/traitx/Groovy12093.groovy
+++ /dev/null
@@ -1,266 +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).
- *
- * <p>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.
- *
- * <pre>
- * 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
- * </pre>
- *
- * Dispatch is implemented in {@code TraitReceiverTransformer}; interface
- * promotion is implemented in {@code TraitASTTransformation}.
- */
-final class Groovy12093 {
-
- // This class covers the @Anchored-specific behaviour: that the marker
- // makes dispatch trait-anchored, the from-trait T.m() and external
- // Trait.m() resolution, the inInterface=false opt-out, 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 <em>callee</em>, 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<Boolean> 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 c91285a1f8..a29f66bc5a 100644
--- a/src/test/groovy/org/codehaus/groovy/transform/traitx/TraitStaticDispatchMatrix.groovy
+++ b/src/test/groovy/org/codehaus/groovy/transform/traitx/TraitStaticDispatchMatrix.groovy
@@ -18,29 +18,19 @@
*/
// Trait-static dispatch behaviour matrix — the executable companion to
-// GEP-22 § Static members. Three plain-class baseline rows (A/B/C), the
-// 12 design rows from GEP-22-progression-analysis.html (with row 11
+// GEP-22 § Static members. Three plain-class baseline rows (A/B/C) and
+// the 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. Rows that depend on per-
+// implementer override visibility (rows 1, 10, 12) opt in via
+// `@groovy.transform.Virtual` on the relevant trait static — plain
+// `static` is declarer-bound, `@Virtual` restores the override-via-
+// implementer path (the dispatch shape in 4.x).
package org.codehaus.groovy.transform.traitx
@@ -62,18 +52,16 @@ class TraitStaticDispatchMatrix {
//
// VERIFIED on 4.0.32: plain Groovy static dispatch is declarer-bound in
// every context (method body, closure body, dynamic, @CompileStatic);
- // only INSTANCE methods are polymorphic. Trait row 1's "override visible
- // to trait method body" is therefore a deliberate departure from this
- // baseline (the per-implementer $static$self mechanism); the row 2
- // closure carve-out keeps trait closure-context dispatch consistent
- // with this baseline. The simplified-model RULE wording "dispatch like
- // a plain class member" was an aspirational fiction — the empirical
- // truth is more nuanced and the baseline below records it.
+ // only INSTANCE methods are polymorphic. Public trait statics match
+ // this baseline by default; the row 1 departure (override visible to
+ // trait body) is the OPT-IN behaviour of `@groovy.transform.Virtual`,
+ // which routes the call through the implementing class via the per-
+ // implementer $static$self mechanism.
// ---- Baseline A — plain class static, this.foo() in METHOD BODY ----
// The control for trait row 1. Plain Groovy: declarer-bound ('base').
- // Trait row 1 returns 'true' (override visible) — that's the trait
- // distinctive, NOT a plain-class semantic.
+ // Trait row 1 returns 'true' (override visible) — that requires
+ // @Virtual; without it the trait matches this baseline.
@Test
void baseline_A_plainStatic_methodBody_isDeclarerBound() {
def r = ev '''
@@ -111,8 +99,8 @@ class TraitStaticDispatchMatrix {
// Confirms ordinary instance dispatch IS polymorphic in closure context
// (row 6 in the trait matrix is the instance-method analogue). Only
// static dispatch falls back to declarer-bound. Together baselines A/B/C
- // show: trait row 1's override-via-implementer in method body is a
- // deliberate Groovy-trait distinctive — neither plain statics nor plain
+ // show: row 1's override-via-implementer (when opted into with @Virtual)
+ // is a Groovy-trait distinctive — neither plain statics nor plain
// instance methods give that shape in closure context unaided.
@Test
void baseline_C_plainInstance_closureBody_isPolymorphic() {
@@ -131,17 +119,22 @@ class TraitStaticDispatchMatrix {
// ---- Row 1 — public static, this./unqualified, impl overrides ----
// Scenario: overridable static defaults (Grails Validateable.defaultNullable).
- // SPEC-NORMATIVE end-state (GEP-22 § Static members, item 4): override
- // visible to trait code. Holds on 4.x (always did) and on 5.0.7+ /
- // 6.0.0-alpha-2+ (where GROOVY-11985 corrects the GROOVY-8854 regression).
- // Red on 5.0.0–5.0.6 and 6.0.0-alpha-1 = bug detector for those releases.
- // Contrast baseline A (plain class same shape — declarer-bound, 'base'):
- // this row's polymorphic answer is the trait-machinery distinctive.
+ // The trait static is marked @Virtual to opt into the per-implementer
+ // override path; plain trait statics (no marker) are declarer-bound
+ // and would return the trait's own copy regardless of any same-named
+ // static on the implementer — see VirtualAnnotationTest for the
+ // un-annotated control. Contrast baseline A (plain class same shape —
+ // declarer-bound, 'base'): the @Virtual marker makes the trait answer
+ // polymorphic at the spot a plain class is not.
@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() }
}
@@ -152,39 +145,15 @@ 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 ----
- // The closure carve-out. Helper-bound on every version (long-standing,
- // not a 5/6 regression). Matches baseline B (plain class same shape —
- // declarer-bound, 'base'): the carve-out aligns trait closure-context
- // dispatch with plain-Groovy semantics. Removing it would extend the
- // trait-machinery distinctive into closures, further from plain Groovy
- // not closer — the simplified-model "Move 3" proposal was retired
- // after the baseline analysis surfaced this.
+ // The closure carve-out: a call from inside a closure body is
+ // helper-bound regardless of @Virtual. Matches baseline B (plain
+ // class same shape — declarer-bound, 'base'): the carve-out aligns
+ // trait closure-context dispatch with plain-Groovy semantics.
@Test
void row02_publicStatic_insideClosure() {
def r = ev '''
@@ -195,7 +164,7 @@ class TraitStaticDispatchMatrix {
class Over implements V { static boolean defaultNullable() { true } }
Over.seenInClosure()
'''
- assert r == false : "row2 closure: expected false on every version (Groovy ${MAJOR}), got ${r} — carve-out aligns with baseline B"
+ assert r == false : "row2 closure: expected false (Groovy ${MAJOR}), got ${r} — carve-out aligns with baseline B"
}
// ---- Row 3 — private trait static is NOT overridable by the impl ----
@@ -258,10 +227,8 @@ class TraitStaticDispatchMatrix {
}
// ---- 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.
+ // Throws MissingMethodException on every shipping 5.x (the trait
+ // interface carries no statics — GEP-22 § Static members, item 9).
@Test
void row07_traitQualified_observed() {
def outcome
@@ -277,29 +244,10 @@ class TraitStaticDispatchMatrix {
} 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)"
+ println "row07 observed on Groovy ${MAJOR}: ${outcome}"
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() {
- 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})"
- }
-
// ---- Row 8 — external Impl.m() works; external T.m() unsupported ----
@Test
void row08_externalAccess() {
@@ -309,25 +257,6 @@ class TraitStaticDispatchMatrix {
}
}
- // ---- 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})"
- }
-
// ---- Row 9 — static FIELD override NOT seen by trait (documented non-goal) ----
// A bare static-field reference in a trait body reads the trait's
// template field, not any same-named static on the implementing class.
@@ -378,18 +307,20 @@ class TraitStaticDispatchMatrix {
// so B.m1() still dispatches m2 through A's forwarder. Row 1's
// override-visible dispatch flows through here: A's override of m2 IS
// visible to m1 (returns 'A.m2'); B's own m2 is NOT (it would require
- // re-composition). Red on 5.0.0–5.0.6 / 6.0.0-alpha-1 — same regression
- // as row 1, same fix in 5.0.7 / 6.0.0-alpha-2 via GROOVY-11985.
+ // re-composition).
@Test
void row10_subclassAnchorsOnDirectImplementer() {
+ // The override-visible part requires @Virtual on the callee (m2);
+ // without it m1's call to m2 is declarer-bound and returns 'T.m2'.
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(), 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"
}
@@ -452,65 +383,32 @@ class TraitStaticDispatchMatrix {
}
// ---- Row 12 — overload across trait/impl under @CompileStatic ----
- // A consequence of row 1's dispatch path: GROOVY-11985 also shifts this
- // overload case from '22' (pre-fix G5/G6 behaviour) back to '21' (G4's
- // long-standing answer, and the post-fix answer on 5.0.7 / 6.0.0-alpha-2).
- // Red on 5.0.0–5.0.6 / 6.0.0-alpha-1 — same regression boundary as
- // rows 1 and 10. Jochen flagged both G4 and G5 as "imho wrong" but the
- // team has not settled an alternative; this row locks the current
- // post-fix outcome so any further change is surfaced for discussion.
+ // A consequence of row 1's dispatch path: when the callee is @Virtual
+ // the trait-side overload set is widened by what the implementer
+ // declares, so A.call2(X2) resolves to A.m(X2) rather than T.m(X1).
+ // Without @Virtual the trait static is declarer-bound and both calls
+ // would return '2'. Jochen flagged the @Virtual outcome here as
+ // "imho wrong" but the team has not settled an alternative; this row
+ // locks the current 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 {}
class X2 extends X1 {}
"" + A.call2(new X1()) + A.call2(new X2())
'''
- println "row12 observed on Groovy ${MAJOR}: ${out} (expected '21' on 4.x and on 5.0.7+ / 6.0.0-alpha-2+)"
- 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)"
+ assert out == '21' : "row12 @Virtual: expected '21', got '${out}'"
}
- // ---- 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)"
- }
}
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 0000000000..38952c0dc4
--- /dev/null
+++ b/src/test/groovy/org/codehaus/groovy/transform/traitx/VirtualAnnotationTest.groovy
@@ -0,0 +1,164 @@
+/*
+ * 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 groovy.test.GroovyAssert
+import org.junit.Test
+
+/**
+ * Coverage for the {@code @Virtual} marker. Plain public trait statics
+ * are declarer-bound (trait-body calls always invoke the trait's own
+ * copy regardless of any same-named static on the implementer);
+ * annotating the trait static with {@code @Virtual} opts into the
+ * per-implementer override path used by Grails-style framework hooks.
+ *
+ * <p>Two dimensions are covered: validation (misapplied markers are
+ * rejected at compile time) and dispatch (with vs without the marker,
+ * across dynamic and {@code @CompileStatic} call sites).
+ */
+final class VirtualAnnotationTest {
+
+ // ============ Default (no annotation) — declarer-bound ============
+
+ @Test
+ void plainStatic_isDeclarerBound_dispatch() {
+ GroovyAssert.assertScript '''
+ trait V {
+ static boolean defaultNullable() { false }
+ static boolean seen() { this.defaultNullable() }
+ }
+ class Over implements V { static boolean defaultNullable() { true } }
+ assert Over.seen() == false // trait's own copy
+ assert Over.defaultNullable() == true // direct call: override wins
+ '''
+ }
+
+ @Test
+ void plainStatic_isDeclarerBound_underCompileStatic() {
+ GroovyAssert.assertScript '''
+ import groovy.transform.CompileStatic
+ @CompileStatic
+ trait V {
+ static boolean defaultNullable() { false }
+ static boolean seen() { this.defaultNullable() }
+ }
+ @CompileStatic class Over implements V { static boolean defaultNullable() { true } }
+ assert Over.seen() == false
+ '''
+ }
+
+ // ============ @Virtual — virtual dispatch via implementer ============
+
+ @Test
+ void virtual_dispatchesThroughImplementer() {
+ GroovyAssert.assertScript '''
+ import groovy.transform.Virtual
+ trait V {
+ @Virtual static boolean defaultNullable() { false }
+ static boolean seen() { this.defaultNullable() }
+ }
+ class Over implements V { static boolean defaultNullable() { true } }
+ class Def implements V { }
+ assert Over.seen() == true // override visible to trait body
+ assert Def.seen() == false // no override -> trait default
+ '''
+ }
+
+ @Test
+ void virtual_unqualifiedCall_dispatchesThroughImplementer() {
+ GroovyAssert.assertScript '''
+ import groovy.transform.Virtual
+ trait V {
+ @Virtual static boolean defaultNullable() { false }
+ static boolean seenUnqualified() { defaultNullable() }
+ }
+ class Over implements V { static boolean defaultNullable() { true } }
+ assert Over.seenUnqualified() == true
+ '''
+ }
+
+ @Test
+ void virtual_underCompileStatic() {
+ GroovyAssert.assertScript '''
+ import groovy.transform.CompileStatic
+ import groovy.transform.Virtual
+ @CompileStatic
+ trait V {
+ @Virtual static boolean defaultNullable() { false }
+ static boolean seen() { this.defaultNullable() }
+ }
+ @CompileStatic class Over implements V { static boolean defaultNullable() { true } }
+ @CompileStatic class Def implements V { }
+ assert Over.seen() == true
+ assert Def.seen() == false
+ '''
+ }
+
+ @Test
+ void virtual_fromInstanceMethod() {
+ GroovyAssert.assertScript '''
+ import groovy.transform.Virtual
+ trait T {
+ @Virtual static String which() { 'trait' }
+ String greet() { which() }
+ }
+ class C implements T { static String which() { 'class' } }
+ class D implements T {}
+ assert new C().greet() == 'class'
+ assert new D().greet() == 'trait'
+ '''
+ }
+
+ // ============ Validation ============
+
+ @Test
+ void virtual_onInstanceMethod_isCompileError() {
+ def err = GroovyAssert.shouldFail '''
+ trait T {
+ @groovy.transform.Virtual
+ String hello() { 'x' }
+ }
+ '''
+ assert err.message.contains('@Virtual can only be applied to public static trait methods')
+ assert err.message.contains('is not static')
+ }
+
+ @Test
+ void virtual_onPrivateStatic_isCompileError() {
+ def err = GroovyAssert.shouldFail '''
+ trait T {
+ @groovy.transform.Virtual
+ private static String hidden() { 'x' }
+ }
+ '''
+ assert err.message.contains('@Virtual can only be applied to public static trait methods')
+ assert err.message.contains('is private')
+ }
+
+ @Test
+ void virtual_onAbstractMethod_isCompileError() {
+ def err = GroovyAssert.shouldFail '''
+ abstract trait T {
+ @groovy.transform.Virtual
+ abstract String hello()
+ }
+ '''
+ assert err.message.contains('@Virtual can only be applied to public static trait methods')
+ }
+}