Skip to content

Commit 60f957e

Browse files
authored
If Else-If construct to switch. (#747)
1 parent 872dff4 commit 60f957e

File tree

4 files changed

+875
-0
lines changed

4 files changed

+875
-0
lines changed
Lines changed: 247 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,247 @@
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.jspecify.annotations.Nullable;
21+
import org.openrewrite.*;
22+
import org.openrewrite.java.JavaIsoVisitor;
23+
import org.openrewrite.java.JavaTemplate;
24+
import org.openrewrite.java.JavaVisitor;
25+
import org.openrewrite.java.search.SemanticallyEqual;
26+
import org.openrewrite.java.search.UsesJavaVersion;
27+
import org.openrewrite.java.tree.Expression;
28+
import org.openrewrite.java.tree.J;
29+
import org.openrewrite.java.tree.Space;
30+
import org.openrewrite.java.tree.Statement;
31+
import org.openrewrite.staticanalysis.groovy.GroovyFileChecker;
32+
import org.openrewrite.staticanalysis.kotlin.KotlinFileChecker;
33+
34+
import java.util.LinkedHashMap;
35+
import java.util.Map;
36+
import java.util.Objects;
37+
import java.util.Optional;
38+
39+
import static org.openrewrite.java.migrate.lang.NullCheck.Matcher.nullCheck;
40+
import static org.openrewrite.java.tree.J.Block.createEmptyBlock;
41+
42+
@Value
43+
@EqualsAndHashCode(callSuper = false)
44+
public class IfElseIfConstructToSwitch extends Recipe {
45+
@Override
46+
public String getDisplayName() {
47+
return "If-else-if-else to switch";
48+
}
49+
50+
@Override
51+
public String getDescription() {
52+
return "Replace if-else-if-else with switch statements. In order to be replaced with a switch, " +
53+
"all conditions must be on the same variable and there must be at least three cases.";
54+
}
55+
56+
@Override
57+
public TreeVisitor<?, ExecutionContext> getVisitor() {
58+
TreeVisitor<?, ExecutionContext> preconditions = Preconditions.and(
59+
new UsesJavaVersion<>(21),
60+
Preconditions.not(new KotlinFileChecker<>()),
61+
Preconditions.not(new GroovyFileChecker<>())
62+
);
63+
return Preconditions.check(preconditions, new JavaVisitor<ExecutionContext>() {
64+
65+
@Override
66+
public J visitIf(J.If if_, ExecutionContext ctx) {
67+
J.Switch switch_ = new SwitchCandidate(if_, getCursor()).buildSwitchTemplate();
68+
if (switch_ != null) {
69+
switch_ = new JavaIsoVisitor<ExecutionContext>() {
70+
@Override
71+
public J.Case visitCase(J.Case case_, ExecutionContext ctx) {
72+
if (case_.getBody() == null) {
73+
return case_;
74+
}
75+
if (case_.getBody() instanceof J.Block &&
76+
((J.Block) case_.getBody()).getStatements().isEmpty() &&
77+
!((J.Block) case_.getBody()).getEnd().isEmpty()) {
78+
return case_.withBody(((J.Block) case_.getBody()).withEnd(Space.EMPTY));
79+
}
80+
return case_.withBody(case_.getBody().withPrefix(Space.SINGLE_SPACE));
81+
}
82+
}.visitSwitch(switch_, ctx);
83+
return super.visitSwitch(switch_, ctx);
84+
}
85+
return super.visitIf(if_, ctx);
86+
}
87+
});
88+
}
89+
90+
private static class SwitchCandidate {
91+
private final Map<J.InstanceOf, Statement> patternMatchers = new LinkedHashMap<>();
92+
private @Nullable Expression nullCheckedParameter = null;
93+
private @Nullable Statement nullCheckedStatement = null;
94+
private @Nullable Statement else_ = null;
95+
private final Cursor cursor;
96+
private final J.If if_;
97+
98+
private boolean potentialCandidate = true;
99+
100+
private SwitchCandidate(J.If if_, Cursor cursor) {
101+
this.if_ = if_;
102+
this.cursor = cursor;
103+
J.If ifPart = if_;
104+
while (potentialCandidate && ifPart != null) {
105+
if (ifPart.getIfCondition().getTree() instanceof J.Binary) {
106+
ifPart = handleNullCheck(ifPart, cursor);
107+
} else if (ifPart.getIfCondition().getTree() instanceof J.InstanceOf) {
108+
ifPart = handleInstanceOfCheck(ifPart);
109+
} else {
110+
potentialCandidate = false;
111+
return;
112+
}
113+
}
114+
potentialCandidate = validatePotentialCandidate();
115+
}
116+
117+
private J.@Nullable If handleNullCheck(J.If ifPart, Cursor cursor) {
118+
Optional<NullCheck> nullCheck = nullCheck().get(ifPart, cursor);
119+
if (nullCheck.isPresent()) {
120+
nullCheckedParameter = nullCheck.get().getNullCheckedParameter();
121+
nullCheckedStatement = nullCheck.get().whenNull();
122+
Statement elsePart = nullCheck.get().whenNotNull();
123+
if (elsePart instanceof J.If) {
124+
ifPart = (J.If) elsePart;
125+
} else {
126+
else_ = elsePart;
127+
ifPart = null;
128+
}
129+
} else {
130+
potentialCandidate = false;
131+
}
132+
return ifPart;
133+
}
134+
135+
private J.@Nullable If handleInstanceOfCheck(J.If ifPart) {
136+
patternMatchers.put((J.InstanceOf) ifPart.getIfCondition().getTree(), ifPart.getThenPart());
137+
J.If.Else elsePart = ifPart.getElsePart();
138+
if (elsePart != null && elsePart.getBody() instanceof J.If) {
139+
ifPart = (J.If) elsePart.getBody();
140+
} else {
141+
else_ = elsePart != null ? elsePart.getBody() : null;
142+
ifPart = null;
143+
}
144+
return ifPart;
145+
}
146+
147+
private boolean validatePotentialCandidate() {
148+
Optional<Expression> switchOn = switchOn();
149+
// all ifs in the chain must be on the same variable in order to be a candidate for switch pattern matching
150+
if (!switchOn.isPresent() || !patternMatchers.keySet().stream()
151+
.map(J.InstanceOf::getExpression)
152+
.allMatch(it -> SemanticallyEqual.areEqual(switchOn.get(), it))) {
153+
return false;
154+
}
155+
// All InstanceOf checks must have a pattern, otherwise we can't use switch pattern matching
156+
// (consider calling org.openrewrite.staticanalysis.InstanceOfPatternMatch - or java 17 upgrade - first)
157+
if (patternMatchers.keySet().stream().anyMatch(instanceOf -> instanceOf.getPattern() == null)) {
158+
return false;
159+
}
160+
boolean nullCaseInSwitch = nullCheckedParameter != null && SemanticallyEqual.areEqual(nullCheckedParameter, switchOn.get());
161+
boolean hasLastElseBlock = else_ != null;
162+
163+
// we need at least 3 cases to use a switch
164+
return 3 <= patternMatchers.size() +
165+
(nullCaseInSwitch ? 1 : 0) +
166+
(hasLastElseBlock ? 1 : 0);
167+
}
168+
169+
public J.@Nullable Switch buildSwitchTemplate() {
170+
Optional<Expression> switchOn = switchOn();
171+
if (!this.potentialCandidate || !switchOn.isPresent()) {
172+
return null;
173+
}
174+
Object[] arguments = new Object[2 + (nullCheckedParameter != null ? 1 : 0) + (patternMatchers.size() * 3)];
175+
arguments[0] = switchOn.get();
176+
StringBuilder switchBody = new StringBuilder("switch (#{any()}) {\n");
177+
int i = 1;
178+
if (nullCheckedParameter != null) {
179+
Statement statement = getStatement(Objects.requireNonNull(nullCheckedStatement));
180+
if (statement instanceof J.Block) {
181+
switchBody.append("case null -> #{}\n");
182+
} else {
183+
switchBody.append("case null -> #{any()};\n");
184+
}
185+
arguments[i++] = statement;
186+
}
187+
for (Map.Entry<J.InstanceOf, Statement> entry : patternMatchers.entrySet()) {
188+
J.InstanceOf instanceOf = entry.getKey();
189+
Statement statement = getStatement(entry.getValue());
190+
if (statement instanceof J.Block) {
191+
switchBody.append("case #{}#{} -> #{}\n");
192+
} else {
193+
switchBody.append("case #{}#{} -> #{any()};\n");
194+
}
195+
arguments[i++] = getClassName(instanceOf);
196+
arguments[i++] = getPattern(instanceOf);
197+
arguments[i++] = statement;
198+
}
199+
if (else_ != null) {
200+
Statement statement = getStatement(else_);
201+
if (statement instanceof J.Block) {
202+
switchBody.append("default -> #{}\n");
203+
} else {
204+
switchBody.append("default -> #{any()};\n");
205+
}
206+
arguments[i] = statement;
207+
} else {
208+
switchBody.append("default -> #{}\n");
209+
arguments[i] = createEmptyBlock();
210+
}
211+
switchBody.append("}\n");
212+
213+
return JavaTemplate.apply(switchBody.toString(), cursor, if_.getCoordinates().replace(), arguments).withPrefix(if_.getPrefix());
214+
}
215+
216+
private Optional<Expression> switchOn() {
217+
return patternMatchers.keySet().stream()
218+
.map(J.InstanceOf::getExpression)
219+
.filter(e -> e instanceof J.FieldAccess || e instanceof J.Identifier)
220+
.findAny();
221+
}
222+
223+
private String getClassName(J.InstanceOf statement) {
224+
if (statement.getClazz() instanceof J.Identifier) {
225+
return ((J.Identifier) statement.getClazz()).getSimpleName();
226+
} else if (statement.getClazz() instanceof J.FieldAccess) {
227+
return ((J.FieldAccess) statement.getClazz()).toString();
228+
}
229+
throw new IllegalStateException("Found unsupported statement where clazz is " + statement.getClazz());
230+
}
231+
232+
private String getPattern(J.InstanceOf statement) {
233+
if (statement.getPattern() instanceof J.Identifier) {
234+
return " " + ((J.Identifier) statement.getPattern()).getSimpleName();
235+
}
236+
return "";
237+
}
238+
239+
private Statement getStatement(Statement statement) {
240+
Statement toAdd = statement;
241+
if (statement instanceof J.Block && ((J.Block) statement).getStatements().size() == 1) {
242+
toAdd = ((J.Block) statement).getStatements().get(0);
243+
}
244+
return toAdd;
245+
}
246+
}
247+
}

src/main/resources/META-INF/rewrite/examples.yml

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5981,6 +5981,52 @@ examples:
59815981
language: java
59825982
---
59835983
type: specs.openrewrite.org/v1beta/example
5984+
recipeName: org.openrewrite.java.migrate.lang.IfElseIfConstructToSwitch
5985+
examples:
5986+
- description: ''
5987+
sources:
5988+
- before: |
5989+
class Test {
5990+
static String formatter(Object obj) {
5991+
String formatted = "initialValue";
5992+
if (obj == null) {
5993+
formatted = "null";
5994+
} else if (obj instanceof Integer i)
5995+
formatted = String.format("int %d", i);
5996+
else if (obj instanceof Long l) {
5997+
formatted = String.format("long %d", l);
5998+
} else if (obj instanceof Double d) {
5999+
formatted = String.format("double %f", d);
6000+
} else if (obj instanceof String s) {
6001+
String str = "String";
6002+
formatted = String.format("%s %s", str, s);
6003+
} else {
6004+
formatted = "unknown";
6005+
}
6006+
return formatted;
6007+
}
6008+
}
6009+
after: |
6010+
class Test {
6011+
static String formatter(Object obj) {
6012+
String formatted = "initialValue";
6013+
switch (obj) {
6014+
case null -> formatted = "null";
6015+
case Integer i -> formatted = String.format("int %d", i);
6016+
case Long l -> formatted = String.format("long %d", l);
6017+
case Double d -> formatted = String.format("double %f", d);
6018+
case String s -> {
6019+
String str = "String";
6020+
formatted = String.format("%s %s", str, s);
6021+
}
6022+
default -> formatted = "unknown";
6023+
}
6024+
return formatted;
6025+
}
6026+
}
6027+
language: java
6028+
---
6029+
type: specs.openrewrite.org/v1beta/example
59846030
recipeName: org.openrewrite.java.migrate.lang.JavaLangAPIs
59856031
examples:
59866032
- description: ''
@@ -6087,6 +6133,43 @@ examples:
60876133
language: java
60886134
---
60896135
type: specs.openrewrite.org/v1beta/example
6136+
recipeName: org.openrewrite.java.migrate.lang.NullCheckAsSwitchCase
6137+
examples:
6138+
- description: ''
6139+
sources:
6140+
- before: |
6141+
class Test {
6142+
static String score(String obj) {
6143+
String formatted = "Score not translated yet";
6144+
if (obj == null) {
6145+
formatted = "You did not enter the test yet";
6146+
}
6147+
switch (obj) {
6148+
case "A", "B" -> formatted = "Very good";
6149+
case "C" -> formatted = "Good";
6150+
case "D" -> formatted = "Hmmm...";
6151+
default -> formatted = "unknown";
6152+
}
6153+
return formatted;
6154+
}
6155+
}
6156+
after: |
6157+
class Test {
6158+
static String score(String obj) {
6159+
String formatted = "Score not translated yet";
6160+
switch (obj) {
6161+
case null -> formatted = "You did not enter the test yet";
6162+
case "A", "B" -> formatted = "Very good";
6163+
case "C" -> formatted = "Good";
6164+
case "D" -> formatted = "Hmmm...";
6165+
default -> formatted = "unknown";
6166+
}
6167+
return formatted;
6168+
}
6169+
}
6170+
language: java
6171+
---
6172+
type: specs.openrewrite.org/v1beta/example
60906173
recipeName: org.openrewrite.java.migrate.lang.RefineSwitchCases
60916174
examples:
60926175
- description: ''

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,7 @@ description: ->-
141141
tags:
142142
- java21
143143
recipeList:
144+
- org.openrewrite.java.migrate.lang.IfElseIfConstructToSwitch
144145
- org.openrewrite.java.migrate.lang.NullCheckAsSwitchCase
145146
- org.openrewrite.java.migrate.lang.RefineSwitchCases
146147
- org.openrewrite.java.migrate.lang.SwitchCaseEnumGuardToLabel

0 commit comments

Comments
 (0)