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
2 changes: 1 addition & 1 deletion src/main/java/groovy/lang/Delegate.java
Original file line number Diff line number Diff line change
Expand Up @@ -160,7 +160,7 @@

/**
* Whether to carry over annotations from the methods of the delegate
* to your delegating method. Currently Closure annotation members are
* to your delegating method. Currently, Closure annotation members are
* not supported.
*
* @return true if generated delegate methods should keep method annotations
Expand Down
2 changes: 0 additions & 2 deletions src/main/java/groovy/transform/NonSealed.java
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@
*/
package groovy.transform;

import org.apache.groovy.lang.annotation.Incubating;
import org.codehaus.groovy.transform.GroovyASTTransformationClass;

import java.lang.annotation.Documented;
Expand All @@ -33,7 +32,6 @@
* @since 4.0.0
*/
@Documented
@Incubating
@Retention(RetentionPolicy.SOURCE)
@Target(ElementType.TYPE)
@GroovyASTTransformationClass("org.codehaus.groovy.transform.NonSealedASTTransformation")
Expand Down
2 changes: 0 additions & 2 deletions src/main/java/groovy/transform/Sealed.java
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@
*/
package groovy.transform;

import org.apache.groovy.lang.annotation.Incubating;
import org.codehaus.groovy.transform.GroovyASTTransformationClass;

