Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
989a6e4
If Else-If construct to switch.
Jenson3210 Jun 2, 2025
05118fd
Fix tests post review comments
Jenson3210 Jun 2, 2025
ab26a9e
renamed iff -> if_
Jenson3210 Jun 2, 2025
64780c6
Fixed nested class instanceOf checks
Jenson3210 Jun 2, 2025
4d2fafc
Fixed test indentation
Jenson3210 Jun 2, 2025
78e262d
Update src/test/java/org/openrewrite/java/migrate/lang/IfElseIfConstr…
Jenson3210 Jun 2, 2025
bee0870
Fix test indentation
Jenson3210 Jun 3, 2025
2463441
Merge branch 'main' into if_else-if_switch
Jenson3210 Jun 3, 2025
e73ff37
Removed the JavaTemplate parameter type
Jenson3210 Jun 4, 2025
d016256
Renamed variables
Jenson3210 Jun 4, 2025
1280679
static import
Jenson3210 Jun 4, 2025
726d3f3
empty default block if no else clause to have correct compilation
Jenson3210 Jun 4, 2025
3cc25dc
use ternary
Jenson3210 Jun 4, 2025
2ccf5de
Update src/main/java/org/openrewrite/java/migrate/lang/IfElseIfConstr…
Jenson3210 Jun 4, 2025
9324cfe
Throws single statements will never reassign the value
Jenson3210 Jun 16, 2025
4e54523
Merge branch 'main' into if_else-if_switch
timtebeek Jun 16, 2025
7fdafd1
Add to `SwitchPatternMatching`
timtebeek Jun 16, 2025
47f1d21
Update examples.yml
timtebeek Jun 16, 2025
6899fc5
Apply formatter
timtebeek Jun 16, 2025
2ab8792
Remove indentation from template argument
timtebeek Jun 16, 2025
892c875
Review comments
Jenson3210 Jun 17, 2025
99182f5
Adapt tests + recipe to not expect pattern matching if not yet present
Jenson3210 Jun 19, 2025
983ea16
Fix a test which expects PatternMatching and added combined test
Jenson3210 Jun 19, 2025
74b9437
Add Precondition and test to run them
Jenson3210 Jun 19, 2025
f6cf355
Add Precondition and test to run them
Jenson3210 Jun 19, 2025
a0c3bf2
Made JavaTemplate non-context aware
Jenson3210 Jun 20, 2025
917e075
Minor polish
timtebeek Jun 20, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,247 @@
/*
* Copyright 2025 the original author or authors.
* <p>
* 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
* <p>
* https://docs.moderne.io/licensing/moderne-source-available-license
* <p>
* 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.jspecify.annotations.Nullable;
import org.openrewrite.*;
import org.openrewrite.java.JavaIsoVisitor;
import org.openrewrite.java.JavaTemplate;
import org.openrewrite.java.JavaVisitor;
import org.openrewrite.java.search.SemanticallyEqual;
import org.openrewrite.java.search.UsesJavaVersion;
import org.openrewrite.java.tree.Expression;
import org.openrewrite.java.tree.J;
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 java.util.LinkedHashMap;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;

import static org.openrewrite.java.migrate.lang.NullCheck.Matcher.nullCheck;
import static org.openrewrite.java.tree.J.Block.createEmptyBlock;

@Value
@EqualsAndHashCode(callSuper = false)
public class IfElseIfConstructToSwitch extends Recipe {
@Override
public String getDisplayName() {
return "If-else-if-else to switch";
}

@Override
public String getDescription() {
return "Replace if-else-if-else with switch statements. In order to be replaced with a switch, " +
"all conditions must be on the same variable and there must be at least three cases.";
}

@Override
public TreeVisitor<?, ExecutionContext> getVisitor() {
TreeVisitor<?, ExecutionContext> preconditions = Preconditions.and(
new UsesJavaVersion<>(21),
Preconditions.not(new KotlinFileChecker<>()),
Preconditions.not(new GroovyFileChecker<>())
);
return Preconditions.check(preconditions, new JavaVisitor<ExecutionContext>() {

@Override
public J visitIf(J.If if_, ExecutionContext ctx) {
J.Switch switch_ = new SwitchCandidate(if_, getCursor()).buildSwitchTemplate();
if (switch_ != null) {
switch_ = new JavaIsoVisitor<ExecutionContext>() {
@Override
public J.Case visitCase(J.Case case_, ExecutionContext ctx) {
if (case_.getBody() == null) {
return case_;
}
if (case_.getBody() instanceof J.Block &&
((J.Block) case_.getBody()).getStatements().isEmpty() &&
!((J.Block) case_.getBody()).getEnd().isEmpty()) {
return case_.withBody(((J.Block) case_.getBody()).withEnd(Space.EMPTY));
}
return case_.withBody(case_.getBody().withPrefix(Space.SINGLE_SPACE));
}
}.visitSwitch(switch_, ctx);
return super.visitSwitch(switch_, ctx);
}
return super.visitIf(if_, ctx);
}
});
}

private static class SwitchCandidate {
private final Map<J.InstanceOf, Statement> patternMatchers = new LinkedHashMap<>();
private @Nullable Expression nullCheckedParameter = null;
private @Nullable Statement nullCheckedStatement = null;
private @Nullable Statement else_ = null;
private final Cursor cursor;
private final J.If if_;

private boolean potentialCandidate = true;

private SwitchCandidate(J.If if_, Cursor cursor) {
this.if_ = if_;
this.cursor = cursor;
J.If ifPart = if_;
while (potentialCandidate && ifPart != null) {
if (ifPart.getIfCondition().getTree() instanceof J.Binary) {
ifPart = handleNullCheck(ifPart, cursor);
} else if (ifPart.getIfCondition().getTree() instanceof J.InstanceOf) {
ifPart = handleInstanceOfCheck(ifPart);
} else {
potentialCandidate = false;
return;
}
}
potentialCandidate = validatePotentialCandidate();
}

private J.@Nullable If handleNullCheck(J.If ifPart, Cursor cursor) {
Optional<NullCheck> nullCheck = nullCheck().get(ifPart, cursor);
if (nullCheck.isPresent()) {
nullCheckedParameter = nullCheck.get().getNullCheckedParameter();
nullCheckedStatement = nullCheck.get().whenNull();
Statement elsePart = nullCheck.get().whenNotNull();
if (elsePart instanceof J.If) {
ifPart = (J.If) elsePart;
} else {
else_ = elsePart;
ifPart = null;
}
} else {
potentialCandidate = false;
}
return ifPart;
}

private J.@Nullable If handleInstanceOfCheck(J.If ifPart) {
patternMatchers.put((J.InstanceOf) ifPart.getIfCondition().getTree(), ifPart.getThenPart());
J.If.Else elsePart = ifPart.getElsePart();
if (elsePart != null && elsePart.getBody() instanceof J.If) {
ifPart = (J.If) elsePart.getBody();
} else {
else_ = elsePart != null ? elsePart.getBody() : null;
ifPart = null;
}
return ifPart;
}

private boolean validatePotentialCandidate() {
Optional<Expression> switchOn = switchOn();
// all ifs in the chain must be on the same variable in order to be a candidate for switch pattern matching
if (!switchOn.isPresent() || !patternMatchers.keySet().stream()
.map(J.InstanceOf::getExpression)
.allMatch(it -> SemanticallyEqual.areEqual(switchOn.get(), it))) {
return false;
}
// All InstanceOf checks must have a pattern, otherwise we can't use switch pattern matching
// (consider calling org.openrewrite.staticanalysis.InstanceOfPatternMatch - or java 17 upgrade - first)
if (patternMatchers.keySet().stream().anyMatch(instanceOf -> instanceOf.getPattern() == null)) {
return false;
}
boolean nullCaseInSwitch = nullCheckedParameter != null && SemanticallyEqual.areEqual(nullCheckedParameter, switchOn.get());
boolean hasLastElseBlock = else_ != null;

// we need at least 3 cases to use a switch
return 3 <= patternMatchers.size() +
(nullCaseInSwitch ? 1 : 0) +
(hasLastElseBlock ? 1 : 0);
}

public J.@Nullable Switch buildSwitchTemplate() {
Optional<Expression> switchOn = switchOn();
if (!this.potentialCandidate || !switchOn.isPresent()) {
return null;
}
Object[] arguments = new Object[2 + (nullCheckedParameter != null ? 1 : 0) + (patternMatchers.size() * 3)];
arguments[0] = switchOn.get();
StringBuilder switchBody = new StringBuilder("switch (#{any()}) {\n");
int i = 1;
if (nullCheckedParameter != null) {
Statement statement = getStatement(Objects.requireNonNull(nullCheckedStatement));
if (statement instanceof J.Block) {
switchBody.append("case null -> #{}\n");
} else {
switchBody.append("case null -> #{any()};\n");
}
arguments[i++] = statement;
}
for (Map.Entry<J.InstanceOf, Statement> entry : patternMatchers.entrySet()) {
J.InstanceOf instanceOf = entry.getKey();
Statement statement = getStatement(entry.getValue());
if (statement instanceof J.Block) {
switchBody.append("case #{}#{} -> #{}\n");
} else {
switchBody.append("case #{}#{} -> #{any()};\n");
}
arguments[i++] = getClassName(instanceOf);
arguments[i++] = getPattern(instanceOf);
arguments[i++] = statement;
}
if (else_ != null) {
Statement statement = getStatement(else_);
if (statement instanceof J.Block) {
switchBody.append("default -> #{}\n");
} else {
switchBody.append("default -> #{any()};\n");
}
arguments[i] = statement;
} else {
switchBody.append("default -> #{}\n");
arguments[i] = createEmptyBlock();
}
switchBody.append("}\n");

return JavaTemplate.apply(switchBody.toString(), cursor, if_.getCoordinates().replace(), arguments).withPrefix(if_.getPrefix());
}

private Optional<Expression> switchOn() {
return patternMatchers.keySet().stream()
.map(J.InstanceOf::getExpression)
.filter(e -> e instanceof J.FieldAccess || e instanceof J.Identifier)
.findAny();
}

private String getClassName(J.InstanceOf statement) {
if (statement.getClazz() instanceof J.Identifier) {
return ((J.Identifier) statement.getClazz()).getSimpleName();
} else if (statement.getClazz() instanceof J.FieldAccess) {
return ((J.FieldAccess) statement.getClazz()).toString();
}
throw new IllegalStateException("Found unsupported statement where clazz is " + statement.getClazz());
}

private String getPattern(J.InstanceOf statement) {
if (statement.getPattern() instanceof J.Identifier) {
return " " + ((J.Identifier) statement.getPattern()).getSimpleName();
}
return "";
}

private Statement getStatement(Statement statement) {
Statement toAdd = statement;
if (statement instanceof J.Block && ((J.Block) statement).getStatements().size() == 1) {
toAdd = ((J.Block) statement).getStatements().get(0);
}
return toAdd;
}
}
}
83 changes: 83 additions & 0 deletions src/main/resources/META-INF/rewrite/examples.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5981,6 +5981,52 @@ examples:
language: java
---
type: specs.openrewrite.org/v1beta/example
recipeName: org.openrewrite.java.migrate.lang.IfElseIfConstructToSwitch
examples:
- description: ''
sources:
- before: |
class Test {
static String formatter(Object obj) {
String formatted = "initialValue";
if (obj == null) {
formatted = "null";
} else if (obj instanceof Integer i)
formatted = String.format("int %d", i);
else if (obj instanceof Long l) {
formatted = String.format("long %d", l);
} else if (obj instanceof Double d) {
formatted = String.format("double %f", d);
} else if (obj instanceof String s) {
String str = "String";
formatted = String.format("%s %s", str, s);
} else {
formatted = "unknown";
}
return formatted;
}
}
after: |
class Test {
static String formatter(Object obj) {
String formatted = "initialValue";
switch (obj) {
case null -> formatted = "null";
case Integer i -> formatted = String.format("int %d", i);
case Long l -> formatted = String.format("long %d", l);
case Double d -> formatted = String.format("double %f", d);
case String s -> {
String str = "String";
formatted = String.format("%s %s", str, s);
}
default -> formatted = "unknown";
}
return formatted;
}
}
language: java
---
type: specs.openrewrite.org/v1beta/example
recipeName: org.openrewrite.java.migrate.lang.JavaLangAPIs
examples:
- description: ''
Expand Down Expand Up @@ -6087,6 +6133,43 @@ examples:
language: java
---
type: specs.openrewrite.org/v1beta/example
recipeName: org.openrewrite.java.migrate.lang.NullCheckAsSwitchCase
examples:
- description: ''
sources:
- before: |
class Test {
static String score(String obj) {
String formatted = "Score not translated yet";
if (obj == null) {
formatted = "You did not enter the test yet";
}
switch (obj) {
case "A", "B" -> formatted = "Very good";
case "C" -> formatted = "Good";
case "D" -> formatted = "Hmmm...";
default -> formatted = "unknown";
}
return formatted;
}
}
after: |
class Test {
static String score(String obj) {
String formatted = "Score not translated yet";
switch (obj) {
case null -> formatted = "You did not enter the test yet";
case "A", "B" -> formatted = "Very good";
case "C" -> formatted = "Good";
case "D" -> formatted = "Hmmm...";
default -> formatted = "unknown";
}
return formatted;
}
}
language: java
---
type: specs.openrewrite.org/v1beta/example
recipeName: org.openrewrite.java.migrate.lang.RefineSwitchCases
examples:
- description: ''
Expand Down
1 change: 1 addition & 0 deletions src/main/resources/META-INF/rewrite/java-version-21.yml
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,7 @@ description: ->-
tags:
- java21
recipeList:
- org.openrewrite.java.migrate.lang.IfElseIfConstructToSwitch
- org.openrewrite.java.migrate.lang.NullCheckAsSwitchCase
- org.openrewrite.java.migrate.lang.RefineSwitchCases
- org.openrewrite.java.migrate.lang.SwitchCaseEnumGuardToLabel
Loading