diff --git a/src/main/java/org/openrewrite/java/migrate/lang/SwitchCaseAssignmentsToSwitchExpression.java b/src/main/java/org/openrewrite/java/migrate/lang/SwitchCaseAssignmentsToSwitchExpression.java index a4f437afd0..31fcad0a02 100644 --- a/src/main/java/org/openrewrite/java/migrate/lang/SwitchCaseAssignmentsToSwitchExpression.java +++ b/src/main/java/org/openrewrite/java/migrate/lang/SwitchCaseAssignmentsToSwitchExpression.java @@ -70,6 +70,7 @@ public J.Block visitBlock(J.Block originalBlock, ExecutionContext ctx) { return block.withStatements(ListUtils.map(block.getStatements(), (index, statement) -> { if (statement == originalSwitch.getAndSet(null)) { doAfterVisit(new InlineVariable().getVisitor()); + doAfterVisit(new SwitchExpressionYieldToArrow().getVisitor()); // We've already converted the switch/assignments to an assignment with a switch expression. return null; } diff --git a/src/main/java/org/openrewrite/java/migrate/lang/SwitchExpressionYieldToArrow.java b/src/main/java/org/openrewrite/java/migrate/lang/SwitchExpressionYieldToArrow.java new file mode 100644 index 0000000000..691dc37cc4 --- /dev/null +++ b/src/main/java/org/openrewrite/java/migrate/lang/SwitchExpressionYieldToArrow.java @@ -0,0 +1,104 @@ +/* + * 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 lombok.EqualsAndHashCode; +import lombok.Value; +import org.openrewrite.ExecutionContext; +import org.openrewrite.Preconditions; +import org.openrewrite.Recipe; +import org.openrewrite.TreeVisitor; +import org.openrewrite.internal.ListUtils; +import org.openrewrite.java.JavaIsoVisitor; +import org.openrewrite.java.search.UsesJavaVersion; +import org.openrewrite.java.tree.J; +import org.openrewrite.java.tree.JContainer; +import org.openrewrite.java.tree.Space; +import org.openrewrite.java.tree.Statement; +import org.openrewrite.staticanalysis.groovy.GroovyFileChecker; +import org.openrewrite.staticanalysis.kotlin.KotlinFileChecker; + +import static java.util.Objects.requireNonNull; + +@Value +@EqualsAndHashCode(callSuper = false) +public class SwitchExpressionYieldToArrow extends Recipe { + @Override + public String getDisplayName() { + return "Convert switch expression yield to arrow"; + } + + @Override + public String getDescription() { + return "Convert switch expressions with colon cases and yield statements to arrow syntax."; + } + + @Override + public TreeVisitor getVisitor() { + TreeVisitor preconditions = Preconditions.and( + new UsesJavaVersion<>(14), + Preconditions.not(new KotlinFileChecker<>()), + Preconditions.not(new GroovyFileChecker<>()) + ); + return Preconditions.check(preconditions, new JavaIsoVisitor() { + @Override + public J.SwitchExpression visitSwitchExpression(J.SwitchExpression switchExpression, ExecutionContext ctx) { + J.SwitchExpression se = super.visitSwitchExpression(switchExpression, ctx); + + // Check if this is a complex case (more than just a yield) + if (anythingOtherThanYield(se)) { + return se; + } + + return se.withCases(se.getCases().withStatements(ListUtils.map(se.getCases().getStatements(), statement -> { + J.Case caseStatement = (J.Case) requireNonNull(statement); + J.Yield yieldStatement = (J.Yield) caseStatement.getStatements().get(0); + + // Add space after the last case label to create space before arrow + JContainer caseLabels = caseStatement.getPadding().getCaseLabels(); + JContainer updatedLabels = caseLabels.getPadding().withElements( + ListUtils.mapLast(caseLabels.getPadding().getElements(), + elem -> elem.withAfter(Space.SINGLE_SPACE)) + ); + + return caseStatement + .withStatements(null) + .withBody(yieldStatement.getValue().withPrefix(Space.SINGLE_SPACE)) + .withType(J.Case.Type.Rule) + .getPadding() + .withCaseLabels(updatedLabels); + }))); + } + + // For now, we only convert switch expressions that consist solely of yield statements + private boolean anythingOtherThanYield(J.SwitchExpression se) { + for (Statement statement : se.getCases().getStatements()) { + if (!(statement instanceof J.Case)) { + return true; + } + + J.Case caseStatement = (J.Case) statement; + if (caseStatement.getType() != J.Case.Type.Statement || + caseStatement.getStatements().size() != 1 || + !(caseStatement.getStatements().get(0) instanceof J.Yield)) { + return true; + } + } + return false; + } + }); + } +} diff --git a/src/main/resources/META-INF/rewrite/examples.yml b/src/main/resources/META-INF/rewrite/examples.yml index 951bcb4e13..d3723be3f2 100644 --- a/src/main/resources/META-INF/rewrite/examples.yml +++ b/src/main/resources/META-INF/rewrite/examples.yml @@ -1025,31 +1025,34 @@ recipeName: org.openrewrite.java.migrate.UpgradePluginsForJava11 examples: - description: '' sources: + - before: project + language: mavenProject - before: | com.mycompany.app my-app 1 - - 1.8.0 - - ro.isdc.wro4j - wro4j-maven-plugin - ${wro4j.version} + org.codehaus.mojo + jaxb2-maven-plugin + 2.3.1 - after: | + path: pom.xml + language: xml +- description: '' + sources: + - before: | com.mycompany.app my-app 1 - 1.10.1 + 1.8.0 @@ -1061,23 +1064,20 @@ examples: - path: pom.xml - language: xml -- description: '' - sources: - - before: project - language: mavenProject - - before: | + after: | com.mycompany.app my-app 1 + + 1.10.1 + - org.codehaus.mojo - jaxb2-maven-plugin - 2.3.1 + ro.isdc.wro4j + wro4j-maven-plugin + ${wro4j.version} @@ -3338,25 +3338,6 @@ examples: type: specs.openrewrite.org/v1beta/example recipeName: org.openrewrite.java.migrate.guava.NoGuava examples: -- description: '' - sources: - - before: | - import com.google.common.base.MoreObjects; - - class A { - Object foo(Object obj) { - return MoreObjects.firstNonNull(obj, "default"); - } - } - after: | - import java.util.Objects; - - class A { - Object foo(Object obj) { - return Objects.requireNonNullElse(obj, "default"); - } - } - language: java - description: '' sources: - before: | @@ -3403,6 +3384,25 @@ examples: } } language: java +- description: '' + sources: + - before: | + import com.google.common.base.MoreObjects; + + class A { + Object foo(Object obj) { + return MoreObjects.firstNonNull(obj, "default"); + } + } + after: | + import java.util.Objects; + + class A { + Object foo(Object obj) { + return Objects.requireNonNullElse(obj, "default"); + } + } + language: java --- type: specs.openrewrite.org/v1beta/example recipeName: org.openrewrite.java.migrate.guava.NoGuavaAtomicsNewReference @@ -6235,7 +6235,7 @@ examples: language: java --- type: specs.openrewrite.org/v1beta/example -recipeName: org.openrewrite.java.migrate.lang.SwitchCaseAssigningToSwitchExpression +recipeName: org.openrewrite.java.migrate.lang.SwitchCaseAssignmentsToSwitchExpression examples: - description: '' sources: @@ -6314,6 +6314,35 @@ examples: language: java --- type: specs.openrewrite.org/v1beta/example +recipeName: org.openrewrite.java.migrate.lang.SwitchExpressionYieldToArrow +examples: +- description: '' + sources: + - before: | + class Test { + String format(String str) { + String formatted = switch (str) { + case "foo": yield "Foo"; + case "bar": yield "Bar"; + case null, default: yield "unknown"; + }; + return formatted; + } + } + after: | + class Test { + String format(String str) { + String formatted = switch (str) { + case "foo" -> "Foo"; + case "bar" -> "Bar"; + case null, default -> "unknown"; + }; + return formatted; + } + } + language: java +--- +type: specs.openrewrite.org/v1beta/example recipeName: org.openrewrite.java.migrate.lang.ThreadStopUnsupported examples: - description: '' diff --git a/src/main/resources/META-INF/rewrite/java-version-17.yml b/src/main/resources/META-INF/rewrite/java-version-17.yml index 20f9c20bc3..770a980df2 100644 --- a/src/main/resources/META-INF/rewrite/java-version-17.yml +++ b/src/main/resources/META-INF/rewrite/java-version-17.yml @@ -60,6 +60,7 @@ recipeList: newVersion: 1.17.x - org.openrewrite.java.migrate.AddLombokMapstructBinding - org.openrewrite.java.migrate.lang.SwitchCaseAssignmentsToSwitchExpression + - org.openrewrite.java.migrate.lang.SwitchExpressionYieldToArrow --- type: specs.openrewrite.org/v1beta/recipe diff --git a/src/test/java/org/openrewrite/java/migrate/lang/SwitchCaseAssignmentsToSwitchExpressionTest.java b/src/test/java/org/openrewrite/java/migrate/lang/SwitchCaseAssignmentsToSwitchExpressionTest.java index cda324c392..94fb9957c0 100644 --- a/src/test/java/org/openrewrite/java/migrate/lang/SwitchCaseAssignmentsToSwitchExpressionTest.java +++ b/src/test/java/org/openrewrite/java/migrate/lang/SwitchCaseAssignmentsToSwitchExpressionTest.java @@ -86,9 +86,9 @@ void doFormat(Object obj) { class Test { void doFormat(Object obj) { String formatted = switch (obj) { - case Integer i: yield String.format("int %d", i); - case Long l: yield String.format("long %d", l); - default: yield "unknown"; + case Integer i -> String.format("int %d", i); + case Long l -> String.format("long %d", l); + default -> "unknown"; }; } } @@ -143,9 +143,9 @@ void doFormat(Object obj) { class Test { void doFormat(Object obj) { String formatted = switch (obj) { - case Integer i: yield String.format("int %d", i); - case Long l: yield String.format("long %d", l); - default: yield "unknown"; + case Integer i -> String.format("int %d", i); + case Long l -> String.format("long %d", l); + default -> "unknown"; }; } } @@ -178,9 +178,9 @@ void doFormat(Object obj) { class Test { void doFormat(Object obj) { String formatted = switch (obj) { - case Integer i: yield String.format("int %d", i); - case Long l: yield String.format("long %d", l); - default: yield "unknown"; + case Integer i -> String.format("int %d", i); + case Long l -> String.format("long %d", l); + default -> "unknown"; }; } } @@ -352,9 +352,9 @@ enum TrafficLight { } void doFormat(TrafficLight light) { String status = switch (light) { - case RED: yield "stop"; - case GREEN: yield "go"; - default: yield "initialValue"; + case RED -> "stop"; + case GREEN -> "go"; + default -> "initialValue"; }; } } @@ -416,9 +416,9 @@ enum TrafficLight { } void doFormat(TrafficLight light) { String status = switch (light) { - case RED: yield "stop"; - case GREEN: yield "go"; - case YELLOW: yield "unsure"; + case RED -> "stop"; + case GREEN -> "go"; + case YELLOW -> "unsure"; }; } } @@ -537,7 +537,7 @@ class Test { void doFormat(int i) { String orig = switch (i) { - default: yield "hello"; + default -> "hello"; }; } } @@ -633,7 +633,7 @@ String doFormat() { class Test { String doFormat() { return switch (1) { - default: yield "foo"; + default -> "foo"; }; } } @@ -679,14 +679,14 @@ void noReturnedExpression() { class Test { String originalVariableNotReturned() { String formatted = switch (1) { - default: yield "foo"; + default -> "foo"; }; return "string"; } String codeBetweenSwitchAndReturn() { String formatted = switch (1) { - default: yield "foo"; + default -> "foo"; }; System.out.println("Hey"); return formatted; @@ -694,7 +694,7 @@ String codeBetweenSwitchAndReturn() { void noReturnedExpression() { String formatted = switch (1) { - default: yield "foo"; + default -> "foo"; }; return; } @@ -725,9 +725,9 @@ void doFormat(String str) { class A { void doFormat(String str) { String formatted = switch (str) { - case "foo": yield "Foo"; - case "bar": yield "Bar"; - case null, default: yield "unknown"; + case "foo" -> "Foo"; + case "bar" -> "Bar"; + case null, default -> "unknown"; }; } } @@ -786,7 +786,7 @@ void doFormat() { class Test { void doFormat() { String formatted = switch (1) { - default: yield "foo"; + default -> "foo"; }; } } @@ -866,7 +866,7 @@ String doFormat() { // last line of the switch // between switch and return return switch (1) { // on the switch after code - default: yield "foo"; + default -> "foo"; }; // after return on the same line } } diff --git a/src/test/java/org/openrewrite/java/migrate/lang/SwitchExpressionYieldToArrowTest.java b/src/test/java/org/openrewrite/java/migrate/lang/SwitchExpressionYieldToArrowTest.java new file mode 100644 index 0000000000..4252aab4a5 --- /dev/null +++ b/src/test/java/org/openrewrite/java/migrate/lang/SwitchExpressionYieldToArrowTest.java @@ -0,0 +1,200 @@ +/* + * 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.Issue; +import org.openrewrite.test.RecipeSpec; +import org.openrewrite.test.RewriteTest; + +import static org.openrewrite.java.Assertions.java; +import static org.openrewrite.java.Assertions.javaVersion; + +class SwitchExpressionYieldToArrowTest implements RewriteTest { + + @Override + public void defaults(RecipeSpec spec) { + spec.recipe(new SwitchExpressionYieldToArrow()) + .allSources(s -> s.markers(javaVersion(17))); + } + + @DocumentExample + @Issue("https://github.com/openrewrite/rewrite-migrate-java/issues/799") + @Test + void convertSwitchExpressionWithYield() { + rewriteRun( + //language=java + java( + """ + class Test { + String format(String str) { + String formatted = switch (str) { + case "foo": yield "Foo"; + case "bar": yield "Bar"; + case null, default: yield "unknown"; + }; + return formatted; + } + } + """, + """ + class Test { + String format(String str) { + String formatted = switch (str) { + case "foo" -> "Foo"; + case "bar" -> "Bar"; + case null, default -> "unknown"; + }; + return formatted; + } + } + """ + ) + ); + } + + @Test + void convertSwitchExpressionWithSimpleYields() { + rewriteRun( + //language=java + java( + """ + class Test { + int getValue(String str) { + return switch (str) { + case "one": yield 1; + case "two": yield 2; + default: yield 0; + }; + } + } + """, + """ + class Test { + int getValue(String str) { + return switch (str) { + case "one" -> 1; + case "two" -> 2; + default -> 0; + }; + } + } + """ + ) + ); + } + + @Test + void convertEnumSwitchExpression() { + rewriteRun( + //language=java + java( + """ + class Test { + enum Color { RED, GREEN, BLUE } + + String colorName(Color color) { + return switch (color) { + case RED: yield "Red"; + case GREEN: yield "Green"; + case BLUE: yield "Blue"; + }; + } + } + """, + """ + class Test { + enum Color { RED, GREEN, BLUE } + + String colorName(Color color) { + return switch (color) { + case RED -> "Red"; + case GREEN -> "Green"; + case BLUE -> "Blue"; + }; + } + } + """ + ) + ); + } + + @Test + void doNotConvertArrowCases() { + rewriteRun( + //language=java + java( + """ + class Test { + String format(String str) { + return switch (str) { + case "foo" -> "Foo"; + case "bar" -> "Bar"; + default -> "Other"; + }; + } + } + """ + ) + ); + } + + @Test + void doNotConvertComplexYieldCases() { + rewriteRun( + //language=java + java( + """ + class Test { + String process(String str) { + return switch (str) { + case "foo": + System.out.println("Processing foo"); + yield "Foo"; + case "bar": yield "Bar"; + default: yield "Other"; + }; + } + } + """ + ) + ); + } + + @Test + void doNotConvertEmptyCases() { + rewriteRun( + java( + """ + class Test { + enum TrafficLight { + RED, GREEN, YELLOW + } + void doFormat(TrafficLight light) { + String status = switch (light) { + case RED: + case GREEN: + case YELLOW: yield "unsure"; + default: yield "unknown"; + }; + } + } + """ + ) + ); + } +}