Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
70 changes: 0 additions & 70 deletions src/main/java/groovy/transform/Anchored.java

This file was deleted.

59 changes: 59 additions & 0 deletions src/main/java/groovy/transform/Virtual.java
Original file line number Diff line number Diff line change
@@ -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 <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 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.
*
* <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. 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 {
}
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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<MethodNode> 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<MethodNode> interfaceStatics = collectInterfaceStatics(cNode);

// add methods
List<MethodNode> nonPublicAPIMethods = new ArrayList<>();
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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<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";
Expand All @@ -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.
*
* <p>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<MethodNode> collectAnchoredOnInterface(final ClassNode traitClass) {
private static List<MethodNode> collectInterfaceStatics(final ClassNode traitClass) {
List<MethodNode> 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<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.
Expand All @@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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));
}
Expand Down
Loading
Loading