This is an automated email from the ASF dual-hosted git repository.

paulk pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/groovy.git


The following commit(s) were added to refs/heads/master by this push:
     new 891ab4e  GROOVY-10340: Refine sealed classes to not use system 
properties
891ab4e is described below

commit 891ab4edb88488be6d2fa1382b021222c4d819a4
Author: Paul King <[email protected]>
AuthorDate: Wed Nov 3 10:46:17 2021 +1000

    GROOVY-10340: Refine sealed classes to not use system properties
---
 src/main/java/groovy/transform/SealedMode.java     | 45 ++++++++++++++++
 src/main/java/groovy/transform/SealedOptions.java  | 48 +++++++++++++++++
 .../groovy/classgen/AsmClassGenerator.java         |  8 +--
 .../groovy/control/CompilerConfiguration.java      | 60 ----------------------
 .../groovy/transform/SealedASTTransformation.java  | 48 ++++++++++++++++-
 .../org/codehaus/groovy/classgen/SealedTest.groovy |  7 +--
 .../groovy/transform/SealedTransformTest.groovy    | 12 +++--
 7 files changed, 155 insertions(+), 73 deletions(-)

diff --git a/src/main/java/groovy/transform/SealedMode.java 
b/src/main/java/groovy/transform/SealedMode.java
new file mode 100644
index 0000000..8b440f8
--- /dev/null
+++ b/src/main/java/groovy/transform/SealedMode.java
@@ -0,0 +1,45 @@
+/*
+ *  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;
+
+/**
+ * Intended mode to use for sealed classes when using the {@code @Sealed} 
annotation (or {@code sealed} keyword).
+ *
+ * @since 4.0.0
+ * @see Sealed
+ */
+public enum SealedMode {
+    /**
+     * Indicate the sealed nature using annotations.
+     * This allows sealed hierarchies in earlier JDKs but for integration 
purposes won't be recognised by Java.
+     */
+    EMULATE,
+
+    /**
+     * Produce Java-like code with sealed nature indicated by "native" 
bytecode information (JEP 360/397/409).
+     * Produces a compile-time error when used on earlier JDKs.
+     */
+    NATIVE,
+
+    /**
+     * Produce native sealed classes when compiling for a suitable target 
bytecode (JDK17+)
+     * and automatically emulate the concept on earlier JDKs.
+     */
+    AUTO
+}
diff --git a/src/main/java/groovy/transform/SealedOptions.java 
b/src/main/java/groovy/transform/SealedOptions.java
new file mode 100644
index 0000000..550143d
--- /dev/null
+++ b/src/main/java/groovy/transform/SealedOptions.java
@@ -0,0 +1,48 @@
+/*
+ *  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.apache.groovy.lang.annotation.Incubating;
+
+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
+ */
[email protected]
+@Retention(RetentionPolicy.SOURCE)
+@Target({ElementType.TYPE})
+@Incubating
+public @interface SealedOptions {
+    /**
+     * Mode to use when creating sealed classes.
+     */
+    SealedMode mode() default SealedMode.AUTO;
+
+    /**
+     * Add annotations even for native sealed classes.
+     * Ignored when emulating sealed classes since then annotations are always 
used.
+     */
+    boolean alwaysAnnotate() default true;
+}
diff --git a/src/main/java/org/codehaus/groovy/classgen/AsmClassGenerator.java 
b/src/main/java/org/codehaus/groovy/classgen/AsmClassGenerator.java
index 52796e7..62be3f4 100644
--- a/src/main/java/org/codehaus/groovy/classgen/AsmClassGenerator.java
+++ b/src/main/java/org/codehaus/groovy/classgen/AsmClassGenerator.java
@@ -20,6 +20,7 @@ package org.codehaus.groovy.classgen;
 
 import groovy.lang.GroovyRuntimeException;
 import groovy.transform.Sealed;
