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/30 07:30:43 UTC

[groovy] 01/02: Trivial tweak: adjust the initial capacity of join expression list

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 459a3db06143f2732e01ab3682614558f875f132
Author: Daniel Sun <su...@apache.org>
AuthorDate: Mon Jul 26 19:13:56 2021 +0800

    Trivial tweak: adjust the initial capacity of join expression list
---
 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   |  84 +++++++++--
 .../org/codehaus/groovy/classgen/Verifier.java     |  28 ++++
 .../ASTTransformationCollectorCodeVisitor.java     |   2 +-
 .../transform/NonSealedASTTransformation.java      |  49 ++++++
 .../groovy/transform/SealedASTTransformation.java  |  63 ++++++++
 .../transform/trait/TraitASTTransformation.java    |   5 +-
 src/spec/doc/_sealed.adoc                          |  85 +++++++++++
 src/spec/doc/_traits.adoc                          |  38 +++++
 src/spec/doc/core-object-orientation.adoc          |   1 +
 src/spec/test/SealedSpecificationTest.groovy       | 104 +++++++++++++
 src/spec/test/TraitsSpecificationTest.groovy       |  30 ++++
 .../groovy/transform/SealedTransformTest.groovy    | 164 +++++++++++++++++++++
 19 files changed, 761 insertions(+), 14 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..4b7091b 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 redirect().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..bb974fc 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;
@@ -46,6 +48,7 @@ import org.codehaus.groovy.control.SourceUnit;
 import org.codehaus.groovy.syntax.Types;
 import org.codehaus.groovy.transform.trait.Traits;
 
+import java.util.Arrays;
 import java.util.HashMap;
 import java.util.LinkedHashSet;
 import java.util.List;
@@ -74,6 +77,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 +113,7 @@ public class ClassCompletionVerifier extends ClassCodeVisitorSupport {
             checkClassForIncorrectModifiers(node);
             checkInterfaceMethodVisibility(node);
             checkAbstractMethodVisibility(node);
-            checkClassForOverwritingFinal(node);
+            checkClassForExtendingFinalOrSealed(node);
             checkMethodsForIncorrectModifiers(node);
             checkMethodsForIncorrectName(node);
             checkMethodsForWeakerAccess(node);
@@ -309,23 +313,79 @@ 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 (superCN == null) return;
-        if (!isFinal(superCN.getModifiers())) return;
-        String msg = "You are not allowed to overwrite the final " + getDescription(superCN) + ".";
-        addError(msg, cn);
+        boolean sealedSuper = superCN != null && superCN.isSealed();
+        boolean sealedInterface = Arrays.stream(cn.getInterfaces()).anyMatch(ClassNode::isSealed);
+        if (nonSealed && !(sealedSuper || sealedInterface)) {
+            addError("The " + getDescription(cn) + " cannot be non-sealed as it has no sealed parent.", cn);
+            return;
+        }
+        if (sealedSuper || sealedInterface) {
+            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;
+            }
+            if (sealedSuper) {
+                checkSealedParent(cn, superCN, isFinal, nonSealed);
+            }
+            if (sealedInterface) {
+                for (ClassNode candidate : cn.getInterfaces()) {
+                    if (candidate.isSealed()) {
+                        checkSealedParent(cn, candidate, isFinal, nonSealed);
+                    }
+                }
+            }
+        }
+        if (superCN == null || !isFinal(superCN.getModifiers())) return;
+        addError("You are not allowed to extend the final " + getDescription(superCN) + ".", cn);
+    }
+
+    private void checkSealedParent(ClassNode cn, ClassNode parent, boolean isFinal, boolean nonSealed) {
+        boolean found = false;
+        for (ClassNode permitted : parent.getPermittedSubclasses()) {
+            if (permitted.equals(cn)) {
+                found = true;
+                break;
+            }
+        }
+        if (!found) {
+            addError("The " + getDescription(cn) + " is not a permitted subclass of the sealed " + getDescription(parent) + ".", cn);
+            return;
+        }
+        boolean explicitlyMarked = nonSealed || cn.isSealed() || isFinal;
+        if (!explicitlyMarked) {
+            addError("The " + getDescription(cn) + " being a child of sealed " + getDescription(parent) + " must be marked final, sealed, or non-sealed.", 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()) {
+                boolean nonSealed = Boolean.TRUE.equals(node.getNodeMetaData(NonSealed.class));
+                boolean isFinal = isFinal(node.getModifiers());
+                checkSealedParent(node, anInterface, isFinal, nonSealed);
             }
         }
     }
