Skip to content
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

Support inner classes for groovy #4825

Merged
merged 9 commits into from
Jan 2, 2025
Merged
Show file tree
Hide file tree
Changes from 8 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 @@ -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
Loading