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 { }
}