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 2026/08/06 22:43:56 UTC
(groovy) 02/02: GROOVY-12236: Allow @ASTTest to be disabled via a groovy.asttest.enable system property
This is an automated email from the ASF dual-hosted git repository.
paulk-asert pushed a commit to branch GROOVY_5_1_X
in repository https://gitbox.apache.org/repos/asf/groovy.git
commit ff9902a2baf0285d3e81f7933bcac4a9e1ac8742
Author: Paul King <pa...@asert.com.au>
AuthorDate: Thu Aug 6 22:55:29 2026 +1000
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.
---
.../groovy/transform/ASTTestTransformation.groovy | 82 ++++++++++++++++++++--
src/main/java/groovy/transform/ASTTest.java | 8 +++
.../groovy/transform/ASTTestEnableTest.groovy | 64 +++++++++++++++++
3 files changed, 150 insertions(+), 4 deletions(-)
diff --git a/src/main/groovy/org/codehaus/groovy/transform/ASTTestTransformation.groovy b/src/main/groovy/org/codehaus/groovy/transform/ASTTestTransformation.groovy
index 456f3bb36e..ec44dabce3 100644
--- a/src/main/groovy/org/codehaus/groovy/transform/ASTTestTransformation.groovy
+++ b/src/main/groovy/org/codehaus/groovy/transform/ASTTestTransformation.groovy
@@ -45,13 +45,36 @@ import static org.codehaus.groovy.ast.tools.GeneralUtils.classX
import static org.codehaus.groovy.ast.tools.GeneralUtils.propX
import static org.codehaus.groovy.control.CompilePhase.fromPhaseNumber as toCompilePhase
+/**
+ * Implements {@link groovy.transform.ASTTest} by scheduling the supplied test closure across compilation phases.
+ */
@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.
+ */
CompilationUnit compilationUnit
+ /**
+ * Validates the annotation and schedules execution of its AST assertions.
+ * <p>
+ * 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')
@@ -81,7 +104,7 @@ class ASTTestTransformation implements ASTTransformation, CompilationUnitAware {
annotationNode.setNodeMetaData(ASTTestTransformation, member)
annotationNode.setMember('value', new ClosureExpression(
Parameter.EMPTY_ARRAY, EmptyStatement.INSTANCE))
- member.variableScope.@parent = null
+ member.variableScope?.@parent = null
ISourceUnitOperation astTester = new ASTTester(astNode: nodes[1], sourceUnit: source, testClosure: annotationNode.getNodeMetaData(ASTTestTransformation))
for (int p = (phase ?: CompilePhase.SEMANTIC_ANALYSIS).phaseNumber, q = (phase ?: CompilePhase.FINALIZATION).phaseNumber; p <= q; p += 1) {
@@ -93,14 +116,28 @@ class ASTTestTransformation implements ASTTransformation, CompilationUnitAware {
private class ASTTester implements ISourceUnitOperation {
+ /**
+ * AST node being exposed to the test closure.
+ */
ASTNode astNode
+ /**
+ * Source unit that declared the annotated node.
+ */
SourceUnit sourceUnit
+ /**
+ * Closure expression that contains the AST assertions.
+ */
ClosureExpression testClosure
private final Binding binding = new Binding([:].withDefault { null })
+ /**
+ * Runs the AST test for the matching source unit.
+ *
+ * @param unit the source unit currently being processed
+ */
@Override
- void call(final SourceUnit source) {
- if (source == sourceUnit) {
+ void call(final SourceUnit unit) {
+ if (unit == sourceUnit) {
test()
}
}
@@ -113,6 +150,8 @@ class ASTTestTransformation implements ASTTransformation, CompilationUnitAware {
sb = sb[testClosure.columnNumber..<sb.length()]
String testSource = sb[0..<sb.lastIndexOf('}')]
+ def compilationUnit = ASTTestTransformation.this.@compilationUnit
+
binding['node'] = astNode
binding['sourceUnit'] = sourceUnit
binding['compilationUnit'] = compilationUnit
@@ -140,8 +179,15 @@ class ASTTestTransformation implements ASTTransformation, CompilationUnitAware {
}
}
- static class LabelFinder extends ClassCodeVisitorSupport {
+ private static class LabelFinder extends ClassCodeVisitorSupport {
+ /**
+ * Finds labeled statements inside the supplied method.
+ *
+ * @param node the method to inspect
+ * @param label the statement label to match
+ * @return matching labeled statements
+ */
static List<Statement> lookup(final MethodNode node, final String label) {
LabelFinder finder = new LabelFinder(label, null)
node.code.visit(finder)
@@ -149,6 +195,13 @@ class ASTTestTransformation implements ASTTransformation, CompilationUnitAware {
finder.targets
}
+ /**
+ * Finds labeled statements inside the supplied class.
+ *
+ * @param node the class whose methods and constructors should be inspected
+ * @param label the statement label to match
+ * @return matching labeled statements
+ */
static List<Statement> lookup(final ClassNode node, final String label) {
LabelFinder finder = new LabelFinder(label, null)
node.methods*.code*.visit(finder)
@@ -161,22 +214,43 @@ class ASTTestTransformation implements ASTTransformation, CompilationUnitAware {
private final SourceUnit unit
private final List<Statement> targets = [] as LinkedList
+ /**
+ * Creates a finder for statements carrying the supplied label.
+ *
+ * @param label the label to search for
+ * @param unit the source unit reported to the visitor infrastructure
+ */
LabelFinder(final String label, final SourceUnit unit) {
this.label = label
this.unit = unit
}
+ /**
+ * Returns the source unit associated with this visitor.
+ *
+ * @return the associated source unit, or {@code null}
+ */
@Override
protected SourceUnit getSourceUnit() {
unit
}
+ /**
+ * Records statements whose labels match the requested target label.
+ *
+ * @param statement the statement currently being visited
+ */
@Override
protected void visitStatement(final Statement statement) {
super.visitStatement(statement)
if (label in statement.statementLabels) targets << statement
}
+ /**
+ * Returns the statements collected for the configured label.
+ *
+ * @return matching labeled statements
+ */
List<Statement> getTargets() {
Collections.unmodifiableList(targets)
}
diff --git a/src/main/java/groovy/transform/ASTTest.java b/src/main/java/groovy/transform/ASTTest.java
index ec384a3d59..2c8de339f2 100644
--- a/src/main/java/groovy/transform/ASTTest.java
+++ b/src/main/java/groovy/transform/ASTTest.java
@@ -51,6 +51,14 @@ import java.lang.annotation.Target;
* <p>
* The <code>node</code> variable refers to the AST node where the AST test annotation is put. In the previous example,
* it means that <i>node</i> refers to the declaration "int x".
+ * <p>
+ * Because the closure is evaluated during compilation, merely <em>compiling</em> 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
+ * <a href="https://github.com/apache/groovy/blob/master/THREAT_MODEL.md">threat model</a> 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 0000000000..000f7afba4
--- /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')
+ }
+}