Skip to content

Commit 9cdf63b

Browse files
authored
Groovy Parser supports basic Enum classes (#5781)
1 parent 89d8e43 commit 9cdf63b

File tree

3 files changed

+238
-84
lines changed

3 files changed

+238
-84
lines changed

rewrite-groovy/src/main/java/org/openrewrite/groovy/GroovyParserVisitor.java

Lines changed: 123 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@
1919
import groovy.transform.Field;
2020
import groovy.transform.Generated;
2121
import groovy.transform.Immutable;
22-
import groovyjarjarasm.asm.Opcodes;
2322
import lombok.Getter;
2423
import lombok.RequiredArgsConstructor;
2524
import lombok.Value;
@@ -240,21 +239,22 @@ public void visitClass(ClassNode clazz) {
240239
List<J.Modifier> modifiers = getModifiers();
241240

242241
Space kindPrefix = whitespace();
243-
J.ClassDeclaration.Kind.Type kindType = null;
242+
J.ClassDeclaration.Kind.Type kindType;
244243
if (sourceStartsWith("class")) {
245244
kindType = J.ClassDeclaration.Kind.Type.Class;
246245
skip("class");
247-
} else if (sourceStartsWith("interface")) {
248-
kindType = J.ClassDeclaration.Kind.Type.Interface;
249-
skip("interface");
250-
} else if (sourceStartsWith("@interface")) {
246+
} else if (clazz.isAnnotationDefinition()) {
251247
kindType = J.ClassDeclaration.Kind.Type.Annotation;
252248
skip("@interface");
253-
} else if (sourceStartsWith("enum")) {
249+
} else if (clazz.isInterface()) {
250+
kindType = J.ClassDeclaration.Kind.Type.Interface;
251+
skip("interface");
252+
} else if (clazz.isEnum()) {
254253
kindType = J.ClassDeclaration.Kind.Type.Enum;
255254
skip("enum");
255+
} else {
256+
throw new IllegalStateException("Unexpected class type: " + name());
256257
}
257-
assert kindType != null;
258258
J.ClassDeclaration.Kind kind = new J.ClassDeclaration.Kind(randomId(), kindPrefix, Markers.EMPTY, emptyList(), kindType);
259259
J.Identifier name = new J.Identifier(randomId(), whitespace(), Markers.EMPTY, emptyList(), name(), typeMapping.type(clazz), null);
260260
JContainer<J.TypeParameter> typeParameterContainer = null;
@@ -310,6 +310,7 @@ public void visitClass(ClassNode clazz) {
310310

311311
J.Block visitClassBlock(ClassNode clazz) {
312312
NavigableMap<LineColumn, ASTNode> sortedByPosition = new TreeMap<>();
313+
List<FieldNode> enumConstants = new ArrayList<>();
313314
for (MethodNode method : clazz.getMethods()) {
314315
// Most synthetic methods do not appear in source code and should be skipped entirely.
315316
if (method.isSynthetic()) {
@@ -350,7 +351,11 @@ class A {
350351
if (!appearsInSource(field)) {
351352
continue;
352353
}
353-
if (field.hasInitialExpression() && field.getInitialExpression() instanceof ConstructorCallExpression) {
354+
if (field.isEnum()) {
355+
enumConstants.add(field);
356+
continue;
357+
}
358+
if (field.getInitialExpression() instanceof ConstructorCallExpression) {
354359
ConstructorCallExpression cce = (ConstructorCallExpression) field.getInitialExpression();
355360
if (cce.isUsingAnonymousInnerClass() && cce.getType() instanceof InnerClassNode) {
356361
fieldInitializers.add((InnerClassNode) cce.getType());
@@ -373,9 +378,35 @@ class A {
373378
sortedByPosition.put(pos(icn), icn);
374379
}
375380

376-
return new J.Block(randomId(), sourceBefore("{"), Markers.EMPTY,
381+
Space blockPrefix = sourceBefore("{");
382+
383+
List<JRightPadded<Statement>> statements = new ArrayList<>();
384+
if (!enumConstants.isEmpty()) {
385+
enumConstants.sort(Comparator.comparing(GroovyParserVisitor::pos));
386+
387+
List<JRightPadded<J.EnumValue>> enumValues = new ArrayList<>();
388+
for (int i = 0; i < enumConstants.size(); i++) {
389+
J.EnumValue enumValue = visitEnumField(enumConstants.get(i));
390+
JRightPadded<J.EnumValue> paddedEnumValue = JRightPadded.build(enumValue).withAfter(whitespace());
391+
if (sourceStartsWith(",")) {
392+
skip(",");
393+
if (i == enumConstants.size() - 1) {
394+
paddedEnumValue = paddedEnumValue.withMarkers(Markers.build(singleton(new TrailingComma(randomId(), whitespace()))));
395+
}
396+
}
397+
enumValues.add(paddedEnumValue);
398+
}
399+
400+
J.EnumValueSet enumValueSet = new J.EnumValueSet(randomId(), EMPTY, Markers.EMPTY, enumValues, sourceStartsWith(";"));
401+
if (enumValueSet.isTerminatedWithSemicolon()) {
402+
skip(";");
403+
}
404+
statements.add(JRightPadded.build(enumValueSet));
405+
}
406+
407+
return new J.Block(randomId(), blockPrefix, Markers.EMPTY,
377408
JRightPadded.build(false),
378-
sortedByPosition.values().stream()
409+
ListUtils.concatAll(statements, sortedByPosition.values().stream()
379410
// anonymous classes will be visited as part of visiting the ConstructorCallExpression
380411
.filter(ast -> !(ast instanceof InnerClassNode && ((InnerClassNode) ast).isAnonymous()))
381412
.map(ast -> {
@@ -391,7 +422,7 @@ class A {
391422
Statement stat = pollQueue();
392423
return maybeSemicolon(stat);
393424
})
394-
.collect(toList()),
425+
.collect(toList())),
395426
sourceBefore("}"));
396427
}
397428

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

404435
@Override
405436
public void visitField(FieldNode field) {
406-
if ((field.getModifiers() & Opcodes.ACC_ENUM) != 0) {
407-
visitEnumField(field);
408-
} else {
409-
visitVariableField(field);
437+
if (field.isEnum()) {
438+
// Enum constants are handled separately in visitClassBlock, thus should be skipped here
439+
return;
410440
}
441+
visitVariableField(field);
411442
}
412443

413-
private void visitEnumField(@SuppressWarnings("unused") FieldNode fieldNode) {
414-
// Requires refactoring visitClass to use a similar pattern as Java11ParserVisitor.
415-
// Currently, each field is visited one at a time, so we cannot construct the EnumValueSet.
416-
throw new UnsupportedOperationException("enum fields are not implemented.");
444+
private J.EnumValue visitEnumField(FieldNode field) {
445+
Space prefix = whitespace();
446+
447+
List<J.Annotation> annotations = visitAndGetAnnotations(field, this);
448+
449+
Space namePrefix = whitespace();
450+
String enumName = field.getName();
451+
skip(enumName);
452+
453+
J.Identifier name = new J.Identifier(randomId(), namePrefix, Markers.EMPTY, emptyList(), enumName, typeMapping.type(field.getType()), typeMapping.variableType(field));
454+
455+
// TODO initializer (enum constructor invocation)
456+
457+
return new J.EnumValue(randomId(), prefix, Markers.EMPTY, annotations, name, null);
417458
}
418459

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

468509
List<J.Annotation> annotations = visitAndGetAnnotations(method, this);
469510
List<J.Modifier> modifiers = getModifiers();
511+
boolean isConstructorOfEnum = false;
470512
boolean isConstructorOfInnerNonStaticClass = false;
471513
J.TypeParameters typeParameters = null;
472514
if (method.getGenericsTypes() != null) {
@@ -484,21 +526,35 @@ public void visitMethod(MethodNode method) {
484526
Space namePrefix = whitespace();
485527
String methodName;
486528
if (method instanceof ConstructorNode) {
487-
/*
488-
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:
489-
class A { class A {
490-
class B { class B {
491-
String s String s
492-
B(String s) { => B(A $p$, String s) {
493-
=> new Object().this$0 = $p$
494-
this.s = s => this.s = s
529+
// To support special constructors well, the groovy compiler adds extra parameters and statements to the constructor under the hood.
530+
// In our LST, we don't need this internal logic.
531+
if (method.getDeclaringClass().isEnum()) {
532+
/*
533+
For enums, there are two extra parameters and wraps the block in a super call:
534+
enum A { enum A {
535+
A1 A1
536+
A(String s) { => A(String __str, int __int, String s) {
537+
println "ss" => super() { println "ss" }
538+
} }
495539
} }
496-
} }
540+
*/
541+
isConstructorOfEnum = true;
542+
} else {
543+
/*
544+
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):
545+
class A { class A {
546+
class B { class B {
547+
String s String s
548+
B(String s) { => B(A $p$, String s) {
549+
=> new Object().this$0 = $p$
550+
this.s = s => this.s = s
551+
} }
552+
} }
553+
}
554+
See also: https://groovy-lang.org/differences.html#_creating_instances_of_non_static_inner_classes
555+
*/
556+
isConstructorOfInnerNonStaticClass = method.getDeclaringClass() instanceof InnerClassNode && (method.getDeclaringClass().getModifiers() & Modifier.STATIC) == 0;
497557
}
498-
In our LST, we don't need this internal logic, so we'll skip the first param + first two statements (ConstructorCallExpression and BlockStatement)}
499-
See also: https://groovy-lang.org/differences.html#_creating_instances_of_non_static_inner_classes
500-
*/
501-
isConstructorOfInnerNonStaticClass = method.getDeclaringClass() instanceof InnerClassNode && (method.getDeclaringClass().getModifiers() & Modifier.STATIC) == 0;
502558
methodName = method.getDeclaringClass().getNameWithoutPackage().replaceFirst(".*\\$", "");
503559
} else if (source.startsWith(method.getName(), cursor)) {
504560
methodName = method.getName();
@@ -521,14 +577,22 @@ class B { class B {
521577
Space beforeParen = sourceBefore("(");
522578
List<JRightPadded<Statement>> params = new ArrayList<>(method.getParameters().length);
523579
Parameter[] unparsedParams = method.getParameters();
524-
for (int i = (isConstructorOfInnerNonStaticClass ? 1 : 0); i < unparsedParams.length; i++) {
580+
int skipParams = isConstructorOfEnum ? 2 : isConstructorOfInnerNonStaticClass ? 1 : 0;
581+
for (int i = skipParams; i < unparsedParams.length; i++) {
525582
Parameter param = unparsedParams[i];
526583

527584
List<J.Annotation> paramAnnotations = visitAndGetAnnotations(param, this);
528585
List<J.Modifier> paramModifiers = getModifiers();
529-
TypeTree paramType = param.isDynamicTyped() ?
530-
new J.Identifier(randomId(), EMPTY, Markers.EMPTY, emptyList(), "", JavaType.ShallowClass.build("java.lang.Object"), null) :
531-
visitTypeTree(param.getOriginType());
586+
TypeTree paramType;
587+
if (param.isDynamicTyped()) {
588+
if (sourceStartsWith("java.lang.Object")) {
589+
paramType = new J.Identifier(randomId(), whitespace(), Markers.EMPTY, emptyList(), skip(name()), JavaType.ShallowClass.build("java.lang.Object"), null);
590+
} else {
591+
paramType = new J.Identifier(randomId(), EMPTY, Markers.EMPTY, emptyList(), "", JavaType.ShallowClass.build("java.lang.Object"), null);
592+
}
593+
} else {
594+
paramType = visitTypeTree(param.getType());
595+
}
532596

533597
Space varargs = null;
534598
if (paramType instanceof J.ArrayType && sourceStartsWith("...")) {
@@ -560,7 +624,7 @@ class B { class B {
560624
singletonList(paramName))).withAfter(rightPad));
561625
}
562626

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

@@ -572,13 +636,23 @@ class B { class B {
572636

573637
J.Block body = null;
574638
if (method.getCode() != null) {
575-
ASTNode code = isConstructorOfInnerNonStaticClass ?
576-
new BlockStatement(
577-
((BlockStatement) method.getCode()).getStatements().subList(2, ((BlockStatement) method.getCode()).getStatements().size()),
578-
((BlockStatement) method.getCode()).getVariableScope()
579-
) :
580-
method.getCode();
581-
body = bodyVisitor.visit(code);
639+
if (isConstructorOfInnerNonStaticClass) {
640+
body = bodyVisitor.visit(
641+
new BlockStatement(
642+
((BlockStatement) method.getCode()).getStatements().subList(2, ((BlockStatement) method.getCode()).getStatements().size()),
643+
((BlockStatement) method.getCode()).getVariableScope()
644+
)
645+
);
646+
} else if (isConstructorOfEnum && ((BlockStatement) method.getCode()).getStatements().size() > 1) {
647+
org.codehaus.groovy.ast.stmt.Statement node = ((BlockStatement) method.getCode()).getStatements().get(1);
648+
if (node instanceof BlockStatement) {
649+
body = bodyVisitor.visit(node);
650+
} else {
651+
body = bodyVisitor.visit(method.getCode());
652+
}
653+
} else {
654+
body = bodyVisitor.visit(method.getCode());
655+
}
582656
}
583657

584658
queue.add(new J.MethodDeclaration(
@@ -1372,7 +1446,7 @@ public void visitConstantExpression(ConstantExpression expression) {
13721446
}
13731447
jType = JavaType.Primitive.Null;
13741448
} else {
1375-
throw new IllegalStateException("Unexpected constant type " + type);
1449+
throw new IllegalStateException("Unexpected constant type: " + type);
13761450
}
13771451

13781452
// 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"
@@ -2960,6 +3034,9 @@ private boolean appearsInSource(ASTNode node) {
29603034
String[] parts = name.split("\\.");
29613035
return sourceStartsWith("@" + name) || sourceStartsWith("@" + parts[parts.length - 1]);
29623036
}
3037+
if (node instanceof ConstructorNode && ((ConstructorNode) node).getDeclaringClass().isEnum()) {
3038+
return ((ConstructorNode) node).getAnnotations(new ClassNode(Generated.class)).isEmpty();
3039+
}
29633040

29643041
return node.getColumnNumber() >= 0 && node.getLineNumber() >= 0 && node.getLastColumnNumber() >= 0 && node.getLastLineNumber() >= 0;
29653042
}

rewrite-groovy/src/test/java/org/openrewrite/groovy/tree/ClassDeclarationTest.java

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -274,6 +274,32 @@ def A() {}
274274
);
275275
}
276276

277+
@Test
278+
void constructorWithDynamicallyTypedParam() {
279+
rewriteRun(
280+
groovy(
281+
"""
282+
class A {
283+
A(dynamicVar) {}
284+
}
285+
"""
286+
)
287+
);
288+
}
289+
290+
@Test
291+
void constructorWithDynamicallyTypedParamWithName() {
292+
rewriteRun(
293+
groovy(
294+
"""
295+
class A {
296+
A(Object a, java.lang.Object b) {}
297+
}
298+
"""
299+
)
300+
);
301+
}
302+
277303
@Test
278304
void constructorForClassInPackage() {
279305
rewriteRun(

0 commit comments

Comments
 (0)