Skip to content

Commit 4a6fc6c

Browse files
authored
Instance main should not be used for certain test cases (#891)
* Instance main should not be used for certain test cases * Exclude the specific test cases * Use a precondition and method matcher; fold conditionals * Use static import * Add note about a limitation of the current approach
1 parent 0663445 commit 4a6fc6c

File tree

2 files changed

+153
-5
lines changed

2 files changed

+153
-5
lines changed

src/main/java/org/openrewrite/java/migrate/lang/MigrateMainMethodToInstanceMain.java

Lines changed: 68 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -20,15 +20,24 @@
2020
import org.openrewrite.Recipe;
2121
import org.openrewrite.TreeVisitor;
2222
import org.openrewrite.java.JavaIsoVisitor;
23+
import org.openrewrite.java.MethodMatcher;
24+
import org.openrewrite.java.search.DeclaresMethod;
2325
import org.openrewrite.java.search.UsesJavaVersion;
2426
import org.openrewrite.java.tree.J;
2527
import org.openrewrite.java.tree.JavaType;
2628
import org.openrewrite.java.tree.TypeUtils;
2729
import org.openrewrite.staticanalysis.VariableReferences;
2830

31+
import java.util.List;
32+
import java.util.concurrent.atomic.AtomicBoolean;
33+
2934
import static java.util.Collections.emptyList;
35+
import static java.util.stream.Collectors.toList;
3036

3137
public class MigrateMainMethodToInstanceMain extends Recipe {
38+
39+
private static final MethodMatcher MAIN_METHOD_MATCHER = new MethodMatcher("*..* main(String[])", false);
40+
3241
@Override
3342
public String getDisplayName() {
3443
return "Migrate `public static void main(String[] args)` to instance `void main()`";
@@ -41,19 +50,23 @@ public String getDescription() {
4150

4251
@Override
4352
public TreeVisitor<?, ExecutionContext> getVisitor() {
44-
return Preconditions.check(new UsesJavaVersion<>(25), new JavaIsoVisitor<ExecutionContext>() {
53+
TreeVisitor<?, ExecutionContext> preconditions = Preconditions.and(
54+
new UsesJavaVersion<>(25),
55+
new DeclaresMethod<>(MAIN_METHOD_MATCHER)
56+
);
57+
return Preconditions.check(preconditions, new JavaIsoVisitor<ExecutionContext>() {
4558
@Override
4659
public J.MethodDeclaration visitMethodDeclaration(J.MethodDeclaration method, ExecutionContext ctx) {
60+
J.ClassDeclaration enclosingClass = getCursor().firstEnclosing(J.ClassDeclaration.class);
4761
J.MethodDeclaration md = super.visitMethodDeclaration(method, ctx);
4862

4963
// Check if this is a main method: public static void main(String[] args)
50-
if (!"main".equals(md.getSimpleName()) ||
64+
if (enclosingClass == null ||
65+
!MAIN_METHOD_MATCHER.matches(md, enclosingClass) ||
5166
md.getReturnTypeExpression() == null ||
5267
md.getReturnTypeExpression().getType() != JavaType.Primitive.Void ||
5368
!md.hasModifier(J.Modifier.Type.Public) ||
5469
!md.hasModifier(J.Modifier.Type.Static) ||
55-
md.getParameters().size() != 1 ||
56-
!(md.getParameters().get(0) instanceof J.VariableDeclarations) ||
5770
md.getBody() == null) {
5871
return md;
5972
}
@@ -65,6 +78,13 @@ public J.MethodDeclaration visitMethodDeclaration(J.MethodDeclaration method, Ex
6578
return md;
6679
}
6780

81+
// Do not migrate in any of these cases
82+
if (hasSpringBootApplicationAnnotation(enclosingClass) ||
83+
!hasNoArgConstructor(enclosingClass) ||
84+
isMainMethodReferenced(md)) {
85+
return md;
86+
}
87+
6888
// Remove the parameter if unused
6989
J.Identifier variableName = param.getVariables().get(0).getName();
7090
if (VariableReferences.findRhsReferences(md.getBody(), variableName).isEmpty()) {
@@ -73,7 +93,50 @@ public J.MethodDeclaration visitMethodDeclaration(J.MethodDeclaration method, Ex
7393
return md.withReturnTypeExpression(md.getReturnTypeExpression().withPrefix(md.getModifiers().get(0).getPrefix()))
7494
.withModifiers(emptyList());
7595
}
96+
97+
private boolean hasSpringBootApplicationAnnotation(J.ClassDeclaration classDecl) {
98+
return classDecl.getLeadingAnnotations().stream()
99+
.anyMatch(ann -> TypeUtils.isOfClassType(ann.getType(), "org.springframework.boot.autoconfigure.SpringBootApplication"));
100+
}
101+
102+
private boolean hasNoArgConstructor(J.ClassDeclaration classDecl) {
103+
List<J.MethodDeclaration> constructors = classDecl.getBody().getStatements().stream()
104+
.filter(stmt -> stmt instanceof J.MethodDeclaration)
105+
.map(stmt -> (J.MethodDeclaration) stmt)
106+
.filter(J.MethodDeclaration::isConstructor)
107+
.collect(toList());
108+
109+
// If no constructors are declared, the class has an implicit no-arg constructor
110+
if (constructors.isEmpty()) {
111+
return true;
112+
}
113+
114+
// Check if any explicit constructor is a no-arg constructor
115+
return constructors.stream()
116+
.anyMatch(ctor -> ctor.getParameters().isEmpty() ||
117+
(ctor.getParameters().size() == 1 && ctor.getParameters().get(0) instanceof J.Empty));
118+
}
119+
120+
private boolean isMainMethodReferenced(J.MethodDeclaration mainMethod) {
121+
J.CompilationUnit cu = getCursor().firstEnclosing(J.CompilationUnit.class);
122+
if (cu == null) {
123+
return false;
124+
}
125+
126+
// XXX Only picks up references in the same compilation unit; convert to scanning recipe if needed
127+
return new JavaIsoVisitor<AtomicBoolean>() {
128+
@Override
129+
public J.MemberReference visitMemberReference(J.MemberReference memberRef, AtomicBoolean referenced) {
130+
// Check if this is a reference to the main method
131+
if ("main".equals(memberRef.getReference().getSimpleName()) &&
132+
memberRef.getMethodType() != null &&
133+
TypeUtils.isOfType(memberRef.getMethodType(), mainMethod.getMethodType())) {
134+
referenced.set(true);
135+
}
136+
return super.visitMemberReference(memberRef, referenced);
137+
}
138+
}.reduce(cu, new AtomicBoolean()).get();
139+
}
76140
});
77141
}
78-
79142
}

src/test/java/org/openrewrite/java/migrate/lang/MigrateMainMethodToInstanceMainTest.java

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717

1818
import org.junit.jupiter.api.Test;
1919
import org.openrewrite.DocumentExample;
20+
import org.openrewrite.java.JavaParser;
2021
import org.openrewrite.test.RecipeSpec;
2122
import org.openrewrite.test.RewriteTest;
2223

@@ -288,4 +289,88 @@ void main() {
288289
)
289290
);
290291
}
292+
293+
@Test
294+
void doNotMigrateMainUsedAsMethodReference() {
295+
//language=java
296+
rewriteRun(
297+
java(
298+
"""
299+
interface MainMethod {
300+
void run(String[] args);
301+
}
302+
"""
303+
),
304+
java(
305+
"""
306+
class Application {
307+
public static void main(String[] args) {
308+
System.out.println("Hello from main");
309+
}
310+
}
311+
312+
class Runner {
313+
void executeMain() {
314+
MainMethod foo = Application::main;
315+
foo.run(null);
316+
}
317+
}
318+
"""
319+
)
320+
);
321+
}
322+
323+
@Test
324+
void doNotMigrateMainWithNonDefaultConstructor() {
325+
//language=java
326+
rewriteRun(
327+
java(
328+
"""
329+
class Application {
330+
public static void main(String[] args) {
331+
System.out.println("Hello!");
332+
}
333+
334+
public Application(String config) {
335+
// Non-default constructor
336+
}
337+
}
338+
"""
339+
)
340+
);
341+
}
342+
343+
@Test
344+
void doNotMigrateMainInSpringBootApplication() {
345+
//language=java
346+
rewriteRun(
347+
spec -> spec.parser(JavaParser.fromJavaVersion().dependsOn(
348+
"""
349+
package org.springframework.boot.autoconfigure;
350+
public @interface SpringBootApplication {}
351+
""",
352+
"""
353+
package org.springframework.boot;
354+
public class SpringApplication {
355+
public static void run(Class<?> primarySource, String... args) {}
356+
}
357+
"""
358+
)),
359+
java(
360+
"""
361+
package com.example.demo;
362+
363+
import org.springframework.boot.SpringApplication;
364+
import org.springframework.boot.autoconfigure.SpringBootApplication;
365+
366+
@SpringBootApplication
367+
class DemoApplication {
368+
public static void main(String[] args) {
369+
SpringApplication.run(DemoApplication.class, args);
370+
}
371+
}
372+
"""
373+
)
374+
);
375+
}
291376
}

0 commit comments

Comments
 (0)