Skip to content

Keep switches SwitchCaseAssignmentsToSwitchExpression can't convert safely (C3) - #1413

Open
RojanFrancis wants to merge 3 commits into
openrewrite:mainfrom
RojanFrancis:c3-switch-assignments-labeled-break
Open

RojanFrancis wants to merge 3 commits into
openrewrite:mainfrom
RojanFrancis:c3-switch-assignments-labeled-break

Conversation

@RojanFrancis

Copy link
Copy Markdown

Fixes C3 of #1330.

What's changed

SwitchCaseAssignmentsToSwitchExpression runs as part of UpgradeToJava21. It now leaves three shapes alone that it used to convert incorrectly, one commit each:

  1. Labeled break (silent behaviour change):

    outer:
    for (int key : keys) {
        String formatted = "initialValue";
        switch (key) {
            case 1:
                formatted = "one";
                break outer;
            default:
                formatted = "other";
        }
        System.out.println(formatted);
    }

    This became String formatted = switch (key) { case 1 -> "one"; default -> "other"; };. It compiles, but the loop is no longer left, so later iterations now run. A labeled break is no longer treated as a plain end of case.

  2. The selector reads the variable (doesn't compile): String s = "a"; switch (s) { case "a": s = "b"; .. } became String s = switch (s) {..}, which reads s before it is initialized.

  3. Selector side effects reach the added default (silent behaviour change): int x = i; switch (i++) {..} without a default became int x = switch (i++) { .. default -> i; }, which reads the incremented i. The recipe now bails out when it would add a default, the initializer isn't a literal, and the selector may have side effects. A literal initializer is still converted (see convertWhenSelectorSideEffectsCannotReachLiteralDefault).

Is this the right change?

All three bail out instead of trying to rewrite, following the "prefer bailing out over guessing" guidance in #1330. For (3) I reused the recipe's existing canHaveSideEffects, so it's conservative: a non-literal initializer with a method-call selector and no default is now left as a statement. If that's too broad, it could be narrowed to "the selector writes a variable the initializer reads", but I didn't want to guess at aliasing through method calls.

The "comments in arrow blocks are lost" bullet (marked SUSPECTED) is not covered here.

Tests

The probe tests failed first: all three shapes were converted. They now pass, along with a positive test for the literal case. All 31 tests in SwitchCaseAssignmentsToSwitchExpressionTest pass.

🤖 Generated with Claude Code

RojanFrancis and others added 3 commits November 3, 2026 19:53
`x = 5; break outer;` became `yield 5`, so the enclosing loop was no
longer left. The result compiled but behaved differently.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`String s = "a"; switch (s) { case "a": s = "b"; .. }` became
`String s = switch (s) {..}`, which reads `s` before it is initialized.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
With `int x = i; switch (i++)`, the added `default -> i` read the
incremented `i`. Literal initializers are still converted.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: In Progress

Development

Successfully merging this pull request may close these issues.

1 participant