Skip to content

Commit 72818ad

Browse files
timtebeekclaude
andauthored
Add OpenRewrite recipe for JEP 512 instance main methods (#852)
* Add OpenRewrite recipe for JEP 512 instance main methods Implements a migration recipe to convert `public static void main(String[] args)` to instance `void main()` when the args parameter is unused, as introduced in JEP 512 (Java 21+). The recipe: - Detects main methods with unused String[] args parameter - Removes public and static modifiers - Removes the unused parameter - Preserves formatting and annotations - Only applies to Java 21+ codebases 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <[email protected]> * Apply formatter * Inline named visitor * Adopt `VariableReferences.findRhsReferences` * Use ListUtils.filter instead of manual ArrayList construction Refactored the modifier filtering logic to use OpenRewrite's ListUtils.filter and ListUtils.mapFirst utilities for cleaner and more idiomatic code. * Condense * Suppress warnings for confusing main methods * Further simplifications for String arrays --------- Co-authored-by: Claude <[email protected]>
1 parent db41a78 commit 72818ad

File tree

3 files changed

+371
-0
lines changed

3 files changed

+371
-0
lines changed
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
/*
2+
* Copyright 2025 the original author or authors.
3+
* <p>
4+
* Licensed under the Moderne Source Available License (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
* <p>
8+
* https://docs.moderne.io/licensing/moderne-source-available-license
9+
* <p>
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.openrewrite.java.migrate.lang;
17+
18+
import org.openrewrite.ExecutionContext;
19+
import org.openrewrite.Preconditions;
20+
import org.openrewrite.Recipe;
21+
import org.openrewrite.TreeVisitor;
22+
import org.openrewrite.java.JavaIsoVisitor;
23+
import org.openrewrite.java.search.UsesJavaVersion;
24+
import org.openrewrite.java.tree.J;
25+
import org.openrewrite.java.tree.JavaType;
26+
import org.openrewrite.java.tree.TypeUtils;
27+
import org.openrewrite.staticanalysis.VariableReferences;
28+
29+
import static java.util.Collections.emptyList;
30+
31+
public class MigrateMainMethodToInstanceMain extends Recipe {
32+
@Override
33+
public String getDisplayName() {
34+
return "Migrate `public static void main(String[] args)` to instance `void main()`";
35+
}
36+
37+
@Override
38+
public String getDescription() {
39+
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+.";
40+
}
41+
42+
@Override
43+
public TreeVisitor<?, ExecutionContext> getVisitor() {
44+
return Preconditions.check(new UsesJavaVersion<>(25), new JavaIsoVisitor<ExecutionContext>() {
45+
@Override
46+
public J.MethodDeclaration visitMethodDeclaration(J.MethodDeclaration method, ExecutionContext ctx) {
47+
J.MethodDeclaration md = super.visitMethodDeclaration(method, ctx);
48+
49+
// Check if this is a main method: public static void main(String[] args)
50+
if (!"main".equals(md.getSimpleName()) ||
51+
md.getReturnTypeExpression() == null ||
52+
md.getReturnTypeExpression().getType() != JavaType.Primitive.Void ||
53+
!md.hasModifier(J.Modifier.Type.Public) ||
54+
!md.hasModifier(J.Modifier.Type.Static) ||
55+
md.getParameters().size() != 1 ||
56+
!(md.getParameters().get(0) instanceof J.VariableDeclarations) ||
57+
md.getBody() == null) {
58+
return md;
59+
}
60+
61+
// Check if parameter is String[] type
62+
J.VariableDeclarations param = (J.VariableDeclarations) md.getParameters().get(0);
63+
JavaType paramType = param.getType();
64+
if (!TypeUtils.isOfClassType(paramType, "java.lang.String") || !(paramType instanceof JavaType.Array)) {
65+
return md;
66+
}
67+
68+
// Remove the parameter if unused
69+
J.Identifier variableName = param.getVariables().get(0).getName();
70+
if (VariableReferences.findRhsReferences(md.getBody(), variableName).isEmpty()) {
71+
md = md.withParameters(emptyList());
72+
}
73+
return md.withReturnTypeExpression(md.getReturnTypeExpression().withPrefix(md.getModifiers().get(0).getPrefix()))
74+
.withModifiers(emptyList());
75+
}
76+
});
77+
}
78+
79+
}

src/main/resources/META-INF/rewrite/java-version-25.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ recipeList:
3131
- org.openrewrite.github.SetupJavaUpgradeJavaVersion:
3232
minimumJavaMajorVersion: 25
3333
- org.openrewrite.java.migrate.io.ReplaceSystemOutWithIOPrint
34+
- org.openrewrite.java.migrate.lang.MigrateMainMethodToInstanceMain
3435
- org.openrewrite.java.migrate.lang.MigrateProcessWaitForDuration
3536
- org.openrewrite.java.migrate.lang.ReplaceUnusedVariablesWithUnderscore
3637
- org.openrewrite.java.migrate.util.MigrateInflaterDeflaterToClose
Lines changed: 291 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,291 @@
1+
/*
2+
* Copyright 2025 the original author or authors.
3+
* <p>
4+
* Licensed under the Moderne Source Available License (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
* <p>
8+
* https://docs.moderne.io/licensing/moderne-source-available-license
9+
* <p>
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.openrewrite.java.migrate.lang;
17+
18+
import org.junit.jupiter.api.Test;
19+
import org.openrewrite.DocumentExample;
20+
import org.openrewrite.test.RecipeSpec;
21+
import org.openrewrite.test.RewriteTest;
22+
23+
import static org.openrewrite.java.Assertions.java;
24+
import static org.openrewrite.java.Assertions.javaVersion;
25+
26+
@SuppressWarnings("ConfusingMainMethod")
27+
class MigrateMainMethodToInstanceMainTest implements RewriteTest {
28+
29+
@Override
30+
public void defaults(RecipeSpec spec) {
31+
spec.recipe(new MigrateMainMethodToInstanceMain())
32+
.allSources(s -> s.markers(javaVersion(25)));
33+
}
34+
35+
@DocumentExample
36+
@Test
37+
void migrateMainMethodWithUnusedArgs() {
38+
//language=java
39+
rewriteRun(
40+
java(
41+
"""
42+
class Application {
43+
public static void main(String[] args) {
44+
System.out.println("Hello, World!");
45+
}
46+
}
47+
""",
48+
"""
49+
class Application {
50+
void main() {
51+
System.out.println("Hello, World!");
52+
}
53+
}
54+
"""
55+
)
56+
);
57+
}
58+
59+
@Test
60+
void retainArgsWhenUsed() {
61+
//language=java
62+
rewriteRun(
63+
java(
64+
"""
65+
class Application {
66+
public static void main(String[] args) {
67+
if (args.length > 0) {
68+
System.out.println("Args provided: " + args[0]);
69+
}
70+
}
71+
}
72+
""",
73+
"""
74+
class Application {
75+
void main(String[] args) {
76+
if (args.length > 0) {
77+
System.out.println("Args provided: " + args[0]);
78+
}
79+
}
80+
}
81+
"""
82+
)
83+
);
84+
}
85+
86+
@Test
87+
void retainArgsWhenUsedInMethodCall() {
88+
//language=java
89+
rewriteRun(
90+
java(
91+
"""
92+
class Application {
93+
public static void main(String[] args) {
94+
processArgs(args);
95+
}
96+
97+
private static void processArgs(String[] args) {
98+
// Process arguments
99+
}
100+
}
101+
""",
102+
"""
103+
class Application {
104+
void main(String[] args) {
105+
processArgs(args);
106+
}
107+
108+
private static void processArgs(String[] args) {
109+
// Process arguments
110+
}
111+
}
112+
"""
113+
)
114+
);
115+
}
116+
117+
@Test
118+
void migrateMainMethodWithEmptyBody() {
119+
//language=java
120+
rewriteRun(
121+
java(
122+
"""
123+
class Application {
124+
public static void main(String[] args) {
125+
}
126+
}
127+
""",
128+
"""
129+
class Application {
130+
void main() {
131+
}
132+
}
133+
"""
134+
)
135+
);
136+
}
137+
138+
@Test
139+
void doNotMigrateNonMainMethod() {
140+
//language=java
141+
rewriteRun(
142+
java(
143+
"""
144+
class Application {
145+
public static void notMain(String[] args) {
146+
System.out.println("Not a main method");
147+
}
148+
}
149+
"""
150+
)
151+
);
152+
}
153+
154+
@Test
155+
void doNotMigrateMainWithDifferentSignature() {
156+
//language=java
157+
rewriteRun(
158+
java(
159+
"""
160+
class Application {
161+
public static int main(String[] args) {
162+
return 0;
163+
}
164+
}
165+
"""
166+
)
167+
);
168+
}
169+
170+
@Test
171+
void doNotMigratePrivateMain() {
172+
//language=java
173+
rewriteRun(
174+
java(
175+
"""
176+
class Application {
177+
private static void main(String[] args) {
178+
System.out.println("Private main");
179+
}
180+
}
181+
"""
182+
)
183+
);
184+
}
185+
186+
@Test
187+
void doNotMigrateInstanceMain() {
188+
//language=java
189+
rewriteRun(
190+
java(
191+
"""
192+
class Application {
193+
public void main(String[] args) {
194+
System.out.println("Already instance main");
195+
}
196+
}
197+
"""
198+
)
199+
);
200+
}
201+
202+
@Test
203+
void migrateMainWithComplexUnusedArgs() {
204+
//language=java
205+
rewriteRun(
206+
java(
207+
"""
208+
class Application {
209+
public static void main(String[] arguments) {
210+
System.out.println("Starting application...");
211+
new Application().run();
212+
}
213+
214+
void run() {
215+
System.out.println("Running...");
216+
}
217+
}
218+
""",
219+
"""
220+
class Application {
221+
void main() {
222+
System.out.println("Starting application...");
223+
new Application().run();
224+
}
225+
226+
void run() {
227+
System.out.println("Running...");
228+
}
229+
}
230+
"""
231+
)
232+
);
233+
}
234+
235+
@Test
236+
void doNotMigrateMainWithMultipleParameters() {
237+
//language=java
238+
rewriteRun(
239+
java(
240+
"""
241+
class Application {
242+
public static void main(String first, String[] args) {
243+
System.out.println("Invalid main method");
244+
}
245+
}
246+
"""
247+
)
248+
);
249+
}
250+
251+
@Test
252+
void doNotMigrateMainWithNoParameters() {
253+
//language=java
254+
rewriteRun(
255+
java(
256+
"""
257+
class Application {
258+
public static void main() {
259+
System.out.println("No parameters");
260+
}
261+
}
262+
"""
263+
)
264+
);
265+
}
266+
267+
@Test
268+
void migrateMainWithAnnotations() {
269+
//language=java
270+
rewriteRun(
271+
java(
272+
"""
273+
class Application {
274+
@SuppressWarnings("unused")
275+
public static void main(String[] args) {
276+
System.out.println("Hello!");
277+
}
278+
}
279+
""",
280+
"""
281+
class Application {
282+
@SuppressWarnings("unused")
283+
void main() {
284+
System.out.println("Hello!");
285+
}
286+
}
287+
"""
288+
)
289+
);
290+
}
291+
}

0 commit comments

Comments
 (0)