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