+import groovy.transform.SealedMode;
 import org.apache.groovy.ast.tools.ExpressionUtils;
 import org.apache.groovy.io.StringBuilderWriter;
 import org.codehaus.groovy.GroovyBugError;
@@ -112,7 +113,6 @@ import org.objectweb.asm.ClassVisitor;
 import org.objectweb.asm.FieldVisitor;
 import org.objectweb.asm.Label;
 import org.objectweb.asm.MethodVisitor;
-import org.objectweb.asm.Opcodes;
 import org.objectweb.asm.RecordComponentVisitor;
 import org.objectweb.asm.Type;
 import org.objectweb.asm.TypePath;
@@ -155,6 +155,7 @@ import static 
org.codehaus.groovy.ast.tools.GeneralUtils.getSetterName;
 import static org.codehaus.groovy.ast.tools.GeneralUtils.maybeFallsThrough;
 import static org.codehaus.groovy.ast.tools.GeneralUtils.propX;
 import static org.codehaus.groovy.ast.tools.GeneralUtils.thisPropX;
+import static 
org.codehaus.groovy.transform.SealedASTTransformation.SEALED_ALWAYS_ANNOTATE;
 import static 
org.codehaus.groovy.transform.sc.StaticCompilationMetadataKeys.PROPERTY_OWNER;
 import static org.objectweb.asm.Opcodes.AASTORE;
 import static org.objectweb.asm.Opcodes.ACC_ENUM;
