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 + ''' + } +}