Skip to content

Commit 5e48fcd

Browse files
More robust Groovy enum parsing (#6062)
* More robust Groovy enum parsing * Polish
1 parent c295c36 commit 5e48fcd

3 files changed

Lines changed: 22 additions & 100 deletions

File tree

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

Lines changed: 13 additions & 98 deletions
Original file line numberDiff line numberDiff line change
@@ -456,109 +456,24 @@ private J.EnumValue visitEnumField(FieldNode field) {
456456
if (sourceStartsWith("(")) {
457457
Space prefixNewClass = whitespace();
458458
skip("(");
459-
460-
// The Groovy AST does not list the enum constructor arguments anywhere, thus first get the arguments as string
461-
int start = cursor;
462-
int argCount = 0;
463-
int depth = 0;
464-
cursor = indexOfNextNonWhitespace(cursor, source);
465-
// Check if there are any arguments at all
466-
if (source.charAt(cursor) != ')') {
467-
argCount = 1; // Start with 1 since there's at least one argument
468-
}
469-
while (!(source.charAt(cursor) == ')' && depth == 0)) {
470-
int cursorBeforeIteration = cursor;
471-
472-
Delimiter delimiter = getDelimiter(null, cursor);
473-
if (delimiter != null) {
474-
cursor += delimiter.open.length();
475-
if ("\"".equals(delimiter.close)) {
476-
// This is to prevent sourceBefore interpreting // in strings as comments
477-
cursor = source.indexOf("\"", cursor) + 1;
478-
while (source.charAt(cursor - 2) == '\\') {
479-
cursor = source.indexOf("\"", cursor) + 1;
480-
}
481-
} else {
482-
sourceBefore(delimiter.close);
483-
}
484-
} else {
485-
name();
486-
// Check for named argument syntax (colon after name)
487-
cursor = indexOfNextNonWhitespace(cursor, source);
488-
if (source.charAt(cursor) == ':') {
489-
cursor++; // Skip the colon
490-
cursor = indexOfNextNonWhitespace(cursor, source);
491-
// Parse the value after the colon
492-
delimiter = getDelimiter(null, cursor);
493-
if (delimiter != null) {
494-
cursor += delimiter.open.length();
495-
if ("\"".equals(delimiter.close)) {
496-
cursor = source.indexOf("\"", cursor) + 1;
497-
while (source.charAt(cursor - 2) == '\\') {
498-
cursor = source.indexOf("\"", cursor) + 1;
499-
}
500-
} else {
501-
sourceBefore(delimiter.close);
502-
}
503-
} else {
504-
name();
505-
}
506-
}
507-
}
508-
cursor = indexOfNextNonWhitespace(cursor, source);
509-
skip(",");
510-
cursor = indexOfNextNonWhitespace(cursor, source);
511-
if (source.charAt(cursor) == '(') {
512-
depth++;
513-
cursor++;
514-
} else if (depth > 0 && source.charAt(cursor) == ')') {
515-
depth--;
516-
cursor++;
517-
}
518-
if (cursor == cursorBeforeIteration) {
519-
// We are not facing an opening of a new parens/delimiter, no whitespace, just a usual piece of code - e.g. an operator like `+`.
520-
// As we are not parsing it, just grabing the code into a String, it's safe to advance.
459+
RewriteGroovyVisitor visitor = new RewriteGroovyVisitor(field, this);
460+
ListExpression arguments = (ListExpression) field.getInitialExpression();
461+
List<JRightPadded<Expression>> list = visitor.convertAll(arguments.getExpressions(), n -> sourceBefore(","), n -> whitespace(), n -> {
462+
if (n == arguments.getExpression(arguments.getExpressions().size() - 1) && source.charAt(cursor) == ',') {
521463
cursor++;
522-
} else {
523-
argCount++;
464+
return Markers.build(singleton(new TrailingComma(randomId(), whitespace())));
524465
}
525-
}
526-
String argsAsString = source.substring(start, cursor);
466+
return Markers.EMPTY;
467+
});
527468
skip(")");
528469

529-
// ... then grab the constructor arguments ...
530-
StringBuilder constructorDeclarationArgs = new StringBuilder();
531-
ConstructorNode ctor = null;
532-
for (ConstructorNode node : field.getDeclaringClass().getDeclaredConstructors()) {
533-
if ((node.getParameters().length - 2) == argCount) {
534-
ctor = node;
535-
break;
470+
MethodNode ctor = null;
471+
for (ConstructorNode constructor : field.getOwner().getDeclaredConstructors()) {
472+
if (constructor.getParameters().length == arguments.getExpressions().size()) {
473+
ctor = constructor;
536474
}
537475
}
538-
if (ctor != null) {
539-
for (int i = 2; i < ctor.getParameters().length; i++) {
540-
Parameter param = ctor.getParameters()[i];
541-
if (i != 2) {
542-
constructorDeclarationArgs.append(", ");
543-
}
544-
constructorDeclarationArgs.append(param.getType().getName()).append(" ").append(param.getName());
545-
}
546-
}
547-
548-
// ... and use the information in a small class to get the constructor invocation arguments anyway
549-
G.CompilationUnit cu = (G.CompilationUnit) GroovyParser.builder().build()
550-
.parse("class A {\n" +
551-
" A(" + constructorDeclarationArgs + ") {}\n" +
552-
" def use() {\n" +
553-
" new A(" + argsAsString + ")\n" +
554-
" }\n" +
555-
"}")
556-
.findFirst().get();
557-
558-
JContainer<Expression> args = ((J.NewClass) (((J.Return) ((J.MethodDeclaration) ((J.ClassDeclaration) cu.getStatements().get(0)).getBody().getStatements().get(1)).getBody().getStatements().get(0)).getExpression()))
559-
.getPadding().getArguments();
560-
561-
initializer = new J.NewClass(randomId(), prefixNewClass, Markers.EMPTY, null, EMPTY, null, args, null, typeMapping.methodType(ctor));
476+
initializer = new J.NewClass(randomId(), prefixNewClass, Markers.EMPTY, null, EMPTY, null, JContainer.build(list), null, typeMapping.methodType(ctor));
562477
}
563478

564479
return new J.EnumValue(randomId(), prefix, Markers.EMPTY, annotations, name, initializer);
@@ -2351,7 +2266,7 @@ public TypeTree visitVariableExpressionType(@Nullable VariableExpression express
23512266

23522267
if (!expression.isDynamicTyped() && source.startsWith(expression.getOriginType().getUnresolvedName(), cursor)) {
23532268
if (cursor + expression.getOriginType().getUnresolvedName().length() < source.length() &&
2354-
!Character.isJavaIdentifierPart(source.charAt(cursor + expression.getOriginType().getUnresolvedName().length()))) {
2269+
!isJavaIdentifierPart(source.charAt(cursor + expression.getOriginType().getUnresolvedName().length()))) {
23552270
typeName = expression.getOriginType().getUnresolvedName();
23562271
skip(typeName);
23572272
}

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

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,8 +35,10 @@
3535
*/
3636
public class LessAstTransformationsCompilationUnit extends CompilationUnit {
3737

38+
private static final int CONVERT_ENUM_OPERATION = 0;
3839
private static final int STATIC_IMPORT_OPERATION = 0;
3940

41+
private int conversionOperation = 0;
4042
private int semanticAnalysisOperation = 0;
4143

4244
public LessAstTransformationsCompilationUnit(CompilerConfiguration configuration, @Nullable CodeSource codeSource, GroovyClassLoader loader, GroovyClassLoader transformLoader) {
@@ -47,7 +49,12 @@ public LessAstTransformationsCompilationUnit(CompilerConfiguration configuration
4749

4850
@Override
4951
public void addPhaseOperation(IPrimaryClassNodeOperation op, int phase) {
50-
if (phase == Phases.SEMANTIC_ANALYSIS) {
52+
if (phase == Phases.CONVERSION) {
53+
if (conversionOperation++ == CONVERT_ENUM_OPERATION) {
54+
// we don't want to register the `EnumVisitor` operation
55+
return;
56+
}
57+
} else if (phase == Phases.SEMANTIC_ANALYSIS) {
5158
if (semanticAnalysisOperation++ == STATIC_IMPORT_OPERATION) {
5259
// we don't want to register the `StaticImportVisitor` operation
5360
return;

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -199,7 +199,7 @@ void enumWithLiteralParameters() {
199199
groovy(
200200
"""
201201
enum A {
202-
ONE(1, "A"),
202+
ONE(1, "A", ),
203203
TWO(2, "B", ")"),
204204
THREE(3, $/C/$, 1);
205205

0 commit comments

Comments
 (0)