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 2021/07/27 13:07:08 UTC
[groovy] 01/01: GROOVY-10148: Groovy should not allow classes to
extend sealed Java classes
This is an automated email from the ASF dual-hosted git repository.
paulk pushed a commit to branch groovy10148
in repository https://gitbox.apache.org/repos/asf/groovy.git
commit 3b4463074c41b37fabf26ac6ed4192f5ec7715b7
Author: Paul King <pa...@asert.com.au>
AuthorDate: Tue Jul 27 22:47:04 2021 +1000
GROOVY-10148: Groovy should not allow classes to extend sealed Java classes
---
src/main/java/groovy/transform/NonSealed.java | 38 +++++++
src/main/java/groovy/transform/Sealed.java | 42 +++++++
.../java/org/codehaus/groovy/ast/ClassNode.java | 12 ++
.../groovy/ast/decompiled/AsmDecompiler.java | 5 +
.../ast/decompiled/ClassSignatureParser.java | 6 +
.../codehaus/groovy/ast/decompiled/ClassStub.java | 1 +
.../groovy/ast/decompiled/DecompiledClassNode.java | 18 +++
.../groovy/classgen/ClassCompletionVerifier.java | 65 +++++++++--
.../org/codehaus/groovy/classgen/Verifier.java | 22 ++++
.../transform/NonSealedASTTransformation.java | 49 ++++++++
.../groovy/transform/SealedASTTransformation.java | 63 +++++++++++
.../groovy/transform/SealedTransformTest.groovy | 124 +++++++++++++++++++++
12 files changed, 435 insertions(+), 10 deletions(-)
diff --git a/src/main/java/groovy/transform/NonSealed.java b/src/main/java/groovy/transform/NonSealed.java
new file mode 100644
index 0000000..5550558
--- /dev/null
+++ b/src/main/java/groovy/transform/NonSealed.java
@@ -0,0 +1,38 @@
+/*
+ * 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 org.codehaus.groovy.transform.GroovyASTTransformationClass;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * Class annotation used to demarcate non-sealed classes.
+ *
+ * @since 4.0.0
+ */
+@java.lang.annotation.Documented
+@Retention(RetentionPolicy.SOURCE)
+@Target({ElementType.TYPE})
+@GroovyASTTransformationClass("org.codehaus.groovy.transform.NonSealedASTTransformation")
+public @interface NonSealed {
+}
diff --git a/src/main/java/groovy/transform/Sealed.java b/src/main/java/groovy/transform/Sealed.java
new file mode 100644
index 0000000..1881e2e
--- /dev/null
+++ b/src/main/java/groovy/transform/Sealed.java
@@ -0,0 +1,42 @@
+/*
+ * 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 org.codehaus.groovy.transform.GroovyASTTransformationClass;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * Class annotation used to assist in the creation of sealed classes.
+ *
+ * @since 4.0.0
+ */
+@java.lang.annotation.Documented
+@Retention(RetentionPolicy.RUNTIME)
+@Target({ElementType.TYPE})
+@GroovyASTTransformationClass("org.codehaus.groovy.transform.SealedASTTransformation")
+public @interface Sealed {
+ /**
+ * List of names of the permitted subclasses.
+ */
+ String[] permittedSubclasses() default {};
+}
diff --git a/src/main/java/org/codehaus/groovy/ast/ClassNode.java b/src/main/java/org/codehaus/groovy/ast/ClassNode.java
index 51fc266..2c9b75c 100644
--- a/src/main/java/org/codehaus/groovy/ast/ClassNode.java
+++ b/src/main/java/org/codehaus/groovy/ast/ClassNode.java
@@ -18,6 +18,7 @@
*/
package org.codehaus.groovy.ast;
+import groovy.transform.Sealed;
import org.apache.groovy.ast.tools.ClassNodeUtils;
import org.codehaus.groovy.GroovyBugError;
import org.codehaus.groovy.ast.expr.BinaryExpression;
@@ -53,6 +54,7 @@ import static org.apache.groovy.ast.tools.MethodNodeUtils.getCodeAsBlock;
import static org.codehaus.groovy.ast.ClassHelper.isObjectType;
import static org.codehaus.groovy.ast.ClassHelper.isPrimitiveBoolean;
import static org.codehaus.groovy.ast.ClassHelper.isPrimitiveVoid;
+import static org.codehaus.groovy.ast.ClassHelper.make;
import static org.objectweb.asm.Opcodes.ACC_ABSTRACT;
import static org.objectweb.asm.Opcodes.ACC_ANNOTATION;
import static org.objectweb.asm.Opcodes.ACC_ENUM;
@@ -140,6 +142,7 @@ public class ClassNode extends AnnotatedNode {
public static final ClassNode[] EMPTY_ARRAY = new ClassNode[0];
public static final ClassNode THIS = new ClassNode(Object.class);
public static final ClassNode SUPER = new ClassNode(Object.class);
+ private static final ClassNode SEALED_TYPE = make(Sealed.class);
private String name;
private int modifiers;
@@ -161,6 +164,7 @@ public class ClassNode extends AnnotatedNode {
private ClassNode superClass;
protected boolean isPrimaryNode;
protected List<InnerClassNode> innerClasses;
+ final private List<ClassNode> permittedSubclasses = new ArrayList<>();
private List<AnnotationNode> typeAnnotations = Collections.emptyList();
/**
@@ -240,6 +244,10 @@ public class ClassNode extends AnnotatedNode {
return redirect().isPrimaryNode || (componentType != null && componentType.isPrimaryClassNode());
}
+ public List<ClassNode> getPermittedSubclasses() {
+ return permittedSubclasses;
+ }
+
/**
* Constructor used by {@code makeArray()} if no real class is available.
*/
@@ -1340,6 +1348,10 @@ public class ClassNode extends AnnotatedNode {
return (getModifiers() & ACC_ABSTRACT) != 0;
}
+ public boolean isSealed() {
+ return !getAnnotations(SEALED_TYPE).isEmpty() || !permittedSubclasses.isEmpty();
+ }
+
public boolean isResolved() {
if (clazz != null) return true;
if (redirect != null) return redirect.isResolved();
diff --git a/src/main/java/org/codehaus/groovy/ast/decompiled/AsmDecompiler.java b/src/main/java/org/codehaus/groovy/ast/decompiled/AsmDecompiler.java
index 00efcf5..0c0cff5 100644
--- a/src/main/java/org/codehaus/groovy/ast/decompiled/AsmDecompiler.java
+++ b/src/main/java/org/codehaus/groovy/ast/decompiled/AsmDecompiler.java
@@ -187,6 +187,11 @@ public abstract class AsmDecompiler {
}
@Override
+ public void visitPermittedSubclass(final String permittedSubclass) {
+ result.permittedSubclasses.add(permittedSubclass);
+ }
+
+ @Override
public FieldVisitor visitField(final int access, final String name, final String desc, final String signature, final Object value) {
FieldStub stub = new FieldStub(name, access, desc, signature, value);
if (result.fields == null) result.fields = new ArrayList<>(1);
diff --git a/src/main/java/org/codehaus/groovy/ast/decompiled/ClassSignatureParser.java b/src/main/java/org/codehaus/groovy/ast/decompiled/ClassSignatureParser.java
index 5d225f0..a77f604 100644
--- a/src/main/java/org/codehaus/groovy/ast/decompiled/ClassSignatureParser.java
+++ b/src/main/java/org/codehaus/groovy/ast/decompiled/ClassSignatureParser.java
@@ -42,6 +42,12 @@ class ClassSignatureParser {
interfaces[i] = resolver.resolveClass(AsmDecompiler.fromInternalName(stub.interfaceNames[i]));
}
classNode.setInterfaces(interfaces);
+ if (!stub.permittedSubclasses.isEmpty()) {
+ List<ClassNode> permittedSubclasses = classNode.getPermittedSubclasses();
+ for (String name : stub.permittedSubclasses) {
+ permittedSubclasses.add(resolver.resolveClass(AsmDecompiler.fromInternalName(name)));
+ }
+ }
}
private static void parseClassSignature(final ClassNode classNode, String signature, final AsmReferenceResolver resolver) {
diff --git a/src/main/java/org/codehaus/groovy/ast/decompiled/ClassStub.java b/src/main/java/org/codehaus/groovy/ast/decompiled/ClassStub.java
index 2c99fe1..e555e7b 100644
--- a/src/main/java/org/codehaus/groovy/ast/decompiled/ClassStub.java
+++ b/src/main/java/org/codehaus/groovy/ast/decompiled/ClassStub.java
@@ -34,6 +34,7 @@ public class ClassStub extends MemberStub {
final String[] interfaceNames;
List<MethodStub> methods;
List<FieldStub> fields;
+ final List<String> permittedSubclasses = new ArrayList<>(1);
// Used to store the real access modifiers for inner classes
int innerClassModifiers = -1;
diff --git a/src/main/java/org/codehaus/groovy/ast/decompiled/DecompiledClassNode.java b/src/main/java/org/codehaus/groovy/ast/decompiled/DecompiledClassNode.java
index c53029f..046dada 100644
--- a/src/main/java/org/codehaus/groovy/ast/decompiled/DecompiledClassNode.java
+++ b/src/main/java/org/codehaus/groovy/ast/decompiled/DecompiledClassNode.java
@@ -18,6 +18,7 @@
*/
package org.codehaus.groovy.ast.decompiled;
+import groovy.transform.Sealed;
import org.codehaus.groovy.ast.AnnotatedNode;
import org.codehaus.groovy.ast.AnnotationNode;
import org.codehaus.groovy.ast.ClassNode;
@@ -27,12 +28,15 @@ import org.codehaus.groovy.ast.GenericsType;
import org.codehaus.groovy.ast.MethodNode;
import org.codehaus.groovy.ast.MixinNode;
import org.codehaus.groovy.classgen.Verifier;
+import org.codehaus.groovy.reflection.ReflectionUtils;
import org.objectweb.asm.Opcodes;
import java.lang.reflect.Modifier;
import java.util.List;
import java.util.function.Supplier;
+import static org.codehaus.groovy.ast.ClassHelper.make;
+
/**
* A {@link ClassNode} kind representing the classes coming from *.class files decompiled using ASM.
*
@@ -43,6 +47,7 @@ public class DecompiledClassNode extends ClassNode {
private final AsmReferenceResolver resolver;
private volatile boolean supersInitialized;
private volatile boolean membersInitialized;
+ private static final ClassNode SEALED_TYPE = make(Sealed.class);
public DecompiledClassNode(ClassStub data, AsmReferenceResolver resolver) {
super(data.className, getFullModifiers(data), null, null, MixinNode.EMPTY_ARRAY);
@@ -173,6 +178,19 @@ public class DecompiledClassNode extends ClassNode {
}
@Override
+ public boolean isSealed() {
+ List<AnnotationStub> annotations = classData.annotations;
+ if (annotations != null) {
+ for (AnnotationStub stub : annotations) {
+ if (stub.className.equals("groovy.transform.Sealed")) {
+ return true;
+ }
+ }
+ }
+ return ReflectionUtils.isSealed(getTypeClass());
+ }
+
+ @Override
public Class getTypeClass() {
return resolver.resolveJvmClass(getName());
}
diff --git a/src/main/java/org/codehaus/groovy/classgen/ClassCompletionVerifier.java b/src/main/java/org/codehaus/groovy/classgen/ClassCompletionVerifier.java
index 4516f83..66bc784 100644
--- a/src/main/java/org/codehaus/groovy/classgen/ClassCompletionVerifier.java
+++ b/src/main/java/org/codehaus/groovy/classgen/ClassCompletionVerifier.java
@@ -18,6 +18,8 @@
*/
package org.codehaus.groovy.classgen;
+import groovy.transform.NonSealed;
+import groovy.transform.Sealed;
import org.apache.groovy.ast.tools.ClassNodeUtils;
import org.codehaus.groovy.ast.ASTNode;
import org.codehaus.groovy.ast.ClassCodeVisitorSupport;
@@ -74,6 +76,7 @@ import static org.objectweb.asm.Opcodes.ACC_STRICT;
import static org.objectweb.asm.Opcodes.ACC_SYNCHRONIZED;
import static org.objectweb.asm.Opcodes.ACC_TRANSIENT;
import static org.objectweb.asm.Opcodes.ACC_VOLATILE;
+
/**
* Checks that a class satisfies various conditions including:
* <ul>
@@ -109,7 +112,7 @@ public class ClassCompletionVerifier extends ClassCodeVisitorSupport {
checkClassForIncorrectModifiers(node);
checkInterfaceMethodVisibility(node);
checkAbstractMethodVisibility(node);
- checkClassForOverwritingFinal(node);
+ checkClassForExtendingFinalOrSealed(node);
checkMethodsForIncorrectModifiers(node);
checkMethodsForIncorrectName(node);
checkMethodsForWeakerAccess(node);
@@ -309,23 +312,65 @@ public class ClassCompletionVerifier extends ClassCodeVisitorSupport {
methodNode.getTypeDescriptor() + "' must not be abstract.", methodNode);
}
- private void checkClassForOverwritingFinal(ClassNode cn) {
+ private void checkClassForExtendingFinalOrSealed(ClassNode cn) {
+ boolean sealed = Boolean.TRUE.equals(cn.getNodeMetaData(Sealed.class));
+ if (sealed && cn.getPermittedSubclasses().isEmpty()) {
+ addError("Sealed " + getDescription(cn) + " has no explicit or implicit permitted classes.", cn);
+ return;
+ }
+ boolean isFinal = isFinal(cn.getModifiers());
+ if (sealed && isFinal) {
+ addError("The " + getDescription(cn) + " cannot be both final and sealed.", cn);
+ return;
+ }
+ boolean nonSealed = Boolean.TRUE.equals(cn.getNodeMetaData(NonSealed.class));
ClassNode superCN = cn.getSuperClass();
+ if (nonSealed && (superCN == null || !superCN.isSealed())) {
+ addError("The " + getDescription(cn) + " cannot be non-sealed as it has no sealed parent.", cn);
+ return;
+ }
if (superCN == null) return;
+ if (superCN.isSealed()) {
+ if (sealed && nonSealed) {
+ addError("The " + getDescription(cn) + " cannot be both sealed and non-sealed.", cn);
+ return;
+ }
+ if (isFinal && nonSealed) {
+ addError("The " + getDescription(cn) + " cannot be both final and non-sealed.", cn);
+ return;
+ }
+ List<ClassNode> permittedSubclasses = superCN.getPermittedSubclasses();
+ boolean found = false;
+ for (ClassNode permitted : permittedSubclasses) {
+ if (permitted.equals(cn)) {
+ found = true;
+ break;
+ }
+ }
+ if (!found) {
+ addError("The " + getDescription(cn) + " is not a permitted subclass of the sealed " + getDescription(superCN) + ".", cn);
+ return;
+ }
+ boolean explicitlyMarked = nonSealed || cn.isSealed() || isFinal;
+ if (!explicitlyMarked) {
+ addError("The " + getDescription(cn) + " being a child of sealed " + getDescription(superCN) + " must be marked final, sealed, or non-sealed.", cn);
+ return;
+ }
+ }
if (!isFinal(superCN.getModifiers())) return;
- String msg = "You are not allowed to overwrite the final " + getDescription(superCN) + ".";
- addError(msg, cn);
+ addError("You are not allowed to extend the final " + getDescription(superCN) + ".", cn);
}
private void checkImplementsAndExtends(ClassNode node) {
- ClassNode cn = node.getSuperClass();
- if (cn.isInterface() && !node.isInterface()) {
- addError("You are not allowed to extend the " + getDescription(cn) + ", use implements instead.", node);
+ ClassNode sn = node.getSuperClass();
+ if (sn.isInterface() && !node.isInterface()) {
+ addError("You are not allowed to extend the " + getDescription(sn) + ", use implements instead.", node);
}
for (ClassNode anInterface : node.getInterfaces()) {
- cn = anInterface;
- if (!cn.isInterface()) {
- addError("You are not allowed to implement the " + getDescription(cn) + ", use extends instead.", node);
+ if (!anInterface.isInterface()) {
+ addError("You are not allowed to implement the " + getDescription(anInterface) + ", use extends instead.", node);
+ } else if (anInterface.isSealed()) {
+ addError("You are not allowed to implement the sealed " + getDescription(anInterface) + ".", node);
}
}
}
diff --git a/src/main/java/org/codehaus/groovy/classgen/Verifier.java b/src/main/java/org/codehaus/groovy/classgen/Verifier.java
index a6bd1bd..8fb4e55 100644
--- a/src/main/java/org/codehaus/groovy/classgen/Verifier.java
+++ b/src/main/java/org/codehaus/groovy/classgen/Verifier.java
@@ -25,6 +25,7 @@ import groovy.lang.MetaClass;
import groovy.transform.CompileStatic;
import groovy.transform.Generated;
import groovy.transform.Internal;
+import groovy.transform.Sealed;
import groovy.transform.stc.POJO;
import org.apache.groovy.ast.tools.ClassNodeUtils;
import org.apache.groovy.util.BeanUtils;
@@ -54,6 +55,7 @@ import org.codehaus.groovy.ast.expr.ConstantExpression;
import org.codehaus.groovy.ast.expr.ConstructorCallExpression;
import org.codehaus.groovy.ast.expr.Expression;
import org.codehaus.groovy.ast.expr.FieldExpression;
+import org.codehaus.groovy.ast.expr.ListExpression;
import org.codehaus.groovy.ast.expr.MethodCallExpression;
import org.codehaus.groovy.ast.expr.VariableExpression;
import org.codehaus.groovy.ast.stmt.BlockStatement;
@@ -104,6 +106,7 @@ import static org.codehaus.groovy.ast.ClassHelper.isObjectType;
import static org.codehaus.groovy.ast.ClassHelper.isPrimitiveBoolean;
import static org.codehaus.groovy.ast.ClassHelper.isPrimitiveDouble;
import static org.codehaus.groovy.ast.ClassHelper.isPrimitiveLong;
+import static org.codehaus.groovy.ast.ClassHelper.make;
import static org.codehaus.groovy.ast.tools.GeneralUtils.binX;
import static org.codehaus.groovy.ast.tools.GeneralUtils.block;
import static org.codehaus.groovy.ast.tools.GeneralUtils.bytecodeX;
@@ -154,6 +157,7 @@ public class Verifier implements GroovyClassVisitor, Opcodes {
private static final Class<?> GENERATED_ANNOTATION = Generated.class;
private static final Class<?> INTERNAL_ANNOTATION = Internal.class;
private static final Class<?> TRANSIENT_ANNOTATION = Transient.class;
+ private static final ClassNode SEALED_TYPE = make(Sealed.class);
// NOTE: timeStamp constants shouldn't belong to Verifier but kept here for binary compatibility
public static final String __TIMESTAMP = "__timeStamp";
@@ -266,10 +270,28 @@ public class Verifier implements GroovyClassVisitor, Opcodes {
node.visitContents(this);
checkForDuplicateMethods(node);
addCovariantMethods(node);
+ addDetectedSealedClasses(node);
checkFinalVariables(node);
}
+ private void addDetectedSealedClasses(ClassNode node) {
+ boolean sealed = Boolean.TRUE.equals(node.getNodeMetaData(Sealed.class));
+ List<ClassNode> permitted = node.getPermittedSubclasses();
+ if (!sealed || !permitted.isEmpty() || node.getModule() == null) return;
+ for (ClassNode possibleSubclass : node.getModule().getClasses()) {
+ if (possibleSubclass.getSuperClass().equals(node)) {
+ permitted.add(possibleSubclass);
+ }
+ }
+ List<Expression> names = new ArrayList<>();
+ for (ClassNode next : permitted) {
+ names.add(constX(next.getName()));
+ }
+ AnnotationNode an = node.getAnnotations(SEALED_TYPE).get(0);
+ an.addMember("permittedSubclasses", new ListExpression(names));
+ }
+
private void checkFinalVariables(final ClassNode node) {
GroovyClassVisitor visitor = new FinalVariableAnalyzer(null, getFinalVariablesCallback());
visitor.visitClass(node);
diff --git a/src/main/java/org/codehaus/groovy/transform/NonSealedASTTransformation.java b/src/main/java/org/codehaus/groovy/transform/NonSealedASTTransformation.java
new file mode 100644
index 0000000..07aefe9
--- /dev/null
+++ b/src/main/java/org/codehaus/groovy/transform/NonSealedASTTransformation.java
@@ -0,0 +1,49 @@
+/*
+ * 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;
+
+import groovy.transform.NonSealed;
+import org.codehaus.groovy.ast.*;
+import org.codehaus.groovy.control.CompilePhase;
+import org.codehaus.groovy.control.SourceUnit;
+
+import static org.codehaus.groovy.ast.ClassHelper.make;
+
+/**
+ * Handles generation of code for the @Sealed annotation.
+ */
+@GroovyASTTransformation(phase = CompilePhase.SEMANTIC_ANALYSIS)
+public class NonSealedASTTransformation extends AbstractASTTransformation {
+
+ private static final Class<?> NON_SEALED_CLASS = NonSealed.class;
+ private static final ClassNode NON_SEALED_TYPE = make(NON_SEALED_CLASS);
+
+ @Override
+ public void visit(ASTNode[] nodes, SourceUnit source) {
+ init(nodes, source);
+ AnnotatedNode parent = (AnnotatedNode) nodes[1];
+ AnnotationNode anno = (AnnotationNode) nodes[0];
+ if (!NON_SEALED_TYPE.equals(anno.getClassNode())) return;
+
+ if (parent instanceof ClassNode) {
+ ClassNode cNode = (ClassNode) parent;
+ cNode.putNodeMetaData(NON_SEALED_CLASS, Boolean.TRUE);
+ }
+ }
+}
diff --git a/src/main/java/org/codehaus/groovy/transform/SealedASTTransformation.java b/src/main/java/org/codehaus/groovy/transform/SealedASTTransformation.java
new file mode 100644
index 0000000..a6666fd
--- /dev/null
+++ b/src/main/java/org/codehaus/groovy/transform/SealedASTTransformation.java
@@ -0,0 +1,63 @@
+/*
+ * 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;
+
+import groovy.transform.Sealed;
+import org.codehaus.groovy.ast.ASTNode;
+import org.codehaus.groovy.ast.AnnotatedNode;
+import org.codehaus.groovy.ast.AnnotationNode;
+import org.codehaus.groovy.ast.ClassHelper;
+import org.codehaus.groovy.ast.ClassNode;
+import org.codehaus.groovy.control.CompilePhase;
+import org.codehaus.groovy.control.SourceUnit;
+
+import java.util.List;
+
+import static org.codehaus.groovy.ast.ClassHelper.make;
+
+/**
+ * Handles generation of code for the @Sealed annotation.
+ */
+@GroovyASTTransformation(phase = CompilePhase.SEMANTIC_ANALYSIS)
+public class SealedASTTransformation extends AbstractASTTransformation {
+
+ private static final Class<?> SEALED_CLASS = Sealed.class;
+ private static final ClassNode SEALED_TYPE = make(SEALED_CLASS);
+ private static final String MY_TYPE_NAME = "@" + SEALED_TYPE.getNameWithoutPackage();
+
+ @Override
+ public void visit(ASTNode[] nodes, SourceUnit source) {
+ init(nodes, source);
+ AnnotatedNode parent = (AnnotatedNode) nodes[1];
+ AnnotationNode anno = (AnnotationNode) nodes[0];
+ if (!SEALED_TYPE.equals(anno.getClassNode())) return;
+
+ if (parent instanceof ClassNode) {
+ ClassNode cNode = (ClassNode) parent;
+ cNode.putNodeMetaData(SEALED_CLASS, Boolean.TRUE);
+ List<String> permittedSubclassNames = getMemberStringList(anno, "permittedSubclasses");
+ List<ClassNode> permittedSubclasses = cNode.getPermittedSubclasses();
+ if (permittedSubclassNames != null) {
+ for (String name : permittedSubclassNames) {
+ permittedSubclasses.add(ClassHelper.make(name));
+ }
+ }
+ }
+ }
+}
diff --git a/src/test/org/codehaus/groovy/transform/SealedTransformTest.groovy b/src/test/org/codehaus/groovy/transform/SealedTransformTest.groovy
new file mode 100644
index 0000000..6e9c74d
--- /dev/null
+++ b/src/test/org/codehaus/groovy/transform/SealedTransformTest.groovy
@@ -0,0 +1,124 @@
+/*
+ * 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
+
+import org.codehaus.groovy.control.MultipleCompilationErrorsException
+import org.junit.Test
+
+import static groovy.test.GroovyAssert.assertScript
+import static groovy.test.GroovyAssert.shouldFail
+
+class SealedTransformTest {
+
+ @Test
+ void testSimpleSealedHierarchy() {
+ assertScript '''
+ import groovy.transform.Sealed
+ import groovy.transform.NonSealed
+
+ @Sealed(permittedSubclasses='Circle,Polygon,Rectangle') class Shape { }
+ final class Circle extends Shape { }
+ @NonSealed class Polygon extends Shape { }
+ final class Pentagon extends Polygon { }
+ @Sealed(permittedSubclasses='Square') class Rectangle extends Shape { }
+ final class Square extends Rectangle { }
+
+ assert [new Circle(), new Square()]*.class.name == ['Circle', 'Square']
+ '''
+ }
+
+ @Test
+ void testSealedChildMustBeMarked() {
+ assert shouldFail(MultipleCompilationErrorsException, '''
+ import groovy.transform.Sealed
+
+ @Sealed(permittedSubclasses='Circle') class Shape { }
+ class Circle extends Shape { }
+ ''').message.contains("The class 'Circle' being a child of sealed class 'Shape' must be marked final, sealed, or non-sealed")
+ }
+
+ @Test
+ void testInvalidExtensionOfSealed() {
+ assert shouldFail(MultipleCompilationErrorsException, '''
+ import groovy.transform.Sealed
+
+ @Sealed(permittedSubclasses='Circle') class Shape { }
+ final class Circle extends Shape { }
+ class Polygon extends Shape { }
+ ''').message.contains("The class 'Polygon' is not a permitted subclass of the sealed class 'Shape'")
+ }
+
+ @Test
+ void testFinalAndSealed() {
+ assert shouldFail(MultipleCompilationErrorsException, '''
+ import groovy.transform.Sealed
+
+ final @Sealed(permittedSubclasses='Circle') class Shape { }
+ final class Circle extends Shape { }
+ ''').message.contains("The class 'Shape' cannot be both final and sealed")
+ }
+
+ @Test
+ void testFinalAndNonSealed() {
+ assert shouldFail(MultipleCompilationErrorsException, '''
+ import groovy.transform.Sealed
+ import groovy.transform.NonSealed
+
+ @Sealed(permittedSubclasses='Circle') class Shape { }
+ final @NonSealed class Circle extends Shape { }
+ ''').message.contains("The class 'Circle' cannot be both final and non-sealed")
+ }
+
+ @Test
+ void testNonSealedNoParent() {
+ assert shouldFail(MultipleCompilationErrorsException, '''
+ import groovy.transform.NonSealed
+
+ @NonSealed class Shape { }
+ ''').message.contains("The class 'Shape' cannot be non-sealed as it has no sealed parent")
+ }
+
+ @Test
+ void testSealedAndNonSealed() {
+ assert shouldFail(MultipleCompilationErrorsException, '''
+ import groovy.transform.Sealed
+ import groovy.transform.NonSealed
+
+ @Sealed(permittedSubclasses='Ellipse') class Shape { }
+ @Sealed(permittedSubclasses='Circle') @NonSealed class Ellipse extends Shape { }
+ final class Circle extends Ellipse { }
+ ''').message.contains("The class 'Ellipse' cannot be both sealed and non-sealed")
+ }
+
+ @Test
+ void testDetectedPermittedClasses() {
+ // If the base class and all subclasses appear in the same source file, the
+ // permittedSubclasses list will be automatically completed if not specified explicitly.
+ // If an explicit list is given, it must be the complete list and won't
+ // be extended with any additional detected subclasses in the same source file.
+ assert new GroovyShell().evaluate('''
+ import groovy.transform.Sealed
+
+ @Sealed class Shape { }
+ final class Square extends Shape { }
+ final class Circle extends Shape { }
+ Shape.getAnnotation(Sealed).permittedSubclasses()
+ ''') == ['Square', 'Circle']
+ }
+}