From 18109b3ff1a5e5830933bd48a0192bf2351b814d Mon Sep 17 00:00:00 2001 From: Paul King Date: Thu, 6 Aug 2026 22:55:29 +1000 Subject: [PATCH] GROOVY-12236: Allow @ASTTest to be disabled via a groovy.asttest.enable system property The @ASTTest closure is evaluated during compilation, so merely compiling source which carries the annotation executes that code, whether or not the compiled result is subsequently run. Embedders which compile source they do not control have no way to turn this off. Add a groovy.asttest.enable system property, defaulting to true, which makes the annotation a no-op when set to false. This mirrors groovy.grape.enable for @Grab. The check is in visit(), so the closure is never scheduled onto a compilation phase rather than being scheduled and skipped. The property is read per-visit rather than into a static final field so that it can be exercised without relying on JVM startup state. Being a system property it is process-wide, exactly as groovy.grape.enable is: a JVM cannot enable @ASTTest for one compilation and disable it for another. --- .../transform/ASTTestTransformation.groovy | 11 ++++ src/main/java/groovy/transform/ASTTest.java | 8 +++ .../groovy/transform/ASTTestEnableTest.groovy | 64 +++++++++++++++++++ 3 files changed, 83 insertions(+) create mode 100644 src/test/groovy/groovy/transform/ASTTestEnableTest.groovy 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') + } +}