This is an automated email from the ASF dual-hosted git repository.

emilles 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 cfe035301c GROOVY-11492, GROOVY-11845: curly brace array for attribute 
default
cfe035301c is described below

commit cfe035301c5aa11519ea8cb3cd3821dcf68bf6d0
Author: Eric Milles <[email protected]>
AuthorDate: Wed Jan 21 13:24:15 2026 -0600

    GROOVY-11492, GROOVY-11845: curly brace array for attribute default
---
 src/antlr/GroovyParser.g4                          |  1 +
 .../apache/groovy/parser/antlr4/AstBuilder.java    | 66 +++++++---------
 .../core/AnnotationDeclaration_01.groovy           | 12 ++-
 .../groovy/gls/annotations/AnnotationTest.groovy   | 91 +++++++++++++++-------
 4 files changed, 98 insertions(+), 72 deletions(-)

diff --git a/src/antlr/GroovyParser.g4 b/src/antlr/GroovyParser.g4
index e97417c651..88e3b8b713 100644
--- a/src/antlr/GroovyParser.g4
+++ b/src/antlr/GroovyParser.g4
@@ -543,6 +543,7 @@ elementValue
 
 elementValueArrayInitializer
     :   LBRACK (elementValue (COMMA elementValue)* COMMA?)? RBRACK
+    |   LBRACE (elementValue COMMA)+ elementValue? RBRACE // avoid ambiguity 
with closure
     ;
 
 // STATEMENTS / BLOCKS
diff --git a/src/main/java/org/apache/groovy/parser/antlr4/AstBuilder.java 
b/src/main/java/org/apache/groovy/parser/antlr4/AstBuilder.java
index a2b5c6f981..635f8dfe2f 100644
--- a/src/main/java/org/apache/groovy/parser/antlr4/AstBuilder.java
+++ b/src/main/java/org/apache/groovy/parser/antlr4/AstBuilder.java
@@ -159,10 +159,8 @@ 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.callX;
 import static org.codehaus.groovy.ast.tools.GeneralUtils.closureX;
-import static org.codehaus.groovy.ast.tools.GeneralUtils.declS;
 import static org.codehaus.groovy.ast.tools.GeneralUtils.declX;
 import static org.codehaus.groovy.ast.tools.GeneralUtils.listX;
-import static org.codehaus.groovy.ast.tools.GeneralUtils.localVarX;
 import static org.codehaus.groovy.ast.tools.GeneralUtils.returnS;
 import static org.codehaus.groovy.ast.tools.GeneralUtils.stmt;
 import static org.codehaus.groovy.ast.tools.GeneralUtils.varX;
