[
https://issues.apache.org/jira/browse/GROOVY-11878?page=com.atlassian.jira.plugin.system.issuetabpanels:comment-tabpanel&focusedCommentId=18067448#comment-18067448
]
ASF GitHub Bot commented on GROOVY-11878:
-----------------------------------------
Copilot commented on code in PR #2400:
URL: https://github.com/apache/groovy/pull/2400#discussion_r2971246127
##########
src/main/java/org/codehaus/groovy/ast/AnnotationNode.java:
##########
@@ -50,6 +50,8 @@ public class AnnotationNode extends ASTNode {
public static final int TYPE_PARAMETER_TARGET = 1 << 8;
public static final int TYPE_USE_TARGET = 1 << 9;
public static final int RECORD_COMPONENT_TARGET = 1 << 10;
+ /** Groovy-only target for statement-level annotations (e.g. on {@code
for}/{@code while} loops). */
+ public static final int STATEMENT_TARGET = 1 << 11;
public static final int TYPE_TARGET = ANNOTATION_TARGET | 1;
// GROOVY-7151
Review Comment:
`STATEMENT_TARGET` is introduced but isn’t included in the default
allowed-target bitset (`0x4FF`) computed when an annotation has no `@Target`.
If statement-level target checking is added later, this will cause *all*
annotations without `@Target` to be treated as disallowed on statements unless
the default mask is updated accordingly.
##########
src/main/java/org/codehaus/groovy/ast/stmt/Statement.java:
##########
@@ -58,6 +61,43 @@ public void copyStatementLabels(final Statement that) {
});
}
+
//--------------------------------------------------------------------------
+ // Statement-level annotation support (Groovy-only; stored in node
metadata)
+
+ private static final Object STATEMENT_ANNOTATIONS_KEY =
"_statementAnnotations_";
Review Comment:
`STATEMENT_ANNOTATIONS_KEY` is a String literal. Using a unique `new
Object()` key (or a dedicated private static final Object instance) avoids
accidental metadata key collisions with other features that may also use the
same String in `ASTNode` metadata.
```suggestion
private static final Object STATEMENT_ANNOTATIONS_KEY = new Object();
```
##########
src/main/java/groovy/transform/Parallel.java:
##########
@@ -0,0 +1,41 @@
+/*
+ * 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.codehaus.groovy.transform.GroovyASTTransformationClass;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * Runs each iteration of an annotated {@code for} loop on a separate thread.
+ * <p>
+ * This annotation is a lightweight demo transform and intentionally favors
simplicity
+ * over production-grade parallel orchestration.
+ *
+ * @since 6.0.0
+ * @see org.codehaus.groovy.transform.ParallelASTTransformation
+ */
+@Documented
+@Retention(RetentionPolicy.SOURCE)
+@GroovyASTTransformationClass("org.codehaus.groovy.transform.ParallelASTTransformation")
+public @interface Parallel {
+}
Review Comment:
PR description notes the sample transforms/annotations aren’t intended to be
merged as production code, but this PR adds them under `src/main/java` (which
will ship as part of the main artifact). If they’re only for experimentation,
consider moving them to test fixtures/samples or excluding them from the merge
to avoid accidentally committing a public API (`groovy.transform.Parallel`) and
behavior that isn’t supported long-term.
##########
src/main/java/org/codehaus/groovy/ast/ClassCodeVisitorSupport.java:
##########
@@ -212,6 +213,7 @@ public void visitExpressionStatement(ExpressionStatement
statement) {
@Override
public void visitForLoop(ForStatement statement) {
visitStatement(statement);
+ visitStatementAnnotations(statement);
if (statement.getValueVariable() != null) {
Review Comment:
These overrides call `visitStatementAnnotations(statement)` unconditionally
for every loop statement. If the intent is only to process statement
annotations when present, consider guarding with `if
(!statement.getStatementAnnotations().isEmpty())` to avoid extra work in hot
visitors (parser/type checker passes) for the common case of unannotated loops.
##########
src/main/java/org/codehaus/groovy/transform/LoopInvariantASTTransformation.java:
##########
@@ -0,0 +1,118 @@
+/*
+ * 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 org.codehaus.groovy.transform;
+
+import groovy.transform.LoopInvariant;
+import org.codehaus.groovy.ast.ASTNode;
+import org.codehaus.groovy.ast.AnnotationNode;
+import org.codehaus.groovy.ast.expr.ConstantExpression;
+import org.codehaus.groovy.ast.expr.Expression;
+import org.codehaus.groovy.ast.stmt.BlockStatement;
+import org.codehaus.groovy.ast.stmt.LoopingStatement;
+import org.codehaus.groovy.ast.stmt.Statement;
+import org.codehaus.groovy.control.CompilePhase;
+import org.codehaus.groovy.control.SourceUnit;
+import org.codehaus.groovy.control.messages.SyntaxErrorMessage;
+import org.codehaus.groovy.syntax.SyntaxException;
+
+import static org.codehaus.groovy.ast.tools.GeneralUtils.args;
+import static org.codehaus.groovy.ast.tools.GeneralUtils.block;
+import static org.codehaus.groovy.ast.tools.GeneralUtils.callThisX;
+import static org.codehaus.groovy.ast.tools.GeneralUtils.constX;
+import static org.codehaus.groovy.ast.tools.GeneralUtils.stmt;
+
+/**
+ * Handles the {@link LoopInvariant} annotation by inserting an {@code assert}
for the
+ * invariant expression string at the top of the annotated loop body.
+ * <p>
+ * The annotation may be applied to {@code for}, {@code while}, or {@code
do-while} loops.
+ * For this demonstration, the invariant string is shown in an assert message.
A production
+ * implementation would parse and evaluate the invariant expression.
Review Comment:
The class Javadoc says this transform inserts an `assert` for the invariant,
but the implementation currently injects a `println` statement. Update the
documentation to match the behavior (or change the injected statement to an
`assert`) so users don’t get misleading semantics.
```suggestion
* Handles the {@link LoopInvariant} annotation by inserting a {@code
println} that reports the
* invariant expression string at the top of the annotated loop body.
* <p>
* The annotation may be applied to {@code for}, {@code while}, or {@code
do-while} loops.
* For this demonstration, the invariant string is printed for visibility at
runtime. A production
* implementation would typically parse and evaluate the invariant
expression (for example, using an assert).
```
##########
src/main/java/org/codehaus/groovy/transform/LoopInvariantASTTransformation.java:
##########
@@ -0,0 +1,118 @@
+/*
+ * 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 org.codehaus.groovy.transform;
+
+import groovy.transform.LoopInvariant;
+import org.codehaus.groovy.ast.ASTNode;
+import org.codehaus.groovy.ast.AnnotationNode;
+import org.codehaus.groovy.ast.expr.ConstantExpression;
+import org.codehaus.groovy.ast.expr.Expression;
+import org.codehaus.groovy.ast.stmt.BlockStatement;
+import org.codehaus.groovy.ast.stmt.LoopingStatement;
+import org.codehaus.groovy.ast.stmt.Statement;
+import org.codehaus.groovy.control.CompilePhase;
+import org.codehaus.groovy.control.SourceUnit;
+import org.codehaus.groovy.control.messages.SyntaxErrorMessage;
+import org.codehaus.groovy.syntax.SyntaxException;
+
+import static org.codehaus.groovy.ast.tools.GeneralUtils.args;
+import static org.codehaus.groovy.ast.tools.GeneralUtils.block;
+import static org.codehaus.groovy.ast.tools.GeneralUtils.callThisX;
+import static org.codehaus.groovy.ast.tools.GeneralUtils.constX;
+import static org.codehaus.groovy.ast.tools.GeneralUtils.stmt;
+
+/**
+ * Handles the {@link LoopInvariant} annotation by inserting an {@code assert}
for the
+ * invariant expression string at the top of the annotated loop body.
+ * <p>
+ * The annotation may be applied to {@code for}, {@code while}, or {@code
do-while} loops.
+ * For this demonstration, the invariant string is shown in an assert message.
A production
+ * implementation would parse and evaluate the invariant expression.
+ *
+ * @since 6.0.0
+ * @see LoopInvariant
+ */
+@GroovyASTTransformation(phase = CompilePhase.SEMANTIC_ANALYSIS)
+public class LoopInvariantASTTransformation implements ASTTransformation {
+
+ @Override
+ public void visit(final ASTNode[] nodes, final SourceUnit source) {
+ if (nodes.length != 2) return;
+ if (!(nodes[0] instanceof AnnotationNode annotation)) return;
+ if
(!annotation.getClassNode().getName().equals(LoopInvariant.class.getName()))
return;
+
+ if (!(nodes[1] instanceof LoopingStatement loopStatement)) {
+ source.getErrorCollector().addError(new SyntaxErrorMessage(
+ new SyntaxException("@LoopInvariant may only be applied to
for, while, or do-while loop statements",
+ annotation.getLineNumber(),
annotation.getColumnNumber()), source));
+ return;
+ }
+
+ Expression invariantExpr = annotation.getMember("value");
+ if (!(invariantExpr instanceof ConstantExpression)) {
+ source.getErrorCollector().addError(new SyntaxErrorMessage(
+ new SyntaxException("@LoopInvariant value must be a
constant string expression",
+ annotation.getLineNumber(),
annotation.getColumnNumber()), source));
+ return;
+ }
+
+ String invariant = (String) ((ConstantExpression)
invariantExpr).getValue();
Review Comment:
`invariantExpr` is only checked to be a `ConstantExpression`, but its value
isn’t verified to be a `String` before casting. A non-string constant would
cause a `ClassCastException` during compilation. Consider checking
`((ConstantExpression) invariantExpr).getValue() instanceof String` and
reporting a syntax error otherwise.
```suggestion
Object invariantValue = ((ConstantExpression)
invariantExpr).getValue();
if (!(invariantValue instanceof String)) {
source.getErrorCollector().addError(new SyntaxErrorMessage(
new SyntaxException("@LoopInvariant value must be a
constant string expression",
annotation.getLineNumber(),
annotation.getColumnNumber()), source));
return;
}
String invariant = (String) invariantValue;
```
##########
src/main/java/org/apache/groovy/parser/antlr4/SemanticPredicates.java:
##########
@@ -186,8 +191,39 @@ public static boolean
isInvalidLocalVariableDeclaration(TokenStream ts) {
!(BuiltInPrimitiveType == tokenType ||
Arrays.binarySearch(MODIFIER_ARRAY, tokenType) >= 0)
&& !Character.isUpperCase(nextCodePoint)
&& nextCodePoint != '@'
- && !(ASSIGN == tokenType3 || (LT == tokenType2 ||
LBRACK == tokenType2));
+ && !(ASSIGN == tokenType3 || (LT == tokenType2 ||
LBRACK == tokenType2))
+ || (nextCodePoint == '@' && isAnnotatedLoopStatement(ts));
}
+ /**
+ * When the input starts with one or more annotations, scan past them and
check whether
+ * the first non-annotation token is a loop keyword ({@code for}, {@code
while}, {@code do}).
+ * If so, the construct is an annotated loop statement, NOT a local
variable declaration.
+ *
+ * @param ts the token stream positioned at the first annotation {@code @}
token
+ * @return {@code true} if annotations are followed by a loop keyword
+ */
+ static boolean isAnnotatedLoopStatement(TokenStream ts) {
+ int idx = 1; // ts.LT(1) is '@'
+ while (ts.LT(idx).getType() == AT) {
+ idx += 2; // skip AT and annotation name
+ // skip qualifier parts of a fully-qualified annotation name, e.g.
@java.lang.Deprecated
+ while (ts.LT(idx).getType() == DOT) {
+ idx += 2; // skip DOT and next name element
+ }
+ // skip annotation arguments (parenthesised), handling nesting
+ if (ts.LT(idx).getType() == LPAREN) {
+ idx++;
+ int depth = 1;
+ while (depth > 0 && ts.LT(idx).getType() !=
org.antlr.v4.runtime.Token.EOF) {
+ int t = ts.LT(idx++).getType();
+ if (t == LPAREN) depth++;
+ else if (t == RPAREN) depth--;
+ }
+ }
+ }
+ int afterAnnotations = ts.LT(idx).getType();
+ return afterAnnotations == FOR || afterAnnotations == WHILE ||
afterAnnotations == DO;
Review Comment:
`isAnnotatedLoopStatement` doesn’t skip `NL` tokens between annotations (or
between the last annotation and the loop keyword). Because `annotationsOpt`
explicitly allows newlines, common formatting like `@Anno\nfor (...)` may not
be recognized here, reintroducing the local-variable-declaration ambiguity this
predicate is meant to avoid. Consider skipping `NL*` (and any other tokens used
by `nls`) while scanning.
##########
src/main/java/org/codehaus/groovy/control/ResolveVisitor.java:
##########
@@ -1393,6 +1393,17 @@ public void visitCatchStatement(final CatchStatement cs)
{
super.visitCatchStatement(cs);
}
+ /**
+ * Resolves the class nodes of any annotations attached to a loop statement
+ * (stored in statement metadata rather than in {@link
org.codehaus.groovy.ast.AnnotatedNode}).
+ */
+ @Override
+ protected void visitStatementAnnotations(final Statement statement) {
+ for (AnnotationNode annotation : statement.getStatementAnnotations()) {
+ visitAnnotation(annotation);
+ }
+ }
+
Review Comment:
`visitStatementAnnotations` currently only resolves statement-level
annotations but does not validate them (e.g., retention/allowed usage). With
the grammar change allowing annotations before loop keywords, non-transform
annotations like `@Deprecated` can now be written on loops and will be silently
ignored, which is inconsistent with existing annotation target checking on
`AnnotatedNode`s. Consider adding validation here (e.g., require SOURCE
retention and/or presence of `@GroovyASTTransformationClass`, and emit a syntax
error otherwise).
```suggestion
visitAnnotation(annotation);
validateStatementAnnotation(annotation, statement);
}
}
/**
* Validates that a statement-level annotation is of an allowed kind.
* <p>
* Currently, only Groovy AST transformation annotations (i.e.
annotations
* annotated with {@code @GroovyASTTransformationClass}) are supported on
* statements such as loops. Other annotations (e.g. {@code @Deprecated})
* would be silently ignored, so we report them as errors for consistency
* with annotation target checking on {@link
org.codehaus.groovy.ast.AnnotatedNode}s.
*/
private void validateStatementAnnotation(final AnnotationNode
annotation, final Statement statement) {
ClassNode annotationType = annotation.getClassNode();
if (annotationType == null) return;
annotationType = annotationType.redirect();
if (annotationType == null) return;
// Only allow Groovy AST transformation annotations on statements.
ClassNode groovyAstTransformClass =
ClassHelper.make(org.codehaus.groovy.transform.GroovyASTTransformationClass.class);
List<AnnotationNode> transformMeta =
annotationType.getAnnotations(groovyAstTransformClass);
if (transformMeta == null || transformMeta.isEmpty()) {
addError("Annotation @" + annotationType.getNameWithoutPackage()
+ " is not a Groovy AST transformation and cannot be used on
statements", annotation);
}
}
```
##########
src/main/java/groovy/transform/LoopInvariant.java:
##########
@@ -0,0 +1,65 @@
+/*
+ * 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.codehaus.groovy.transform.GroovyASTTransformationClass;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * Specifies a loop invariant for a {@code for} or {@code while} loop.
+ * <p>
+ * The invariant is a Groovy boolean expression that must hold at the start of
each
+ * iteration. The AST transform inserts an {@code assert} for this condition
at the
+ * top of the loop body, providing lightweight design-by-contract support
inspired
+ * by languages such as Dafny.
Review Comment:
The annotation Javadoc describes injecting an `assert` for the invariant
condition, but the current transform implementation injects a `println`. Align
this documentation with the actual behavior (or update the transform) to avoid
promising contract checking that isn’t happening.
##########
src/main/java/org/apache/groovy/parser/antlr4/AstBuilder.java:
##########
@@ -470,7 +470,9 @@ public ForStatement visitForStmtAlt(final ForStmtAltContext
ctx) {
Statement loopBody = this.unpackStatement((Statement)
this.visit(ctx.statement()));
- return configureAST(maker.apply(loopBody), ctx);
+ ForStatement forStatement = configureAST(maker.apply(loopBody), ctx);
+
visitAnnotationsOpt(ctx.annotationsOpt()).forEach(forStatement::addStatementAnnotation);
+ return forStatement;
Review Comment:
This change introduces new syntax/AST behavior (annotations on loop
statements + statement-level annotation plumbing) but no corresponding tests
were added. There are existing parser tests under
`src/test/groovy/org/apache/groovy/parser/antlr4/`; consider adding coverage
that parses annotated `for`/`while`/`do-while` (including multi-line
annotations) and verifies the annotations are attached to the loop statement
and that local AST transforms are triggered.
> Allow AST transforms to be applicable in more places, initially loop
> statements
> -------------------------------------------------------------------------------
>
> Key: GROOVY-11878
> URL: https://issues.apache.org/jira/browse/GROOVY-11878
> Project: Groovy
> Issue Type: New Feature
> Reporter: Paul King
> Priority: Major
>
> The JVM doesn't support adding annotations in the bytecode for statements and
> expressions. That doesn't mean that Groovy couldn't support source-level AST
> transforms in those places. Code injection can happen in the normal way, and
> the transform will be "forgotten" after compilation.
> Examples might be:
> {code}
> @Parallel
> for (int i in 1..4) {
> println i ** 2
> }
> {code}
> which might have this output:
> {noformat}
> 1
> 16
> 9
> 4
> {noformat}
> Or these:
> {code}
> int sum = 0
> @Invariant({ 0 <= i && i <= 4 })
> for (int i in 0..4) {
> sum += i
> }
> assert sum == 10
> {code}
> {code}
> int i = 10
> @Invariant({ i >= 0 })
> @Ensures({ i > old.i }) // i decreases
> while (i > 0) {
> i = i - 1
> }
> assert i == 0
> {code}
--
This message was sent by Atlassian Jira
(v8.20.10#820010)