diff --git a/rewrite-groovy/src/main/java/org/openrewrite/groovy/GroovyParserVisitor.java b/rewrite-groovy/src/main/java/org/openrewrite/groovy/GroovyParserVisitor.java index 598dd66f579..783740c03c2 100644 --- a/rewrite-groovy/src/main/java/org/openrewrite/groovy/GroovyParserVisitor.java +++ b/rewrite-groovy/src/main/java/org/openrewrite/groovy/GroovyParserVisitor.java @@ -45,6 +45,7 @@ import org.openrewrite.java.tree.*; import org.openrewrite.marker.Markers; +import java.lang.reflect.Modifier; import java.math.BigDecimal; import java.nio.charset.Charset; import java.nio.file.Path; @@ -392,11 +393,11 @@ class A { JRightPadded.build(false), sortedByPosition.values().stream() .flatMap(asts -> asts.stream() + // anonymous classes will be visited as part of visiting the ConstructorCallExpression + .filter(ast -> !(ast instanceof InnerClassNode && ((InnerClassNode) ast).isAnonymous())) .map(ast -> { if (ast instanceof FieldNode) { visitField((FieldNode) ast); - } else if (ast instanceof ConstructorNode) { - visitConstructor((ConstructorNode) ast); } else if (ast instanceof MethodNode) { visitMethod((MethodNode) ast); } else if (ast instanceof ClassNode) { @@ -521,15 +522,34 @@ public void visitMethod(MethodNode method) { List annotations = visitAndGetAnnotations(method); List modifiers = visitModifiers(method.getModifiers()); - Optional redundantDef = maybeRedundantDef(method.getReturnType(), method.getName()); - TypeTree returnType = visitTypeTree(method.getReturnType()); + boolean isConstructor = method instanceof ConstructorNode; + boolean isConstructorOfInnerNonStaticClass = false; + Optional redundantDef = isConstructor ? Optional.empty() : maybeRedundantDef(method.getReturnType(), method.getName()); + TypeTree returnType = isConstructor ? null : visitTypeTree(method.getReturnType()); - // Method name might be in quotes Space namePrefix = whitespace(); String methodName; - if (source.startsWith(method.getName(), cursor)) { + if (isConstructor) { + /* + 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 + } } + } } + } + 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().getName().replaceFirst(".*\\$", ""); + } else if (source.startsWith(method.getName(), cursor)) { methodName = method.getName(); } else { + // Method name might be in quotes char openingQuote = source.charAt(cursor); methodName = openingQuote + method.getName() + openingQuote; } @@ -547,7 +567,7 @@ public void visitMethod(MethodNode method) { Space beforeParen = sourceBefore("("); List> params = new ArrayList<>(method.getParameters().length); Parameter[] unparsedParams = method.getParameters(); - for (int i = 0; i < unparsedParams.length; i++) { + for (int i = (isConstructorOfInnerNonStaticClass ? 1 : 0); i < unparsedParams.length; i++) { Parameter param = unparsedParams[i]; List paramAnnotations = visitAndGetAnnotations(param); @@ -589,7 +609,7 @@ varargs, emptyList(), singletonList(paramName))).withAfter(rightPad)); } - if (unparsedParams.length == 0) { + if (unparsedParams.length == 0 || (isConstructorOfInnerNonStaticClass && unparsedParams.length == 1)) { params.add(JRightPadded.build(new J.Empty(randomId(), sourceBefore(")"), Markers.EMPTY))); } @@ -599,8 +619,16 @@ varargs, emptyList(), Markers.EMPTY ); - J.Block body = method.getCode() == null ? null : - bodyVisitor.visit(method.getCode()); + 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); + } queue.add(new J.MethodDeclaration( randomId(), fmt, @@ -618,98 +646,6 @@ varargs, emptyList(), )); } - @Override - public void visitConstructor(ConstructorNode constructor) { - Space fmt = whitespace(); - - List annotations = visitAndGetAnnotations(constructor); - List modifiers = visitModifiers(constructor.getModifiers()); - - // Constructor name might be in quotes - Space namePrefix = whitespace(); - String constructorName; - if (source.startsWith(constructor.getDeclaringClass().getName(), cursor)) { - constructorName = constructor.getDeclaringClass().getName(); - } else { - char openingQuote = source.charAt(cursor); - constructorName = openingQuote + constructor.getName() + openingQuote; - } - cursor += constructorName.length(); - J.Identifier name = new J.Identifier(randomId(), - namePrefix, - Markers.EMPTY, - emptyList(), - constructorName, - null, null); - - RewriteGroovyVisitor bodyVisitor = new RewriteGroovyVisitor(constructor, this); - - // Parameter has no visit implementation, so we've got to do this by hand - Space beforeParen = sourceBefore("("); - List> params = new ArrayList<>(constructor.getParameters().length); - Parameter[] unparsedParams = constructor.getParameters(); - for (int i = 0; i < unparsedParams.length; i++) { - Parameter param = unparsedParams[i]; - - List paramAnnotations = visitAndGetAnnotations(param); - - TypeTree paramType; - if (param.isDynamicTyped()) { - paramType = new J.Identifier(randomId(), EMPTY, Markers.EMPTY, emptyList(), "", JavaType.ShallowClass.build("java.lang.Object"), null); - } else { - paramType = visitTypeTree(param.getOriginType()); - } - JRightPadded paramName = JRightPadded.build( - new J.VariableDeclarations.NamedVariable(randomId(), EMPTY, Markers.EMPTY, - new J.Identifier(randomId(), whitespace(), Markers.EMPTY, emptyList(), param.getName(), null, null), - emptyList(), null, null) - ); - cursor += param.getName().length(); - - org.codehaus.groovy.ast.expr.Expression defaultValue = param.getInitialExpression(); - if (defaultValue != null) { - paramName = paramName.withElement(paramName.getElement().getPadding() - .withInitializer(new JLeftPadded<>( - sourceBefore("="), - new RewriteGroovyVisitor(defaultValue, this).visit(defaultValue), - Markers.EMPTY))); - } - Space rightPad = sourceBefore(i == unparsedParams.length - 1 ? ")" : ","); - - params.add(JRightPadded.build((Statement) new J.VariableDeclarations(randomId(), EMPTY, - Markers.EMPTY, paramAnnotations, emptyList(), paramType, - null, emptyList(), - singletonList(paramName))).withAfter(rightPad)); - } - - if (unparsedParams.length == 0) { - params.add(JRightPadded.build(new J.Empty(randomId(), sourceBefore(")"), Markers.EMPTY))); - } - - JContainer throws_ = constructor.getExceptions().length == 0 ? null : JContainer.build( - sourceBefore("throws"), - bodyVisitor.visitRightPadded(constructor.getExceptions(), null), - Markers.EMPTY - ); - - J.Block body = constructor.getCode() == null ? null : - bodyVisitor.visit(constructor.getCode()); - - queue.add(new J.MethodDeclaration( - randomId(), fmt, Markers.EMPTY, - annotations, - modifiers, - null, - null, - new J.MethodDeclaration.IdentifierWithAnnotations(name, emptyList()), - JContainer.build(beforeParen, params, Markers.EMPTY), - throws_, - body, - null, - typeMapping.methodType(constructor) - )); - } - public List visitAndGetAnnotations(AnnotatedNode node) { if (node.getAnnotations().isEmpty()) { return emptyList(); diff --git a/rewrite-groovy/src/test/java/org/openrewrite/groovy/tree/ClassDeclarationTest.java b/rewrite-groovy/src/test/java/org/openrewrite/groovy/tree/ClassDeclarationTest.java index e3acb1f3d33..c3991346c24 100644 --- a/rewrite-groovy/src/test/java/org/openrewrite/groovy/tree/ClassDeclarationTest.java +++ b/rewrite-groovy/src/test/java/org/openrewrite/groovy/tree/ClassDeclarationTest.java @@ -342,7 +342,6 @@ class RewriteSettings extends groovy.lang.Script { } @Test - @ExpectedToFail("Anonymous inner class is not yet supported") // https://groovy-lang.org/objectorientation.html#_anonymous_inner_class void anonymousInnerClass() { rewriteRun( groovy( @@ -350,6 +349,7 @@ void anonymousInnerClass() { interface Something {} class Test { + Something something = new Something() {} static def test() { new Something() {} } @@ -358,4 +358,76 @@ static def test() { ) ); } + + @Test + @Issue("https://github.com/openrewrite/rewrite/issues/4063") + void nestedClassWithoutParameters() { + rewriteRun( + groovy( + """ + class A { + class B { + B() {} + } + } + """ + ) + ); + } + + @Test + @Issue("https://github.com/openrewrite/rewrite/issues/4063") + void nestedClass() { + rewriteRun( + groovy( + """ + class A { + class B { + String a;String[] b + B(String $a, String... b) { + this.a = $a + this.b = b + } + } + } + """ + ) + ); + } + + @Test + @Issue("https://github.com/openrewrite/rewrite/issues/4063") + void nestedStaticClassWithoutParameters() { + rewriteRun( + groovy( + """ + class A { + static class B { + B() {} + } + } + """ + ) + ); + } + + @Test + @Issue("https://github.com/openrewrite/rewrite/issues/4063") + void nestedStaticClass() { + rewriteRun( + groovy( + """ + class A { + static class B { + String a;String[] b + B(String a, String... b) { + this.a = a + this.b = b + } + } + } + """ + ) + ); + } } diff --git a/rewrite-groovy/src/test/java/org/openrewrite/groovy/tree/RealWorldGroovyTest.java b/rewrite-groovy/src/test/java/org/openrewrite/groovy/tree/RealWorldGroovyTest.java index ea9da82cd73..2f9998c197a 100644 --- a/rewrite-groovy/src/test/java/org/openrewrite/groovy/tree/RealWorldGroovyTest.java +++ b/rewrite-groovy/src/test/java/org/openrewrite/groovy/tree/RealWorldGroovyTest.java @@ -135,7 +135,6 @@ public void apply(Project project) { } @Test - @ExpectedToFail("Anonymous inner class is not yet supported") // https://groovy-lang.org/objectorientation.html#_anonymous_inner_class @Issue("https://github.com/spring-projects/spring-ldap/blob/v3.4.1/buildSrc/src/test/resources/samples/integrationtest/withgroovy/src/integration-test/groovy/sample/TheTest.groovy") void springLdapTheTest() { rewriteRun(