diff --git a/build.gradle.kts b/build.gradle.kts index e59a089ccb..3e39a436c7 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -46,6 +46,7 @@ dependencies { runtimeOnly("org.openrewrite:rewrite-java-11") runtimeOnly("org.openrewrite:rewrite-java-17") runtimeOnly("org.openrewrite:rewrite-java-21") + runtimeOnly("org.openrewrite:rewrite-java-25") runtimeOnly("tech.picnic.error-prone-support:error-prone-contrib:latest.release:recipes") diff --git a/src/main/java/org/openrewrite/java/migrate/lang/MigrateProcessWaitForDuration.java b/src/main/java/org/openrewrite/java/migrate/lang/MigrateProcessWaitForDuration.java new file mode 100644 index 0000000000..de9a03348c --- /dev/null +++ b/src/main/java/org/openrewrite/java/migrate/lang/MigrateProcessWaitForDuration.java @@ -0,0 +1,122 @@ +/* + * 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.jspecify.annotations.Nullable; +import org.openrewrite.ExecutionContext; +import org.openrewrite.Preconditions; +import org.openrewrite.Recipe; +import org.openrewrite.TreeVisitor; +import org.openrewrite.java.JavaTemplate; +import org.openrewrite.java.JavaVisitor; +import org.openrewrite.java.MethodMatcher; +import org.openrewrite.java.search.UsesJavaVersion; +import org.openrewrite.java.tree.Expression; +import org.openrewrite.java.tree.J; +import org.openrewrite.staticanalysis.SimplifyDurationCreationUnits; + +public class MigrateProcessWaitForDuration extends Recipe { + + private static final MethodMatcher PROCESS_WAIT_FOR_MATCHER = new MethodMatcher("java.lang.Process waitFor(long, java.util.concurrent.TimeUnit)"); + + @Override + public String getDisplayName() { + return "Use `Process#waitFor(Duration)`"; + } + + @Override + public String getDescription() { + return "Use `Process#waitFor(Duration)` instead of `Process#waitFor(long, TimeUnit)` in Java 25 or higher."; + } + + @Override + public TreeVisitor getVisitor() { + return Preconditions.check(new UsesJavaVersion<>(25), new JavaVisitor() { + @Override + public J visitMethodInvocation(J.MethodInvocation method, ExecutionContext ctx) { + J.MethodInvocation mi = (J.MethodInvocation) super.visitMethodInvocation(method, ctx); + + if (PROCESS_WAIT_FOR_MATCHER.matches(mi)) { + Expression valueArg = mi.getArguments().get(0); + Expression unitArg = mi.getArguments().get(1); + String timeUnitName = getTimeUnitName(unitArg); + String durationMethod = getDurationMethod(timeUnitName); + + boolean isSimpleValue = valueArg instanceof J.Literal || valueArg instanceof J.Identifier; + + maybeRemoveImport("java.util.concurrent.TimeUnit"); + maybeRemoveImport("java.util.concurrent.TimeUnit." + timeUnitName); + maybeAddImport("java.time.Duration"); + maybeAddImport("java.time.temporal.ChronoUnit"); + + doAfterVisit(new SimplifyDurationCreationUnits().getVisitor()); + + if (isSimpleValue && "MICROSECONDS".equals(timeUnitName)) { + return JavaTemplate.builder("Duration.of(#{any(long)}, ChronoUnit.MICROS)") + .imports("java.time.Duration", "java.time.temporal.ChronoUnit") + .build() + .apply(getCursor(), mi.getCoordinates().replaceArguments(), valueArg); + } + if (isSimpleValue && durationMethod != null) { + return JavaTemplate.builder("Duration." + durationMethod + "(#{any(long)})") + .imports("java.time.Duration") + .build() + .apply(getCursor(), mi.getCoordinates().replaceArguments(), valueArg); + } + return JavaTemplate.builder("Duration.of(#{any(long)}, #{any(java.util.concurrent.TimeUnit)}.toChronoUnit())") + .imports("java.time.Duration") + .build() + .apply(getCursor(), mi.getCoordinates().replaceArguments(), valueArg, unitArg); + } + return mi; + } + + private @Nullable String getTimeUnitName(Expression timeUnitArg) { + if (timeUnitArg instanceof J.FieldAccess) { + J.FieldAccess fa = (J.FieldAccess) timeUnitArg; + return fa.getSimpleName(); + } + if (timeUnitArg instanceof J.Identifier) { + J.Identifier id = (J.Identifier) timeUnitArg; + return id.getSimpleName(); + } + return null; + } + + private @Nullable String getDurationMethod(@Nullable String timeUnitName) { + if (timeUnitName == null) { + return null; + } + switch (timeUnitName) { + case "NANOSECONDS": + return "ofNanos"; + case "MILLISECONDS": + return "ofMillis"; + case "SECONDS": + return "ofSeconds"; + case "MINUTES": + return "ofMinutes"; + case "HOURS": + return "ofHours"; + case "DAYS": + return "ofDays"; + default: + return null; + } + } + }); + } +} 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 bd442115aa..b14fc80916 100644 --- a/src/main/resources/META-INF/rewrite/java-version-25.yml +++ b/src/main/resources/META-INF/rewrite/java-version-25.yml @@ -27,6 +27,7 @@ tags: recipeList: - org.openrewrite.java.migrate.UpgradeToJava21 - org.openrewrite.java.migrate.UpgradeBuildToJava25 + - org.openrewrite.java.migrate.lang.MigrateProcessWaitForDuration - org.openrewrite.java.migrate.AccessController - org.openrewrite.java.migrate.RemoveSecurityPolicy - org.openrewrite.java.migrate.RemoveSecurityManager diff --git a/src/test/java/org/openrewrite/java/migrate/lang/MigrateProcessWaitForDurationTest.java b/src/test/java/org/openrewrite/java/migrate/lang/MigrateProcessWaitForDurationTest.java new file mode 100644 index 0000000000..7c0ac68617 --- /dev/null +++ b/src/test/java/org/openrewrite/java/migrate/lang/MigrateProcessWaitForDurationTest.java @@ -0,0 +1,322 @@ +/* + * 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.junit.jupiter.api.condition.EnabledForJreRange; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; +import org.openrewrite.DocumentExample; +import org.openrewrite.test.RecipeSpec; +import org.openrewrite.test.RewriteTest; + +import static org.junit.jupiter.api.condition.JRE.JAVA_25; +import static org.openrewrite.java.Assertions.java; +import static org.openrewrite.java.Assertions.javaVersion; + +class MigrateProcessWaitForDurationTest implements RewriteTest { + + @Override + public void defaults(RecipeSpec spec) { + spec + .allSources(src -> src.markers(javaVersion(25))) + .recipe(new MigrateProcessWaitForDuration()); + } + + @DocumentExample + @Test + void migrateProcessWaitForWithStaticImport() { + rewriteRun( + //language=java + java( + """ + import static java.util.concurrent.TimeUnit.SECONDS; + import static java.util.concurrent.TimeUnit.MILLISECONDS; + + class Test { + void test(Process process) throws Exception { + process.waitFor(5, SECONDS); + process.waitFor(100, MILLISECONDS); + } + } + """, + """ + import java.time.Duration; + + class Test { + void test(Process process) throws Exception { + process.waitFor(Duration.ofSeconds(5)); + process.waitFor(Duration.ofMillis(100)); + } + } + """ + ) + ); + } + + @CsvSource(textBlock = """ + SECONDS, 5, ofSeconds + MINUTES, 2, ofMinutes + HOURS, 23, ofHours + DAYS, 7, ofDays + MILLISECONDS, 1001, ofMillis + NANOSECONDS, 1000000, ofNanos + """) + @ParameterizedTest + void migrateProcessWaitForWithExpressiveMethods(String timeUnit, String value, String durationMethod) { + rewriteRun( + java( + """ + import java.util.concurrent.TimeUnit; + + class Test { + void test(Process process) throws Exception { + process.waitFor(%s, TimeUnit.%s); + } + } + """.formatted(value, timeUnit), + """ + import java.time.Duration; + + class Test { + void test(Process process) throws Exception { + process.waitFor(Duration.%s(%s)); + } + } + """.formatted(durationMethod, value) + ) + ); + } + + @Test + void migrateProcessWaitForWithMicroseconds() { + rewriteRun( + //language=java + java( + """ + import java.util.concurrent.TimeUnit; + + class Test { + void test(Process process) throws Exception { + process.waitFor(999, TimeUnit.MICROSECONDS); + } + } + """, + """ + import java.time.Duration; + import java.time.temporal.ChronoUnit; + + class Test { + void test(Process process) throws Exception { + process.waitFor(Duration.of(999, ChronoUnit.MICROS)); + } + } + """ + ) + ); + } + + @Test + void migrateProcessWaitForWithVariables() { + rewriteRun( + //language=java + java( + """ + import java.util.concurrent.TimeUnit; + + class Test { + void test(Process process) throws Exception { + long timeout = 5; + TimeUnit unit = TimeUnit.SECONDS; + process.waitFor(timeout, unit); + } + } + """, + """ + import java.time.Duration; + import java.util.concurrent.TimeUnit; + + class Test { + void test(Process process) throws Exception { + long timeout = 5; + TimeUnit unit = TimeUnit.SECONDS; + process.waitFor(Duration.of(timeout, unit.toChronoUnit())); + } + } + """ + ) + ); + } + + @Test + void migrateProcessWaitForWithMethodCall() { + rewriteRun( + //language=java + java( + """ + import java.util.concurrent.TimeUnit; + + class Test { + private long getTimeout() { + return 10; + } + + void test(Process process) throws Exception { + process.waitFor(getTimeout(), TimeUnit.SECONDS); + } + } + """, + """ + import java.time.Duration; + import java.util.concurrent.TimeUnit; + + class Test { + private long getTimeout() { + return 10; + } + + void test(Process process) throws Exception { + process.waitFor(Duration.of(getTimeout(), TimeUnit.SECONDS.toChronoUnit())); + } + } + """ + ) + ); + } + + @Test + void migrateProcessWaitForWithExpression() { + rewriteRun( + //language=java + java( + """ + import java.util.concurrent.TimeUnit; + + class Test { + void test(Process process) throws Exception { + long timeout = 5; + process.waitFor(timeout * 2, TimeUnit.MINUTES); + } + } + """, + """ + import java.time.Duration; + import java.util.concurrent.TimeUnit; + + class Test { + void test(Process process) throws Exception { + long timeout = 5; + process.waitFor(Duration.of(timeout * 2, TimeUnit.MINUTES.toChronoUnit())); + } + } + """ + ) + ); + } + + @Test + void migrateProcessWaitForWithReturnValue() { + rewriteRun( + //language=java + java( + """ + import java.util.concurrent.TimeUnit; + + class Test { + void test(Process process) throws Exception { + if (process.waitFor(5, TimeUnit.SECONDS)) { + System.out.println("Process completed within timeout"); + } + } + } + """, + """ + import java.time.Duration; + + class Test { + void test(Process process) throws Exception { + if (process.waitFor(Duration.ofSeconds(5))) { + System.out.println("Process completed within timeout"); + } + } + } + """ + ) + ); + } + + @Test + void migrateProcessWaitForWithChainedCalls() { + rewriteRun( + //language=java + java( + """ + import java.util.concurrent.TimeUnit; + + class Test { + void test() throws Exception { + new ProcessBuilder("echo", "hello").start().waitFor(3, TimeUnit.SECONDS); + } + } + """, + """ + import java.time.Duration; + + class Test { + void test() throws Exception { + new ProcessBuilder("echo", "hello").start().waitFor(Duration.ofSeconds(3)); + } + } + """ + ) + ); + } + + @EnabledForJreRange(min = JAVA_25) + @Test + void noChangeForWaitForDuration() { + rewriteRun( + //language=java + java( + """ + import java.time.Duration; + + class Test { + void test(Process process) throws Exception { + process.waitFor(Duration.ofSeconds(5)); + } + } + """ + ) + ); + } + + @Test + void noChangeForParameterlessWaitFor() { + rewriteRun( + //language=java + java( + """ + class Test { + void test(Process process) throws Exception { + process.waitFor(); + } + } + """ + ) + ); + } +}