Skip to content

FallThrough: don't add a break after a guarded early exit - #1008

Open
Niloyyy wants to merge 1 commit into
openrewrite:mainfrom
Niloyyy:fallthrough-guarded-early-exit
Open

FallThrough: don't add a break after a guarded early exit#1008
Niloyyy wants to merge 1 commit into
openrewrite:mainfrom
Niloyyy:fallthrough-guarded-early-exit

Conversation

@Niloyyy

@Niloyyy Niloyyy commented Aug 16, 2026

Copy link
Copy Markdown
Contributor

Per your call in the issue — the recipe should leave this shape alone.

What was happening

FallThrough treated a case ending in an if without an else as an unintentional fall-through and appended a break, which changed behavior:

case ']':
    if (inlineMacroMode) {
        - sb.append("]");
        break;
    }
    break;   // <-- added; `]` is no longer escaped when !inlineMacroMode
case '#':
case '*':
    if (!escaping) {
        sb.append("++");
        escaping = true;
    }
    sb.append(ch);
    break;

breaks(Statement) returned false for an if with no else, no relief comment was present, so a break was appended.

The change

An if without an else whose then part completes abruptly is a guarded early exit — the statements after it are reachable only when the guard is false, so a fall-through past it is deliberate rather than an omission.

I added this as a separate guardsWithEarlyExit check rather than relaxing breaks(..), because the two mean different things. breaks(..) answers "does this always complete abruptly", and it is used recursively for try, switch and blocks. Loosening it there regressed nestedBlocks, where

try {
    if (true) {
        return 1;
    }
} catch (Exception e) {
    ...
}

can still complete normally and so genuinely does need the break. guardsWithEarlyExit therefore unwraps only blocks and labels, and deliberately does not descend into try or switch.

The trade-off is that the same shape can be a real omission, so some detection is lost. That seemed to be the intent of "not making any change here makes most sense", but happy to narrow it further if you'd rather.

Tests

Full suite locally on JDK 21: 2277 tests, 0 failures.

@Niloyyy

Niloyyy commented Aug 21, 2026

Copy link
Copy Markdown
Contributor Author

While looking at #229 I noticed this change also covers it — it's the same shape, an if without an else whose then part completes abruptly.

On main:

case 0:
    if (true) return i;
    break;      // added
default:
    throw new IllegalStateException();

which fails to compile with error: missing return statement. On this branch the case is left unchanged and it compiles.

It doesn't do the rest of what #229 asks for, though. The @ExpectedToFail switchAsLastStatement test (added in 1a65c15) expects a // fall through comment to be inserted instead:

case 0:
    if (true) return i;
    // fall through
default:

This branch adds no comment, so that test still fails and stays @ExpectedToFail.

Would you like the comment insertion as well, either here or as a follow-up? Happy to do it — I just didn't want to assume the recipe should start writing comments without checking first.

@Niloyyy

Niloyyy commented Aug 21, 2026

Copy link
Copy Markdown
Contributor Author

@timtebeek this is the fix PR from #460.

@knutwannheden the question above is really yours — for #229, would you still want the // fall through comment inserted, or is leaving the case unchanged enough?

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.

breaking switch logic: org.openrewrite.staticanalysis.CodeCleanup: JavadocToAsciidocTransformerConfigItemTest.escape:315

1 participant