diff --git a/src/main/java/org/codehaus/groovy/classgen/Verifier.java b/src/main/java/org/codehaus/groovy/classgen/Verifier.java
index a6bd1bd..758a3fa 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";
@@ -227,6 +231,7 @@ public class Verifier implements GroovyClassVisitor, Opcodes {
             if (classNode.getNodeMetaData(ClassNodeSkip.class) == null) {
                 classNode.setNodeMetaData(ClassNodeSkip.class, true);
             }
+            addDetectedSealedClasses(node);
             return;
         }
 
@@ -266,10 +271,33 @@ 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);
+            }
+            for (ClassNode iface : possibleSubclass.getInterfaces()) {
+                if (iface.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/ASTTransformationCollectorCodeVisitor.java b/src/main/java/org/codehaus/groovy/transform/ASTTransformationCollectorCodeVisitor.java
index 8638b72..3b578dc 100644
--- a/src/main/java/org/codehaus/groovy/transform/ASTTransformationCollectorCodeVisitor.java
+++ b/src/main/java/org/codehaus/groovy/transform/ASTTransformationCollectorCodeVisitor.java
@@ -294,7 +294,7 @@ public class ASTTransformationCollectorCodeVisitor extends ClassCodeVisitorSuppo
             source.getErrorCollector().addError(new SimpleMessage(error, source));
         }
 
-        if (!Traits.isTrait(classNode) || transformClass == TraitASTTransformation.class) {
+        if (!Traits.isTrait(classNode) || transformClass == TraitASTTransformation.class || transformClass == SealedASTTransformation.class) {
             classNode.addTransform((Class<? extends ASTTransformation>) transformClass, annotation);
         }
     }
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/main/java/org/codehaus/groovy/transform/trait/TraitASTTransformation.java b/src/main/java/org/codehaus/groovy/transform/trait/TraitASTTransformation.java
index 4ba4965..3463b3e 100644
--- a/src/main/java/org/codehaus/groovy/transform/trait/TraitASTTransformation.java
+++ b/src/main/java/org/codehaus/groovy/transform/trait/TraitASTTransformation.java
@@ -19,6 +19,7 @@
 package org.codehaus.groovy.transform.trait;
 
 import groovy.transform.CompilationUnitAware;
+import groovy.transform.Sealed;
 import org.codehaus.groovy.ast.ASTNode;
 import org.codehaus.groovy.ast.AnnotatedNode;
 import org.codehaus.groovy.ast.AnnotationNode;
@@ -104,6 +105,7 @@ public class TraitASTTransformation extends AbstractASTTransformation implements
 
     private static final ClassNode INVOKERHELPER_CLASSNODE = ClassHelper.make(InvokerHelper.class);
     private static final ClassNode OVERRIDE_CLASSNODE = ClassHelper.make(Override.class);
+    private static final ClassNode SEALED_CLASSNODE = ClassHelper.make(Sealed.class);
 
     private SourceUnit sourceUnit;
     private CompilationUnit compilationUnit;
@@ -368,7 +370,8 @@ public class TraitASTTransformation extends AbstractASTTransformation implements
     private static void copyClassAnnotations(final ClassNode cNode, final ClassNode helper) {
         List<AnnotationNode> annotations = cNode.getAnnotations();
         for (AnnotationNode annotation : annotations) {
-            if (!annotation.getClassNode().equals(Traits.TRAIT_CLASSNODE)) {
+            if (!annotation.getClassNode().equals(Traits.TRAIT_CLASSNODE)
+                    && !annotation.getClassNode().equals(SEALED_CLASSNODE)) {
                 helper.addAnnotation(annotation);
             }
         }
diff --git a/src/spec/doc/_sealed.adoc b/src/spec/doc/_sealed.adoc
new file mode 100644
index 0000000..c3c3776
--- /dev/null
+++ b/src/spec/doc/_sealed.adoc
@@ -0,0 +1,85 @@
+//////////////////////////////////////////
+
+  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.
+
+//////////////////////////////////////////
+
+= Sealed hierarchies
+
+Sealed classes, interfaces and traits restrict which subclasses can extend/implement them.
+A final class allows no extension. A public non-final class allows extension by anyone.
+Access modifiers like protected and package-private give some ability to restrict inheritance
+hierarchies but often at the expense of flexible use of those hierarchies.
+
+Sealed hierarchies provide full inheritance within a known hierarchy of classes, interfaces and traits
+but disable or only provide controlled inheritance outside the hierarchy.
+
+For example, suppose we want to create a shape hierarchy containing
+only circles and squares. We also want a shape interface to
+be able to refer to instances in our hierarchy. We can create the
+hierarchy as follows:
+
+[source,groovy]
+----
+include::../test/SealedSpecificationTest.groovy[tags=simple_interface,indent=0]
+----
+
+We can have a reference of type `ShapeI` which can point to either a `Circle` or `Square`
+and, since our classes are `final`, we know no additional classes will be added to our hierarchy in the future.
+At least not without changing the `permittedSubclasses` and recompiling.
+
+In general, we might want to have some parts of our class hierarchy
+immediately locked down like we have here, where we marked the
+subclasses as `final` but other times we might want to allow further
+controlled inheritance.
+
+[source,groovy]
+----
+include::../test/SealedSpecificationTest.groovy[tags=general_sealed_class,indent=0]
+----
+
+In this example, our permitted subclasses for `Shape` are `Circle`, `Polygon`, and `Rectangle`.
+`Circle` is `final` and hence that part of the hierarchy cannot be extended.
+`Polygon` is marked as `@NonSealed` and that means our heiarchy is open to any further extension
+by subclassing `Polygon` as seen for `Pentagon`.
+`Rectangle` is itself sealed which means that part of the hierarchy can be extended
+but only in a controlled way.
+
+Sealed classes are useful for creating enum-like related classes
+which need to contain instance specific data. For instance, we might have the following enum:
+
+[source,groovy]
+----
+include::../test/SealedSpecificationTest.groovy[tags=weather_enum,indent=0]
+----
+
+but we now wish to also add weather specific instance data to weather forecasts.
+We can alter our abstraction as follows:
+
+[source,groovy]
+----
+include::../test/SealedSpecificationTest.groovy[tags=weather_sealed,indent=0]
+----
+
+Sealed hierarchies are also useful when specifying Algebraic or Abstract Data Types (ADTs) as shown in the following example:
+
+[source,groovy]
+----
+include::../test/SealedSpecificationTest.groovy[tags=sealed_ADT,indent=0]
+----
+
diff --git a/src/spec/doc/_traits.adoc b/src/spec/doc/_traits.adoc
index ca373ed..fb0b34d 100644
--- a/src/spec/doc/_traits.adoc
+++ b/src/spec/doc/_traits.adoc
@@ -838,6 +838,44 @@ class 'MyDevice' implements trait 'Communicating' but does not extend self type
 In conclusion, self types are a powerful way of declaring constraints on traits without having to declare the contract
 directly in the trait or having to use casts everywhere, maintaining separation of concerns as tight as it should be.
 
+=== Differences with Sealed annotation
+
+Both `@Sealed` and `@SelfType` restrict classes which use a trait but in orthogonal ways.
+Consider the following example:
+
+[source,groovy]
+----
+include::../test/TraitsSpecificationTest.groovy[tags=selftype_sealed_volume,indent=0]
+----
+<1> All usages of the `HasVolume` trait must implement or extend both `HasHeight` and `HasArea`
+<2> Only `UnitCube` or `UnitCylinder` can use the trait
+
+For the degenerate case where a single class implements a trait, e.g.:
+
+[source,groovy]
+----
+final class Foo implements FooTrait {}
+----
+
+Then, either:
+
+[source,groovy]
+----
+@SelfType(Foo)
+trait FooTrait {}
+----
+
+or:
+
+[source,groovy]
+----
+@Sealed(permittedSubclasses='Foo') // <1>
+trait FooTrait {}
+----
+<1> Or just `@Sealed` if `Foo` and `FooTrait` are in the same source file
+
+could express this constraint. Generally, the former of these is preferred.
+
 == Limitations
 === Compatibility with AST transformations
 
diff --git a/src/spec/doc/core-object-orientation.adoc b/src/spec/doc/core-object-orientation.adoc
index 60e7740..5bcbacc 100644
--- a/src/spec/doc/core-object-orientation.adoc
+++ b/src/spec/doc/core-object-orientation.adoc
@@ -1235,3 +1235,4 @@ single one corresponding to `@CompileStatic(TypeCheckingMode.SKIP)`.
 
 include::_traits.adoc[leveloffset=+1]
 
+include::_sealed.adoc[leveloffset=+1]
diff --git a/src/spec/test/SealedSpecificationTest.groovy b/src/spec/test/SealedSpecificationTest.groovy
new file mode 100644
index 0000000..6f3a137
--- /dev/null
+++ b/src/spec/test/SealedSpecificationTest.groovy
@@ -0,0 +1,104 @@
+/*
+ *  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.
+ */
+
+
+import groovy.test.GroovyTestCase
+
+/**
+ * Specification tests for the traits feature
+ */
+class SealedSpecificationTest extends GroovyTestCase {
+
+    void testSealedADT() {
+        assertScript '''
+// tag::sealed_ADT[]
+import groovy.transform.*
+
+@Sealed interface Tree<T> {}
+@Singleton final class Empty implements Tree {
+    String toString() { 'Empty' }
+}
+@Canonical final class Node<T> implements Tree<T> {
+    T value
+    Tree<T> left, right
+}
+
+Tree<Integer> tree = new Node<>(42, new Node<>(0, Empty.instance, Empty.instance), Empty.instance)
+assert tree.toString() == 'Node(42, Node(0, Empty, Empty), Empty)'
+// end::sealed_ADT[]
+'''
+    }
+
+    void testSimpleSealedHierarchyInterfaces() {
+        assertScript '''
+import groovy.transform.Sealed
+
+// tag::simple_interface[]
+@Sealed(permittedSubclasses='Circle,Square') interface ShapeI { }
+final class Circle implements ShapeI { }
+final class Square implements ShapeI { }
+// end::simple_interface[]
+
+assert [new Circle(), new Square()]*.class.name == ['Circle', 'Square']
+'''
+    }
+
+    void testSimpleSealedHierarchyClasses() {
+        assertScript '''
+import groovy.transform.Sealed
+import groovy.transform.NonSealed
+
+// tag::general_sealed_class[]
+@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 { }
+// end::general_sealed_class[]
+
+assert [new Circle(), new Square()]*.class.name == ['Circle', 'Square']
+'''
+    }
+
+    void testEnum() {
+        assertScript '''
+// tag::weather_enum[]
+enum Weather { Rainy, Cloudy, Sunny }
+def forecast = [Weather.Rainy, Weather.Sunny, Weather.Cloudy]
+assert forecast.toString() == '[Rainy, Sunny, Cloudy]'
+// end::weather_enum[]
+'''
+    }
+
+    void testSealedWeather() {
+        assertScript '''
+import groovy.transform.*
+
+// tag::weather_sealed[]
+@Sealed abstract class Weather { }
+@Immutable(includeNames=true) class Rainy extends Weather { Integer expectedRainfall }
+@Immutable(includeNames=true) class Sunny extends Weather { Integer expectedTemp }
+@Immutable(includeNames=true) class Cloudy extends Weather { Integer expectedUV }
+def forecast = [new Rainy(12), new Sunny(35), new Cloudy(6)]
+assert forecast.toString() == '[Rainy(expectedRainfall:12), Sunny(expectedTemp:35), Cloudy(expectedUV:6)]'
+// end::weather_sealed[]
+'''
+    }
+}
diff --git a/src/spec/test/TraitsSpecificationTest.groovy b/src/spec/test/TraitsSpecificationTest.groovy
index 2e1cdd8..94330b9 100644
--- a/src/spec/test/TraitsSpecificationTest.groovy
+++ b/src/spec/test/TraitsSpecificationTest.groovy
@@ -829,6 +829,36 @@ class SecurityService {
         assert message.contains("class 'MyDevice' implements trait 'Communicating' but does not extend self type class 'Device'")
     }
 
+    void testSelfType() {
+        assertScript '''
+import groovy.transform.*
+
+// tag::selftype_sealed_volume[]
+interface HasHeight { double getHeight() }
+interface HasArea { double getArea() }
+
+@SelfType([HasHeight, HasArea])                       // <1>
+@Sealed(permittedSubclasses='UnitCylinder,UnitCube')  // <2>
+trait HasVolume {
+    double getVolume() { height * area }
+}
+
+final class UnitCube implements HasVolume, HasHeight, HasArea {
+    double height = 1d
+    double area = 1d
+}
+
+final class UnitCylinder implements HasVolume, HasHeight, HasArea {
+    double height = 1d
+    double area = Math.PI * 0.5d**2
+}
+
+assert new UnitCube().volume == 1d
+assert new UnitCylinder().volume == 0.7853981633974483d
+// end::selftype_sealed_volume[]
+'''
+    }
+
     static class PrintCategory {
         static StringBuilder BUFFER = new StringBuilder()
         static void println(Object self, String message) {
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..6c904bd
--- /dev/null
+++ b/src/test/org/codehaus/groovy/transform/SealedTransformTest.groovy
@@ -0,0 +1,164 @@
+/*
+ *  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 testSimpleSealedHierarchyTraits() {
+        assertScript '''
+            import groovy.transform.Sealed
+
+            @Sealed(permittedSubclasses='Diamond,Star') trait ShapeT { }
+            final class Diamond implements ShapeT { }
+            final class Star implements ShapeT { }
+
+            assert [new Diamond(), new Star()]*.class.name == ['Diamond', 'Star']
+        '''
+    }
+
+    @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 testInferredPermittedAuxiliaryClasses() {
+        // 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']
+    }
+
+    @Test
+    void testInferredPermittedAuxiliaryInterfaces() {
+        assert new GroovyShell().evaluate('''
+            import groovy.transform.Sealed
+
+            @Sealed interface Shape { }
+            @Sealed interface Polygon extends Shape { }
+            final class Circle implements Shape { }
+            final class Rectangle implements Polygon { }
+            [Shape.getAnnotation(Sealed).permittedSubclasses(),
+             Polygon.getAnnotation(Sealed).permittedSubclasses()]
+        ''') == [['Polygon', 'Circle'], ['Rectangle']]
+    }
+
+    @Test
+    void testInferredPermittedNestedClasses() {
+        assert new GroovyShell().evaluate('''
+            import groovy.transform.Sealed
+
+            @Sealed class Shape {
+                final class Triangle extends Shape { }
+                final class Polygon extends Shape { }
+            }
+            Shape.getAnnotation(Sealed).permittedSubclasses()
+        ''') == ['Shape$Triangle', 'Shape$Polygon']
+    }
+}