Skip to content

Groovy Parser supports basic Enum classes #5781

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 25 commits into from
Jul 25, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@
import groovy.transform.Field;
import groovy.transform.Generated;
import groovy.transform.Immutable;
import groovyjarjarasm.asm.Opcodes;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import lombok.Value;
Expand Down Expand Up @@ -240,21 +239,22 @@ public void visitClass(ClassNode clazz) {
List<J.Modifier> modifiers = getModifiers();

Space kindPrefix = whitespace();
J.ClassDeclaration.Kind.Type kindType = null;
J.ClassDeclaration.Kind.Type kindType;
if (sourceStartsWith("class")) {
kindType = J.ClassDeclaration.Kind.Type.Class;
skip("class");
} else if (sourceStartsWith("interface")) {
kindType = J.ClassDeclaration.Kind.Type.Interface;
skip("interface");
} else if (sourceStartsWith("@interface")) {
} else if (clazz.isAnnotationDefinition()) {
kindType = J.ClassDeclaration.Kind.Type.Annotation;
skip("@interface");
} else if (sourceStartsWith("enum")) {
} else if (clazz.isInterface()) {
kindType = J.ClassDeclaration.Kind.Type.Interface;
skip("interface");
} else if (clazz.isEnum()) {
kindType = J.ClassDeclaration.Kind.Type.Enum;
skip("enum");
} else {
throw new IllegalStateException("Unexpected class type: " + name());
}
assert kindType != null;
J.ClassDeclaration.Kind kind = new J.ClassDeclaration.Kind(randomId(), kindPrefix, Markers.EMPTY, emptyList(), kindType);
J.Identifier name = new J.Identifier(randomId(), whitespace(), Markers.EMPTY, emptyList(), name(), typeMapping.type(clazz), null);
JContainer<J.TypeParameter> typeParameterContainer = null;
Expand Down Expand Up @@ -310,6 +310,7 @@ public void visitClass(ClassNode clazz) {

J.Block visitClassBlock(ClassNode clazz) {
NavigableMap<LineColumn, ASTNode> sortedByPosition = new TreeMap<>();
List<FieldNode> enumConstants = new ArrayList<>();
for (MethodNode method : clazz.getMethods()) {
// Most synthetic methods do not appear in source code and should be skipped entirely.
if (method.isSynthetic()) {
Expand Down Expand Up @@ -350,7 +351,11 @@ class A {
if (!appearsInSource(field)) {
continue;
}
if (field.hasInitialExpression() && field.getInitialExpression() instanceof ConstructorCallExpression) {
if (field.isEnum()) {
enumConstants.add(field);
continue;
}
if (field.getInitialExpression() instanceof ConstructorCallExpression) {
ConstructorCallExpression cce = (ConstructorCallExpression) field.getInitialExpression();
if (cce.isUsingAnonymousInnerClass() && cce.getType() instanceof InnerClassNode) {
fieldInitializers.add((InnerClassNode) cce.getType());
Expand All @@ -373,9 +378,35 @@ class A {
sortedByPosition.put(pos(icn), icn);
}

return new J.Block(randomId(), sourceBefore("{"), Markers.EMPTY,
Space blockPrefix = sourceBefore("{");

List<JRightPadded<Statement>> statements = new ArrayList<>();
if (!enumConstants.isEmpty()) {
enumConstants.sort(Comparator.comparing(GroovyParserVisitor::pos));

List<JRightPadded<J.EnumValue>> enumValues = new ArrayList<>();
for (int i = 0; i < enumConstants.size(); i++) {
J.EnumValue enumValue = visitEnumField(enumConstants.get(i));
JRightPadded<J.EnumValue> paddedEnumValue = JRightPadded.build(enumValue).withAfter(whitespace());
if (sourceStartsWith(",")) {
skip(",");
if (i == enumConstants.size() - 1) {
paddedEnumValue = paddedEnumValue.withMarkers(Markers.build(singleton(new TrailingComma(randomId(), whitespace()))));
}
}
enumValues.add(paddedEnumValue);
}

J.EnumValueSet enumValueSet = new J.EnumValueSet(randomId(), EMPTY, Markers.EMPTY, enumValues, sourceStartsWith(";"));
if (enumValueSet.isTerminatedWithSemicolon()) {
skip(";");
}
statements.add(JRightPadded.build(enumValueSet));
}

return new J.Block(randomId(), blockPrefix, Markers.EMPTY,
JRightPadded.build(false),
sortedByPosition.values().stream()
ListUtils.concatAll(statements, sortedByPosition.values().stream()
// anonymous classes will be visited as part of visiting the ConstructorCallExpression
.filter(ast -> !(ast instanceof InnerClassNode && ((InnerClassNode) ast).isAnonymous()))
.map(ast -> {
Expand All @@ -391,7 +422,7 @@ class A {
Statement stat = pollQueue();
return maybeSemicolon(stat);
})
.collect(toList()),
.collect(toList())),
sourceBefore("}"));
}

Expand All @@ -403,17 +434,27 @@ public void visitBlockStatement(BlockStatement statement) {

@Override
public void visitField(FieldNode field) {
if ((field.getModifiers() & Opcodes.ACC_ENUM) != 0) {
visitEnumField(field);
} else {
visitVariableField(field);
if (field.isEnum()) {
// Enum constants are handled separately in visitClassBlock, thus should be skipped here
return;
}
visitVariableField(field);
}

private void visitEnumField(@SuppressWarnings("unused") FieldNode fieldNode) {
// Requires refactoring visitClass to use a similar pattern as Java11ParserVisitor.
// Currently, each field is visited one at a time, so we cannot construct the EnumValueSet.
throw new UnsupportedOperationException("enum fields are not implemented.");
private J.EnumValue visitEnumField(FieldNode field) {
Space prefix = whitespace();

List<J.Annotation> annotations = visitAndGetAnnotations(field, this);

Space namePrefix = whitespace();
String enumName = field.getName();
skip(enumName);

J.Identifier name = new J.Identifier(randomId(), namePrefix, Markers.EMPTY, emptyList(), enumName, typeMapping.type(field.getType()), typeMapping.variableType(field));

// TODO initializer (enum constructor invocation)

return new J.EnumValue(randomId(), prefix, Markers.EMPTY, annotations, name, null);
}

private void visitVariableField(FieldNode field) {
Expand Down Expand Up @@ -467,6 +508,7 @@ public void visitMethod(MethodNode method) {

List<J.Annotation> annotations = visitAndGetAnnotations(method, this);
List<J.Modifier> modifiers = getModifiers();
boolean isConstructorOfEnum = false;
boolean isConstructorOfInnerNonStaticClass = false;
J.TypeParameters typeParameters = null;
if (method.getGenericsTypes() != null) {
Expand All @@ -484,21 +526,35 @@ public void visitMethod(MethodNode method) {
Space namePrefix = whitespace();
String methodName;
if (method instanceof ConstructorNode) {
/*
To support Java syntax for non-static inner classes, the groovy compiler uses an extra parameter with a reference to its parent class under the hood:
class A { class A {
class B { class B {
String s String s
B(String s) { => B(A $p$, String s) {
=> new Object().this$0 = $p$
this.s = s => this.s = s
// To support special constructors well, the groovy compiler adds extra parameters and statements to the constructor under the hood.
// In our LST, we don't need this internal logic.
if (method.getDeclaringClass().isEnum()) {
/*
For enums, there are two extra parameters and wraps the block in a super call:
enum A { enum A {
A1 A1
A(String s) { => A(String __str, int __int, String s) {
println "ss" => super() { println "ss" }
} }
} }
} }
*/
isConstructorOfEnum = true;
} else {
/*
For Java syntax for non-static inner classes, there's an extra parameter with a reference to its parent class and two statements (ConstructorCallExpression and BlockStatement):
class A { class A {
class B { class B {
String s String s
B(String s) { => B(A $p$, String s) {
=> new Object().this$0 = $p$
this.s = s => this.s = s
} }
} }
}
See also: https://groovy-lang.org/differences.html#_creating_instances_of_non_static_inner_classes
*/
isConstructorOfInnerNonStaticClass = method.getDeclaringClass() instanceof InnerClassNode && (method.getDeclaringClass().getModifiers() & Modifier.STATIC) == 0;
}
In our LST, we don't need this internal logic, so we'll skip the first param + first two statements (ConstructorCallExpression and BlockStatement)}
See also: https://groovy-lang.org/differences.html#_creating_instances_of_non_static_inner_classes
*/
isConstructorOfInnerNonStaticClass = method.getDeclaringClass() instanceof InnerClassNode && (method.getDeclaringClass().getModifiers() & Modifier.STATIC) == 0;
methodName = method.getDeclaringClass().getNameWithoutPackage().replaceFirst(".*\\$", "");
} else if (source.startsWith(method.getName(), cursor)) {
methodName = method.getName();
Expand All @@ -521,14 +577,22 @@ class B { class B {
Space beforeParen = sourceBefore("(");
List<JRightPadded<Statement>> params = new ArrayList<>(method.getParameters().length);
Parameter[] unparsedParams = method.getParameters();
for (int i = (isConstructorOfInnerNonStaticClass ? 1 : 0); i < unparsedParams.length; i++) {
int skipParams = isConstructorOfEnum ? 2 : isConstructorOfInnerNonStaticClass ? 1 : 0;
for (int i = skipParams; i < unparsedParams.length; i++) {
Parameter param = unparsedParams[i];

List<J.Annotation> paramAnnotations = visitAndGetAnnotations(param, this);
List<J.Modifier> paramModifiers = getModifiers();
TypeTree paramType = param.isDynamicTyped() ?
new J.Identifier(randomId(), EMPTY, Markers.EMPTY, emptyList(), "", JavaType.ShallowClass.build("java.lang.Object"), null) :
visitTypeTree(param.getOriginType());
TypeTree paramType;
if (param.isDynamicTyped()) {
if (sourceStartsWith("java.lang.Object")) {
paramType = new J.Identifier(randomId(), whitespace(), Markers.EMPTY, emptyList(), skip(name()), JavaType.ShallowClass.build("java.lang.Object"), null);
} else {
paramType = new J.Identifier(randomId(), EMPTY, Markers.EMPTY, emptyList(), "", JavaType.ShallowClass.build("java.lang.Object"), null);
}
} else {
paramType = visitTypeTree(param.getType());
}

Space varargs = null;
if (paramType instanceof J.ArrayType && sourceStartsWith("...")) {
Expand Down Expand Up @@ -560,7 +624,7 @@ class B { class B {
singletonList(paramName))).withAfter(rightPad));
}

if (unparsedParams.length == 0 || (isConstructorOfInnerNonStaticClass && unparsedParams.length == 1)) {
if (unparsedParams.length == skipParams) {
params.add(JRightPadded.build(new J.Empty(randomId(), sourceBefore(")"), Markers.EMPTY)));
}

Expand All @@ -572,13 +636,23 @@ class B { class B {

J.Block body = null;
if (method.getCode() != null) {
ASTNode code = isConstructorOfInnerNonStaticClass ?
new BlockStatement(
((BlockStatement) method.getCode()).getStatements().subList(2, ((BlockStatement) method.getCode()).getStatements().size()),
((BlockStatement) method.getCode()).getVariableScope()
) :
method.getCode();
body = bodyVisitor.visit(code);
if (isConstructorOfInnerNonStaticClass) {
body = bodyVisitor.visit(
new BlockStatement(
((BlockStatement) method.getCode()).getStatements().subList(2, ((BlockStatement) method.getCode()).getStatements().size()),
((BlockStatement) method.getCode()).getVariableScope()
)
);
} else if (isConstructorOfEnum && ((BlockStatement) method.getCode()).getStatements().size() > 1) {
org.codehaus.groovy.ast.stmt.Statement node = ((BlockStatement) method.getCode()).getStatements().get(1);
if (node instanceof BlockStatement) {
body = bodyVisitor.visit(node);
} else {
body = bodyVisitor.visit(method.getCode());
}
} else {
body = bodyVisitor.visit(method.getCode());
}
}

queue.add(new J.MethodDeclaration(
Expand Down Expand Up @@ -1372,7 +1446,7 @@ public void visitConstantExpression(ConstantExpression expression) {
}
jType = JavaType.Primitive.Null;
} else {
throw new IllegalStateException("Unexpected constant type " + type);
throw new IllegalStateException("Unexpected constant type: " + type);
}

// Get the string literal from the source, as numeric literals may have a unary operator, underscores, dots and can be followed by "L", "f", or "d"
Expand Down Expand Up @@ -2960,6 +3034,9 @@ private boolean appearsInSource(ASTNode node) {
String[] parts = name.split("\\.");
return sourceStartsWith("@" + name) || sourceStartsWith("@" + parts[parts.length - 1]);
}
if (node instanceof ConstructorNode && ((ConstructorNode) node).getDeclaringClass().isEnum()) {
return ((ConstructorNode) node).getAnnotations(new ClassNode(Generated.class)).isEmpty();
}

return node.getColumnNumber() >= 0 && node.getLineNumber() >= 0 && node.getLastColumnNumber() >= 0 && node.getLastLineNumber() >= 0;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -274,6 +274,32 @@ def A() {}
);
}

@Test
void constructorWithDynamicallyTypedParam() {
rewriteRun(
groovy(
"""
class A {
A(dynamicVar) {}
}
"""
)
);
}

@Test
void constructorWithDynamicallyTypedParamWithName() {
rewriteRun(
groovy(
"""
class A {
A(Object a, java.lang.Object b) {}
}
"""
)
);
}

@Test
void constructorForClassInPackage() {
rewriteRun(
Expand Down
Loading