Skip to content

Commit 7d8eea8

Browse files
committed
Skip main method migration when referenced as static API
Convert `MigrateMainMethodToInstanceMain` to a `ScanningRecipe` so it detects `Main.main(...)` invocations and `Main::main` references across all compilation units, not just the same file. Migrating the declaring class to `void main()` would otherwise leave call sites broken. Fixes #1083
1 parent b6c609d commit 7d8eea8

2 files changed

Lines changed: 98 additions & 26 deletions

File tree

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

Lines changed: 53 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,12 @@
1515
*/
1616
package org.openrewrite.java.migrate.lang;
1717

18-
import lombok.Getter;
18+
import lombok.EqualsAndHashCode;
19+
import lombok.Value;
20+
import org.jspecify.annotations.Nullable;
1921
import org.openrewrite.ExecutionContext;
2022
import org.openrewrite.Preconditions;
21-
import org.openrewrite.Recipe;
23+
import org.openrewrite.ScanningRecipe;
2224
import org.openrewrite.TreeVisitor;
2325
import org.openrewrite.java.JavaIsoVisitor;
2426
import org.openrewrite.java.MethodMatcher;
@@ -29,24 +31,57 @@
2931
import org.openrewrite.java.tree.TypeUtils;
3032
import org.openrewrite.staticanalysis.VariableReferences;
3133

34+
import java.util.HashSet;
3235
import java.util.List;
33-
import java.util.concurrent.atomic.AtomicBoolean;
36+
import java.util.Set;
3437

3538
import static java.util.Collections.emptyList;
3639
import static java.util.stream.Collectors.toList;
3740

38-
public class MigrateMainMethodToInstanceMain extends Recipe {
41+
@Value
42+
@EqualsAndHashCode(callSuper = false)
43+
public class MigrateMainMethodToInstanceMain extends ScanningRecipe<Set<JavaType.FullyQualified>> {
3944

4045
private static final MethodMatcher MAIN_METHOD_MATCHER = new MethodMatcher("*..* main(String[])", false);
4146

42-
@Getter
43-
final String displayName = "Migrate `public static void main(String[] args)` to instance `void main()`";
47+
String displayName = "Migrate `public static void main(String[] args)` to instance `void main()`";
4448

45-
@Getter
46-
final String description = "Migrate `public static void main(String[] args)` method to instance `void main()` method when the `args` parameter is unused, as supported by JEP 512 in Java 25+.";
49+
String description = "Migrate `public static void main(String[] args)` method to instance `void main()` method when the `args` parameter is unused, as supported by JEP 512 in Java 25+.";
4750

4851
@Override
49-
public TreeVisitor<?, ExecutionContext> getVisitor() {
52+
public Set<JavaType.FullyQualified> getInitialValue(ExecutionContext ctx) {
53+
return new HashSet<>();
54+
}
55+
56+
@Override
57+
public TreeVisitor<?, ExecutionContext> getScanner(Set<JavaType.FullyQualified> referencedMains) {
58+
return new JavaIsoVisitor<ExecutionContext>() {
59+
@Override
60+
public J.MethodInvocation visitMethodInvocation(J.MethodInvocation method, ExecutionContext ctx) {
61+
if (MAIN_METHOD_MATCHER.matches(method.getMethodType())) {
62+
recordDeclaringType(method.getMethodType());
63+
}
64+
return super.visitMethodInvocation(method, ctx);
65+
}
66+
67+
@Override
68+
public J.MemberReference visitMemberReference(J.MemberReference memberRef, ExecutionContext ctx) {
69+
if (MAIN_METHOD_MATCHER.matches(memberRef.getMethodType())) {
70+
recordDeclaringType(memberRef.getMethodType());
71+
}
72+
return super.visitMemberReference(memberRef, ctx);
73+
}
74+
75+
private void recordDeclaringType(JavaType.@Nullable Method methodType) {
76+
if (methodType != null && methodType.getDeclaringType() != null) {
77+
referencedMains.add(methodType.getDeclaringType());
78+
}
79+
}
80+
};
81+
}
82+
83+
@Override
84+
public TreeVisitor<?, ExecutionContext> getVisitor(Set<JavaType.FullyQualified> referencedMains) {
5085
TreeVisitor<?, ExecutionContext> preconditions = Preconditions.and(
5186
new UsesJavaVersion<>(25),
5287
new DeclaresMethod<>(MAIN_METHOD_MATCHER)
@@ -78,7 +113,7 @@ public J.MethodDeclaration visitMethodDeclaration(J.MethodDeclaration method, Ex
78113
// Do not migrate in any of these cases
79114
if (hasSpringBootApplicationAnnotation(enclosingClass) ||
80115
!hasNoArgConstructor(enclosingClass) ||
81-
isMainMethodReferenced(md)) {
116+
isMainMethodReferenced(enclosingClass)) {
82117
return md;
83118
}
84119

@@ -115,25 +150,17 @@ private boolean hasNoArgConstructor(J.ClassDeclaration classDecl) {
115150
ctor.hasModifier(J.Modifier.Type.Public));
116151
}
117152

118-
private boolean isMainMethodReferenced(J.MethodDeclaration mainMethod) {
119-
J.CompilationUnit cu = getCursor().firstEnclosing(J.CompilationUnit.class);
120-
if (cu == null) {
153+
private boolean isMainMethodReferenced(J.ClassDeclaration classDecl) {
154+
JavaType.FullyQualified type = classDecl.getType();
155+
if (type == null) {
121156
return false;
122157
}
123-
124-
// XXX Only picks up references in the same compilation unit; convert to scanning recipe if needed
125-
return new JavaIsoVisitor<AtomicBoolean>() {
126-
@Override
127-
public J.MemberReference visitMemberReference(J.MemberReference memberRef, AtomicBoolean referenced) {
128-
// Check if this is a reference to the main method
129-
if ("main".equals(memberRef.getReference().getSimpleName()) &&
130-
memberRef.getMethodType() != null &&
131-
TypeUtils.isOfType(memberRef.getMethodType(), mainMethod.getMethodType())) {
132-
referenced.set(true);
133-
}
134-
return super.visitMemberReference(memberRef, referenced);
158+
for (JavaType.FullyQualified referenced : referencedMains) {
159+
if (TypeUtils.isOfType(referenced, type)) {
160+
return true;
135161
}
136-
}.reduce(cu, new AtomicBoolean()).get();
162+
}
163+
return false;
137164
}
138165
});
139166
}

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

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -320,6 +320,51 @@ void executeMain() {
320320
);
321321
}
322322

323+
@Test
324+
void doNotMigrateMainInvokedAsStaticFromAnotherFile() {
325+
//language=java
326+
rewriteRun(
327+
java(
328+
"""
329+
class Main {
330+
public static void main(String[] args) {}
331+
}
332+
"""
333+
),
334+
java(
335+
"""
336+
class MainTest {
337+
void test() {
338+
Main.main(new String[] {});
339+
}
340+
}
341+
"""
342+
)
343+
);
344+
}
345+
346+
@Test
347+
void doNotMigrateMainInvokedAsStaticFromSameFile() {
348+
//language=java
349+
rewriteRun(
350+
java(
351+
"""
352+
class Application {
353+
public static void main(String[] args) {
354+
System.out.println("Hello!");
355+
}
356+
}
357+
358+
class Runner {
359+
void executeMain() {
360+
Application.main(new String[] {});
361+
}
362+
}
363+
"""
364+
)
365+
);
366+
}
367+
323368
@Test
324369
void doNotMigrateMainWithNonDefaultConstructor() {
325370
//language=java

0 commit comments

Comments
 (0)