@@ -377,8 +378,7 @@ public class AsmClassGenerator extends ClassGenerator {
             for (Iterator<InnerClassNode> it = classNode.getInnerClasses(); 
it.hasNext(); ) {
                 makeInnerClassEntry(it.next());
             }
-            if (bytecodeVersion >= Opcodes.V17 &&
-                    context.getCompileUnit().getConfig().isSealedNative()) {
+            if (classNode.getNodeMetaData(SealedMode.class) == 
SealedMode.NATIVE) {
                 for (ClassNode sub: classNode.getPermittedSubclasses()) {
                     classVisitor.visitPermittedSubclass(sub.getName());
                 }
@@ -2086,7 +2086,7 @@ public class AsmClassGenerator extends ClassGenerator {
             // skip built-in properties
             if (an.isBuiltIn()) continue;
             if (an.hasSourceRetention()) continue;
-            if (an.getClassNode().getName().equals(Sealed.class.getName()) && 
!context.getCompileUnit().getConfig().isSealedAnnotations()) continue;
+            if (an.getClassNode().getName().equals(Sealed.class.getName()) && 
sourceNode.getNodeMetaData(SealedMode.class) == SealedMode.NATIVE && 
Boolean.FALSE.equals(sourceNode.getNodeMetaData(SEALED_ALWAYS_ANNOTATE))) 
continue;
 
             AnnotationVisitor av = getAnnotationVisitor(targetNode, an, 
visitor);
             visitAnnotationAttributes(an, av);
diff --git 
a/src/main/java/org/codehaus/groovy/control/CompilerConfiguration.java 
b/src/main/java/org/codehaus/groovy/control/CompilerConfiguration.java
index 3d04bd3..829bcaa 100644
--- a/src/main/java/org/codehaus/groovy/control/CompilerConfiguration.java
+++ b/src/main/java/org/codehaus/groovy/control/CompilerConfiguration.java
@@ -389,16 +389,6 @@ public class CompilerConfiguration {
     private boolean previewFeatures;
 
     /**
-     * Whether Sealed class information is natively generated for target 
bytecode >= 17 (JEP 360/397/409)
-     */
-    private boolean sealedNative;
-
-    /**
-     * Whether Sealed annotations are generated
-     */
-    private boolean sealedAnnotations;
-
-    /**
      * Whether logging class generation is enabled
      */
     private boolean logClassgen;
@@ -443,8 +433,6 @@ public class CompilerConfiguration {
      *   <tr><td><code>groovy.target.directory</code></td><td>{@link 
#getTargetDirectory}</td></tr>
      *   <tr><td><code>groovy.parameters</code></td><td>{@link 
#getParameters()}</td></tr>
      *   <tr><td><code>groovy.preview.features</code></td><td>{@link 
#isPreviewFeatures}</td></tr>
-     *   <tr><td><code>groovy.sealed.native.disable</code></td><td>{@link 
#isSealedNative}</td></tr>
-     *   <tr><td><code>groovy.sealed.annotations.disable</code></td><td>{@link 
#isSealedAnnotations}</td></tr>
      *   <tr><td><code>groovy.default.scriptExtension</code></td><td>{@link 
#getDefaultScriptExtension}</td></tr>
      * </table>
      * </blockquote>
@@ -468,8 +456,6 @@ public class CompilerConfiguration {
         warningLevel = WarningMessage.LIKELY_ERRORS;
         parameters = getBooleanSafe("groovy.parameters");
         previewFeatures = getBooleanSafe("groovy.preview.features");
-        sealedNative = !getBooleanSafe("groovy.sealed.native.disable");
-        sealedAnnotations = 
!getBooleanSafe("groovy.sealed.annotations.disable");
         logClassgen = getBooleanSafe("groovy.log.classgen");
         logClassgenStackTraceMaxDepth = 
getIntegerSafe("groovy.log.classgen.stacktrace.max.depth", 0);
         sourceEncoding = getSystemPropertySafe("groovy.source.encoding",
@@ -518,8 +504,6 @@ public class CompilerConfiguration {
         
setMinimumRecompilationInterval(configuration.getMinimumRecompilationInterval());
         setTargetBytecode(configuration.getTargetBytecode());
         setPreviewFeatures(configuration.isPreviewFeatures());
-        setSealedNative(configuration.isSealedNative());
-        setSealedAnnotations(configuration.isSealedAnnotations());
         setLogClassgen(configuration.isLogClassgen());
         
setLogClassgenStackTraceMaxDepth(configuration.getLogClassgenStackTraceMaxDepth());
         setDefaultScriptExtension(configuration.getDefaultScriptExtension());
@@ -575,8 +559,6 @@ public class CompilerConfiguration {
      *   <tr><td><code>groovy.target.bytecode</code></td><td>{@link 
#getTargetBytecode}</td></tr>
      *   <tr><td><code>groovy.parameters</code></td><td>{@link 
#getParameters()}</td></tr>
      *   <tr><td><code>groovy.preview.features</code></td><td>{@link 
#isPreviewFeatures}</td></tr>
-     *   <tr><td><code>groovy.sealed.native.disable</code></td><td>{@link 
#isSealedNative}</td></tr>
-     *   <tr><td><code>groovy.sealed.annotations.disable</code></td><td>{@link 
#isSealedAnnotations}</td></tr>
      *   <tr><td><code>groovy.classpath</code></td><td>{@link 
#getClasspath}</td></tr>
      *   <tr><td><code>groovy.output.verbose</code></td><td>{@link 
#getVerbose}</td></tr>
      *   <tr><td><code>groovy.output.debug</code></td><td>{@link 
#getDebug}</td></tr>
@@ -773,12 +755,6 @@ public class CompilerConfiguration {
         text = configuration.getProperty("groovy.preview.features");
         if (text != null) setPreviewFeatures(text.equalsIgnoreCase("true"));
 
-        text = configuration.getProperty("groovy.sealed.native.disable");
-        if (text != null) setSealedNative(!text.equalsIgnoreCase("false"));
-
-        text = configuration.getProperty("groovy.sealed.annotations.disable");
-        if (text != null) 
setSealedAnnotations(!text.equalsIgnoreCase("false"));
-
         text = configuration.getProperty("groovy.log.classgen");
         if (text != null) setLogClassgen(text.equalsIgnoreCase("true"));
 
@@ -1135,42 +1111,6 @@ public class CompilerConfiguration {
     }
 
     /**
-     * Whether Sealed class information is natively generated (JEP 
360/397/409).
-     *
-     * @return sealedNative
-     */
-    public boolean isSealedNative() {
-        return sealedNative;
-    }
-
-    /**
-     * Sets whether Sealed class information is natively generated (JEP 
360/397/409).
-     *
-     * @param sealedNative whether to generate permittedSubclass information 
in the class file for target bytecode >= 17
-     */
-    public void setSealedNative(final boolean sealedNative) {
-        this.sealedNative = sealedNative;
-    }
-
-    /**
-     * Whether Sealed annotations are generated.
-     *
-     * @return sealedAnnotations
-     */
-    public boolean isSealedAnnotations() {
-        return sealedAnnotations;
-    }
-
-    /**
-     * Sets whether Sealed annotations are generated.
-     *
-     * @param sealedAnnotations whether to generate {@code @Sealed} 
annotations for sealed types
-     */
-    public void setSealedAnnotations(final boolean sealedAnnotations) {
-        this.sealedAnnotations = sealedAnnotations;
-    }
-
-    /**
      * Returns whether logging class generation is enabled
      *
      * @return whether logging class generation is enabled
diff --git 
a/src/main/java/org/codehaus/groovy/transform/SealedASTTransformation.java 
b/src/main/java/org/codehaus/groovy/transform/SealedASTTransformation.java
index ff7b032..04fda04 100644
--- a/src/main/java/org/codehaus/groovy/transform/SealedASTTransformation.java
+++ b/src/main/java/org/codehaus/groovy/transform/SealedASTTransformation.java
@@ -19,12 +19,17 @@
 package org.codehaus.groovy.transform;
 
 import groovy.transform.Sealed;
+import groovy.transform.SealedMode;
+import groovy.transform.SealedOptions;
 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.ast.expr.ClassExpression;
+import org.codehaus.groovy.ast.expr.Expression;
+import org.codehaus.groovy.ast.expr.PropertyExpression;
 import org.codehaus.groovy.control.CompilePhase;
+import org.codehaus.groovy.control.CompilerConfiguration;
 import org.codehaus.groovy.control.SourceUnit;
 
 import java.util.List;
@@ -39,6 +44,8 @@ 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 ClassNode SEALED_OPTIONS_TYPE = 
make(SealedOptions.class);
+    public static final String SEALED_ALWAYS_ANNOTATE = 
"groovy.transform.SealedOptions.alwaysAnnotate";
 
     @Override
     public void visit(ASTNode[] nodes, SourceUnit source) {
@@ -58,10 +65,49 @@ public class SealedASTTransformation extends 
AbstractASTTransformation {
                 return;
             }
             cNode.putNodeMetaData(SEALED_CLASS, Boolean.TRUE);
+            boolean isPostJDK17 = false;
+            String message = "Expecting JDK17+ but unable to determine target 
bytecode";
+            if (sourceUnit != null) {
+                CompilerConfiguration config = sourceUnit.getConfiguration();
+                String targetBytecode = config.getTargetBytecode();
+                isPostJDK17 = 
CompilerConfiguration.isPostJDK17(targetBytecode);
+                message = "Expecting JDK17+ but found " + targetBytecode;
+            }
+            List<AnnotationNode> annotations = 
cNode.getAnnotations(SEALED_OPTIONS_TYPE);
+            AnnotationNode options = annotations.isEmpty() ? null : 
annotations.get(0);
+            SealedMode mode = getMode(options, "mode");
+            boolean doNotAnnotate = options != null && memberHasValue(options, 
"alwaysAnnotate", Boolean.FALSE);
+
+            boolean isNative = isPostJDK17 && mode != SealedMode.EMULATE;
+            if (doNotAnnotate) {
+                cNode.putNodeMetaData(SEALED_ALWAYS_ANNOTATE, Boolean.FALSE);
+            }
+            if (isNative) {
+                cNode.putNodeMetaData(SealedMode.class, SealedMode.NATIVE);
+            } else if (mode == SealedMode.NATIVE) {
+                addError(message + " when attempting to create a native sealed 
class", cNode);
+            }
             List<ClassNode> newSubclasses = getMemberClassList(anno, 
"permittedSubclasses");
             if (newSubclasses != null) {
                 cNode.getPermittedSubclasses().addAll(newSubclasses);
             }
         }
     }
+
+    private static SealedMode getMode(AnnotationNode node, String name) {
+        if (node != null) {
+            final Expression member = node.getMember(name);
+            if (member instanceof PropertyExpression) {
+                PropertyExpression prop = (PropertyExpression) member;
+                Expression oe = prop.getObjectExpression();
+                if (oe instanceof ClassExpression) {
+                    ClassExpression ce = (ClassExpression) oe;
+                    if 
(ce.getType().getName().equals("groovy.transform.SealedMode")) {
+                        return SealedMode.valueOf(prop.getPropertyAsString());
+                    }
+                }
+            }
+        }
+        return null;
+    }
 }
diff --git a/src/test/groovy/org/codehaus/groovy/classgen/SealedTest.groovy 
b/src/test/groovy/org/codehaus/groovy/classgen/SealedTest.groovy
index f3b07502..8fae661 100644
--- a/src/test/groovy/org/codehaus/groovy/classgen/SealedTest.groovy
+++ b/src/test/groovy/org/codehaus/groovy/classgen/SealedTest.groovy
@@ -19,7 +19,6 @@
 package org.codehaus.groovy.classgen
 
 import groovy.transform.CompileStatic
-import org.codehaus.groovy.control.CompilerConfiguration
 import org.junit.Test
 
 import static groovy.test.GroovyAssert.assertScript
@@ -44,13 +43,15 @@ class SealedTest {
     @Test
     void testInferredPermittedNestedClassesWithNativeDisabled() {
         assumeTrue(isAtLeastJdk('17.0'))
-        assertScript(new GroovyShell(new CompilerConfiguration(sealedNative: 
false)), '''
+        assertScript '''
+            import groovy.transform.*
+            @SealedOptions(mode=SealedMode.EMULATE)
             sealed class Shape {
                 final class Triangle extends Shape { }
                 final class Polygon extends Shape { }
             }
             List<Class> classes = Shape.getPermittedSubclasses()
             assert !classes
-        ''')
+        '''
     }
 }
diff --git a/src/test/org/codehaus/groovy/transform/SealedTransformTest.groovy 
b/src/test/org/codehaus/groovy/transform/SealedTransformTest.groovy
index bdcc8d4..b375a8b 100644
--- a/src/test/org/codehaus/groovy/transform/SealedTransformTest.groovy
+++ b/src/test/org/codehaus/groovy/transform/SealedTransformTest.groovy
@@ -19,12 +19,13 @@
 package org.codehaus.groovy.transform
 
 import groovy.transform.Sealed
-import org.codehaus.groovy.control.CompilerConfiguration
 import org.codehaus.groovy.control.MultipleCompilationErrorsException
 import org.junit.Test
 
 import static groovy.test.GroovyAssert.assertScript
+import static groovy.test.GroovyAssert.isAtLeastJdk
 import static groovy.test.GroovyAssert.shouldFail
+import static org.junit.Assume.assumeTrue
 
 class SealedTransformTest {
 
@@ -208,11 +209,12 @@ class SealedTransformTest {
 
     @Test
     void testInferredPermittedNestedClassesWithAnnosDisabled() {
-        def config = new CompilerConfiguration(sealedAnnotations: false)
-        def shapeClass = new GroovyShell(config).evaluate('''
-            import groovy.transform.Sealed
+        assumeTrue(isAtLeastJdk('17.0'))
+        def shapeClass = new GroovyShell().evaluate('''
+            import groovy.transform.SealedOptions
 
-            @Sealed class Shape {
+            @SealedOptions(alwaysAnnotate = false)
+            sealed class Shape {
                 final class Triangle extends Shape { }
                 final class Polygon extends Shape { }
             }

Reply via email to