2020import org .openrewrite .Recipe ;
2121import org .openrewrite .TreeVisitor ;
2222import org .openrewrite .java .JavaIsoVisitor ;
23+ import org .openrewrite .java .MethodMatcher ;
24+ import org .openrewrite .java .search .DeclaresMethod ;
2325import org .openrewrite .java .search .UsesJavaVersion ;
2426import org .openrewrite .java .tree .J ;
2527import org .openrewrite .java .tree .JavaType ;
2628import org .openrewrite .java .tree .TypeUtils ;
2729import org .openrewrite .staticanalysis .VariableReferences ;
2830
31+ import java .util .List ;
32+ import java .util .concurrent .atomic .AtomicBoolean ;
33+
2934import static java .util .Collections .emptyList ;
35+ import static java .util .stream .Collectors .toList ;
3036
3137public 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}
0 commit comments