From 1435e014ae8a7c46126773d39ffb20a3faaf8753 Mon Sep 17 00:00:00 2001 From: Tim te Beek Date: Thu, 23 Oct 2025 10:27:11 +0200 Subject: [PATCH 1/5] Instance main should not be used for certain test cases --- .../MigrateMainMethodToInstanceMainTest.java | 85 +++++++++++++++++++ 1 file changed, 85 insertions(+) diff --git a/src/test/java/org/openrewrite/java/migrate/lang/MigrateMainMethodToInstanceMainTest.java b/src/test/java/org/openrewrite/java/migrate/lang/MigrateMainMethodToInstanceMainTest.java index 9ceb82ab0c..c17854c9e0 100644 --- a/src/test/java/org/openrewrite/java/migrate/lang/MigrateMainMethodToInstanceMainTest.java +++ b/src/test/java/org/openrewrite/java/migrate/lang/MigrateMainMethodToInstanceMainTest.java @@ -17,6 +17,7 @@ import org.junit.jupiter.api.Test; import org.openrewrite.DocumentExample; +import org.openrewrite.java.JavaParser; import org.openrewrite.test.RecipeSpec; import org.openrewrite.test.RewriteTest; @@ -288,4 +289,88 @@ void main() { ) ); } + + @Test + void doNotMigrateMainUsedAsMethodReference() { + //language=java + rewriteRun( + java( + """ + interface MainMethod { + void run(String[] args); + } + """ + ), + java( + """ + class Application { + public static void main(String[] args) { + System.out.println("Hello from main"); + } + } + + class Runner { + void executeMain() { + MainMethod foo = Application::main; + foo.run(null); + } + } + """ + ) + ); + } + + @Test + void doNotMigrateMainWithNonDefaultConstructor() { + //language=java + rewriteRun( + java( + """ + class Application { + public static void main(String[] args) { + System.out.println("Hello!"); + } + + public Application(String config) { + // Non-default constructor + } + } + """ + ) + ); + } + + @Test + void doNotMigrateMainInSpringBootApplication() { + //language=java + rewriteRun( + spec -> spec.parser(JavaParser.fromJavaVersion().dependsOn( + """ + package org.springframework.boot.autoconfigure; + public @interface SpringBootApplication {} + """, + """ + package org.springframework.boot; + public class SpringApplication { + public static void run(Class primarySource, String... args) {} + } + """ + )), + java( + """ + package com.example.demo; + + import org.springframework.boot.SpringApplication; + import org.springframework.boot.autoconfigure.SpringBootApplication; + + @SpringBootApplication + class DemoApplication { + public static void main(String[] args) { + SpringApplication.run(DemoApplication.class, args); + } + } + """ + ) + ); + } } From 0a78466b99dd3568ad160c97af5cc52d1799509d Mon Sep 17 00:00:00 2001 From: Tim te Beek Date: Thu, 23 Oct 2025 10:48:35 +0200 Subject: [PATCH 2/5] Exclude the specific test cases --- .../lang/MigrateMainMethodToInstanceMain.java | 70 ++++++++++++++++++- 1 file changed, 69 insertions(+), 1 deletion(-) diff --git a/src/main/java/org/openrewrite/java/migrate/lang/MigrateMainMethodToInstanceMain.java b/src/main/java/org/openrewrite/java/migrate/lang/MigrateMainMethodToInstanceMain.java index 1d0b684c03..886d7d7909 100644 --- a/src/main/java/org/openrewrite/java/migrate/lang/MigrateMainMethodToInstanceMain.java +++ b/src/main/java/org/openrewrite/java/migrate/lang/MigrateMainMethodToInstanceMain.java @@ -26,6 +26,10 @@ import org.openrewrite.java.tree.TypeUtils; import org.openrewrite.staticanalysis.VariableReferences; +import java.util.List; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.stream.Collectors; + import static java.util.Collections.emptyList; public class MigrateMainMethodToInstanceMain extends Recipe { @@ -65,6 +69,27 @@ public J.MethodDeclaration visitMethodDeclaration(J.MethodDeclaration method, Ex return md; } + // Get the enclosing class + J.ClassDeclaration enclosingClass = getCursor().firstEnclosing(J.ClassDeclaration.class); + if (enclosingClass == null) { + return md; + } + + // Check 1: Do not migrate if class has @SpringBootApplication annotation + if (hasSpringBootApplicationAnnotation(enclosingClass)) { + return md; + } + + // Check 2: Do not migrate if class doesn't have a no-arg constructor + if (!hasNoArgConstructor(enclosingClass)) { + return md; + } + + // Check 3: Do not migrate if main method is used as a method reference + if (isMainMethodReferenced(md)) { + return md; + } + // Remove the parameter if unused J.Identifier variableName = param.getVariables().get(0).getName(); if (VariableReferences.findRhsReferences(md.getBody(), variableName).isEmpty()) { @@ -73,7 +98,50 @@ public J.MethodDeclaration visitMethodDeclaration(J.MethodDeclaration method, Ex return md.withReturnTypeExpression(md.getReturnTypeExpression().withPrefix(md.getModifiers().get(0).getPrefix())) .withModifiers(emptyList()); } + + private boolean hasSpringBootApplicationAnnotation(J.ClassDeclaration classDecl) { + return classDecl.getLeadingAnnotations().stream() + .anyMatch(ann -> TypeUtils.isOfClassType(ann.getType(), "org.springframework.boot.autoconfigure.SpringBootApplication")); + } + + private boolean hasNoArgConstructor(J.ClassDeclaration classDecl) { + List constructors = classDecl.getBody().getStatements().stream() + .filter(stmt -> stmt instanceof J.MethodDeclaration) + .map(stmt -> (J.MethodDeclaration) stmt) + .filter(J.MethodDeclaration::isConstructor) + .collect(Collectors.toList()); + + // If no constructors are declared, the class has an implicit no-arg constructor + if (constructors.isEmpty()) { + return true; + } + + // Check if any explicit constructor is a no-arg constructor + return constructors.stream() + .anyMatch(ctor -> ctor.getParameters().isEmpty() || + (ctor.getParameters().size() == 1 && ctor.getParameters().get(0) instanceof J.Empty)); + } + + private boolean isMainMethodReferenced(J.MethodDeclaration mainMethod) { + J.CompilationUnit cu = getCursor().firstEnclosing(J.CompilationUnit.class); + if (cu == null) { + return false; + } + + // Search for method references to main + return new JavaIsoVisitor(){ + @Override + public J.MemberReference visitMemberReference(J.MemberReference memberRef, AtomicBoolean referenced) { + // Check if this is a reference to the main method + if ("main".equals(memberRef.getReference().getSimpleName()) && + memberRef.getMethodType() != null && + TypeUtils.isOfType(memberRef.getMethodType(), mainMethod.getMethodType())) { + referenced.set(true); + } + return super.visitMemberReference(memberRef, referenced); + } + }.reduce(cu, new AtomicBoolean()).get(); + } }); } - } From e4397c6aeaf20fb2c8193e474d56c75d7d1fbd29 Mon Sep 17 00:00:00 2001 From: Tim te Beek Date: Thu, 23 Oct 2025 10:57:41 +0200 Subject: [PATCH 3/5] Use a precondition and method matcher; fold conditionals --- .../lang/MigrateMainMethodToInstanceMain.java | 39 ++++++++----------- 1 file changed, 17 insertions(+), 22 deletions(-) diff --git a/src/main/java/org/openrewrite/java/migrate/lang/MigrateMainMethodToInstanceMain.java b/src/main/java/org/openrewrite/java/migrate/lang/MigrateMainMethodToInstanceMain.java index 886d7d7909..001773d146 100644 --- a/src/main/java/org/openrewrite/java/migrate/lang/MigrateMainMethodToInstanceMain.java +++ b/src/main/java/org/openrewrite/java/migrate/lang/MigrateMainMethodToInstanceMain.java @@ -20,6 +20,8 @@ import org.openrewrite.Recipe; import org.openrewrite.TreeVisitor; import org.openrewrite.java.JavaIsoVisitor; +import org.openrewrite.java.MethodMatcher; +import org.openrewrite.java.search.DeclaresMethod; import org.openrewrite.java.search.UsesJavaVersion; import org.openrewrite.java.tree.J; import org.openrewrite.java.tree.JavaType; @@ -33,6 +35,9 @@ import static java.util.Collections.emptyList; public class MigrateMainMethodToInstanceMain extends Recipe { + + private static final MethodMatcher MAIN_METHOD_MATCHER = new MethodMatcher("*..* main(String[])", false); + @Override public String getDisplayName() { return "Migrate `public static void main(String[] args)` to instance `void main()`"; @@ -45,19 +50,23 @@ public String getDescription() { @Override public TreeVisitor getVisitor() { - return Preconditions.check(new UsesJavaVersion<>(25), new JavaIsoVisitor() { + TreeVisitor preconditions = Preconditions.and( + new UsesJavaVersion<>(25), + new DeclaresMethod<>(MAIN_METHOD_MATCHER) + ); + return Preconditions.check(preconditions, new JavaIsoVisitor() { @Override public J.MethodDeclaration visitMethodDeclaration(J.MethodDeclaration method, ExecutionContext ctx) { + J.ClassDeclaration enclosingClass = getCursor().firstEnclosing(J.ClassDeclaration.class); J.MethodDeclaration md = super.visitMethodDeclaration(method, ctx); // Check if this is a main method: public static void main(String[] args) - if (!"main".equals(md.getSimpleName()) || + if (enclosingClass == null || + !MAIN_METHOD_MATCHER.matches(md, enclosingClass) || md.getReturnTypeExpression() == null || md.getReturnTypeExpression().getType() != JavaType.Primitive.Void || !md.hasModifier(J.Modifier.Type.Public) || !md.hasModifier(J.Modifier.Type.Static) || - md.getParameters().size() != 1 || - !(md.getParameters().get(0) instanceof J.VariableDeclarations) || md.getBody() == null) { return md; } @@ -69,24 +78,10 @@ public J.MethodDeclaration visitMethodDeclaration(J.MethodDeclaration method, Ex return md; } - // Get the enclosing class - J.ClassDeclaration enclosingClass = getCursor().firstEnclosing(J.ClassDeclaration.class); - if (enclosingClass == null) { - return md; - } - - // Check 1: Do not migrate if class has @SpringBootApplication annotation - if (hasSpringBootApplicationAnnotation(enclosingClass)) { - return md; - } - - // Check 2: Do not migrate if class doesn't have a no-arg constructor - if (!hasNoArgConstructor(enclosingClass)) { - return md; - } - - // Check 3: Do not migrate if main method is used as a method reference - if (isMainMethodReferenced(md)) { + // Do not migrate in any of these cases + if (hasSpringBootApplicationAnnotation(enclosingClass) || + !hasNoArgConstructor(enclosingClass) || + isMainMethodReferenced(md)) { return md; } From 6e9b837cc22de701c0c85d7b3cfebf0eba15f0fa Mon Sep 17 00:00:00 2001 From: Tim te Beek Date: Thu, 23 Oct 2025 10:58:22 +0200 Subject: [PATCH 4/5] Use static import --- .../java/migrate/lang/MigrateMainMethodToInstanceMain.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/main/java/org/openrewrite/java/migrate/lang/MigrateMainMethodToInstanceMain.java b/src/main/java/org/openrewrite/java/migrate/lang/MigrateMainMethodToInstanceMain.java index 001773d146..3d3e340a70 100644 --- a/src/main/java/org/openrewrite/java/migrate/lang/MigrateMainMethodToInstanceMain.java +++ b/src/main/java/org/openrewrite/java/migrate/lang/MigrateMainMethodToInstanceMain.java @@ -30,9 +30,9 @@ import java.util.List; import java.util.concurrent.atomic.AtomicBoolean; -import java.util.stream.Collectors; import static java.util.Collections.emptyList; +import static java.util.stream.Collectors.toList; public class MigrateMainMethodToInstanceMain extends Recipe { @@ -104,7 +104,7 @@ private boolean hasNoArgConstructor(J.ClassDeclaration classDecl) { .filter(stmt -> stmt instanceof J.MethodDeclaration) .map(stmt -> (J.MethodDeclaration) stmt) .filter(J.MethodDeclaration::isConstructor) - .collect(Collectors.toList()); + .collect(toList()); // If no constructors are declared, the class has an implicit no-arg constructor if (constructors.isEmpty()) { @@ -124,7 +124,7 @@ private boolean isMainMethodReferenced(J.MethodDeclaration mainMethod) { } // Search for method references to main - return new JavaIsoVisitor(){ + return new JavaIsoVisitor() { @Override public J.MemberReference visitMemberReference(J.MemberReference memberRef, AtomicBoolean referenced) { // Check if this is a reference to the main method From 02b67cc5e3e523ca0aff4e4e2bbcc94ddb3d840b Mon Sep 17 00:00:00 2001 From: Tim te Beek Date: Thu, 23 Oct 2025 11:03:20 +0200 Subject: [PATCH 5/5] Add note about a limitation of the current approach --- .../java/migrate/lang/MigrateMainMethodToInstanceMain.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/org/openrewrite/java/migrate/lang/MigrateMainMethodToInstanceMain.java b/src/main/java/org/openrewrite/java/migrate/lang/MigrateMainMethodToInstanceMain.java index 3d3e340a70..027c8c73bb 100644 --- a/src/main/java/org/openrewrite/java/migrate/lang/MigrateMainMethodToInstanceMain.java +++ b/src/main/java/org/openrewrite/java/migrate/lang/MigrateMainMethodToInstanceMain.java @@ -123,7 +123,7 @@ private boolean isMainMethodReferenced(J.MethodDeclaration mainMethod) { return false; } - // Search for method references to main + // XXX Only picks up references in the same compilation unit; convert to scanning recipe if needed return new JavaIsoVisitor() { @Override public J.MemberReference visitMemberReference(J.MemberReference memberRef, AtomicBoolean referenced) {