diff --git a/src/main/groovy/org/codehaus/groovy/transform/ASTTestTransformation.groovy b/src/main/groovy/org/codehaus/groovy/transform/ASTTestTransformation.groovy index cd42ea6370c..ec44dabce32 100644 --- a/src/main/groovy/org/codehaus/groovy/transform/ASTTestTransformation.groovy +++ b/src/main/groovy/org/codehaus/groovy/transform/ASTTestTransformation.groovy @@ -51,6 +51,13 @@ import static org.codehaus.groovy.control.CompilePhase.fromPhaseNumber as toComp @GroovyASTTransformation(phase = CompilePhase.SEMANTIC_ANALYSIS) class ASTTestTransformation implements ASTTransformation, CompilationUnitAware { + /** + * System property which, when set to {@code false}, makes {@link groovy.transform.ASTTest} a no-op. + * Compiling source containing the annotation then no longer evaluates its closure. Mirrors the + * {@code groovy.grape.enable} switch for {@code @Grab}. + */ + public static final String ENABLE_PROPERTY = 'groovy.asttest.enable' + /** * Compilation unit that owns the current transformation. */ @@ -58,12 +65,16 @@ class ASTTestTransformation implements ASTTransformation, CompilationUnitAware { /** * Validates the annotation and schedules execution of its AST assertions. + *
+ * Does nothing when {@value #ENABLE_PROPERTY} is set to {@code false}. * * @param nodes the annotation node and annotated AST node * @param source the source unit containing the annotation */ @Override void visit(final ASTNode[] nodes, final SourceUnit source) { + if (!Boolean.parseBoolean(System.getProperty(ENABLE_PROPERTY, 'true'))) return + AnnotationNode annotationNode = nodes[0] def member = annotationNode.getMember('phase') diff --git a/src/main/java/groovy/transform/ASTTest.java b/src/main/java/groovy/transform/ASTTest.java index 12fecc35484..3021f71db0b 100644 --- a/src/main/java/groovy/transform/ASTTest.java +++ b/src/main/java/groovy/transform/ASTTest.java @@ -53,6 +53,14 @@ *
* The node variable refers to the AST node where the AST test annotation is put. In the previous example,
* it means that node refers to the declaration "int x".
+ *
+ * Because the closure is evaluated during compilation, merely compiling source which carries this + * annotation executes that code, whether or not the compiled result is subsequently run. Setting the + * {@code groovy.asttest.enable} system property to {@code false} makes the annotation a no-op, in the same + * way that {@code groovy.grape.enable} turns off {@code @Grab} dependency resolution. Consider doing so when + * compiling source you do not control; see the Apache Groovy + * threat model for what compiling + * such source does and does not imply. * * @since 2.0.0 */ diff --git a/src/test/groovy/groovy/transform/ASTTestEnableTest.groovy b/src/test/groovy/groovy/transform/ASTTestEnableTest.groovy new file mode 100644 index 00000000000..000f7afba41 --- /dev/null +++ b/src/test/groovy/groovy/transform/ASTTestEnableTest.groovy @@ -0,0 +1,64 @@ +/* + * 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 groovy.junit6.plugin.ForkedJvm +import org.junit.jupiter.api.Test + +import static groovy.test.GroovyAssert.assertScript +import static groovy.test.GroovyAssert.shouldFail + +/** + * Tests the {@code groovy.asttest.enable} switch for the {@link ASTTest} AST transform. + */ +final class ASTTestEnableTest { + + /** Compiling this fails only if the test closure is actually evaluated. */ + private static final String SCRIPT_WITH_FAILING_AST_TEST = ''' + @groovy.transform.ASTTest(value = { + assert false : 'test closure was evaluated' + }) + class C {} + new C() + ''' + + @Test + void testEnabledByDefault() { + assert System.getProperty('groovy.asttest.enable') == null + + def error = shouldFail(SCRIPT_WITH_FAILING_AST_TEST) + assert error.message.contains('test closure was evaluated') + } + + @Test + @ForkedJvm(systemProperties = ['groovy.asttest.enable=false']) + void testDisabledBySystemProperty() { + assert System.getProperty('groovy.asttest.enable') == 'false' + + // the annotation is a no-op, so the failing closure never runs + assertScript SCRIPT_WITH_FAILING_AST_TEST + } + + @Test + @ForkedJvm(systemProperties = ['groovy.asttest.enable=true']) + void testExplicitlyEnabledBySystemProperty() { + def error = shouldFail(SCRIPT_WITH_FAILING_AST_TEST) + assert error.message.contains('test closure was evaluated') + } +}