diff --git a/src/main/java/org/openrewrite/java/migrate/lang/MigrateMainMethodToInstanceMain.java b/src/main/java/org/openrewrite/java/migrate/lang/MigrateMainMethodToInstanceMain.java new file mode 100644 index 0000000000..1d0b684c03 --- /dev/null +++ b/src/main/java/org/openrewrite/java/migrate/lang/MigrateMainMethodToInstanceMain.java @@ -0,0 +1,79 @@ +/* + * Copyright 2025 the original author or authors. + *

+ * Licensed under the Moderne Source Available License (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * https://docs.moderne.io/licensing/moderne-source-available-license + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.openrewrite.java.migrate.lang; + +import org.openrewrite.ExecutionContext; +import org.openrewrite.Preconditions; +import org.openrewrite.Recipe; +import org.openrewrite.TreeVisitor; +import org.openrewrite.java.JavaIsoVisitor; +import org.openrewrite.java.search.UsesJavaVersion; +import org.openrewrite.java.tree.J; +import org.openrewrite.java.tree.JavaType; +import org.openrewrite.java.tree.TypeUtils; +import org.openrewrite.staticanalysis.VariableReferences; + +import static java.util.Collections.emptyList; + +public class MigrateMainMethodToInstanceMain extends Recipe { + @Override + public String getDisplayName() { + return "Migrate `public static void main(String[] args)` to instance `void main()`"; + } + + @Override + public String getDescription() { + return "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+."; + } + + @Override + public TreeVisitor getVisitor() { + return Preconditions.check(new UsesJavaVersion<>(25), new JavaIsoVisitor() { + @Override + public J.MethodDeclaration visitMethodDeclaration(J.MethodDeclaration method, ExecutionContext ctx) { + 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()) || + 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; + } + + // Check if parameter is String[] type + J.VariableDeclarations param = (J.VariableDeclarations) md.getParameters().get(0); + JavaType paramType = param.getType(); + if (!TypeUtils.isOfClassType(paramType, "java.lang.String") || !(paramType instanceof JavaType.Array)) { + return md; + } + + // Remove the parameter if unused + J.Identifier variableName = param.getVariables().get(0).getName(); + if (VariableReferences.findRhsReferences(md.getBody(), variableName).isEmpty()) { + md = md.withParameters(emptyList()); + } + return md.withReturnTypeExpression(md.getReturnTypeExpression().withPrefix(md.getModifiers().get(0).getPrefix())) + .withModifiers(emptyList()); + } + }); + } + +} diff --git a/src/main/resources/META-INF/rewrite/java-version-25.yml b/src/main/resources/META-INF/rewrite/java-version-25.yml index 5b3f2f4e4d..a569cccddf 100644 --- a/src/main/resources/META-INF/rewrite/java-version-25.yml +++ b/src/main/resources/META-INF/rewrite/java-version-25.yml @@ -31,6 +31,7 @@ recipeList: - org.openrewrite.github.SetupJavaUpgradeJavaVersion: minimumJavaMajorVersion: 25 - org.openrewrite.java.migrate.io.ReplaceSystemOutWithIOPrint + - org.openrewrite.java.migrate.lang.MigrateMainMethodToInstanceMain - org.openrewrite.java.migrate.lang.MigrateProcessWaitForDuration - org.openrewrite.java.migrate.lang.ReplaceUnusedVariablesWithUnderscore - org.openrewrite.java.migrate.util.MigrateInflaterDeflaterToClose diff --git a/src/test/java/org/openrewrite/java/migrate/lang/MigrateMainMethodToInstanceMainTest.java b/src/test/java/org/openrewrite/java/migrate/lang/MigrateMainMethodToInstanceMainTest.java new file mode 100644 index 0000000000..9ceb82ab0c --- /dev/null +++ b/src/test/java/org/openrewrite/java/migrate/lang/MigrateMainMethodToInstanceMainTest.java @@ -0,0 +1,291 @@ +/* + * Copyright 2025 the original author or authors. + *

+ * Licensed under the Moderne Source Available License (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * https://docs.moderne.io/licensing/moderne-source-available-license + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.openrewrite.java.migrate.lang; + +import org.junit.jupiter.api.Test; +import org.openrewrite.DocumentExample; +import org.openrewrite.test.RecipeSpec; +import org.openrewrite.test.RewriteTest; + +import static org.openrewrite.java.Assertions.java; +import static org.openrewrite.java.Assertions.javaVersion; + +@SuppressWarnings("ConfusingMainMethod") +class MigrateMainMethodToInstanceMainTest implements RewriteTest { + + @Override + public void defaults(RecipeSpec spec) { + spec.recipe(new MigrateMainMethodToInstanceMain()) + .allSources(s -> s.markers(javaVersion(25))); + } + + @DocumentExample + @Test + void migrateMainMethodWithUnusedArgs() { + //language=java + rewriteRun( + java( + """ + class Application { + public static void main(String[] args) { + System.out.println("Hello, World!"); + } + } + """, + """ + class Application { + void main() { + System.out.println("Hello, World!"); + } + } + """ + ) + ); + } + + @Test + void retainArgsWhenUsed() { + //language=java + rewriteRun( + java( + """ + class Application { + public static void main(String[] args) { + if (args.length > 0) { + System.out.println("Args provided: " + args[0]); + } + } + } + """, + """ + class Application { + void main(String[] args) { + if (args.length > 0) { + System.out.println("Args provided: " + args[0]); + } + } + } + """ + ) + ); + } + + @Test + void retainArgsWhenUsedInMethodCall() { + //language=java + rewriteRun( + java( + """ + class Application { + public static void main(String[] args) { + processArgs(args); + } + + private static void processArgs(String[] args) { + // Process arguments + } + } + """, + """ + class Application { + void main(String[] args) { + processArgs(args); + } + + private static void processArgs(String[] args) { + // Process arguments + } + } + """ + ) + ); + } + + @Test + void migrateMainMethodWithEmptyBody() { + //language=java + rewriteRun( + java( + """ + class Application { + public static void main(String[] args) { + } + } + """, + """ + class Application { + void main() { + } + } + """ + ) + ); + } + + @Test + void doNotMigrateNonMainMethod() { + //language=java + rewriteRun( + java( + """ + class Application { + public static void notMain(String[] args) { + System.out.println("Not a main method"); + } + } + """ + ) + ); + } + + @Test + void doNotMigrateMainWithDifferentSignature() { + //language=java + rewriteRun( + java( + """ + class Application { + public static int main(String[] args) { + return 0; + } + } + """ + ) + ); + } + + @Test + void doNotMigratePrivateMain() { + //language=java + rewriteRun( + java( + """ + class Application { + private static void main(String[] args) { + System.out.println("Private main"); + } + } + """ + ) + ); + } + + @Test + void doNotMigrateInstanceMain() { + //language=java + rewriteRun( + java( + """ + class Application { + public void main(String[] args) { + System.out.println("Already instance main"); + } + } + """ + ) + ); + } + + @Test + void migrateMainWithComplexUnusedArgs() { + //language=java + rewriteRun( + java( + """ + class Application { + public static void main(String[] arguments) { + System.out.println("Starting application..."); + new Application().run(); + } + + void run() { + System.out.println("Running..."); + } + } + """, + """ + class Application { + void main() { + System.out.println("Starting application..."); + new Application().run(); + } + + void run() { + System.out.println("Running..."); + } + } + """ + ) + ); + } + + @Test + void doNotMigrateMainWithMultipleParameters() { + //language=java + rewriteRun( + java( + """ + class Application { + public static void main(String first, String[] args) { + System.out.println("Invalid main method"); + } + } + """ + ) + ); + } + + @Test + void doNotMigrateMainWithNoParameters() { + //language=java + rewriteRun( + java( + """ + class Application { + public static void main() { + System.out.println("No parameters"); + } + } + """ + ) + ); + } + + @Test + void migrateMainWithAnnotations() { + //language=java + rewriteRun( + java( + """ + class Application { + @SuppressWarnings("unused") + public static void main(String[] args) { + System.out.println("Hello!"); + } + } + """, + """ + class Application { + @SuppressWarnings("unused") + void main() { + System.out.println("Hello!"); + } + } + """ + ) + ); + } +}