import java.lang.annotation.Documented;
Expand All @@ -33,7 +32,6 @@
* @since 4.0.0
*/
@Documented
@Incubating
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@GroovyASTTransformationClass({
Expand Down
3 changes: 0 additions & 3 deletions src/main/java/groovy/transform/SealedOptions.java
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,6 @@
*/
package groovy.transform;

import org.apache.groovy.lang.annotation.Incubating;

import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
Expand All @@ -32,7 +30,6 @@
* @since 4.0.0
*/
@Documented
@Incubating
@Retention(RetentionPolicy.SOURCE)
@Target(ElementType.TYPE)
public @interface SealedOptions {
Expand Down
3 changes: 0 additions & 3 deletions src/main/java/org/codehaus/groovy/ast/ClassNode.java
Original file line number Diff line number Diff line change
Expand Up @@ -629,15 +629,13 @@ public void setMixins(final MixinNode[] mixins) {
/**
* @return permitted subclasses of sealed type, may initially be empty in early compiler phases
*/
@Incubating
public List<ClassNode> getPermittedSubclasses() {
if (redirect != null)
return redirect.getPermittedSubclasses();
lazyClassInit();
return permittedSubclasses;
}

@Incubating
public void setPermittedSubclasses(final List<ClassNode> permittedSubclasses) {
if (redirect != null) {
redirect.setPermittedSubclasses(permittedSubclasses);
Expand Down Expand Up @@ -1916,7 +1914,6 @@ public boolean isRecord() {
* @return {@code true} for native and emulated (annotation based) sealed classes
* @since 4.0.0
*/
@Incubating
public boolean isSealed() {
if (redirect != null) return redirect.isSealed();
return !getAnnotations(ClassHelper.SEALED_TYPE).isEmpty() || !getPermittedSubclasses().isEmpty();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,11 @@
*/
package org.codehaus.groovy.tools.javac;

import groovy.transform.NonSealed;
import groovy.transform.PackageScope;
import groovy.transform.PackageScopeTarget;
import groovy.transform.Sealed;
import groovy.transform.SealedOptions;
import org.apache.groovy.ast.tools.ExpressionUtils;
import org.apache.groovy.io.StringBuilderWriter;
import org.codehaus.groovy.ast.AnnotatedNode;
Expand Down Expand Up @@ -113,6 +116,9 @@
import static org.codehaus.groovy.ast.tools.GenericsUtils.createGenericsSpec;
import static org.codehaus.groovy.ast.tools.WideningCategories.isFloatingCategory;
import static org.codehaus.groovy.ast.tools.WideningCategories.isLongCategory;
import static org.codehaus.groovy.transform.SealedASTTransformation.getEffectivePermittedSubclasses;
import static org.codehaus.groovy.transform.SealedASTTransformation.sealedSkipAnnotationFromSource;
import static org.codehaus.groovy.transform.SealedASTTransformation.wouldBeNativeSealed;

/**
* Generates Java stub source for Groovy classes during joint compilation.
Expand All @@ -125,6 +131,9 @@ public class JavaStubGenerator {
private final List<ConstructorNode> constructors = new ArrayList<>();
private final Map<String, MethodNode> propertyMethods = new LinkedHashMap<>();
private final static ClassNode PACKAGE_SCOPE_TYPE = makeCached(PackageScope.class);
private final static ClassNode SEALED_TYPE = makeCached(Sealed.class);
private final static ClassNode NON_SEALED_TYPE = makeCached(NonSealed.class);
private final static ClassNode SEALED_OPTIONS_TYPE = makeCached(SealedOptions.class);

private ModuleNode currentModule;

Expand Down Expand Up @@ -371,6 +380,21 @@ protected FinalVariableAnalyzer.VariableNotFinalCallback getFinalVariablesCallba
// as plain classes (the existing behaviour).
boolean isRecordStub = !isEnum && !isInterface && !isAnnotationDefinition
&& isNativeRecordStub(classNode);
// Emit native sealed declarations (sealed keyword + permits clause)
// when the class will receive native sealed bytecode. EMULATE mode
// is intentionally skipped: GEP-13 specifies emulated sealed types
// are invisible to the Java compiler.
boolean isSealedStub = !isAnnotationDefinition && !isEnum
&& isNativeSealedStub(classNode);
// Java requires final / sealed / non-sealed on every permitted
// subtype of a sealed type. Groovy allows implicit non-sealed, so
// emit the non-sealed keyword in the stub when the parent is
// sealed (or a sealed interface is implemented) and this class
// is neither final nor sealed.
boolean isNonSealedStub = !isAnnotationDefinition && !isEnum && !isInterface && !isRecordStub
&& !isSealedStub
&& (classNode.getModifiers() & Opcodes.ACC_FINAL) == 0
&& hasSealedSupertype(classNode);
printAnnotations(out, classNode);

int flags = classNode.getModifiers();
Expand All @@ -382,6 +406,9 @@ protected FinalVariableAnalyzer.VariableNotFinalCallback getFinalVariablesCallba
if (isRecordStub) flags &= ~Opcodes.ACC_FINAL;
printModifiers(out, flags);

if (isSealedStub) out.print("sealed ");
else if (isNonSealedStub) out.print("non-sealed ");

if (isInterface) {
if (isAnnotationDefinition) {
out.print("@");
Expand Down Expand Up @@ -427,6 +454,17 @@ protected FinalVariableAnalyzer.VariableNotFinalCallback getFinalVariablesCallba
out.print(" ");
printType(out, interfaces[interfaces.length - 1]);
}
if (isSealedStub) {
List<ClassNode> permitted = getEffectivePermittedSubclasses(classNode);
if (!permitted.isEmpty()) {
out.println();
out.print(" permits ");
for (int i = 0, n = permitted.size(); i < n; i += 1) {
if (i > 0) out.print(", ");
printType(out, permitted.get(i));
}
}
}
out.println(" {");

printFields(out, classNode, isInterface, isRecordStub);
Expand All @@ -452,6 +490,31 @@ private boolean isNativeRecordStub(final ClassNode classNode) {
return RecordTypeASTTransformation.wouldBeNativeRecord(classNode, targetBytecode);
}

private boolean isNativeSealedStub(final ClassNode classNode) {
if (currentModule == null || currentModule.getContext() == null) return false;
String targetBytecode = currentModule.getContext().getConfiguration().getTargetBytecode();
return wouldBeNativeSealed(classNode, targetBytecode);
}

private boolean hasSealedSupertype(final ClassNode classNode) {
if (isNativeSupertypeSealed(classNode.getSuperClass())) return true;
ClassNode[] ifaces = classNode.getInterfaces();
if (ifaces != null) {
for (ClassNode iface : ifaces) {
if (isNativeSupertypeSealed(iface)) return true;
}
}
return false;
}

private boolean isNativeSupertypeSealed(final ClassNode supertype) {
if (supertype == null) return false;
// Already-compiled (e.g. JDK17+ sealed Java class loaded from bytecode).
if (supertype.isSealed() && !supertype.isPrimaryClassNode()) return true;
// Same compilation unit, will be emitted as native sealed.
return isNativeSealedStub(supertype);
}

private void printRecordHeader(final PrintWriter out, final ClassNode classNode) {
List<PropertyNode> components = getInstanceProperties(classNode);
out.print('(');
Expand Down Expand Up @@ -1037,9 +1100,19 @@ private void printParams(final PrintWriter out, final MethodNode methodNode) {
}

private void printAnnotations(final PrintWriter out, final AnnotatedNode annotated) {
// @NonSealed and @SealedOptions have SOURCE retention, so they never appear in
// the bytecode surface a Java consumer sees — suppress them from the stub.
// @Sealed has RUNTIME retention; suppress it only when the source emits the
// sealed keyword and @SealedOptions(alwaysAnnotate=false) (parallels the
// bytecode-side filter in AsmClassGenerator).
boolean skipSealedAnno = annotated instanceof ClassNode cn
&& isNativeSealedStub(cn) && sealedSkipAnnotationFromSource(cn);
for (AnnotationNode annotation : annotated.getAnnotations()) {
if (!annotation.getClassNode().equals(PACKAGE_SCOPE_TYPE))
printAnnotation(out, annotation);
ClassNode type = annotation.getClassNode();
if (type.equals(PACKAGE_SCOPE_TYPE)) continue;
if (type.equals(NON_SEALED_TYPE) || type.equals(SEALED_OPTIONS_TYPE)) continue;
if (skipSealedAnno && type.equals(SEALED_TYPE)) continue;
printAnnotation(out, annotation);
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,18 +21,21 @@
import groovy.transform.Sealed;
import groovy.transform.SealedMode;
import groovy.transform.SealedOptions;
import org.apache.groovy.lang.annotation.Incubating;
import org.codehaus.groovy.ast.ASTNode;
import org.codehaus.groovy.ast.AnnotatedNode;
import org.codehaus.groovy.ast.AnnotationNode;
import org.codehaus.groovy.ast.ClassNode;
import org.codehaus.groovy.ast.expr.ClassExpression;
import org.codehaus.groovy.ast.expr.ConstantExpression;
import org.codehaus.groovy.ast.expr.Expression;
import org.codehaus.groovy.ast.expr.ListExpression;
import org.codehaus.groovy.ast.expr.PropertyExpression;
import org.codehaus.groovy.control.CompilePhase;
import org.codehaus.groovy.control.CompilerConfiguration;
import org.codehaus.groovy.control.SourceUnit;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

import static org.codehaus.groovy.ast.ClassHelper.make;
Expand Down Expand Up @@ -109,7 +112,6 @@ public void visit(ASTNode[] nodes, SourceUnit source) {
*
* @return true for a native sealed class
*/
@Incubating
public static boolean sealedNative(AnnotatedNode node) {
return node.getNodeMetaData(SealedMode.class) == SealedMode.NATIVE;
}
Expand All @@ -121,11 +123,101 @@ public static boolean sealedNative(AnnotatedNode node) {
*
* @return true if a {@code Sealed} annotation is not required for this node
*/
@Incubating
public static boolean sealedSkipAnnotation(AnnotatedNode node) {
return Boolean.FALSE.equals(node.getNodeMetaData(SEALED_ALWAYS_ANNOTATE_KEY));
}

/**
* Reports whether {@code cNode} would receive native sealed bytecode for the
* given {@code targetBytecode} level. Independent of compile-phase ordering,
* so callers earlier than {@code SEMANTIC_ANALYSIS} (e.g. the joint-compile
* stub generator) can use it.
*
* @param cNode the class being checked
* @param targetBytecode the target bytecode level
* (see {@link CompilerConfiguration#getTargetBytecode()})
*/
public static boolean wouldBeNativeSealed(final ClassNode cNode, final String targetBytecode) {
if (cNode == null || !cNode.isSealed()) return false;
if (targetBytecode == null || targetBytecode.trim().isEmpty()) return false;
if (!isAtLeast(targetBytecode, CompilerConfiguration.JDK17)) return false;
List<AnnotationNode> opts = cNode.getAnnotations(SEALED_OPTIONS_TYPE);
AnnotationNode options = opts.isEmpty() ? null : opts.get(0);
return getMode(options, "mode") != SealedMode.EMULATE;
}

/**
* Returns the explicitly-declared permitted-subclass list for {@code cNode},
* either from the populated {@code permittedSubclasses} field (after the
* transform has run) or from the {@code @Sealed} annotation's
* {@code permittedSubclasses} member. An empty list means no explicit list
* is declared in source — inference happens later via
* {@code SealedCompletionASTTransformation}. Independent of compile-phase
* ordering, so callers earlier than {@code SEMANTIC_ANALYSIS} can use it.
*/
public static List<ClassNode> getDeclaredPermittedSubclasses(final ClassNode cNode) {
if (cNode == null) return Collections.emptyList();
List<ClassNode> populated = cNode.getPermittedSubclasses();
if (!populated.isEmpty()) return populated;
List<ClassNode> result = new ArrayList<>();
for (AnnotationNode anno : cNode.getAnnotations(SEALED_TYPE)) {
Expression m = anno.getMember("permittedSubclasses");
if (m instanceof ListExpression le) {
for (Expression e : le.getExpressions()) {
if (e instanceof ClassExpression ce) result.add(ce.getType());
}
} else if (m instanceof ClassExpression ce) {
result.add(ce.getType());
}
}
return result;
}

/**
* Returns the effective permitted-subclass list for {@code cNode}: the
* explicitly-declared list when present, otherwise the inferred list
* obtained by scanning the enclosing compilation unit for direct
* subtypes (mirroring {@code SealedCompletionASTTransformation}). Phase
* independent, so callers earlier than {@code CANONICALIZATION} can use it.
*/
public static List<ClassNode> getEffectivePermittedSubclasses(final ClassNode cNode) {
List<ClassNode> declared = getDeclaredPermittedSubclasses(cNode);
if (!declared.isEmpty()) return declared;
if (cNode == null || cNode.getModule() == null) return declared;
if (cNode.getAnnotations(SEALED_TYPE).isEmpty()) return declared;
List<ClassNode> inferred = new ArrayList<>();
for (ClassNode possibleSubclass : cNode.getModule().getClasses()) {
if (possibleSubclass.equals(cNode)) continue;
if (cNode.equals(possibleSubclass.getSuperClass())) {
inferred.add(possibleSubclass);
continue;
}
for (ClassNode iface : possibleSubclass.getInterfaces()) {
if (cNode.equals(iface)) {
inferred.add(possibleSubclass);
break;
}
}
}
return inferred;
}

/**
* Reports whether the source for {@code cNode} uses
* {@code @SealedOptions(alwaysAnnotate=false)} — independent of compile-phase
* ordering, so callers earlier than {@code SEMANTIC_ANALYSIS} can use it.
*/
public static boolean sealedSkipAnnotationFromSource(final ClassNode cNode) {
if (cNode == null) return false;
for (AnnotationNode opts : cNode.getAnnotations(SEALED_OPTIONS_TYPE)) {
Expression m = opts.getMember("alwaysAnnotate");
if (m instanceof ConstantExpression ce && Boolean.FALSE.equals(ce.getValue())) {
return true;
}
}
return false;
}

private static SealedMode getMode(AnnotationNode node, String name) {
if (node != null) {
final Expression member = node.getMember(name);
Expand Down
4 changes: 2 additions & 2 deletions src/spec/doc/_sealed.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@

//////////////////////////////////////////

= Sealed hierarchies (incubating)
= Sealed hierarchies

Sealed classes, interfaces and traits restrict which subclasses can extend/implement them.
Prior to sealed classes, class hierarchy designers had two main options:
Expand Down Expand Up @@ -143,7 +143,7 @@ EMULATE::
Indicates the class is sealed using the `@Sealed` annotation.
This mechanism works with the Groovy compiler for JDK8+ but is not recognised by the Java compiler.
AUTO::
Produces a native record for JDK17+ and emulates the record otherwise.
Produces a native sealed class for JDK17+ and emulates the sealed nature otherwise.

Whether you use the `sealed` keyword or the `@Sealed` annotation
is independent of the mode.
Loading
Loading