Skip to content

Commit 97410ab

Browse files
Jenson3210jevanlingentimtebeek
authored
Refine switch cases (#749)
* Refine switch cases * Re-adding no-op block * renamed case -> case_ * Update src/main/java/org/openrewrite/java/migrate/lang/RefineSwitchCases.java Co-authored-by: Jacob van Lingen <jacobvanlingen@hotmail.com> * Update src/main/java/org/openrewrite/java/migrate/lang/RefineSwitchCases.java Co-authored-by: Jacob van Lingen <jacobvanlingen@hotmail.com> * empty one-line J.Blocks * no overhead method call * Minor polish * Show problematic test case * Avoid problematic case by checking for existing guard * Avoid needless calculation of label variables if there's no `if` * Handle unexpected elements in `getConditionVariables` * Rename methods to show intent, as they are not simple `get` methods * Use `Set` and `containsAll` * Prefer to keep body on next line, for a smaller easier diff --------- Co-authored-by: Jacob van Lingen <jacobvanlingen@hotmail.com> Co-authored-by: Tim te Beek <tim@moderne.io>
1 parent e596aad commit 97410ab

File tree

3 files changed

+386
-1
lines changed

3 files changed

+386
-1
lines changed
Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
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 lombok.EqualsAndHashCode;
19+
import lombok.Value;
20+
import org.openrewrite.*;
21+
import org.openrewrite.internal.ListUtils;
22+
import org.openrewrite.java.JavaIsoVisitor;
23+
import org.openrewrite.java.tree.Expression;
24+
import org.openrewrite.java.tree.J;
25+
import org.openrewrite.java.tree.Space;
26+
import org.openrewrite.java.tree.Statement;
27+
import org.openrewrite.staticanalysis.kotlin.KotlinFileChecker;
28+
29+
import java.util.ArrayList;
30+
import java.util.HashSet;
31+
import java.util.List;
32+
import java.util.Set;
33+
34+
import static java.util.Collections.singletonList;
35+
import static java.util.stream.Collectors.toSet;
36+
import static org.openrewrite.java.tree.J.Block.createEmptyBlock;
37+
38+
@Value
39+
@EqualsAndHashCode(callSuper = false)
40+
public class RefineSwitchCases extends Recipe {
41+
@Override
42+
public String getDisplayName() {
43+
return "Use switch cases refinement when possible";
44+
}
45+
46+
@Override
47+
public String getDescription() {
48+
return "Use guarded switch case labels and guards if all the statements in the switch block do if/else if/else on the guarded label.";
49+
}
50+
51+
@Override
52+
public TreeVisitor<?, ExecutionContext> getVisitor() {
53+
return Preconditions.check(Preconditions.not(new KotlinFileChecker<>()), new JavaIsoVisitor<ExecutionContext>() {
54+
@Override
55+
public J.Switch visitSwitch(J.Switch sw, ExecutionContext ctx) {
56+
J.Switch switch_ = super.visitSwitch(sw, ctx);
57+
J.Switch mappedSwitch = switch_.withCases(switch_.getCases()
58+
.withStatements(ListUtils.flatMap(switch_.getCases().getStatements(), statement -> {
59+
if (statement instanceof J.Case) {
60+
J.Case case_ = (J.Case) statement;
61+
if (!(case_.getBody() instanceof J.Block) || case_.getGuard() != null) {
62+
return statement;
63+
}
64+
List<Statement> caseStatements = ((J.Block) case_.getBody()).getStatements();
65+
if (caseStatements.size() == 1 && caseStatements.get(0) instanceof J.If) {
66+
J.If if_ = (J.If) caseStatements.get(0);
67+
if (extractLabelVariables(case_)
68+
.containsAll(extractConditionVariables(if_.getIfCondition().getTree()))) {
69+
// Replace case with multiple cases
70+
return createGuardedCases(case_, if_);
71+
}
72+
}
73+
}
74+
return statement;
75+
})));
76+
return new JavaIsoVisitor<ExecutionContext>() {
77+
@Override
78+
public J.Case visitCase(J.Case case_, ExecutionContext ctx) {
79+
// Remove any trailing new line in empty case body
80+
if (case_.getBody() instanceof J.Block) {
81+
J.Block body = (J.Block) case_.getBody();
82+
if (body.getStatements().isEmpty() && !body.getEnd().isEmpty()) {
83+
return case_.withBody(body.withEnd(Space.EMPTY));
84+
}
85+
}
86+
return case_;
87+
}
88+
}.visitSwitch(maybeAutoFormat(switch_, mappedSwitch, ctx), ctx);
89+
}
90+
91+
private Set<String> extractLabelVariables(J.Case case_) {
92+
return case_.getCaseLabels().stream()
93+
.filter(J.VariableDeclarations.class::isInstance)
94+
.map(J.VariableDeclarations.class::cast)
95+
.map(J.VariableDeclarations::getVariables)
96+
.flatMap(List::stream)
97+
.map(J.VariableDeclarations.NamedVariable::getName)
98+
.map(J.Identifier::getSimpleName)
99+
.collect(toSet());
100+
}
101+
102+
private Set<String> extractConditionVariables(Expression expression) {
103+
return new JavaIsoVisitor<Set<String>>() {
104+
@Override
105+
public J.Identifier visitIdentifier(J.Identifier identifier, Set<String> identifiers) {
106+
identifiers.add(identifier.getSimpleName());
107+
return super.visitIdentifier(identifier, identifiers);
108+
}
109+
110+
@Override
111+
public J.MethodInvocation visitMethodInvocation(J.MethodInvocation method, Set<String> identifiers) {
112+
// Do not pull out method name as identifier, just any select and arguments
113+
super.visit(method.getSelect(), identifiers);
114+
method.getArguments().forEach(arg -> super.visit(arg, identifiers));
115+
return method;
116+
}
117+
}.reduce(expression, new HashSet<>());
118+
}
119+
120+
private List<J.Case> createGuardedCases(J.Case case_, J.If if_) {
121+
if (case_.getBody() == null) {
122+
return singletonList(case_);
123+
}
124+
List<J.Case> cases = new ArrayList<>();
125+
Statement caseBody = if_.getThenPart();
126+
if (caseBody instanceof J.Block && ((J.Block) caseBody).getStatements().size() == 1) {
127+
caseBody = ((J.Block) caseBody).getStatements().get(0);
128+
}
129+
cases.add(case_.withId(Tree.randomId()).withGuard(if_.getIfCondition().getTree().withPrefix(Space.SINGLE_SPACE)).withBody(caseBody));
130+
if (if_.getElsePart() == null) {
131+
if (case_.getBody() instanceof J.Block) {
132+
cases.add(case_.withBody(createEmptyBlock().withPrefix(Space.SINGLE_SPACE)));
133+
}
134+
} else if (if_.getElsePart().getBody() instanceof J.If) {
135+
cases.addAll(createGuardedCases(case_, (J.If) if_.getElsePart().getBody()));
136+
} else {
137+
cases.add(case_.withBody(if_.getElsePart().getBody()));
138+
}
139+
140+
return cases;
141+
}
142+
});
143+
}
144+
}

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

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -135,8 +135,11 @@ recipeList:
135135
type: specs.openrewrite.org/v1beta/recipe
136136
name: org.openrewrite.java.migrate.SwitchPatternMatching
137137
displayName: Adopt switch pattern matching (JEP 441)
138-
description: "[JEP 441](https://openjdk.org/jeps/441) describes how some switch statements can be improved with pattern matching."
138+
description: ->-
139+
[JEP 441](https://openjdk.org/jeps/441) describes how some switch statements can be improved with pattern matching.
140+
This recipe applies some of those improvements where applicable.
139141
tags:
140142
- java21
141143
recipeList:
144+
- org.openrewrite.java.migrate.lang.RefineSwitchCases
142145
- org.openrewrite.java.migrate.lang.SwitchCaseEnumGuardToLabel
Lines changed: 238 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,238 @@
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+
25+
class RefineSwitchCasesTest implements RewriteTest {
26+
@Override
27+
public void defaults(RecipeSpec spec) {
28+
spec.recipe(new RefineSwitchCases());
29+
}
30+
31+
@Test
32+
@DocumentExample
33+
void refineCases() {
34+
rewriteRun(
35+
//language=java
36+
java(
37+
"""
38+
class Test {
39+
static void score(Object obj) {
40+
switch (obj) {
41+
case null -> System.out.println("You did not enter the test yet");
42+
case Integer i -> {
43+
if (i >= 5 && i <= 10)
44+
System.out.println("You got it");
45+
else if (i >= 0 && i < 5)
46+
System.out.println("Shame");
47+
else
48+
System.out.println("Sorry?");
49+
}
50+
case String s -> {
51+
if (s.equalsIgnoreCase("YES"))
52+
System.out.println("You got it");
53+
else if ("NO".equalsIgnoreCase(s))
54+
System.out.println("Shame");
55+
else
56+
System.out.println("Sorry?");
57+
}
58+
}
59+
}
60+
}
61+
""",
62+
"""
63+
class Test {
64+
static void score(Object obj) {
65+
switch (obj) {
66+
case null -> System.out.println("You did not enter the test yet");
67+
case Integer i when i >= 5 && i <= 10 ->
68+
System.out.println("You got it");
69+
case Integer i when i >= 0 && i < 5 ->
70+
System.out.println("Shame");
71+
case Integer i ->
72+
System.out.println("Sorry?");
73+
case String s when s.equalsIgnoreCase("YES") ->
74+
System.out.println("You got it");
75+
case String s when "NO".equalsIgnoreCase(s) ->
76+
System.out.println("Shame");
77+
case String s ->
78+
System.out.println("Sorry?");
79+
}
80+
}
81+
}
82+
"""
83+
)
84+
);
85+
}
86+
87+
@Test
88+
void preferExpressionUsage() {
89+
rewriteRun(
90+
//language=java
91+
java(
92+
"""
93+
class Test {
94+
static void score(Object obj) {
95+
switch (obj) {
96+
case null -> System.out.println("You did not enter the test yet");
97+
case Integer i -> {
98+
if (i >= 5 && i <= 10)
99+
System.out.println("You got it");
100+
else if (i >= 0 && i < 5) {
101+
System.out.println("Shame");
102+
} else
103+
System.out.println("Sorry?");
104+
}
105+
case String s -> {
106+
System.out.println("You got it");
107+
}
108+
}
109+
}
110+
}
111+
""",
112+
"""
113+
class Test {
114+
static void score(Object obj) {
115+
switch (obj) {
116+
case null -> System.out.println("You did not enter the test yet");
117+
case Integer i when i >= 5 && i <= 10 ->
118+
System.out.println("You got it");
119+
case Integer i when i >= 0 && i < 5 ->
120+
System.out.println("Shame");
121+
case Integer i ->
122+
System.out.println("Sorry?");
123+
case String s -> {
124+
System.out.println("You got it");
125+
}
126+
}
127+
}
128+
}
129+
"""
130+
)
131+
);
132+
}
133+
134+
@Test
135+
void emptyBlockToAvoidDefault() {
136+
rewriteRun(
137+
//language=java
138+
java(
139+
"""
140+
class Test {
141+
static void score(Object obj) {
142+
switch (obj) {
143+
case Integer i -> {
144+
if (i >= 5 && i <= 10)
145+
System.out.println("You got it");
146+
else if (i >= 0 && i < 5) {
147+
System.out.println("Shame");
148+
}
149+
}
150+
default -> System.out.println("Sorry?");
151+
}
152+
}
153+
}
154+
""",
155+
"""
156+
class Test {
157+
static void score(Object obj) {
158+
switch (obj) {
159+
case Integer i when i >= 5 && i <= 10 ->
160+
System.out.println("You got it");
161+
case Integer i when i >= 0 && i < 5 ->
162+
System.out.println("Shame");
163+
case Integer i -> {}
164+
default -> System.out.println("Sorry?");
165+
}
166+
}
167+
}
168+
"""
169+
)
170+
);
171+
}
172+
173+
@Test
174+
void fieldReference() {
175+
rewriteRun(
176+
//language=java
177+
java(
178+
"""
179+
class Test {
180+
static void score(Object obj) {
181+
switch (obj) {
182+
case null -> System.out.println("You did not enter the test yet");
183+
case Integer i -> {
184+
if (i >= 5 && i <= 10)
185+
System.out.println("You got it");
186+
else if (i >= 0 && i < Integer.MAX_VALUE)
187+
System.out.println("Shame");
188+
else
189+
System.out.println("Sorry?");
190+
}
191+
}
192+
}
193+
}
194+
""",
195+
"""
196+
class Test {
197+
static void score(Object obj) {
198+
switch (obj) {
199+
case null -> System.out.println("You did not enter the test yet");
200+
case Integer i when i >= 5 && i <= 10 ->
201+
System.out.println("You got it");
202+
case Integer i when i >= 0 && i < Integer.MAX_VALUE ->
203+
System.out.println("Shame");
204+
case Integer i ->
205+
System.out.println("Sorry?");
206+
}
207+
}
208+
}
209+
"""
210+
)
211+
);
212+
}
213+
214+
@Test
215+
void noChangeWhenAlreadyGuarded() {
216+
rewriteRun(
217+
//language=java
218+
java(
219+
"""
220+
class Test {
221+
static void score(Object obj) {
222+
switch (obj) {
223+
case Integer i when i == 7 -> {
224+
if (i >= 5 && i <= 10)
225+
System.out.println("You got it");
226+
else if (i >= 0 && i < 5)
227+
System.out.println("Shame");
228+
else
229+
System.out.println("Sorry?");
230+
}
231+
}
232+
}
233+
}
234+
"""
235+
)
236+
);
237+
}
238+
}

0 commit comments

Comments
 (0)