Skip to content

Commit

Permalink
Support inner classes for groovy (#4825)
Browse files Browse the repository at this point in the history
* Support inner classes

* Better text

* Improvement

* Support nested class without arguments

* Improve test

* Improvement

* Improvement

* Support anonymous inner classes
  • Loading branch information
jevanlingen authored Jan 2, 2025
1 parent 1f83ea3 commit 1aee2af
Show file tree
Hide file tree
Showing 3 changed files with 111 additions and 104 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -521,15 +522,34 @@ public void visitMethod(MethodNode method) {

List<J.Annotation> annotations = visitAndGetAnnotations(method);
List<J.Modifier> modifiers = visitModifiers(method.getModifiers());
Optional<RedundantDef> redundantDef = maybeRedundantDef(method.getReturnType(), method.getName());
TypeTree returnType = visitTypeTree(method.getReturnType());
boolean isConstructor = method instanceof ConstructorNode;
boolean isConstructorOfInnerNonStaticClass = false;
Optional<RedundantDef> 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;
}
Expand All @@ -547,7 +567,7 @@ public void visitMethod(MethodNode method) {
Space beforeParen = sourceBefore("(");
List<JRightPadded<Statement>> 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<J.Annotation> paramAnnotations = visitAndGetAnnotations(param);
Expand Down Expand Up @@ -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)));
}

Expand All @@ -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,
Expand All @@ -618,98 +646,6 @@ varargs, emptyList(),
));
}

@Override
public void visitConstructor(ConstructorNode constructor) {
Space fmt = whitespace();

List<J.Annotation> annotations = visitAndGetAnnotations(constructor);
List<J.Modifier> 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<JRightPadded<Statement>> params = new ArrayList<>(constructor.getParameters().length);
Parameter[] unparsedParams = constructor.getParameters();
for (int i = 0; i < unparsedParams.length; i++) {
Parameter param = unparsedParams[i];

List<J.Annotation> 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<J.VariableDeclarations.NamedVariable> 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<NameTree> 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<J.Annotation> visitAndGetAnnotations(AnnotatedNode node) {
if (node.getAnnotations().isEmpty()) {
return emptyList();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -342,14 +342,14 @@ 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(
"""
interface Something {}
class Test {
Something something = new Something() {}
static def test() {
new Something() {}
}
Expand All @@ -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
}
}
}
"""
)
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down

0 comments on commit 1aee2af

Please sign in to comment.