@@ -1820,19 +1818,24 @@ public class AstBuilder extends 
GroovyParserBaseVisitor<Object> {
     }
 
     private MethodNode createMethodNodeForClass(final MethodDeclarationContext 
ctx, final ModifierManager modifierManager, final String methodName, final 
ClassNode returnType, final Parameter[] parameters, final ClassNode[] 
exceptions, Statement code, final ClassNode classNode, int modifiers) {
-        if (asBoolean(ctx.elementValue())) { // the code of annotation method
-            code = configureAST(
-                    new ExpressionStatement(
-                            this.visitElementValue(ctx.elementValue())),
-                    ctx.elementValue());
+        ElementValueContext elementValue = ctx.elementValue();
+        if (asBoolean(elementValue)) {
+            assert this.isAnnotationDeclaration(classNode);
+            Expression defaultValue = this.visitElementValue(elementValue);
+            if (returnType.isArray() && defaultValue instanceof 
ClosureExpression closure // GROOVY-11492
+                    && ClosureUtils.hasImplicitParameter(closure) && 
closure.getCode() instanceof BlockStatement block) {
+                defaultValue = listX(block.getStatements().stream().map(s -> 
((ExpressionStatement) s).getExpression()).toList());
+            }
+            code = configureAST(stmt(defaultValue), elementValue);
+        }
 
+        if (classNode.isInterface() && !modifierManager.containsAny(STATIC) && 
!(isTrue(classNode, IS_INTERFACE_WITH_DEFAULT_METHODS) && 
modifierManager.containsAny(DEFAULT, PRIVATE))) {
+            modifiers |= Opcodes.ACC_ABSTRACT;
         }
 
-        modifiers |= !modifierManager.containsAny(STATIC) && 
classNode.isInterface() && !(isTrue(classNode, 
IS_INTERFACE_WITH_DEFAULT_METHODS) && modifierManager.containsAny(DEFAULT, 
PRIVATE)) ? Opcodes.ACC_ABSTRACT : 0;
         MethodNode methodNode = new MethodNode(methodName, modifiers, 
returnType, parameters, exceptions, code);
+        methodNode.setAnnotationDefault(asBoolean(elementValue));
         classNode.addMethod(methodNode);
-
-        methodNode.setAnnotationDefault(asBoolean(ctx.elementValue()));
         return methodNode;
     }
 
@@ -2246,34 +2249,19 @@ public class AstBuilder extends 
GroovyParserBaseVisitor<Object> {
      *        ,it may be a command expression
      */
     private void validateInvalidMethodDefinition(final Expression baseExpr, 
final Expression arguments) {
-        if (baseExpr instanceof VariableExpression) {
-            if (isBuiltInType(baseExpr) || 
Character.isUpperCase(baseExpr.getText().codePointAt(0))) {
-                if (arguments instanceof ArgumentListExpression) {
-                    List<Expression> expressionList = 
((ArgumentListExpression) arguments).getExpressions();
-                    if (1 == expressionList.size()) {
-                        final Expression expression = expressionList.get(0);
-                        if (expression instanceof MethodCallExpression mce) {
-                            final Expression methodCallArguments = 
mce.getArguments();
-
-                            // check the method call tails with a closure
-                            if (methodCallArguments instanceof 
ArgumentListExpression) {
-                                List<Expression> 
methodCallArgumentExpressionList = ((ArgumentListExpression) 
methodCallArguments).getExpressions();
-                                final int argumentCnt = 
methodCallArgumentExpressionList.size();
-                                if (argumentCnt > 0) {
-                                    final Expression lastArgumentExpression = 
methodCallArgumentExpressionList.get(argumentCnt - 1);
-                                    if (lastArgumentExpression instanceof 
ClosureExpression) {
-                                        if 
(ClosureUtils.hasImplicitParameter(((ClosureExpression) 
lastArgumentExpression))) {
-                                            throw createParsingFailedException(
-                                                    "Method definition not 
expected here",
-                                                    
tuple(baseExpr.getLineNumber(), baseExpr.getColumnNumber()),
-                                                    
tuple(expression.getLastLineNumber(), expression.getLastColumnNumber())
-                                            );
-                                        }
-                                    }
-                                }
-                            }
-                        }
-                    }
+        if (baseExpr instanceof VariableExpression && arguments instanceof 
ArgumentListExpression argsList
+                && (isBuiltInType(baseExpr) || 
Character.isUpperCase(baseExpr.getText().codePointAt(0)))) {
+            List<Expression> exprList = argsList.getExpressions();
+            if (exprList.size() == 1
+                    && exprList.get(0) instanceof MethodCallExpression callExpr
+                    && callExpr.getArguments() instanceof 
ArgumentListExpression callArgs) {
+                exprList = callArgs.getExpressions(); // check the method call 
tails with a closure
+                if (!exprList.isEmpty() && last(exprList) instanceof 
ClosureExpression closure && ClosureUtils.hasImplicitParameter(closure)) {
+                    throw createParsingFailedException(
+                            "Method definition not expected here",
+                            tuple(baseExpr.getLineNumber(), 
baseExpr.getColumnNumber()),
+                            tuple(callExpr.getLastLineNumber(), 
callExpr.getLastColumnNumber())
+                    );
                 }
             }
         }
@@ -4226,7 +4214,7 @@ public class AstBuilder extends 
GroovyParserBaseVisitor<Object> {
 
     @Override
     public ListExpression visitElementValueArrayInitializer(final 
ElementValueArrayInitializerContext ctx) {
-        return configureAST(new 
ListExpression(ctx.elementValue().stream().map(this::visitElementValue).collect(Collectors.toList())),
 ctx);
+        return 
configureAST(listX(ctx.elementValue().stream().map(this::visitElementValue).toList()),
 ctx);
     }
 
     @Override
diff --git a/src/test-resources/core/AnnotationDeclaration_01.groovy 
b/src/test-resources/core/AnnotationDeclaration_01.groovy
index 8a085914db..fcf4254a70 100644
--- a/src/test-resources/core/AnnotationDeclaration_01.groovy
+++ b/src/test-resources/core/AnnotationDeclaration_01.groovy
@@ -16,8 +16,7 @@
  *  specific language governing permissions and limitations
  *  under the License.
  */
-import org.codehaus.groovy.transform.GroovyASTTransformationClass
-
+import org.codehaus.groovy.transform.*
 import java.lang.annotation.Documented
 import java.lang.annotation.ElementType
 import java.lang.annotation.Retention
@@ -58,4 +57,11 @@ import java.lang.annotation.Target
 
 @interface C {
     String name() default ''
-}
\ No newline at end of file
+}
+
+//GROOVY-11492
+@interface D {
+    String[] zero() default { }
+    String[] one () default { "foo" }
+    String[] two () default { "foo", "bar" }
+}
diff --git a/src/test/groovy/gls/annotations/AnnotationTest.groovy 
b/src/test/groovy/gls/annotations/AnnotationTest.groovy
index 58433125b2..d542d60194 100644
--- a/src/test/groovy/gls/annotations/AnnotationTest.groovy
+++ b/src/test/groovy/gls/annotations/AnnotationTest.groovy
@@ -19,7 +19,9 @@
 package gls.annotations
 
 import groovy.test.NotYetImplemented
-import org.junit.Test
+import org.junit.jupiter.api.Test
+import org.junit.jupiter.params.ParameterizedTest
+import org.junit.jupiter.params.provider.ValueSource
 
 import static groovy.test.GroovyAssert.assertScript
 import static groovy.test.GroovyAssert.shouldFail
@@ -73,7 +75,7 @@ final class AnnotationTest {
      */
     @Test
     void testOmittingBracketsForSingleValueArrayParameter() {
-        shell.parse '''
+        assertScript shell, '''
             import gls.annotations.*
 
             class Book {}
@@ -97,7 +99,7 @@ final class AnnotationTest {
         // If this should be changed, then further discussion is needed.
         shouldFail shell, '''
             @interface X {
-                int x() default "1" // must be integer
+                int x() default '1' // must be integer
             }
         '''
 
@@ -138,46 +140,75 @@ final class AnnotationTest {
         assertScript shell, '''
             @Retention(RetentionPolicy.RUNTIME)
             @Target(ElementType.TYPE)
-            @interface Temp {
-                String[] bar() default '1' // coerced to list as per Java but 
must be correct type
+            @interface A {
+                String[] values() default '1' // coerced to list as per Java 
but must be correct type
             }
 
-            @Temp
-            class Bar {}
-
-            assert Bar.getAnnotation(Temp).bar() == ['1']
+            @A class B { }
+            String[] array = ['1']
+            assert B.getAnnotation(A).values() == array
         '''
 
-        shouldFail shell, '''
-            @interface X {
-                String[] x() default ["1",2] // list must contain elements of 
correct type
+        def err = shouldFail shell, '''
+            @interface A {
+                String[] values() default ['1',2] // list must contain 
elements of correct type
             }
         '''
+        assert err =~ /Attribute 'values' should have type 'java.lang.String'; 
but found type 'int' in @A/
+    }
 
-        shell.parse '''
-            @interface X {
-                String[] x() default ["a","b"]
+    // GROOVY-11492
+    @ParameterizedTest
+    @ValueSource(strings=['','"a"','"a","b"','"a","b",'])
+    void testArrayDefault2(String values) {
+        assertScript shell, """
+            @Retention(RetentionPolicy.RUNTIME)
+            @Target(ElementType.TYPE)
+            @interface A {
+                String[] values() default [$values]
             }
-        '''
+
+            @A class B { }
+            String[] array = [$values]
+            assert B.getAnnotation(A).values() == array
+        """
+
+        assertScript shell, """
+            @Retention(RetentionPolicy.RUNTIME)
+            @Target(ElementType.TYPE)
+            @interface A {
+                String[] values() default {$values}
+            }
+
+            @A class B { }
+            String[] array = [$values]
+            assert B.getAnnotation(A).values() == array
+        """
     }
 
     @Test
     void testClassDefault() {
-        shouldFail shell, '''
-            @interface X {
-                Class x() default "1" // must be list
+        shell.parse '''
+            @interface A {
+                Class c() default Number
             }
         '''
 
         shell.parse '''
-            @interface X {
-                Class x() default Number.class // class with .class
+            @interface A {
+                Class c() default Number.class
             }
         '''
 
         shell.parse '''
-            @interface X {
-                Class x() default Number
+            @interface A {
+                Class c() default { -> } // closure
+            }
+        '''
+
+        shouldFail shell, '''
+            @interface A {
+                Class c() default '1' // must be class
             }
         '''
     }
@@ -186,7 +217,7 @@ final class AnnotationTest {
     void testEnumDefault() {
         shouldFail shell, '''
             @interface X {
-                ElementType x() default "1" // must be Enum
+                ElementType x() default '1' // must be Enum
             }
         '''
 
@@ -207,7 +238,7 @@ final class AnnotationTest {
     void testAnnotationDefault() {
         shouldFail shell, '''
             @interface X {
-                Target x() default "1" // must be Annotation
+                Target x() default '1' // must be Annotation
             }
         '''
 
@@ -250,7 +281,7 @@ final class AnnotationTest {
     void testParameter() {
         shouldFail shell, '''
             @interface X {
-                String x(int x) default "" // annotation members can't have 
parameters
+                String x(int x) default '' // annotation members can't have 
parameters
             }
         '''
     }
@@ -300,22 +331,22 @@ final class AnnotationTest {
                 String stringValue()
                 int intValue()
                 int defaultInt() default 1
-                String defaultString() default ""
+                String defaultString() default ''
                 Class defaultClass() default Integer.class
                 ElementType defaultEnum() default ElementType.TYPE
                 Target defaultAnnotation() default @Target([ElementType.TYPE])
             }
 
-            @MyAnnotation(stringValue = "for class", intValue = 100)
+            @MyAnnotation(stringValue = 'for class', intValue = 100)
             class Foo {}
 
             Annotation[] annotations = Foo.class.annotations
             assert annotations.size() == 1
             MyAnnotation my = annotations[0]
-            assert my.stringValue() == "for class"
+            assert my.stringValue() == 'for class'
             assert my.intValue() == 100
             assert my.defaultInt() == 1
-            assert my.defaultString() == ""
+            assert my.defaultString() == ''
             assert my.defaultClass() == Integer
 
             assert my.defaultEnum() == ElementType.TYPE

Reply via email to