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