Repository: openrewrite/rewrite-static-analysis
Pull request #520: DefaultComesLast should correctly support fall through cases
Author: login: JohannisK
Labels: name: bug
description: Something isn't working
name: recipe
Created: 2025-04-17T08:46:22Z | Merged: 2025-04-22T18:30:55Z

## What's changed?
Reimplemented logic in DefaultComesLast Visitor

## What's your motivation?
The code now assumes every case is a fall through case, best case scenario it falls through 0 times.
It handles the bug correctly.
 
- Fixes https://github.com/openrewrite/rewrite-static-analysis/issues/4


Diff:
diff --git a/src/main/java/org/openrewrite/staticanalysis/DefaultComesLastVisitor.java b/src/main/java/org/openrewrite/staticanalysis/DefaultComesLastVisitor.java
index e2f6533a4a..a1d98f9858 100644
--- a/src/main/java/org/openrewrite/staticanalysis/DefaultComesLastVisitor.java
+++ b/src/main/java/org/openrewrite/staticanalysis/DefaultComesLastVisitor.java
@@ -17,6 +17,7 @@
 
 import lombok.EqualsAndHashCode;
 import lombok.Value;
+import org.jspecify.annotations.Nullable;
 import org.openrewrite.Tree;
 import org.openrewrite.internal.ListUtils;
 import org.openrewrite.java.JavaIsoVisitor;
@@ -29,7 +30,8 @@
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.List;
-import java.util.stream.Collectors;
+
+import static java.util.stream.Collectors.toList;
 
 @Value
 @EqualsAndHashCode(callSuper = false)
@@ -41,117 +43,126 @@ public J.Switch visitSwitch(J.Switch switch_, P p) {
         J.Switch s = visitAndCast(switch_, p, super::visitSwitch);
 
         if (!isDefaultCaseLastOrNotPresent(switch_)) {
-            List<J.Case> cases = s.getCases().getStatements().stream().map(J.Case.class::cast).collect(Collectors.toList());
-            List<J.Case> fixedCases = new ArrayList<>(cases.size());
-
-            int defaultCaseIndex = -1;
-            J.Case defaultCase = null;
-
-            for (int i = 0; i < cases.size(); i++) {
-                J.Case aCase = cases.get(i);
-
-                // skip cases with bodies for now
-                if (aCase.getBody() != null) {
-                    return s;
-                }
-                if (isDefaultCase(aCase)) {
-                    defaultCaseIndex = i;
-                    defaultCase = aCase;
+            List<J.Case> cases = s.getCases().getStatements().stream().map(J.Case.class::cast).collect(toList());
+            List<J.Case> casesWithDefaultLast = maybeReorderCases(cases, p);
+            if (casesWithDefaultLast != null) {
+                boolean changed = true;
+                if (cases.size() == casesWithDefaultLast.size()) {
+                    changed = false;
+                    for (int i = 0; i < cases.size(); i++) {
+                        if (cases.get(i) != casesWithDefaultLast.get(i)) {
+                            changed = true;
+                            break;
+                        }
+                    }
                 }
-            }
 
-            List<J.Case> casesGroupedWithDefault = new ArrayList<>();
-            boolean foundNonEmptyCase = false;
-            for (int i = defaultCaseIndex - 1; i >= 0; i--) {
-                J.Case aCase = cases.get(i);
-                if (aCase.getStatements().isEmpty() && !foundNonEmptyCase) {
-                    casesGroupedWithDefault.add(0, aCase);
-                } else {
-                    foundNonEmptyCase = true;
-                    fixedCases.add(0, aCase);
+                if (changed) {
+                    s = s.withCases(s.getCases().withStatements(casesWithDefaultLast.stream().map(Statement.class::cast).collect(toList())));
                 }
             }
+        }
+        return s;
+    }
 
-            foundNonEmptyCase = false;
-            for (int i = defaultCaseIndex + 1; i < cases.size(); i++) {
-                J.Case aCase = cases.get(i);
-                if (defaultCase != null && defaultCase.getStatements().isEmpty() &&
-                    aCase.getStatements().isEmpty() && !foundNonEmptyCase) {
-                    casesGroupedWithDefault.add(aCase);
-                } else {
-                    if (defaultCase != null && defaultCase.getStatements().isEmpty() && !foundNonEmptyCase) {
-                        // the last case grouped with default can be non-empty. it will be flipped with
-                        // the default case, including its statements
-                        casesGroupedWithDefault.add(aCase);
-                    }
-                    foundNonEmptyCase = true;
-                    fixedCases.add(aCase);
-                }
-            }
+    private @Nullable List<J.Case> maybeReorderCases(List<J.Case> cases, P p) {
+        List<J.Case> fallThroughCases = new ArrayList<>();
+        List<J.Case> defaultCases = new ArrayList<>();
+        List<J.Case> casesWithDefaultLast = new ArrayList<>();
 
-            if (defaultCase != null && !casesGroupedWithDefault.isEmpty()) {
-                J.Case lastGroupedWithDefault = casesGroupedWithDefault.get(casesGroupedWithDefault.size() - 1);
-                if (!lastGroupedWithDefault.getStatements().isEmpty()) {
-                    casesGroupedWithDefault.set(casesGroupedWithDefault.size() - 1,
-                            lastGroupedWithDefault.withStatements(Collections.emptyList()));
-                    defaultCase = defaultCase.withStatements(lastGroupedWithDefault.getStatements());
-                }
+        boolean defaultCaseFound = false;
+        for (int i = 0; i < cases.size(); i++) {
+            J.Case aCase = cases.get(i);
+            if (aCase.getBody() != null) {
+                return null;
             }
-
-            J.Case lastNotGroupedWithDefault = fixedCases.get(fixedCases.size() - 1);
-            if (!lastNotGroupedWithDefault.getStatements().stream().reduce((s1, s2) -> s2)
-                    .map(stat -> stat instanceof J.Break || stat instanceof J.Continue ||
-                                 stat instanceof J.Return || stat instanceof J.Throw)
-                    .orElse(false)) {
-
-                // add a break statement since this case is now no longer last and would fall through
-                List<Statement> statementsOfCaseBeingMoved = new ArrayList<>(lastNotGroupedWithDefault.getStatements());
-                J.Break breakStatement = autoFormat(
-                        new J.Break(Tree.randomId(), Space.EMPTY, Markers.EMPTY, null),
-                        p
-                );
-                statementsOfCaseBeingMoved.add(breakStatement);
-
-                lastNotGroupedWithDefault = lastNotGroupedWithDefault.withStatements(
-                        ListUtils.map(statementsOfCaseBeingMoved, stmt -> autoFormat(stmt, p, getCursor()))
-                );
-                fixedCases.set(fixedCases.size() - 1, lastNotGroupedWithDefault);
+            fallThroughCases.add(aCase);
+            if (isDefaultCase(aCase)) {
+                defaultCaseFound = true;
             }
-
-            fixedCases.addAll(casesGroupedWithDefault);
-            if (defaultCase != null) {
-                if (defaultCase.getStatements().stream().reduce((s1, s2) -> s2)
-                        .map(stat -> stat instanceof J.Break || stat instanceof J.Continue || isVoidReturn(stat))
-                        .orElse(false)) {
-                    List<Statement> fixedDefaultStatements = new ArrayList<>(defaultCase.getStatements());
-                    fixedDefaultStatements.remove(fixedDefaultStatements.size() - 1);
-                    fixedCases.add(defaultCase.withStatements(fixedDefaultStatements));
+            if (i == cases.size() - 1 || !isFallthroughCase(aCase)) {
+                if (defaultCaseFound) {
+                    defaultCases.addAll(fallThroughCases);
+                    fallThroughCases.clear();
+                    defaultCaseFound = false;
                 } else {
-                    fixedCases.add(defaultCase);
+                    casesWithDefaultLast.addAll(fallThroughCases);
+                    fallThroughCases.clear();
                 }
             }
+        }
 
-            boolean changed = true;
-            if (cases.size() == fixedCases.size()) {
-                changed = false;
-                for (int i = 0; i < cases.size(); i++) {
-                    if (cases.get(i) != fixedCases.get(i)) {
-                        changed = true;
-                        break;
-                    }
+        casesWithDefaultLast = addBreakToLastCase(casesWithDefaultLast, p);
+        casesWithDefaultLast.addAll(maybeReorderFallthroughCases(defaultCases, p));
+        casesWithDefaultLast = ListUtils.mapLast(casesWithDefaultLast, this::removeBreak);
+        return casesWithDefaultLast;
+    }
+
+    private List<J.Case> maybeReorderFallthroughCases(List<J.Case> cases, P p) {
+        List<J.Case> preDefaultCases = new ArrayList<>();
+        List<J.Case> postDefaultCases = new ArrayList<>();
+        J.Case defaultCase = null;
+        for (int i = 0; i < cases.size(); i++) {
+            J.Case aCase = cases.get(i);
+            if (isDefaultCase(aCase)) {
+                if (!aCase.getStatements().isEmpty()) {
+                    return cases;
                 }
+                defaultCase = aCase;
+            } else if (defaultCase != null) {
+                if (!aCase.getStatements().isEmpty() && i != cases.size() - 1) {
+                    return cases;
+                } else {
+                    postDefaultCases.add(aCase);
+                }
+            } else {
+                preDefaultCases.add(aCase);
             }
+        }
 
-            if (changed) {
-                s = s.withCases(s.getCases().withStatements(fixedCases.stream().map(Statement.class::cast).collect(Collectors.toList())));
-            }
+        List<J.Case> fixedCases = new ArrayList<>();
+        fixedCases.addAll(preDefaultCases);
+        if (!postDefaultCases.isEmpty()) {
+            List<Statement> statements = postDefaultCases.get(postDefaultCases.size() - 1).getStatements();
+            defaultCase = defaultCase.withStatements(statements);
+            fixedCases.addAll(ListUtils.mapLast(postDefaultCases, e -> e.withStatements(Collections.emptyList())));
         }
+        fixedCases.add(defaultCase);
+        return fixedCases;
+    }
 
-        return s;
+    private List<J.Case> addBreakToLastCase(List<J.Case> cases, P p) {
+        return ListUtils.mapLast(cases, e -> {
+            if (isFallthroughCase(e)) {
+                return addBreak(e, p);
+            }
+            return e;
+        });
     }
 
-    private boolean isVoidReturn(Statement stat) {
-        return stat instanceof J.Return && ((J.Return) stat).getExpression() == null;
+    private boolean isFallthroughCase(J.Case aCase) {
+        return aCase.getStatements().isEmpty() ||
+              !(aCase.getStatements().get(aCase.getStatements().size() - 1) instanceof J.Break ||
+                    aCase.getStatements().get(aCase.getStatements().size() - 1) instanceof J.Continue ||
+                    aCase.getStatements().get(aCase.getStatements().size() - 1) instanceof J.Return ||
+                    aCase.getStatements().get(aCase.getStatements().size() - 1) instanceof J.Throw);
+    }
+
+    private J.Case addBreak(J.Case e, P p) {
+        J.Break breakStatement = autoFormat(
+              new J.Break(Tree.randomId(), Space.EMPTY, Markers.EMPTY, null),
+              p
+        );
+        List<Statement> statements = e.getStatements();
+        statements.add(breakStatement);
+        return e.withStatements(ListUtils.map(statements, stmt -> autoFormat(stmt, p, getCursor())));
+    }
+
+    private J.Case removeBreak(J.Case aCase) {
+        if (!aCase.getStatements().isEmpty() && aCase.getStatements().get(aCase.getStatements().size() - 1) instanceof J.Break) {
+            aCase = aCase.withStatements(aCase.getStatements().subList(0, aCase.getStatements().size() - 1));
+        }
+        return aCase;
     }
 
     private boolean isDefaultCaseLastOrNotPresent(J.Switch switch_) {
diff --git a/src/test/java/org/openrewrite/staticanalysis/DefaultComesLastTest.java b/src/test/java/org/openrewrite/staticanalysis/DefaultComesLastTest.java
index ea046e2544..e21dafa254 100644
--- a/src/test/java/org/openrewrite/staticanalysis/DefaultComesLastTest.java
+++ b/src/test/java/org/openrewrite/staticanalysis/DefaultComesLastTest.java
@@ -204,6 +204,39 @@ public int foo(int n) {
         );
     }
 
+    @Test
+    void defaultIsNotLastAndReturnsNonVoid() {
+        rewriteRun(
+          //language=java
+          java(
+            """
+              class Test {
+                  public int foo(int n) {
+                      switch (n) {
+                          default:
+                              return 2;
+                          case 1:
+                              return 1;
+                      }
+                  }
+              }
+              """,
+            """
+              class Test {
+                  public int foo(int n) {
+                      switch (n) {
+                          case 1:
+                              return 1;
+                          default:
+                              return 2;
+                      }
+                  }
+              }
+              """
+          )
+        );
+    }
+
     @Test
     void dontAddBreaksIfCasesArentMoving() {
         rewriteRun(
@@ -302,7 +335,7 @@ int bar(Product product) {
     @EnabledForJreRange(min = JRE.JAVA_21)
     @Issue("https://github.com/openrewrite/rewrite-static-analysis/issues/461")
     @Test
-    void exhaustiveSwitch(){
+    void exhaustiveSwitch() {
         //language=java
         rewriteRun(
           java(
@@ -320,4 +353,200 @@ static int coverage(Object obj) {
           )
         );
     }
+
+    @Issue("https://github.com/openrewrite/rewrite-static-analysis/issues/4")
+    @Test
+    void moveDefaultToLastWithFallThrough() {
+        rewriteRun(
+          //language=java
+          java(
+            """
+              class Test {
+                  int n;
+                  {
+                      switch (n) {
+                          case 1:
+                              break;
+                          case 2:
+                              break;
+                          default:
+                          case 3:
+                              System.out.println("case3");
+                      }
+                  }
+              }
+              """,
+            """
+              class Test {
+                  int n;
+                  {
+                      switch (n) {
+                          case 1:
+                              break;
+                          case 2:
+                              break;
+                          case 3:
+                          default:
+                              System.out.println("case3");
+                      }
+                  }
+              }
+              """
+          )
+        );
+    }
+
+    @Issue("https://github.com/openrewrite/rewrite-static-analysis/issues/4")
+    @Test
+    void dontMoveDefaultToLastWithFallThroughAndStatements() {
+        rewriteRun(
+          //language=java
+          java(
+            """
+              class Test {
+                  int n;
+                  {
+                      switch (n) {
+                          case 1:
+                              break;
+                          case 2:
+                              break;
+                          default:
+                              System.out.println("default");
+                          case 3:
+                              System.out.println("case3");
+                          case 4:
+                              System.out.println("case4");
+                      }
+                  }
+              }
+              """
+          )
+        );
+    }
+
+    @Issue("https://github.com/openrewrite/rewrite-static-analysis/issues/4")
+    @Test
+    void dontMoveDefaultToLastFromMiddleWithFallThroughAndStatements() {
+        rewriteRun(
+          //language=java
+          java(
+            """
+              class Test {
+                  int n;
+                  {
+                      switch (n) {
+                          case 1:
+                              break;
+                          case 2:
+                              break;
+                          case 3:
+                              System.out.println("case3");
+                          default:
+                              System.out.println("default");
+                          case 4:
+                              System.out.println("case4");
+                      }
+                  }
+              }
+              """
+          )
+        );
+    }
+
+    @Issue("https://github.com/openrewrite/rewrite-static-analysis/issues/4")
+    @Test
+    void moveFallThroughBlockWithDefaultWithStatements() {
+        rewriteRun(
+          //language=java
+          java(
+            """
+              class Test {
+                  int n;
+                  {
+                      switch (n) {
+                          case 1:
+                              break;
+                          case 3:
+                              System.out.println("case3");
+                          default:
+                              System.out.println("default");
+                          case 4:
+                              System.out.println("case4");
+                              break;
+                          case 2:
+                              break;
+                      }
+                  }
+              }
+              """,
+            """
+              class Test {
+                  int n;
+                  {
+                      switch (n) {
+                          case 1:
+                              break;
+                          case 2:
+                              break;
+                          case 3:
+                              System.out.println("case3");
+                          default:
+                              System.out.println("default");
+                          case 4:
+                              System.out.println("case4");
+                      }
+                  }
+              }
+              """
+          )
+        );
+    }
+
+    @Issue("https://github.com/openrewrite/rewrite-static-analysis/issues/4")
+    @Test
+    void moveFallThroughBlockWithDefaultWithoutStatements() {
+        rewriteRun(
+          //language=java
+          java(
+            """
+              class Test {
+                  int n;
+                  {
+                      switch (n) {
+                          case 1:
+                              break;
+                          case 3:
+                              System.out.println("case3");
+                          default:
+                          case 4:
+                              System.out.println("case4");
+                              break;
+                          case 2:
+                              break;
+                      }
+                  }
+              }
+              """,
+            """
+              class Test {
+                  int n;
+                  {
+                      switch (n) {
+                          case 1:
+                              break;
+                          case 2:
+                              break;
+                          case 3:
+                              System.out.println("case3");
+                          case 4:
+                          default:
+                              System.out.println("case4");
+                      }
+                  }
+              }
+              """
+          )
+        );
+    }
 }


Reviews:
user: login: github-actions[bot]
body: Some suggestions could not be made:
* build.gradle.kts
  * lines 23-22
* src/main/java/org/openrewrite/staticanalysis/DefaultComesLastVisitor.java
  * lines 20-20
  * lines 230-234
* src/test/java/org/openrewrite/staticanalysis/RemoveUnusedPrivateMethodsTest.java
  * lines 101-102
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-17T08:48:32Z
user: login: github-actions[bot]
body: Some suggestions could not be made:
* build.gradle.kts
  * lines 23-22
* src/main/java/org/openrewrite/staticanalysis/DefaultComesLastVisitor.java
  * lines 20-20
  * lines 232-236
* src/test/java/org/openrewrite/staticanalysis/RemoveUnusedPrivateMethodsTest.java
  * lines 101-102
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-17T09:00:59Z
user: login: github-actions[bot]
body: Some suggestions could not be made:
* build.gradle.kts
  * lines 23-22
* src/main/java/org/openrewrite/staticanalysis/DefaultComesLastVisitor.java
  * lines 191-195
* src/test/java/org/openrewrite/staticanalysis/RemoveUnusedPrivateMethodsTest.java
  * lines 101-102
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-17T11:51:56Z
user: login: github-actions[bot]
body: Some suggestions could not be made:
* src/test/java/org/openrewrite/staticanalysis/RemoveUnusedPrivateMethodsTest.java
  * lines 101-102
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-17T11:55:34Z
user: login: JohannisK
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-17T11:56:45Z
user: login: greg-at-moderne
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-04-17T12:42:50Z
user: login: github-actions[bot]
body: Some suggestions could not be made:
* src/test/java/org/openrewrite/staticanalysis/RemoveUnusedPrivateMethodsTest.java
  * lines 101-102
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-17T12:55:45Z
user: login: JohannisK
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-17T13:25:27Z
user: login: JohannisK
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-17T13:25:40Z
user: login: github-actions[bot]
body: Some suggestions could not be made:
* src/test/java/org/openrewrite/staticanalysis/RemoveUnusedPrivateMethodsTest.java
  * lines 101-102
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-17T13:28:32Z
user: login: github-actions[bot]
body: Some suggestions could not be made:
* src/test/java/org/openrewrite/staticanalysis/RemoveUnusedPrivateMethodsTest.java
  * lines 101-102
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-18T07:26:47Z
user: login: jevanlingen
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-18T07:47:26Z
user: login: github-actions[bot]
body: Some suggestions could not be made:
* src/test/java/org/openrewrite/staticanalysis/RemoveUnusedPrivateMethodsTest.java
  * lines 101-102
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-18T07:53:05Z
user: login: jevanlingen
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-18T07:53:24Z
user: login: jevanlingen
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-18T07:55:36Z
user: login: jevanlingen
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-18T07:58:32Z
user: login: JohannisK
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-18T08:00:15Z
user: login: github-actions[bot]
body: Some suggestions could not be made:
* src/test/java/org/openrewrite/staticanalysis/RemoveUnusedPrivateMethodsTest.java
  * lines 101-102
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-18T08:00:28Z
user: login: github-actions[bot]
body: Some suggestions could not be made:
* src/test/java/org/openrewrite/staticanalysis/RemoveUnusedPrivateMethodsTest.java
  * lines 101-102
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-18T08:05:10Z
user: login: github-actions[bot]
body: Some suggestions could not be made:
* src/test/java/org/openrewrite/staticanalysis/RemoveUnusedPrivateMethodsTest.java
  * lines 101-102
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-18T08:10:04Z
user: login: jevanlingen
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-04-18T08:30:25Z
user: login: timtebeek
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-18T11:33:42Z
user: login: greg-at-moderne
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-18T14:04:43Z
user: login: timtebeek
body: I've discussed this with Johan, he's going to explore two options
1. Either move the whole block of fall through cases that include `default` to the end
2. Or not make any change at all when there's fallthrough we can't handle well.
state: CHANGES_REQUESTED
author_association: MEMBER
submitted_at: 2025-04-18T14:11:51Z
user: login: github-actions[bot]
body: Some suggestions could not be made:
* src/test/java/org/openrewrite/staticanalysis/RemoveUnusedPrivateMethodsTest.java
  * lines 101-102
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-18T14:16:33Z
user: login: JohannisK
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-18T14:18:24Z

Review comments:
diff_hunk: @@ -26,9 +27,7 @@
 import org.openrewrite.java.tree.Statement;
 import org.openrewrite.marker.Markers;
 
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.List;
+import java.util.*;
path: src/main/java/org/openrewrite/staticanalysis/DefaultComesLastVisitor.java
user: login: github-actions[bot]
body: ```suggestion
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
```
created_at: 2025-04-17T08:48:32Z
author_association: CONTRIBUTOR
diff_hunk: @@ -17,6 +17,8 @@
 
 import lombok.EqualsAndHashCode;
 import lombok.Value;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
path: src/main/java/org/openrewrite/staticanalysis/DefaultComesLastVisitor.java
user: login: github-actions[bot]
body: ```suggestion
import org.jspecify.annotations.Nullable;
```
created_at: 2025-04-17T11:51:56Z
author_association: CONTRIBUTOR
diff_hunk: @@ -26,9 +28,7 @@
 import org.openrewrite.java.tree.Statement;
 import org.openrewrite.marker.Markers;
 
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.List;
+import java.util.*;
path: src/main/java/org/openrewrite/staticanalysis/DefaultComesLastVisitor.java
user: login: github-actions[bot]
body: ```suggestion
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
```
created_at: 2025-04-17T11:51:56Z
author_association: CONTRIBUTOR
diff_hunk: @@ -42,114 +42,159 @@ public J.Switch visitSwitch(J.Switch switch_, P p) {
 
         if (!isDefaultCaseLastOrNotPresent(switch_)) {
             List<J.Case> cases = s.getCases().getStatements().stream().map(J.Case.class::cast).collect(Collectors.toList());
-            List<J.Case> fixedCases = new ArrayList<>(cases.size());
+            List<J.Case> casesWithDefaultLast = maybeReorderCases(p, cases);
+            if (casesWithDefaultLast != null) {
+                s = maybeUpdateSwitch(cases, casesWithDefaultLast, s);
+            }
+        }
 
-            int defaultCaseIndex = -1;
-            J.Case defaultCase = null;
+        return s;
+    }
 
-            for (int i = 0; i < cases.size(); i++) {
-                J.Case aCase = cases.get(i);
+    private @Nullable List<J.Case> maybeReorderCases(P p, List<J.Case> cases) {
+        List<J.Case> fallThroughCases = new ArrayList<>(cases.size());
+        List<J.Case> defaultCases = new ArrayList<>(cases.size());
+        List<J.Case> casesWithDefaultLast = new ArrayList<>(cases.size());
 
-                // skip cases with bodies for now
-                if (aCase.getBody() != null) {
-                    return s;
-                }
-                if (isDefaultCase(aCase)) {
-                    defaultCaseIndex = i;
-                    defaultCase = aCase;
-                }
+        boolean defaultCaseFound = false;
+        for (int i = 0; i < cases.size(); i++) {
+            J.Case aCase = cases.get(i);
+            if (aCase.getBody() != null) {
+                return null;
             }
-
-            List<J.Case> casesGroupedWithDefault = new ArrayList<>();
-            boolean foundNonEmptyCase = false;
-            for (int i = defaultCaseIndex - 1; i >= 0; i--) {
-                J.Case aCase = cases.get(i);
-                if (aCase.getStatements().isEmpty() && !foundNonEmptyCase) {
-                    casesGroupedWithDefault.add(0, aCase);
-                } else {
-                    foundNonEmptyCase = true;
-                    fixedCases.add(0, aCase);
-                }
+            fallThroughCases.add(aCase);
+            if (isDefaultCase(aCase)) {
+                defaultCaseFound = true;
             }
-
-            foundNonEmptyCase = false;
-            for (int i = defaultCaseIndex + 1; i < cases.size(); i++) {
-                J.Case aCase = cases.get(i);
-                if (defaultCase != null && defaultCase.getStatements().isEmpty() &&
-                    aCase.getStatements().isEmpty() && !foundNonEmptyCase) {
-                    casesGroupedWithDefault.add(aCase);
+            if (i == cases.size() - 1 || !isFallthroughCase(aCase)) {
+                if (defaultCaseFound) {
+                    defaultCases.addAll(fallThroughCases);
+                    fallThroughCases.clear();
+                    defaultCaseFound = false;
                 } else {
-                    if (defaultCase != null && defaultCase.getStatements().isEmpty() && !foundNonEmptyCase) {
-                        // the last case grouped with default can be non-empty. it will be flipped with
-                        // the default case, including its statements
-                        casesGroupedWithDefault.add(aCase);
-                    }
-                    foundNonEmptyCase = true;
-                    fixedCases.add(aCase);
+                    casesWithDefaultLast.addAll(fallThroughCases);
+                    fallThroughCases.clear();
                 }
             }
+        }
 
-            if (defaultCase != null && !casesGroupedWithDefault.isEmpty()) {
-                J.Case lastGroupedWithDefault = casesGroupedWithDefault.get(casesGroupedWithDefault.size() - 1);
-                if (!lastGroupedWithDefault.getStatements().isEmpty()) {
-                    casesGroupedWithDefault.set(casesGroupedWithDefault.size() - 1,
-                            lastGroupedWithDefault.withStatements(Collections.emptyList()));
-                    defaultCase = defaultCase.withStatements(lastGroupedWithDefault.getStatements());
-                }
-            }
+        casesWithDefaultLast = addBreakToLastCase(p, casesWithDefaultLast);
+        casesWithDefaultLast.addAll(maybeReorderFallthroughCases(p, defaultCases));
+        casesWithDefaultLast = removeBreakFromLastCase(casesWithDefaultLast);
+        return casesWithDefaultLast;
+    }
 
-            J.Case lastNotGroupedWithDefault = fixedCases.get(fixedCases.size() - 1);
-            if (!lastNotGroupedWithDefault.getStatements().stream().reduce((s1, s2) -> s2)
-                    .map(stat -> stat instanceof J.Break || stat instanceof J.Continue ||
-                                 stat instanceof J.Return || stat instanceof J.Throw)
-                    .orElse(false)) {
-
-                // add a break statement since this case is now no longer last and would fall through
-                List<Statement> statementsOfCaseBeingMoved = new ArrayList<>(lastNotGroupedWithDefault.getStatements());
-                J.Break breakStatement = autoFormat(
-                        new J.Break(Tree.randomId(), Space.EMPTY, Markers.EMPTY, null),
-                        p
-                );
-                statementsOfCaseBeingMoved.add(breakStatement);
-
-                lastNotGroupedWithDefault = lastNotGroupedWithDefault.withStatements(
-                        ListUtils.map(statementsOfCaseBeingMoved, stmt -> autoFormat(stmt, p, getCursor()))
-                );
-                fixedCases.set(fixedCases.size() - 1, lastNotGroupedWithDefault);
+    private @NotNull List<J.Case> maybeReorderFallthroughCases(P p, List<J.Case> cases) {
path: src/main/java/org/openrewrite/staticanalysis/DefaultComesLastVisitor.java
user: login: github-actions[bot]
body: ```suggestion
    private List<J.Case> maybeReorderFallthroughCases(P p, List<J.Case> cases) {
```
created_at: 2025-04-17T11:51:56Z
author_association: CONTRIBUTOR
diff_hunk: @@ -320,4 +353,148 @@ static int coverage(Object obj) {
           )
         );
     }
+
+    @Issue("https://github.com/openrewrite/rewrite-static-analysis/issues/4")
+    @Test
+    void moveDefaultToLastWithFallThrough() {
+        rewriteRun(
+          //language=java
+          java(
+            """
+              class Test {
+                  int n;
+                  {
+                      switch (n) {
+                          case 1:
+                              break;
+                          case 2:
+                              break;
+                          default:
+                          case 3:
+                              System.out.println("case3");
+                      }
+                  }
+              }
+              """,
+            """
+              class Test {
+                  int n;
+                  {
+                      switch (n) {
+                          case 1:
+                              break;
+                          case 2:
+                              break;
+                          case 3:
+                          default:
+                              System.out.println("case3");
+                      }
+                  }
+              }
+              """
+          )
+        );
+    }
+
+    @Issue("https://github.com/openrewrite/rewrite-static-analysis/issues/4")
+    @Test
+    void moveDefaultToLastWithFallThroughAndStatements() {
path: src/test/java/org/openrewrite/staticanalysis/DefaultComesLastTest.java
user: login: JohannisK
body: Is this behavior desirable?
created_at: 2025-04-17T11:56:45Z
author_association: CONTRIBUTOR
diff_hunk: @@ -320,4 +353,148 @@ static int coverage(Object obj) {
           )
         );
     }
+
+    @Issue("https://github.com/openrewrite/rewrite-static-analysis/issues/4")
+    @Test
+    void moveDefaultToLastWithFallThrough() {
+        rewriteRun(
+          //language=java
+          java(
+            """
+              class Test {
+                  int n;
+                  {
+                      switch (n) {
+                          case 1:
+                              break;
+                          case 2:
+                              break;
+                          default:
+                          case 3:
+                              System.out.println("case3");
+                      }
+                  }
+              }
+              """,
+            """
+              class Test {
+                  int n;
+                  {
+                      switch (n) {
+                          case 1:
+                              break;
+                          case 2:
+                              break;
+                          case 3:
+                          default:
+                              System.out.println("case3");
+                      }
+                  }
+              }
+              """
+          )
+        );
+    }
+
+    @Issue("https://github.com/openrewrite/rewrite-static-analysis/issues/4")
+    @Test
+    void moveDefaultToLastWithFallThroughAndStatements() {
path: src/test/java/org/openrewrite/staticanalysis/DefaultComesLastTest.java
user: login: greg-at-moderne
body: I think so. Kind of an artificial example, but the outcome is more readable, if you ask me.
created_at: 2025-04-17T12:33:02Z
author_association: CONTRIBUTOR
diff_hunk: @@ -42,116 +45,157 @@ public J.Switch visitSwitch(J.Switch switch_, P p) {
 
         if (!isDefaultCaseLastOrNotPresent(switch_)) {
             List<J.Case> cases = s.getCases().getStatements().stream().map(J.Case.class::cast).collect(Collectors.toList());
-            List<J.Case> fixedCases = new ArrayList<>(cases.size());
+            List<J.Case> casesWithDefaultLast = maybeReorderCases(p, cases);
+            if (casesWithDefaultLast != null) {
+                s = maybeUpdateSwitch(cases, casesWithDefaultLast, s);
+            }
+        }
 
-            int defaultCaseIndex = -1;
-            J.Case defaultCase = null;
+        return s;
+    }
 
-            for (int i = 0; i < cases.size(); i++) {
-                J.Case aCase = cases.get(i);
+    private @Nullable List<J.Case> maybeReorderCases(P p, List<J.Case> cases) {
+        List<J.Case> fallThroughCases = new ArrayList<>(cases.size());
+        List<J.Case> defaultCases = new ArrayList<>(cases.size());
+        List<J.Case> casesWithDefaultLast = new ArrayList<>(cases.size());
 
-                // skip cases with bodies for now
-                if (aCase.getBody() != null) {
-                    return s;
-                }
-                if (isDefaultCase(aCase)) {
-                    defaultCaseIndex = i;
-                    defaultCase = aCase;
-                }
+        boolean defaultCaseFound = false;
+        for (int i = 0; i < cases.size(); i++) {
+            J.Case aCase = cases.get(i);
+            if (aCase.getBody() != null) {
+                return null;
             }
-
-            List<J.Case> casesGroupedWithDefault = new ArrayList<>();
-            boolean foundNonEmptyCase = false;
-            for (int i = defaultCaseIndex - 1; i >= 0; i--) {
-                J.Case aCase = cases.get(i);
-                if (aCase.getStatements().isEmpty() && !foundNonEmptyCase) {
-                    casesGroupedWithDefault.add(0, aCase);
-                } else {
-                    foundNonEmptyCase = true;
-                    fixedCases.add(0, aCase);
-                }
+            fallThroughCases.add(aCase);
+            if (isDefaultCase(aCase)) {
+                defaultCaseFound = true;
             }
-
-            foundNonEmptyCase = false;
-            for (int i = defaultCaseIndex + 1; i < cases.size(); i++) {
-                J.Case aCase = cases.get(i);
-                if (defaultCase != null && defaultCase.getStatements().isEmpty() &&
-                    aCase.getStatements().isEmpty() && !foundNonEmptyCase) {
-                    casesGroupedWithDefault.add(aCase);
+            if (i == cases.size() - 1 || !isFallthroughCase(aCase)) {
+                if (defaultCaseFound) {
+                    defaultCases.addAll(fallThroughCases);
+                    fallThroughCases.clear();
+                    defaultCaseFound = false;
                 } else {
-                    if (defaultCase != null && defaultCase.getStatements().isEmpty() && !foundNonEmptyCase) {
-                        // the last case grouped with default can be non-empty. it will be flipped with
-                        // the default case, including its statements
-                        casesGroupedWithDefault.add(aCase);
-                    }
-                    foundNonEmptyCase = true;
-                    fixedCases.add(aCase);
+                    casesWithDefaultLast.addAll(fallThroughCases);
+                    fallThroughCases.clear();
                 }
             }
+        }
 
-            if (defaultCase != null && !casesGroupedWithDefault.isEmpty()) {
-                J.Case lastGroupedWithDefault = casesGroupedWithDefault.get(casesGroupedWithDefault.size() - 1);
-                if (!lastGroupedWithDefault.getStatements().isEmpty()) {
-                    casesGroupedWithDefault.set(casesGroupedWithDefault.size() - 1,
-                            lastGroupedWithDefault.withStatements(Collections.emptyList()));
-                    defaultCase = defaultCase.withStatements(lastGroupedWithDefault.getStatements());
-                }
-            }
+        casesWithDefaultLast = addBreakToLastCase(p, casesWithDefaultLast);
+        casesWithDefaultLast.addAll(maybeReorderFallthroughCases(p, defaultCases));
+        casesWithDefaultLast = removeBreakFromLastCase(casesWithDefaultLast);
+        return casesWithDefaultLast;
+    }
 
-            J.Case lastNotGroupedWithDefault = fixedCases.get(fixedCases.size() - 1);
-            if (!lastNotGroupedWithDefault.getStatements().stream().reduce((s1, s2) -> s2)
-                    .map(stat -> stat instanceof J.Break || stat instanceof J.Continue ||
-                                 stat instanceof J.Return || stat instanceof J.Throw)
-                    .orElse(false)) {
-
-                // add a break statement since this case is now no longer last and would fall through
-                List<Statement> statementsOfCaseBeingMoved = new ArrayList<>(lastNotGroupedWithDefault.getStatements());
-                J.Break breakStatement = autoFormat(
-                        new J.Break(Tree.randomId(), Space.EMPTY, Markers.EMPTY, null),
-                        p
-                );
-                statementsOfCaseBeingMoved.add(breakStatement);
-
-                lastNotGroupedWithDefault = lastNotGroupedWithDefault.withStatements(
-                        ListUtils.map(statementsOfCaseBeingMoved, stmt -> autoFormat(stmt, p, getCursor()))
-                );
-                fixedCases.set(fixedCases.size() - 1, lastNotGroupedWithDefault);
+    private @NonNull List<J.Case> maybeReorderFallthroughCases(P p, List<J.Case> cases) {
+        J.Case defaultCase = null;
+        List<J.Case> preDefault = new ArrayList<>();
+        List<J.Case> postDefault = new ArrayList<>();
+        List<Statement> defaultStatements = new ArrayList<>();
+        for (J.Case aCase : cases) {
+            if (isDefaultCase(aCase)) {
+                defaultCase = aCase;
+            } else if (defaultCase == null) {
+                preDefault.add(aCase);
+            } else {
+                postDefault.add(aCase);
+                defaultStatements.addAll(aCase.getStatements());
             }
-
-            fixedCases.addAll(casesGroupedWithDefault);
-            if (defaultCase != null) {
-                if (defaultCase.getStatements().stream().reduce((s1, s2) -> s2)
-                        .map(stat -> stat instanceof J.Break || stat instanceof J.Continue || isVoidReturn(stat))
-                        .orElse(false)) {
-                    List<Statement> fixedDefaultStatements = new ArrayList<>(defaultCase.getStatements());
-                    fixedDefaultStatements.remove(fixedDefaultStatements.size() - 1);
-                    fixedCases.add(defaultCase.withStatements(fixedDefaultStatements));
+        }
+        List<Statement> defaultCaseStatements = defaultCase.getStatements();
+        defaultStatements.addAll(0, defaultCaseStatements);
+        List<J.Case> fixedCases = new ArrayList<>(preDefault.size() + postDefault.size() + 1);
+        fixedCases.addAll(maybeUpdatePreDefaultCases(preDefault, defaultCaseStatements));
+        fixedCases.addAll(postDefault);
+        if (!fixedCases.isEmpty()) {
+            List<Statement> lastStatements = fixedCases.get(fixedCases.size() - 1).getStatements();
+            if (!lastStatements.isEmpty()) {
+                if (!new HashSet<>(lastStatements).containsAll(defaultStatements)) {
+                    fixedCases = addBreakToLastCase(p, fixedCases);
                 } else {
-                    fixedCases.add(defaultCase);
+                    J.Case last = fixedCases.remove(fixedCases.size() - 1);
+                    fixedCases.add(last.withStatements(Collections.emptyList()));
                 }
             }
+        }
+        fixedCases.add(defaultCase.withStatements(ListUtils.map(defaultStatements, stmt -> autoFormat(stmt, p, getCursor()))));
+        return fixedCases;
+    }
 
-            boolean changed = true;
-            if (cases.size() == fixedCases.size()) {
-                changed = false;
-                for (int i = 0; i < cases.size(); i++) {
-                    if (cases.get(i) != fixedCases.get(i)) {
-                        changed = true;
-                        break;
-                    }
+    private J.Switch maybeUpdateSwitch(List<J.Case> cases, List<J.Case> casesWithDefaultLast, J.Switch s) {
+        boolean changed = true;
+        if (cases.size() == casesWithDefaultLast.size()) {
+            changed = false;
+            for (int i = 0; i < cases.size(); i++) {
+                if (cases.get(i) != casesWithDefaultLast.get(i)) {
+                    changed = true;
+                    break;
                 }
             }
-
-            if (changed) {
-                s = s.withCases(s.getCases().withStatements(fixedCases.stream().map(Statement.class::cast).collect(Collectors.toList())));
-            }
         }
 
+        if (changed) {
+            s = s.withCases(s.getCases().withStatements(casesWithDefaultLast.stream().map(Statement.class::cast).collect(Collectors.toList())));
+        }
         return s;
     }
 
-    private boolean isVoidReturn(Statement stat) {
-        return stat instanceof J.Return && ((J.Return) stat).getExpression() == null;
+    private List<J.Case> maybeUpdatePreDefaultCases(List<J.Case> preDefault, List<Statement> defaultCaseStatements) {
+        List<J.Case> result = new ArrayList<>();
+        if (!preDefault.isEmpty()) {
+            result.addAll(preDefault.subList(0, preDefault.size() - 1));
+            J.Case last = preDefault.get(preDefault.size() - 1);
+            List<Statement> statements = last.getStatements();
+            statements.addAll(defaultCaseStatements);
+            result.add(last.withStatements(statements));
+        }
+        return result;
+    }
+
+    private List<J.Case> addBreakToLastCase(P p, List<J.Case> cases) {
+        if(cases.isEmpty()) {
+            return cases;
+        }
+        List<J.Case> result = new ArrayList<>(cases.size());
+        result.addAll(cases.subList(0, cases.size() - 1));
+        J.Case aCase = cases.get(cases.size() - 1);
+        if (isFallthroughCase(aCase)) {
+            J.Break breakStatement = autoFormat(
+                  new J.Break(Tree.randomId(), Space.EMPTY, Markers.EMPTY, null),
+                  p
+            );
+            List<Statement> statements = aCase.getStatements();
+            statements.add(breakStatement);
+            aCase = aCase.withStatements(ListUtils.map(statements, stmt -> autoFormat(stmt, p, getCursor())));
+        }
+        result.add(aCase);
+        return result;
path: src/main/java/org/openrewrite/staticanalysis/DefaultComesLastVisitor.java
user: login: greg-at-moderne
body: This could probably be simplified using OpenRewrite's specific `ListUtils.mapLast()`
created_at: 2025-04-17T12:38:09Z
author_association: CONTRIBUTOR
diff_hunk: @@ -42,116 +45,157 @@ public J.Switch visitSwitch(J.Switch switch_, P p) {
 
         if (!isDefaultCaseLastOrNotPresent(switch_)) {
             List<J.Case> cases = s.getCases().getStatements().stream().map(J.Case.class::cast).collect(Collectors.toList());
-            List<J.Case> fixedCases = new ArrayList<>(cases.size());
+            List<J.Case> casesWithDefaultLast = maybeReorderCases(p, cases);
+            if (casesWithDefaultLast != null) {
+                s = maybeUpdateSwitch(cases, casesWithDefaultLast, s);
+            }
+        }
 
-            int defaultCaseIndex = -1;
-            J.Case defaultCase = null;
+        return s;
+    }
 
-            for (int i = 0; i < cases.size(); i++) {
-                J.Case aCase = cases.get(i);
+    private @Nullable List<J.Case> maybeReorderCases(P p, List<J.Case> cases) {
+        List<J.Case> fallThroughCases = new ArrayList<>(cases.size());
+        List<J.Case> defaultCases = new ArrayList<>(cases.size());
+        List<J.Case> casesWithDefaultLast = new ArrayList<>(cases.size());
 
-                // skip cases with bodies for now
-                if (aCase.getBody() != null) {
-                    return s;
-                }
-                if (isDefaultCase(aCase)) {
-                    defaultCaseIndex = i;
-                    defaultCase = aCase;
-                }
+        boolean defaultCaseFound = false;
+        for (int i = 0; i < cases.size(); i++) {
+            J.Case aCase = cases.get(i);
+            if (aCase.getBody() != null) {
+                return null;
             }
-
-            List<J.Case> casesGroupedWithDefault = new ArrayList<>();
-            boolean foundNonEmptyCase = false;
-            for (int i = defaultCaseIndex - 1; i >= 0; i--) {
-                J.Case aCase = cases.get(i);
-                if (aCase.getStatements().isEmpty() && !foundNonEmptyCase) {
-                    casesGroupedWithDefault.add(0, aCase);
-                } else {
-                    foundNonEmptyCase = true;
-                    fixedCases.add(0, aCase);
-                }
+            fallThroughCases.add(aCase);
+            if (isDefaultCase(aCase)) {
+                defaultCaseFound = true;
             }
-
-            foundNonEmptyCase = false;
-            for (int i = defaultCaseIndex + 1; i < cases.size(); i++) {
-                J.Case aCase = cases.get(i);
-                if (defaultCase != null && defaultCase.getStatements().isEmpty() &&
-                    aCase.getStatements().isEmpty() && !foundNonEmptyCase) {
-                    casesGroupedWithDefault.add(aCase);
+            if (i == cases.size() - 1 || !isFallthroughCase(aCase)) {
+                if (defaultCaseFound) {
+                    defaultCases.addAll(fallThroughCases);
+                    fallThroughCases.clear();
+                    defaultCaseFound = false;
                 } else {
-                    if (defaultCase != null && defaultCase.getStatements().isEmpty() && !foundNonEmptyCase) {
-                        // the last case grouped with default can be non-empty. it will be flipped with
-                        // the default case, including its statements
-                        casesGroupedWithDefault.add(aCase);
-                    }
-                    foundNonEmptyCase = true;
-                    fixedCases.add(aCase);
+                    casesWithDefaultLast.addAll(fallThroughCases);
+                    fallThroughCases.clear();
                 }
             }
+        }
 
-            if (defaultCase != null && !casesGroupedWithDefault.isEmpty()) {
-                J.Case lastGroupedWithDefault = casesGroupedWithDefault.get(casesGroupedWithDefault.size() - 1);
-                if (!lastGroupedWithDefault.getStatements().isEmpty()) {
-                    casesGroupedWithDefault.set(casesGroupedWithDefault.size() - 1,
-                            lastGroupedWithDefault.withStatements(Collections.emptyList()));
-                    defaultCase = defaultCase.withStatements(lastGroupedWithDefault.getStatements());
-                }
-            }
+        casesWithDefaultLast = addBreakToLastCase(p, casesWithDefaultLast);
+        casesWithDefaultLast.addAll(maybeReorderFallthroughCases(p, defaultCases));
+        casesWithDefaultLast = removeBreakFromLastCase(casesWithDefaultLast);
+        return casesWithDefaultLast;
+    }
 
-            J.Case lastNotGroupedWithDefault = fixedCases.get(fixedCases.size() - 1);
-            if (!lastNotGroupedWithDefault.getStatements().stream().reduce((s1, s2) -> s2)
-                    .map(stat -> stat instanceof J.Break || stat instanceof J.Continue ||
-                                 stat instanceof J.Return || stat instanceof J.Throw)
-                    .orElse(false)) {
-
-                // add a break statement since this case is now no longer last and would fall through
-                List<Statement> statementsOfCaseBeingMoved = new ArrayList<>(lastNotGroupedWithDefault.getStatements());
-                J.Break breakStatement = autoFormat(
-                        new J.Break(Tree.randomId(), Space.EMPTY, Markers.EMPTY, null),
-                        p
-                );
-                statementsOfCaseBeingMoved.add(breakStatement);
-
-                lastNotGroupedWithDefault = lastNotGroupedWithDefault.withStatements(
-                        ListUtils.map(statementsOfCaseBeingMoved, stmt -> autoFormat(stmt, p, getCursor()))
-                );
-                fixedCases.set(fixedCases.size() - 1, lastNotGroupedWithDefault);
+    private @NonNull List<J.Case> maybeReorderFallthroughCases(P p, List<J.Case> cases) {
+        J.Case defaultCase = null;
+        List<J.Case> preDefault = new ArrayList<>();
+        List<J.Case> postDefault = new ArrayList<>();
+        List<Statement> defaultStatements = new ArrayList<>();
+        for (J.Case aCase : cases) {
+            if (isDefaultCase(aCase)) {
+                defaultCase = aCase;
+            } else if (defaultCase == null) {
+                preDefault.add(aCase);
+            } else {
+                postDefault.add(aCase);
+                defaultStatements.addAll(aCase.getStatements());
             }
-
-            fixedCases.addAll(casesGroupedWithDefault);
-            if (defaultCase != null) {
-                if (defaultCase.getStatements().stream().reduce((s1, s2) -> s2)
-                        .map(stat -> stat instanceof J.Break || stat instanceof J.Continue || isVoidReturn(stat))
-                        .orElse(false)) {
-                    List<Statement> fixedDefaultStatements = new ArrayList<>(defaultCase.getStatements());
-                    fixedDefaultStatements.remove(fixedDefaultStatements.size() - 1);
-                    fixedCases.add(defaultCase.withStatements(fixedDefaultStatements));
+        }
+        List<Statement> defaultCaseStatements = defaultCase.getStatements();
+        defaultStatements.addAll(0, defaultCaseStatements);
+        List<J.Case> fixedCases = new ArrayList<>(preDefault.size() + postDefault.size() + 1);
+        fixedCases.addAll(maybeUpdatePreDefaultCases(preDefault, defaultCaseStatements));
+        fixedCases.addAll(postDefault);
+        if (!fixedCases.isEmpty()) {
+            List<Statement> lastStatements = fixedCases.get(fixedCases.size() - 1).getStatements();
+            if (!lastStatements.isEmpty()) {
+                if (!new HashSet<>(lastStatements).containsAll(defaultStatements)) {
+                    fixedCases = addBreakToLastCase(p, fixedCases);
                 } else {
-                    fixedCases.add(defaultCase);
+                    J.Case last = fixedCases.remove(fixedCases.size() - 1);
+                    fixedCases.add(last.withStatements(Collections.emptyList()));
                 }
             }
+        }
+        fixedCases.add(defaultCase.withStatements(ListUtils.map(defaultStatements, stmt -> autoFormat(stmt, p, getCursor()))));
+        return fixedCases;
+    }
 
-            boolean changed = true;
-            if (cases.size() == fixedCases.size()) {
-                changed = false;
-                for (int i = 0; i < cases.size(); i++) {
-                    if (cases.get(i) != fixedCases.get(i)) {
-                        changed = true;
-                        break;
-                    }
+    private J.Switch maybeUpdateSwitch(List<J.Case> cases, List<J.Case> casesWithDefaultLast, J.Switch s) {
+        boolean changed = true;
+        if (cases.size() == casesWithDefaultLast.size()) {
+            changed = false;
+            for (int i = 0; i < cases.size(); i++) {
+                if (cases.get(i) != casesWithDefaultLast.get(i)) {
+                    changed = true;
+                    break;
                 }
             }
-
-            if (changed) {
-                s = s.withCases(s.getCases().withStatements(fixedCases.stream().map(Statement.class::cast).collect(Collectors.toList())));
-            }
         }
 
+        if (changed) {
+            s = s.withCases(s.getCases().withStatements(casesWithDefaultLast.stream().map(Statement.class::cast).collect(Collectors.toList())));
+        }
         return s;
     }
 
-    private boolean isVoidReturn(Statement stat) {
-        return stat instanceof J.Return && ((J.Return) stat).getExpression() == null;
+    private List<J.Case> maybeUpdatePreDefaultCases(List<J.Case> preDefault, List<Statement> defaultCaseStatements) {
+        List<J.Case> result = new ArrayList<>();
+        if (!preDefault.isEmpty()) {
+            result.addAll(preDefault.subList(0, preDefault.size() - 1));
+            J.Case last = preDefault.get(preDefault.size() - 1);
+            List<Statement> statements = last.getStatements();
+            statements.addAll(defaultCaseStatements);
+            result.add(last.withStatements(statements));
+        }
+        return result;
+    }
+
+    private List<J.Case> addBreakToLastCase(P p, List<J.Case> cases) {
+        if(cases.isEmpty()) {
+            return cases;
+        }
+        List<J.Case> result = new ArrayList<>(cases.size());
+        result.addAll(cases.subList(0, cases.size() - 1));
+        J.Case aCase = cases.get(cases.size() - 1);
+        if (isFallthroughCase(aCase)) {
+            J.Break breakStatement = autoFormat(
+                  new J.Break(Tree.randomId(), Space.EMPTY, Markers.EMPTY, null),
+                  p
+            );
+            List<Statement> statements = aCase.getStatements();
+            statements.add(breakStatement);
+            aCase = aCase.withStatements(ListUtils.map(statements, stmt -> autoFormat(stmt, p, getCursor())));
+        }
+        result.add(aCase);
+        return result;
+    }
+
+    private List<J.Case> removeBreakFromLastCase(List<J.Case> cases) {
+        if (cases.isEmpty()) {
+            return cases;
+        }
+        List<J.Case> result = new ArrayList<>(cases.size());
+        result.addAll(cases.subList(0, cases.size() - 1));
+        J.Case aCase = cases.get(cases.size() - 1);
+        result.add(removeCaseEnd(aCase));
+        return result;
path: src/main/java/org/openrewrite/staticanalysis/DefaultComesLastVisitor.java
user: login: greg-at-moderne
body: Same story with `ListUtils.mapLast()`
created_at: 2025-04-17T12:38:47Z
author_association: CONTRIBUTOR
diff_hunk: @@ -42,116 +45,157 @@ public J.Switch visitSwitch(J.Switch switch_, P p) {
 
         if (!isDefaultCaseLastOrNotPresent(switch_)) {
             List<J.Case> cases = s.getCases().getStatements().stream().map(J.Case.class::cast).collect(Collectors.toList());
-            List<J.Case> fixedCases = new ArrayList<>(cases.size());
+            List<J.Case> casesWithDefaultLast = maybeReorderCases(p, cases);
+            if (casesWithDefaultLast != null) {
+                s = maybeUpdateSwitch(cases, casesWithDefaultLast, s);
+            }
+        }
 
-            int defaultCaseIndex = -1;
-            J.Case defaultCase = null;
+        return s;
+    }
 
-            for (int i = 0; i < cases.size(); i++) {
-                J.Case aCase = cases.get(i);
+    private @Nullable List<J.Case> maybeReorderCases(P p, List<J.Case> cases) {
+        List<J.Case> fallThroughCases = new ArrayList<>(cases.size());
+        List<J.Case> defaultCases = new ArrayList<>(cases.size());
+        List<J.Case> casesWithDefaultLast = new ArrayList<>(cases.size());
 
-                // skip cases with bodies for now
-                if (aCase.getBody() != null) {
-                    return s;
-                }
-                if (isDefaultCase(aCase)) {
-                    defaultCaseIndex = i;
-                    defaultCase = aCase;
-                }
+        boolean defaultCaseFound = false;
+        for (int i = 0; i < cases.size(); i++) {
+            J.Case aCase = cases.get(i);
+            if (aCase.getBody() != null) {
+                return null;
             }
-
-            List<J.Case> casesGroupedWithDefault = new ArrayList<>();
-            boolean foundNonEmptyCase = false;
-            for (int i = defaultCaseIndex - 1; i >= 0; i--) {
-                J.Case aCase = cases.get(i);
-                if (aCase.getStatements().isEmpty() && !foundNonEmptyCase) {
-                    casesGroupedWithDefault.add(0, aCase);
-                } else {
-                    foundNonEmptyCase = true;
-                    fixedCases.add(0, aCase);
-                }
+            fallThroughCases.add(aCase);
+            if (isDefaultCase(aCase)) {
+                defaultCaseFound = true;
             }
-
-            foundNonEmptyCase = false;
-            for (int i = defaultCaseIndex + 1; i < cases.size(); i++) {
-                J.Case aCase = cases.get(i);
-                if (defaultCase != null && defaultCase.getStatements().isEmpty() &&
-                    aCase.getStatements().isEmpty() && !foundNonEmptyCase) {
-                    casesGroupedWithDefault.add(aCase);
+            if (i == cases.size() - 1 || !isFallthroughCase(aCase)) {
+                if (defaultCaseFound) {
+                    defaultCases.addAll(fallThroughCases);
+                    fallThroughCases.clear();
+                    defaultCaseFound = false;
                 } else {
-                    if (defaultCase != null && defaultCase.getStatements().isEmpty() && !foundNonEmptyCase) {
-                        // the last case grouped with default can be non-empty. it will be flipped with
-                        // the default case, including its statements
-                        casesGroupedWithDefault.add(aCase);
-                    }
-                    foundNonEmptyCase = true;
-                    fixedCases.add(aCase);
+                    casesWithDefaultLast.addAll(fallThroughCases);
+                    fallThroughCases.clear();
                 }
             }
+        }
 
-            if (defaultCase != null && !casesGroupedWithDefault.isEmpty()) {
-                J.Case lastGroupedWithDefault = casesGroupedWithDefault.get(casesGroupedWithDefault.size() - 1);
-                if (!lastGroupedWithDefault.getStatements().isEmpty()) {
-                    casesGroupedWithDefault.set(casesGroupedWithDefault.size() - 1,
-                            lastGroupedWithDefault.withStatements(Collections.emptyList()));
-                    defaultCase = defaultCase.withStatements(lastGroupedWithDefault.getStatements());
-                }
-            }
+        casesWithDefaultLast = addBreakToLastCase(p, casesWithDefaultLast);
+        casesWithDefaultLast.addAll(maybeReorderFallthroughCases(p, defaultCases));
+        casesWithDefaultLast = removeBreakFromLastCase(casesWithDefaultLast);
+        return casesWithDefaultLast;
+    }
 
-            J.Case lastNotGroupedWithDefault = fixedCases.get(fixedCases.size() - 1);
-            if (!lastNotGroupedWithDefault.getStatements().stream().reduce((s1, s2) -> s2)
-                    .map(stat -> stat instanceof J.Break || stat instanceof J.Continue ||
-                                 stat instanceof J.Return || stat instanceof J.Throw)
-                    .orElse(false)) {
-
-                // add a break statement since this case is now no longer last and would fall through
-                List<Statement> statementsOfCaseBeingMoved = new ArrayList<>(lastNotGroupedWithDefault.getStatements());
-                J.Break breakStatement = autoFormat(
-                        new J.Break(Tree.randomId(), Space.EMPTY, Markers.EMPTY, null),
-                        p
-                );
-                statementsOfCaseBeingMoved.add(breakStatement);
-
-                lastNotGroupedWithDefault = lastNotGroupedWithDefault.withStatements(
-                        ListUtils.map(statementsOfCaseBeingMoved, stmt -> autoFormat(stmt, p, getCursor()))
-                );
-                fixedCases.set(fixedCases.size() - 1, lastNotGroupedWithDefault);
+    private @NonNull List<J.Case> maybeReorderFallthroughCases(P p, List<J.Case> cases) {
+        J.Case defaultCase = null;
+        List<J.Case> preDefault = new ArrayList<>();
+        List<J.Case> postDefault = new ArrayList<>();
+        List<Statement> defaultStatements = new ArrayList<>();
+        for (J.Case aCase : cases) {
+            if (isDefaultCase(aCase)) {
+                defaultCase = aCase;
+            } else if (defaultCase == null) {
+                preDefault.add(aCase);
+            } else {
+                postDefault.add(aCase);
+                defaultStatements.addAll(aCase.getStatements());
             }
-
-            fixedCases.addAll(casesGroupedWithDefault);
-            if (defaultCase != null) {
-                if (defaultCase.getStatements().stream().reduce((s1, s2) -> s2)
-                        .map(stat -> stat instanceof J.Break || stat instanceof J.Continue || isVoidReturn(stat))
-                        .orElse(false)) {
-                    List<Statement> fixedDefaultStatements = new ArrayList<>(defaultCase.getStatements());
-                    fixedDefaultStatements.remove(fixedDefaultStatements.size() - 1);
-                    fixedCases.add(defaultCase.withStatements(fixedDefaultStatements));
+        }
+        List<Statement> defaultCaseStatements = defaultCase.getStatements();
+        defaultStatements.addAll(0, defaultCaseStatements);
+        List<J.Case> fixedCases = new ArrayList<>(preDefault.size() + postDefault.size() + 1);
+        fixedCases.addAll(maybeUpdatePreDefaultCases(preDefault, defaultCaseStatements));
+        fixedCases.addAll(postDefault);
+        if (!fixedCases.isEmpty()) {
+            List<Statement> lastStatements = fixedCases.get(fixedCases.size() - 1).getStatements();
+            if (!lastStatements.isEmpty()) {
+                if (!new HashSet<>(lastStatements).containsAll(defaultStatements)) {
+                    fixedCases = addBreakToLastCase(p, fixedCases);
                 } else {
-                    fixedCases.add(defaultCase);
+                    J.Case last = fixedCases.remove(fixedCases.size() - 1);
+                    fixedCases.add(last.withStatements(Collections.emptyList()));
                 }
             }
+        }
+        fixedCases.add(defaultCase.withStatements(ListUtils.map(defaultStatements, stmt -> autoFormat(stmt, p, getCursor()))));
+        return fixedCases;
+    }
 
-            boolean changed = true;
-            if (cases.size() == fixedCases.size()) {
-                changed = false;
-                for (int i = 0; i < cases.size(); i++) {
-                    if (cases.get(i) != fixedCases.get(i)) {
-                        changed = true;
-                        break;
-                    }
+    private J.Switch maybeUpdateSwitch(List<J.Case> cases, List<J.Case> casesWithDefaultLast, J.Switch s) {
+        boolean changed = true;
+        if (cases.size() == casesWithDefaultLast.size()) {
+            changed = false;
+            for (int i = 0; i < cases.size(); i++) {
+                if (cases.get(i) != casesWithDefaultLast.get(i)) {
+                    changed = true;
+                    break;
                 }
             }
-
-            if (changed) {
-                s = s.withCases(s.getCases().withStatements(fixedCases.stream().map(Statement.class::cast).collect(Collectors.toList())));
-            }
         }
 
+        if (changed) {
+            s = s.withCases(s.getCases().withStatements(casesWithDefaultLast.stream().map(Statement.class::cast).collect(Collectors.toList())));
+        }
         return s;
     }
 
-    private boolean isVoidReturn(Statement stat) {
-        return stat instanceof J.Return && ((J.Return) stat).getExpression() == null;
+    private List<J.Case> maybeUpdatePreDefaultCases(List<J.Case> preDefault, List<Statement> defaultCaseStatements) {
+        List<J.Case> result = new ArrayList<>();
+        if (!preDefault.isEmpty()) {
+            result.addAll(preDefault.subList(0, preDefault.size() - 1));
+            J.Case last = preDefault.get(preDefault.size() - 1);
+            List<Statement> statements = last.getStatements();
+            statements.addAll(defaultCaseStatements);
+            result.add(last.withStatements(statements));
+        }
+        return result;
+    }
+
+    private List<J.Case> addBreakToLastCase(P p, List<J.Case> cases) {
+        if(cases.isEmpty()) {
+            return cases;
+        }
+        List<J.Case> result = new ArrayList<>(cases.size());
+        result.addAll(cases.subList(0, cases.size() - 1));
+        J.Case aCase = cases.get(cases.size() - 1);
+        if (isFallthroughCase(aCase)) {
+            J.Break breakStatement = autoFormat(
+                  new J.Break(Tree.randomId(), Space.EMPTY, Markers.EMPTY, null),
+                  p
+            );
+            List<Statement> statements = aCase.getStatements();
+            statements.add(breakStatement);
+            aCase = aCase.withStatements(ListUtils.map(statements, stmt -> autoFormat(stmt, p, getCursor())));
+        }
+        result.add(aCase);
+        return result;
+    }
+
+    private List<J.Case> removeBreakFromLastCase(List<J.Case> cases) {
+        if (cases.isEmpty()) {
+            return cases;
+        }
+        List<J.Case> result = new ArrayList<>(cases.size());
+        result.addAll(cases.subList(0, cases.size() - 1));
+        J.Case aCase = cases.get(cases.size() - 1);
+        result.add(removeCaseEnd(aCase));
+        return result;
+    }
+
+    private boolean isFallthroughCase(J.Case aCase) {
+        return aCase.getStatements().isEmpty() ||
+              !(aCase.getStatements().get(aCase.getStatements().size() - 1) instanceof J.Break ||
+                    aCase.getStatements().get(aCase.getStatements().size() - 1) instanceof J.Continue ||
+                    aCase.getStatements().get(aCase.getStatements().size() - 1) instanceof J.Return ||
+                    aCase.getStatements().get(aCase.getStatements().size() - 1) instanceof J.Throw);
+    }
+
+    private J.Case removeCaseEnd(J.Case aCase) {
path: src/main/java/org/openrewrite/staticanalysis/DefaultComesLastVisitor.java
user: login: greg-at-moderne
body: TBH I was a bit puzzled by the name "case end" at first. Maybe "removeCaseFinalStatement"?

(not sure, just sharing my thoughts)
created_at: 2025-04-17T12:39:53Z
author_association: CONTRIBUTOR
diff_hunk: @@ -42,116 +45,157 @@ public J.Switch visitSwitch(J.Switch switch_, P p) {
 
         if (!isDefaultCaseLastOrNotPresent(switch_)) {
             List<J.Case> cases = s.getCases().getStatements().stream().map(J.Case.class::cast).collect(Collectors.toList());
-            List<J.Case> fixedCases = new ArrayList<>(cases.size());
+            List<J.Case> casesWithDefaultLast = maybeReorderCases(p, cases);
+            if (casesWithDefaultLast != null) {
+                s = maybeUpdateSwitch(cases, casesWithDefaultLast, s);
+            }
+        }
 
-            int defaultCaseIndex = -1;
-            J.Case defaultCase = null;
+        return s;
+    }
 
-            for (int i = 0; i < cases.size(); i++) {
-                J.Case aCase = cases.get(i);
+    private @Nullable List<J.Case> maybeReorderCases(P p, List<J.Case> cases) {
path: src/main/java/org/openrewrite/staticanalysis/DefaultComesLastVisitor.java
user: login: greg-at-moderne
body: Minor thing, I think the convention is to have `P p` as the last argument, not first.
created_at: 2025-04-17T12:41:03Z
author_association: CONTRIBUTOR
diff_hunk: @@ -42,116 +45,157 @@ public J.Switch visitSwitch(J.Switch switch_, P p) {
 
         if (!isDefaultCaseLastOrNotPresent(switch_)) {
             List<J.Case> cases = s.getCases().getStatements().stream().map(J.Case.class::cast).collect(Collectors.toList());
-            List<J.Case> fixedCases = new ArrayList<>(cases.size());
+            List<J.Case> casesWithDefaultLast = maybeReorderCases(p, cases);
+            if (casesWithDefaultLast != null) {
+                s = maybeUpdateSwitch(cases, casesWithDefaultLast, s);
+            }
+        }
 
-            int defaultCaseIndex = -1;
-            J.Case defaultCase = null;
+        return s;
+    }
 
-            for (int i = 0; i < cases.size(); i++) {
-                J.Case aCase = cases.get(i);
+    private @Nullable List<J.Case> maybeReorderCases(P p, List<J.Case> cases) {
+        List<J.Case> fallThroughCases = new ArrayList<>(cases.size());
+        List<J.Case> defaultCases = new ArrayList<>(cases.size());
+        List<J.Case> casesWithDefaultLast = new ArrayList<>(cases.size());
 
-                // skip cases with bodies for now
-                if (aCase.getBody() != null) {
-                    return s;
-                }
-                if (isDefaultCase(aCase)) {
-                    defaultCaseIndex = i;
-                    defaultCase = aCase;
-                }
+        boolean defaultCaseFound = false;
+        for (int i = 0; i < cases.size(); i++) {
+            J.Case aCase = cases.get(i);
+            if (aCase.getBody() != null) {
+                return null;
             }
-
-            List<J.Case> casesGroupedWithDefault = new ArrayList<>();
-            boolean foundNonEmptyCase = false;
-            for (int i = defaultCaseIndex - 1; i >= 0; i--) {
-                J.Case aCase = cases.get(i);
-                if (aCase.getStatements().isEmpty() && !foundNonEmptyCase) {
-                    casesGroupedWithDefault.add(0, aCase);
-                } else {
-                    foundNonEmptyCase = true;
-                    fixedCases.add(0, aCase);
-                }
+            fallThroughCases.add(aCase);
+            if (isDefaultCase(aCase)) {
+                defaultCaseFound = true;
             }
-
-            foundNonEmptyCase = false;
-            for (int i = defaultCaseIndex + 1; i < cases.size(); i++) {
-                J.Case aCase = cases.get(i);
-                if (defaultCase != null && defaultCase.getStatements().isEmpty() &&
-                    aCase.getStatements().isEmpty() && !foundNonEmptyCase) {
-                    casesGroupedWithDefault.add(aCase);
+            if (i == cases.size() - 1 || !isFallthroughCase(aCase)) {
+                if (defaultCaseFound) {
+                    defaultCases.addAll(fallThroughCases);
+                    fallThroughCases.clear();
+                    defaultCaseFound = false;
                 } else {
-                    if (defaultCase != null && defaultCase.getStatements().isEmpty() && !foundNonEmptyCase) {
-                        // the last case grouped with default can be non-empty. it will be flipped with
-                        // the default case, including its statements
-                        casesGroupedWithDefault.add(aCase);
-                    }
-                    foundNonEmptyCase = true;
-                    fixedCases.add(aCase);
+                    casesWithDefaultLast.addAll(fallThroughCases);
+                    fallThroughCases.clear();
                 }
             }
+        }
 
-            if (defaultCase != null && !casesGroupedWithDefault.isEmpty()) {
-                J.Case lastGroupedWithDefault = casesGroupedWithDefault.get(casesGroupedWithDefault.size() - 1);
-                if (!lastGroupedWithDefault.getStatements().isEmpty()) {
-                    casesGroupedWithDefault.set(casesGroupedWithDefault.size() - 1,
-                            lastGroupedWithDefault.withStatements(Collections.emptyList()));
-                    defaultCase = defaultCase.withStatements(lastGroupedWithDefault.getStatements());
-                }
-            }
+        casesWithDefaultLast = addBreakToLastCase(p, casesWithDefaultLast);
+        casesWithDefaultLast.addAll(maybeReorderFallthroughCases(p, defaultCases));
+        casesWithDefaultLast = removeBreakFromLastCase(casesWithDefaultLast);
+        return casesWithDefaultLast;
+    }
 
-            J.Case lastNotGroupedWithDefault = fixedCases.get(fixedCases.size() - 1);
-            if (!lastNotGroupedWithDefault.getStatements().stream().reduce((s1, s2) -> s2)
-                    .map(stat -> stat instanceof J.Break || stat instanceof J.Continue ||
-                                 stat instanceof J.Return || stat instanceof J.Throw)
-                    .orElse(false)) {
-
-                // add a break statement since this case is now no longer last and would fall through
-                List<Statement> statementsOfCaseBeingMoved = new ArrayList<>(lastNotGroupedWithDefault.getStatements());
-                J.Break breakStatement = autoFormat(
-                        new J.Break(Tree.randomId(), Space.EMPTY, Markers.EMPTY, null),
-                        p
-                );
-                statementsOfCaseBeingMoved.add(breakStatement);
-
-                lastNotGroupedWithDefault = lastNotGroupedWithDefault.withStatements(
-                        ListUtils.map(statementsOfCaseBeingMoved, stmt -> autoFormat(stmt, p, getCursor()))
-                );
-                fixedCases.set(fixedCases.size() - 1, lastNotGroupedWithDefault);
+    private @NonNull List<J.Case> maybeReorderFallthroughCases(P p, List<J.Case> cases) {
+        J.Case defaultCase = null;
+        List<J.Case> preDefault = new ArrayList<>();
+        List<J.Case> postDefault = new ArrayList<>();
+        List<Statement> defaultStatements = new ArrayList<>();
+        for (J.Case aCase : cases) {
+            if (isDefaultCase(aCase)) {
+                defaultCase = aCase;
+            } else if (defaultCase == null) {
+                preDefault.add(aCase);
+            } else {
+                postDefault.add(aCase);
+                defaultStatements.addAll(aCase.getStatements());
             }
-
-            fixedCases.addAll(casesGroupedWithDefault);
-            if (defaultCase != null) {
-                if (defaultCase.getStatements().stream().reduce((s1, s2) -> s2)
-                        .map(stat -> stat instanceof J.Break || stat instanceof J.Continue || isVoidReturn(stat))
-                        .orElse(false)) {
-                    List<Statement> fixedDefaultStatements = new ArrayList<>(defaultCase.getStatements());
-                    fixedDefaultStatements.remove(fixedDefaultStatements.size() - 1);
-                    fixedCases.add(defaultCase.withStatements(fixedDefaultStatements));
+        }
+        List<Statement> defaultCaseStatements = defaultCase.getStatements();
+        defaultStatements.addAll(0, defaultCaseStatements);
+        List<J.Case> fixedCases = new ArrayList<>(preDefault.size() + postDefault.size() + 1);
+        fixedCases.addAll(maybeUpdatePreDefaultCases(preDefault, defaultCaseStatements));
+        fixedCases.addAll(postDefault);
+        if (!fixedCases.isEmpty()) {
+            List<Statement> lastStatements = fixedCases.get(fixedCases.size() - 1).getStatements();
+            if (!lastStatements.isEmpty()) {
+                if (!new HashSet<>(lastStatements).containsAll(defaultStatements)) {
+                    fixedCases = addBreakToLastCase(p, fixedCases);
                 } else {
-                    fixedCases.add(defaultCase);
+                    J.Case last = fixedCases.remove(fixedCases.size() - 1);
+                    fixedCases.add(last.withStatements(Collections.emptyList()));
                 }
             }
+        }
+        fixedCases.add(defaultCase.withStatements(ListUtils.map(defaultStatements, stmt -> autoFormat(stmt, p, getCursor()))));
+        return fixedCases;
+    }
 
-            boolean changed = true;
-            if (cases.size() == fixedCases.size()) {
-                changed = false;
-                for (int i = 0; i < cases.size(); i++) {
-                    if (cases.get(i) != fixedCases.get(i)) {
-                        changed = true;
-                        break;
-                    }
+    private J.Switch maybeUpdateSwitch(List<J.Case> cases, List<J.Case> casesWithDefaultLast, J.Switch s) {
path: src/main/java/org/openrewrite/staticanalysis/DefaultComesLastVisitor.java
user: login: greg-at-moderne
body: This is called once, right?
If so, I am not sure if this is worth extracting to a separate method.

No strong feelings here.
created_at: 2025-04-17T12:42:42Z
author_association: CONTRIBUTOR
diff_hunk: @@ -42,116 +45,157 @@ public J.Switch visitSwitch(J.Switch switch_, P p) {
 
         if (!isDefaultCaseLastOrNotPresent(switch_)) {
             List<J.Case> cases = s.getCases().getStatements().stream().map(J.Case.class::cast).collect(Collectors.toList());
-            List<J.Case> fixedCases = new ArrayList<>(cases.size());
+            List<J.Case> casesWithDefaultLast = maybeReorderCases(p, cases);
+            if (casesWithDefaultLast != null) {
+                s = maybeUpdateSwitch(cases, casesWithDefaultLast, s);
+            }
+        }
 
-            int defaultCaseIndex = -1;
-            J.Case defaultCase = null;
+        return s;
+    }
 
-            for (int i = 0; i < cases.size(); i++) {
-                J.Case aCase = cases.get(i);
+    private @Nullable List<J.Case> maybeReorderCases(P p, List<J.Case> cases) {
+        List<J.Case> fallThroughCases = new ArrayList<>(cases.size());
+        List<J.Case> defaultCases = new ArrayList<>(cases.size());
+        List<J.Case> casesWithDefaultLast = new ArrayList<>(cases.size());
 
-                // skip cases with bodies for now
-                if (aCase.getBody() != null) {
-                    return s;
-                }
-                if (isDefaultCase(aCase)) {
-                    defaultCaseIndex = i;
-                    defaultCase = aCase;
-                }
+        boolean defaultCaseFound = false;
+        for (int i = 0; i < cases.size(); i++) {
+            J.Case aCase = cases.get(i);
+            if (aCase.getBody() != null) {
+                return null;
             }
-
-            List<J.Case> casesGroupedWithDefault = new ArrayList<>();
-            boolean foundNonEmptyCase = false;
-            for (int i = defaultCaseIndex - 1; i >= 0; i--) {
-                J.Case aCase = cases.get(i);
-                if (aCase.getStatements().isEmpty() && !foundNonEmptyCase) {
-                    casesGroupedWithDefault.add(0, aCase);
-                } else {
-                    foundNonEmptyCase = true;
-                    fixedCases.add(0, aCase);
-                }
+            fallThroughCases.add(aCase);
+            if (isDefaultCase(aCase)) {
+                defaultCaseFound = true;
             }
-
-            foundNonEmptyCase = false;
-            for (int i = defaultCaseIndex + 1; i < cases.size(); i++) {
-                J.Case aCase = cases.get(i);
-                if (defaultCase != null && defaultCase.getStatements().isEmpty() &&
-                    aCase.getStatements().isEmpty() && !foundNonEmptyCase) {
-                    casesGroupedWithDefault.add(aCase);
+            if (i == cases.size() - 1 || !isFallthroughCase(aCase)) {
+                if (defaultCaseFound) {
+                    defaultCases.addAll(fallThroughCases);
+                    fallThroughCases.clear();
+                    defaultCaseFound = false;
                 } else {
-                    if (defaultCase != null && defaultCase.getStatements().isEmpty() && !foundNonEmptyCase) {
-                        // the last case grouped with default can be non-empty. it will be flipped with
-                        // the default case, including its statements
-                        casesGroupedWithDefault.add(aCase);
-                    }
-                    foundNonEmptyCase = true;
-                    fixedCases.add(aCase);
+                    casesWithDefaultLast.addAll(fallThroughCases);
+                    fallThroughCases.clear();
                 }
             }
+        }
 
-            if (defaultCase != null && !casesGroupedWithDefault.isEmpty()) {
-                J.Case lastGroupedWithDefault = casesGroupedWithDefault.get(casesGroupedWithDefault.size() - 1);
-                if (!lastGroupedWithDefault.getStatements().isEmpty()) {
-                    casesGroupedWithDefault.set(casesGroupedWithDefault.size() - 1,
-                            lastGroupedWithDefault.withStatements(Collections.emptyList()));
-                    defaultCase = defaultCase.withStatements(lastGroupedWithDefault.getStatements());
-                }
-            }
+        casesWithDefaultLast = addBreakToLastCase(p, casesWithDefaultLast);
+        casesWithDefaultLast.addAll(maybeReorderFallthroughCases(p, defaultCases));
+        casesWithDefaultLast = removeBreakFromLastCase(casesWithDefaultLast);
+        return casesWithDefaultLast;
+    }
 
-            J.Case lastNotGroupedWithDefault = fixedCases.get(fixedCases.size() - 1);
-            if (!lastNotGroupedWithDefault.getStatements().stream().reduce((s1, s2) -> s2)
-                    .map(stat -> stat instanceof J.Break || stat instanceof J.Continue ||
-                                 stat instanceof J.Return || stat instanceof J.Throw)
-                    .orElse(false)) {
-
-                // add a break statement since this case is now no longer last and would fall through
-                List<Statement> statementsOfCaseBeingMoved = new ArrayList<>(lastNotGroupedWithDefault.getStatements());
-                J.Break breakStatement = autoFormat(
-                        new J.Break(Tree.randomId(), Space.EMPTY, Markers.EMPTY, null),
-                        p
-                );
-                statementsOfCaseBeingMoved.add(breakStatement);
-
-                lastNotGroupedWithDefault = lastNotGroupedWithDefault.withStatements(
-                        ListUtils.map(statementsOfCaseBeingMoved, stmt -> autoFormat(stmt, p, getCursor()))
-                );
-                fixedCases.set(fixedCases.size() - 1, lastNotGroupedWithDefault);
+    private @NonNull List<J.Case> maybeReorderFallthroughCases(P p, List<J.Case> cases) {
+        J.Case defaultCase = null;
+        List<J.Case> preDefault = new ArrayList<>();
+        List<J.Case> postDefault = new ArrayList<>();
+        List<Statement> defaultStatements = new ArrayList<>();
+        for (J.Case aCase : cases) {
+            if (isDefaultCase(aCase)) {
+                defaultCase = aCase;
+            } else if (defaultCase == null) {
+                preDefault.add(aCase);
+            } else {
+                postDefault.add(aCase);
+                defaultStatements.addAll(aCase.getStatements());
             }
-
-            fixedCases.addAll(casesGroupedWithDefault);
-            if (defaultCase != null) {
-                if (defaultCase.getStatements().stream().reduce((s1, s2) -> s2)
-                        .map(stat -> stat instanceof J.Break || stat instanceof J.Continue || isVoidReturn(stat))
-                        .orElse(false)) {
-                    List<Statement> fixedDefaultStatements = new ArrayList<>(defaultCase.getStatements());
-                    fixedDefaultStatements.remove(fixedDefaultStatements.size() - 1);
-                    fixedCases.add(defaultCase.withStatements(fixedDefaultStatements));
+        }
+        List<Statement> defaultCaseStatements = defaultCase.getStatements();
+        defaultStatements.addAll(0, defaultCaseStatements);
+        List<J.Case> fixedCases = new ArrayList<>(preDefault.size() + postDefault.size() + 1);
+        fixedCases.addAll(maybeUpdatePreDefaultCases(preDefault, defaultCaseStatements));
+        fixedCases.addAll(postDefault);
+        if (!fixedCases.isEmpty()) {
+            List<Statement> lastStatements = fixedCases.get(fixedCases.size() - 1).getStatements();
+            if (!lastStatements.isEmpty()) {
+                if (!new HashSet<>(lastStatements).containsAll(defaultStatements)) {
+                    fixedCases = addBreakToLastCase(p, fixedCases);
                 } else {
-                    fixedCases.add(defaultCase);
+                    J.Case last = fixedCases.remove(fixedCases.size() - 1);
+                    fixedCases.add(last.withStatements(Collections.emptyList()));
                 }
             }
+        }
+        fixedCases.add(defaultCase.withStatements(ListUtils.map(defaultStatements, stmt -> autoFormat(stmt, p, getCursor()))));
+        return fixedCases;
+    }
 
-            boolean changed = true;
-            if (cases.size() == fixedCases.size()) {
-                changed = false;
-                for (int i = 0; i < cases.size(); i++) {
-                    if (cases.get(i) != fixedCases.get(i)) {
-                        changed = true;
-                        break;
-                    }
+    private J.Switch maybeUpdateSwitch(List<J.Case> cases, List<J.Case> casesWithDefaultLast, J.Switch s) {
+        boolean changed = true;
+        if (cases.size() == casesWithDefaultLast.size()) {
+            changed = false;
+            for (int i = 0; i < cases.size(); i++) {
+                if (cases.get(i) != casesWithDefaultLast.get(i)) {
+                    changed = true;
+                    break;
                 }
             }
-
-            if (changed) {
-                s = s.withCases(s.getCases().withStatements(fixedCases.stream().map(Statement.class::cast).collect(Collectors.toList())));
-            }
         }
 
+        if (changed) {
+            s = s.withCases(s.getCases().withStatements(casesWithDefaultLast.stream().map(Statement.class::cast).collect(Collectors.toList())));
+        }
         return s;
     }
 
-    private boolean isVoidReturn(Statement stat) {
-        return stat instanceof J.Return && ((J.Return) stat).getExpression() == null;
+    private List<J.Case> maybeUpdatePreDefaultCases(List<J.Case> preDefault, List<Statement> defaultCaseStatements) {
+        List<J.Case> result = new ArrayList<>();
+        if (!preDefault.isEmpty()) {
+            result.addAll(preDefault.subList(0, preDefault.size() - 1));
+            J.Case last = preDefault.get(preDefault.size() - 1);
+            List<Statement> statements = last.getStatements();
+            statements.addAll(defaultCaseStatements);
+            result.add(last.withStatements(statements));
+        }
+        return result;
+    }
+
+    private List<J.Case> addBreakToLastCase(P p, List<J.Case> cases) {
+        if(cases.isEmpty()) {
+            return cases;
+        }
+        List<J.Case> result = new ArrayList<>(cases.size());
+        result.addAll(cases.subList(0, cases.size() - 1));
+        J.Case aCase = cases.get(cases.size() - 1);
+        if (isFallthroughCase(aCase)) {
+            J.Break breakStatement = autoFormat(
+                  new J.Break(Tree.randomId(), Space.EMPTY, Markers.EMPTY, null),
+                  p
+            );
+            List<Statement> statements = aCase.getStatements();
+            statements.add(breakStatement);
+            aCase = aCase.withStatements(ListUtils.map(statements, stmt -> autoFormat(stmt, p, getCursor())));
+        }
+        result.add(aCase);
+        return result;
+    }
+
+    private List<J.Case> removeBreakFromLastCase(List<J.Case> cases) {
+        if (cases.isEmpty()) {
+            return cases;
+        }
+        List<J.Case> result = new ArrayList<>(cases.size());
+        result.addAll(cases.subList(0, cases.size() - 1));
+        J.Case aCase = cases.get(cases.size() - 1);
+        result.add(removeCaseEnd(aCase));
+        return result;
+    }
+
+    private boolean isFallthroughCase(J.Case aCase) {
+        return aCase.getStatements().isEmpty() ||
+              !(aCase.getStatements().get(aCase.getStatements().size() - 1) instanceof J.Break ||
+                    aCase.getStatements().get(aCase.getStatements().size() - 1) instanceof J.Continue ||
+                    aCase.getStatements().get(aCase.getStatements().size() - 1) instanceof J.Return ||
+                    aCase.getStatements().get(aCase.getStatements().size() - 1) instanceof J.Throw);
+    }
+
+    private J.Case removeCaseEnd(J.Case aCase) {
path: src/main/java/org/openrewrite/staticanalysis/DefaultComesLastVisitor.java
user: login: JohannisK
body: Renamed to removeBreak
created_at: 2025-04-17T13:25:27Z
author_association: CONTRIBUTOR
diff_hunk: @@ -42,116 +45,157 @@ public J.Switch visitSwitch(J.Switch switch_, P p) {
 
         if (!isDefaultCaseLastOrNotPresent(switch_)) {
             List<J.Case> cases = s.getCases().getStatements().stream().map(J.Case.class::cast).collect(Collectors.toList());
-            List<J.Case> fixedCases = new ArrayList<>(cases.size());
+            List<J.Case> casesWithDefaultLast = maybeReorderCases(p, cases);
+            if (casesWithDefaultLast != null) {
+                s = maybeUpdateSwitch(cases, casesWithDefaultLast, s);
+            }
+        }
 
-            int defaultCaseIndex = -1;
-            J.Case defaultCase = null;
+        return s;
+    }
 
-            for (int i = 0; i < cases.size(); i++) {
-                J.Case aCase = cases.get(i);
+    private @Nullable List<J.Case> maybeReorderCases(P p, List<J.Case> cases) {
+        List<J.Case> fallThroughCases = new ArrayList<>(cases.size());
+        List<J.Case> defaultCases = new ArrayList<>(cases.size());
+        List<J.Case> casesWithDefaultLast = new ArrayList<>(cases.size());
 
-                // skip cases with bodies for now
-                if (aCase.getBody() != null) {
-                    return s;
-                }
-                if (isDefaultCase(aCase)) {
-                    defaultCaseIndex = i;
-                    defaultCase = aCase;
-                }
+        boolean defaultCaseFound = false;
+        for (int i = 0; i < cases.size(); i++) {
+            J.Case aCase = cases.get(i);
+            if (aCase.getBody() != null) {
+                return null;
             }
-
-            List<J.Case> casesGroupedWithDefault = new ArrayList<>();
-            boolean foundNonEmptyCase = false;
-            for (int i = defaultCaseIndex - 1; i >= 0; i--) {
-                J.Case aCase = cases.get(i);
-                if (aCase.getStatements().isEmpty() && !foundNonEmptyCase) {
-                    casesGroupedWithDefault.add(0, aCase);
-                } else {
-                    foundNonEmptyCase = true;
-                    fixedCases.add(0, aCase);
-                }
+            fallThroughCases.add(aCase);
+            if (isDefaultCase(aCase)) {
+                defaultCaseFound = true;
             }
-
-            foundNonEmptyCase = false;
-            for (int i = defaultCaseIndex + 1; i < cases.size(); i++) {
-                J.Case aCase = cases.get(i);
-                if (defaultCase != null && defaultCase.getStatements().isEmpty() &&
-                    aCase.getStatements().isEmpty() && !foundNonEmptyCase) {
-                    casesGroupedWithDefault.add(aCase);
+            if (i == cases.size() - 1 || !isFallthroughCase(aCase)) {
+                if (defaultCaseFound) {
+                    defaultCases.addAll(fallThroughCases);
+                    fallThroughCases.clear();
+                    defaultCaseFound = false;
                 } else {
-                    if (defaultCase != null && defaultCase.getStatements().isEmpty() && !foundNonEmptyCase) {
-                        // the last case grouped with default can be non-empty. it will be flipped with
-                        // the default case, including its statements
-                        casesGroupedWithDefault.add(aCase);
-                    }
-                    foundNonEmptyCase = true;
-                    fixedCases.add(aCase);
+                    casesWithDefaultLast.addAll(fallThroughCases);
+                    fallThroughCases.clear();
                 }
             }
+        }
 
-            if (defaultCase != null && !casesGroupedWithDefault.isEmpty()) {
-                J.Case lastGroupedWithDefault = casesGroupedWithDefault.get(casesGroupedWithDefault.size() - 1);
-                if (!lastGroupedWithDefault.getStatements().isEmpty()) {
-                    casesGroupedWithDefault.set(casesGroupedWithDefault.size() - 1,
-                            lastGroupedWithDefault.withStatements(Collections.emptyList()));
-                    defaultCase = defaultCase.withStatements(lastGroupedWithDefault.getStatements());
-                }
-            }
+        casesWithDefaultLast = addBreakToLastCase(p, casesWithDefaultLast);
+        casesWithDefaultLast.addAll(maybeReorderFallthroughCases(p, defaultCases));
+        casesWithDefaultLast = removeBreakFromLastCase(casesWithDefaultLast);
+        return casesWithDefaultLast;
+    }
 
-            J.Case lastNotGroupedWithDefault = fixedCases.get(fixedCases.size() - 1);
-            if (!lastNotGroupedWithDefault.getStatements().stream().reduce((s1, s2) -> s2)
-                    .map(stat -> stat instanceof J.Break || stat instanceof J.Continue ||
-                                 stat instanceof J.Return || stat instanceof J.Throw)
-                    .orElse(false)) {
-
-                // add a break statement since this case is now no longer last and would fall through
-                List<Statement> statementsOfCaseBeingMoved = new ArrayList<>(lastNotGroupedWithDefault.getStatements());
-                J.Break breakStatement = autoFormat(
-                        new J.Break(Tree.randomId(), Space.EMPTY, Markers.EMPTY, null),
-                        p
-                );
-                statementsOfCaseBeingMoved.add(breakStatement);
-
-                lastNotGroupedWithDefault = lastNotGroupedWithDefault.withStatements(
-                        ListUtils.map(statementsOfCaseBeingMoved, stmt -> autoFormat(stmt, p, getCursor()))
-                );
-                fixedCases.set(fixedCases.size() - 1, lastNotGroupedWithDefault);
+    private @NonNull List<J.Case> maybeReorderFallthroughCases(P p, List<J.Case> cases) {
+        J.Case defaultCase = null;
+        List<J.Case> preDefault = new ArrayList<>();
+        List<J.Case> postDefault = new ArrayList<>();
+        List<Statement> defaultStatements = new ArrayList<>();
+        for (J.Case aCase : cases) {
+            if (isDefaultCase(aCase)) {
+                defaultCase = aCase;
+            } else if (defaultCase == null) {
+                preDefault.add(aCase);
+            } else {
+                postDefault.add(aCase);
+                defaultStatements.addAll(aCase.getStatements());
             }
-
-            fixedCases.addAll(casesGroupedWithDefault);
-            if (defaultCase != null) {
-                if (defaultCase.getStatements().stream().reduce((s1, s2) -> s2)
-                        .map(stat -> stat instanceof J.Break || stat instanceof J.Continue || isVoidReturn(stat))
-                        .orElse(false)) {
-                    List<Statement> fixedDefaultStatements = new ArrayList<>(defaultCase.getStatements());
-                    fixedDefaultStatements.remove(fixedDefaultStatements.size() - 1);
-                    fixedCases.add(defaultCase.withStatements(fixedDefaultStatements));
+        }
+        List<Statement> defaultCaseStatements = defaultCase.getStatements();
+        defaultStatements.addAll(0, defaultCaseStatements);
+        List<J.Case> fixedCases = new ArrayList<>(preDefault.size() + postDefault.size() + 1);
+        fixedCases.addAll(maybeUpdatePreDefaultCases(preDefault, defaultCaseStatements));
+        fixedCases.addAll(postDefault);
+        if (!fixedCases.isEmpty()) {
+            List<Statement> lastStatements = fixedCases.get(fixedCases.size() - 1).getStatements();
+            if (!lastStatements.isEmpty()) {
+                if (!new HashSet<>(lastStatements).containsAll(defaultStatements)) {
+                    fixedCases = addBreakToLastCase(p, fixedCases);
                 } else {
-                    fixedCases.add(defaultCase);
+                    J.Case last = fixedCases.remove(fixedCases.size() - 1);
+                    fixedCases.add(last.withStatements(Collections.emptyList()));
                 }
             }
+        }
+        fixedCases.add(defaultCase.withStatements(ListUtils.map(defaultStatements, stmt -> autoFormat(stmt, p, getCursor()))));
+        return fixedCases;
+    }
 
-            boolean changed = true;
-            if (cases.size() == fixedCases.size()) {
-                changed = false;
-                for (int i = 0; i < cases.size(); i++) {
-                    if (cases.get(i) != fixedCases.get(i)) {
-                        changed = true;
-                        break;
-                    }
+    private J.Switch maybeUpdateSwitch(List<J.Case> cases, List<J.Case> casesWithDefaultLast, J.Switch s) {
path: src/main/java/org/openrewrite/staticanalysis/DefaultComesLastVisitor.java
user: login: JohannisK
body: Me neither :)
created_at: 2025-04-17T13:25:40Z
author_association: CONTRIBUTOR
diff_hunk: @@ -42,116 +45,139 @@ public J.Switch visitSwitch(J.Switch switch_, P p) {
 
         if (!isDefaultCaseLastOrNotPresent(switch_)) {
             List<J.Case> cases = s.getCases().getStatements().stream().map(J.Case.class::cast).collect(Collectors.toList());
-            List<J.Case> fixedCases = new ArrayList<>(cases.size());
-
-            int defaultCaseIndex = -1;
-            J.Case defaultCase = null;
-
-            for (int i = 0; i < cases.size(); i++) {
-                J.Case aCase = cases.get(i);
-
-                // skip cases with bodies for now
-                if (aCase.getBody() != null) {
-                    return s;
-                }
-                if (isDefaultCase(aCase)) {
-                    defaultCaseIndex = i;
-                    defaultCase = aCase;
+            List<J.Case> casesWithDefaultLast = maybeReorderCases(cases, p);
+            if (casesWithDefaultLast != null) {
+                boolean changed = true;
+                if (cases.size() == casesWithDefaultLast.size()) {
+                    changed = false;
+                    for (int i = 0; i < cases.size(); i++) {
+                        if (cases.get(i) != casesWithDefaultLast.get(i)) {
+                            changed = true;
+                            break;
+                        }
+                    }
                 }
-            }
 
-            List<J.Case> casesGroupedWithDefault = new ArrayList<>();
-            boolean foundNonEmptyCase = false;
-            for (int i = defaultCaseIndex - 1; i >= 0; i--) {
-                J.Case aCase = cases.get(i);
-                if (aCase.getStatements().isEmpty() && !foundNonEmptyCase) {
-                    casesGroupedWithDefault.add(0, aCase);
-                } else {
-                    foundNonEmptyCase = true;
-                    fixedCases.add(0, aCase);
+                if (changed) {
+                    s = s.withCases(s.getCases().withStatements(casesWithDefaultLast.stream().map(Statement.class::cast).collect(Collectors.toList())));
path: src/main/java/org/openrewrite/staticanalysis/DefaultComesLastVisitor.java
user: login: jevanlingen
body: ```suggestion
                    s = s.withCases(s.getCases().withStatements(casesWithDefaultLast.stream().map(Statement.class::cast).collect(toList())));
```
created_at: 2025-04-18T07:47:25Z
author_association: CONTRIBUTOR
diff_hunk: @@ -41,117 +45,140 @@ public J.Switch visitSwitch(J.Switch switch_, P p) {
         J.Switch s = visitAndCast(switch_, p, super::visitSwitch);
 
         if (!isDefaultCaseLastOrNotPresent(switch_)) {
-            List<J.Case> cases = s.getCases().getStatements().stream().map(J.Case.class::cast).collect(Collectors.toList());
-            List<J.Case> fixedCases = new ArrayList<>(cases.size());
-
-            int defaultCaseIndex = -1;
-            J.Case defaultCase = null;
-
-            for (int i = 0; i < cases.size(); i++) {
-                J.Case aCase = cases.get(i);
-
-                // skip cases with bodies for now
-                if (aCase.getBody() != null) {
-                    return s;
-                }
-                if (isDefaultCase(aCase)) {
-                    defaultCaseIndex = i;
-                    defaultCase = aCase;
+            List<J.Case> cases = s.getCases().getStatements().stream().map(J.Case.class::cast).collect(toList());
+            List<J.Case> casesWithDefaultLast = maybeReorderCases(cases, p);
+            if (casesWithDefaultLast != null) {
+                boolean changed = true;
+                if (cases.size() == casesWithDefaultLast.size()) {
+                    changed = false;
+                    for (int i = 0; i < cases.size(); i++) {
+                        if (cases.get(i) != casesWithDefaultLast.get(i)) {
+                            changed = true;
+                            break;
+                        }
+                    }
                 }
-            }
 
-            List<J.Case> casesGroupedWithDefault = new ArrayList<>();
-            boolean foundNonEmptyCase = false;
-            for (int i = defaultCaseIndex - 1; i >= 0; i--) {
-                J.Case aCase = cases.get(i);
-                if (aCase.getStatements().isEmpty() && !foundNonEmptyCase) {
-                    casesGroupedWithDefault.add(0, aCase);
-                } else {
-                    foundNonEmptyCase = true;
-                    fixedCases.add(0, aCase);
+                if (changed) {
+                    s = s.withCases(s.getCases().withStatements(casesWithDefaultLast.stream().map(Statement.class::cast).collect(toList())));
                 }
             }
+        }
+        return s;
+    }
+
+    private @Nullable List<J.Case> maybeReorderCases(List<J.Case> cases, P p) {
path: src/main/java/org/openrewrite/staticanalysis/DefaultComesLastVisitor.java
user: login: jevanlingen
body: I would go with a non-nullable here and return an `emptyList()`.
created_at: 2025-04-18T07:53:24Z
author_association: CONTRIBUTOR
diff_hunk: @@ -41,117 +45,140 @@ public J.Switch visitSwitch(J.Switch switch_, P p) {
         J.Switch s = visitAndCast(switch_, p, super::visitSwitch);
 
         if (!isDefaultCaseLastOrNotPresent(switch_)) {
-            List<J.Case> cases = s.getCases().getStatements().stream().map(J.Case.class::cast).collect(Collectors.toList());
-            List<J.Case> fixedCases = new ArrayList<>(cases.size());
-
-            int defaultCaseIndex = -1;
-            J.Case defaultCase = null;
-
-            for (int i = 0; i < cases.size(); i++) {
-                J.Case aCase = cases.get(i);
-
-                // skip cases with bodies for now
-                if (aCase.getBody() != null) {
-                    return s;
-                }
-                if (isDefaultCase(aCase)) {
-                    defaultCaseIndex = i;
-                    defaultCase = aCase;
+            List<J.Case> cases = s.getCases().getStatements().stream().map(J.Case.class::cast).collect(toList());
+            List<J.Case> casesWithDefaultLast = maybeReorderCases(cases, p);
+            if (casesWithDefaultLast != null) {
+                boolean changed = true;
+                if (cases.size() == casesWithDefaultLast.size()) {
+                    changed = false;
+                    for (int i = 0; i < cases.size(); i++) {
+                        if (cases.get(i) != casesWithDefaultLast.get(i)) {
+                            changed = true;
+                            break;
+                        }
+                    }
                 }
-            }
 
-            List<J.Case> casesGroupedWithDefault = new ArrayList<>();
-            boolean foundNonEmptyCase = false;
-            for (int i = defaultCaseIndex - 1; i >= 0; i--) {
-                J.Case aCase = cases.get(i);
-                if (aCase.getStatements().isEmpty() && !foundNonEmptyCase) {
-                    casesGroupedWithDefault.add(0, aCase);
-                } else {
-                    foundNonEmptyCase = true;
-                    fixedCases.add(0, aCase);
+                if (changed) {
+                    s = s.withCases(s.getCases().withStatements(casesWithDefaultLast.stream().map(Statement.class::cast).collect(toList())));
                 }
             }
+        }
+        return s;
+    }
+
+    private @Nullable List<J.Case> maybeReorderCases(List<J.Case> cases, P p) {
+        List<J.Case> fallThroughCases = new ArrayList<>(cases.size());
+        List<J.Case> defaultCases = new ArrayList<>(cases.size());
+        List<J.Case> casesWithDefaultLast = new ArrayList<>(cases.size());
 
-            foundNonEmptyCase = false;
-            for (int i = defaultCaseIndex + 1; i < cases.size(); i++) {
-                J.Case aCase = cases.get(i);
-                if (defaultCase != null && defaultCase.getStatements().isEmpty() &&
-                    aCase.getStatements().isEmpty() && !foundNonEmptyCase) {
-                    casesGroupedWithDefault.add(aCase);
+        boolean defaultCaseFound = false;
+        for (int i = 0; i < cases.size(); i++) {
+            J.Case aCase = cases.get(i);
+            if (aCase.getBody() != null) {
+                return null;
+            }
+            fallThroughCases.add(aCase);
+            if (isDefaultCase(aCase)) {
+                defaultCaseFound = true;
+            }
+            if (i == cases.size() - 1 || !isFallthroughCase(aCase)) {
+                if (defaultCaseFound) {
+                    defaultCases.addAll(fallThroughCases);
+                    fallThroughCases.clear();
+                    defaultCaseFound = false;
                 } else {
-                    if (defaultCase != null && defaultCase.getStatements().isEmpty() && !foundNonEmptyCase) {
-                        // the last case grouped with default can be non-empty. it will be flipped with
-                        // the default case, including its statements
-                        casesGroupedWithDefault.add(aCase);
-                    }
-                    foundNonEmptyCase = true;
-                    fixedCases.add(aCase);
+                    casesWithDefaultLast.addAll(fallThroughCases);
+                    fallThroughCases.clear();
                 }
             }
+        }
 
-            if (defaultCase != null && !casesGroupedWithDefault.isEmpty()) {
-                J.Case lastGroupedWithDefault = casesGroupedWithDefault.get(casesGroupedWithDefault.size() - 1);
-                if (!lastGroupedWithDefault.getStatements().isEmpty()) {
-                    casesGroupedWithDefault.set(casesGroupedWithDefault.size() - 1,
-                            lastGroupedWithDefault.withStatements(Collections.emptyList()));
-                    defaultCase = defaultCase.withStatements(lastGroupedWithDefault.getStatements());
-                }
-            }
+        casesWithDefaultLast = addBreakToLastCase(casesWithDefaultLast, p);
+        casesWithDefaultLast.addAll(maybeReorderFallthroughCases(defaultCases, p));
+        casesWithDefaultLast = ListUtils.mapLast(casesWithDefaultLast, this::removeBreak);
+        return casesWithDefaultLast;
+    }
 
-            J.Case lastNotGroupedWithDefault = fixedCases.get(fixedCases.size() - 1);
-            if (!lastNotGroupedWithDefault.getStatements().stream().reduce((s1, s2) -> s2)
-                    .map(stat -> stat instanceof J.Break || stat instanceof J.Continue ||
-                                 stat instanceof J.Return || stat instanceof J.Throw)
-                    .orElse(false)) {
-
-                // add a break statement since this case is now no longer last and would fall through
-                List<Statement> statementsOfCaseBeingMoved = new ArrayList<>(lastNotGroupedWithDefault.getStatements());
-                J.Break breakStatement = autoFormat(
-                        new J.Break(Tree.randomId(), Space.EMPTY, Markers.EMPTY, null),
-                        p
-                );
-                statementsOfCaseBeingMoved.add(breakStatement);
-
-                lastNotGroupedWithDefault = lastNotGroupedWithDefault.withStatements(
-                        ListUtils.map(statementsOfCaseBeingMoved, stmt -> autoFormat(stmt, p, getCursor()))
-                );
-                fixedCases.set(fixedCases.size() - 1, lastNotGroupedWithDefault);
+    private @NonNull List<J.Case> maybeReorderFallthroughCases(List<J.Case> cases, P p) {
path: src/main/java/org/openrewrite/staticanalysis/DefaultComesLastVisitor.java
user: login: jevanlingen
body: ```suggestion
    private List<J.Case> maybeReorderFallthroughCases(List<J.Case> cases, P p) {
```

No need for this annotation, as there is already a JSpecify package-info.java file in this folder.
created_at: 2025-04-18T07:55:36Z
author_association: CONTRIBUTOR
diff_hunk: @@ -41,117 +45,140 @@ public J.Switch visitSwitch(J.Switch switch_, P p) {
         J.Switch s = visitAndCast(switch_, p, super::visitSwitch);
 
         if (!isDefaultCaseLastOrNotPresent(switch_)) {
-            List<J.Case> cases = s.getCases().getStatements().stream().map(J.Case.class::cast).collect(Collectors.toList());
-            List<J.Case> fixedCases = new ArrayList<>(cases.size());
-
-            int defaultCaseIndex = -1;
-            J.Case defaultCase = null;
-
-            for (int i = 0; i < cases.size(); i++) {
-                J.Case aCase = cases.get(i);
-
-                // skip cases with bodies for now
-                if (aCase.getBody() != null) {
-                    return s;
-                }
-                if (isDefaultCase(aCase)) {
-                    defaultCaseIndex = i;
-                    defaultCase = aCase;
+            List<J.Case> cases = s.getCases().getStatements().stream().map(J.Case.class::cast).collect(toList());
+            List<J.Case> casesWithDefaultLast = maybeReorderCases(cases, p);
+            if (casesWithDefaultLast != null) {
+                boolean changed = true;
+                if (cases.size() == casesWithDefaultLast.size()) {
+                    changed = false;
+                    for (int i = 0; i < cases.size(); i++) {
+                        if (cases.get(i) != casesWithDefaultLast.get(i)) {
+                            changed = true;
+                            break;
+                        }
+                    }
                 }
-            }
 
-            List<J.Case> casesGroupedWithDefault = new ArrayList<>();
-            boolean foundNonEmptyCase = false;
-            for (int i = defaultCaseIndex - 1; i >= 0; i--) {
-                J.Case aCase = cases.get(i);
-                if (aCase.getStatements().isEmpty() && !foundNonEmptyCase) {
-                    casesGroupedWithDefault.add(0, aCase);
-                } else {
-                    foundNonEmptyCase = true;
-                    fixedCases.add(0, aCase);
+                if (changed) {
+                    s = s.withCases(s.getCases().withStatements(casesWithDefaultLast.stream().map(Statement.class::cast).collect(toList())));
                 }
             }
+        }
+        return s;
+    }
+
+    private @Nullable List<J.Case> maybeReorderCases(List<J.Case> cases, P p) {
+        List<J.Case> fallThroughCases = new ArrayList<>(cases.size());
+        List<J.Case> defaultCases = new ArrayList<>(cases.size());
+        List<J.Case> casesWithDefaultLast = new ArrayList<>(cases.size());
 
-            foundNonEmptyCase = false;
-            for (int i = defaultCaseIndex + 1; i < cases.size(); i++) {
-                J.Case aCase = cases.get(i);
-                if (defaultCase != null && defaultCase.getStatements().isEmpty() &&
-                    aCase.getStatements().isEmpty() && !foundNonEmptyCase) {
-                    casesGroupedWithDefault.add(aCase);
+        boolean defaultCaseFound = false;
+        for (int i = 0; i < cases.size(); i++) {
+            J.Case aCase = cases.get(i);
+            if (aCase.getBody() != null) {
+                return null;
+            }
+            fallThroughCases.add(aCase);
+            if (isDefaultCase(aCase)) {
+                defaultCaseFound = true;
+            }
+            if (i == cases.size() - 1 || !isFallthroughCase(aCase)) {
+                if (defaultCaseFound) {
+                    defaultCases.addAll(fallThroughCases);
+                    fallThroughCases.clear();
+                    defaultCaseFound = false;
                 } else {
-                    if (defaultCase != null && defaultCase.getStatements().isEmpty() && !foundNonEmptyCase) {
-                        // the last case grouped with default can be non-empty. it will be flipped with
-                        // the default case, including its statements
-                        casesGroupedWithDefault.add(aCase);
-                    }
-                    foundNonEmptyCase = true;
-                    fixedCases.add(aCase);
+                    casesWithDefaultLast.addAll(fallThroughCases);
+                    fallThroughCases.clear();
                 }
             }
+        }
 
-            if (defaultCase != null && !casesGroupedWithDefault.isEmpty()) {
-                J.Case lastGroupedWithDefault = casesGroupedWithDefault.get(casesGroupedWithDefault.size() - 1);
-                if (!lastGroupedWithDefault.getStatements().isEmpty()) {
-                    casesGroupedWithDefault.set(casesGroupedWithDefault.size() - 1,
-                            lastGroupedWithDefault.withStatements(Collections.emptyList()));
-                    defaultCase = defaultCase.withStatements(lastGroupedWithDefault.getStatements());
-                }
-            }
+        casesWithDefaultLast = addBreakToLastCase(casesWithDefaultLast, p);
+        casesWithDefaultLast.addAll(maybeReorderFallthroughCases(defaultCases, p));
+        casesWithDefaultLast = ListUtils.mapLast(casesWithDefaultLast, this::removeBreak);
+        return casesWithDefaultLast;
+    }
 
-            J.Case lastNotGroupedWithDefault = fixedCases.get(fixedCases.size() - 1);
-            if (!lastNotGroupedWithDefault.getStatements().stream().reduce((s1, s2) -> s2)
-                    .map(stat -> stat instanceof J.Break || stat instanceof J.Continue ||
-                                 stat instanceof J.Return || stat instanceof J.Throw)
-                    .orElse(false)) {
-
-                // add a break statement since this case is now no longer last and would fall through
-                List<Statement> statementsOfCaseBeingMoved = new ArrayList<>(lastNotGroupedWithDefault.getStatements());
-                J.Break breakStatement = autoFormat(
-                        new J.Break(Tree.randomId(), Space.EMPTY, Markers.EMPTY, null),
-                        p
-                );
-                statementsOfCaseBeingMoved.add(breakStatement);
-
-                lastNotGroupedWithDefault = lastNotGroupedWithDefault.withStatements(
-                        ListUtils.map(statementsOfCaseBeingMoved, stmt -> autoFormat(stmt, p, getCursor()))
-                );
-                fixedCases.set(fixedCases.size() - 1, lastNotGroupedWithDefault);
+    private @NonNull List<J.Case> maybeReorderFallthroughCases(List<J.Case> cases, P p) {
+        J.Case defaultCase = null;
+        List<J.Case> preDefault = new ArrayList<>();
+        List<J.Case> postDefault = new ArrayList<>();
+        List<Statement> defaultStatements = new ArrayList<>();
+        for (J.Case aCase : cases) {
+            if (isDefaultCase(aCase)) {
+                defaultCase = aCase;
+            } else if (defaultCase == null) {
+                preDefault.add(aCase);
+            } else {
+                postDefault.add(aCase);
+                defaultStatements.addAll(aCase.getStatements());
             }
-
-            fixedCases.addAll(casesGroupedWithDefault);
-            if (defaultCase != null) {
-                if (defaultCase.getStatements().stream().reduce((s1, s2) -> s2)
-                        .map(stat -> stat instanceof J.Break || stat instanceof J.Continue || isVoidReturn(stat))
-                        .orElse(false)) {
-                    List<Statement> fixedDefaultStatements = new ArrayList<>(defaultCase.getStatements());
-                    fixedDefaultStatements.remove(fixedDefaultStatements.size() - 1);
-                    fixedCases.add(defaultCase.withStatements(fixedDefaultStatements));
+        }
+        List<Statement> defaultCaseStatements = defaultCase.getStatements();
path: src/main/java/org/openrewrite/staticanalysis/DefaultComesLastVisitor.java
user: login: jevanlingen
body: Method invocation 'getStatements' may produce 'NullPointerException' 
created_at: 2025-04-18T07:58:32Z
author_association: CONTRIBUTOR
diff_hunk: @@ -41,117 +45,140 @@ public J.Switch visitSwitch(J.Switch switch_, P p) {
         J.Switch s = visitAndCast(switch_, p, super::visitSwitch);
 
         if (!isDefaultCaseLastOrNotPresent(switch_)) {
-            List<J.Case> cases = s.getCases().getStatements().stream().map(J.Case.class::cast).collect(Collectors.toList());
-            List<J.Case> fixedCases = new ArrayList<>(cases.size());
-
-            int defaultCaseIndex = -1;
-            J.Case defaultCase = null;
-
-            for (int i = 0; i < cases.size(); i++) {
-                J.Case aCase = cases.get(i);
-
-                // skip cases with bodies for now
-                if (aCase.getBody() != null) {
-                    return s;
-                }
-                if (isDefaultCase(aCase)) {
-                    defaultCaseIndex = i;
-                    defaultCase = aCase;
+            List<J.Case> cases = s.getCases().getStatements().stream().map(J.Case.class::cast).collect(toList());
+            List<J.Case> casesWithDefaultLast = maybeReorderCases(cases, p);
+            if (casesWithDefaultLast != null) {
+                boolean changed = true;
+                if (cases.size() == casesWithDefaultLast.size()) {
+                    changed = false;
+                    for (int i = 0; i < cases.size(); i++) {
+                        if (cases.get(i) != casesWithDefaultLast.get(i)) {
+                            changed = true;
+                            break;
+                        }
+                    }
                 }
-            }
 
-            List<J.Case> casesGroupedWithDefault = new ArrayList<>();
-            boolean foundNonEmptyCase = false;
-            for (int i = defaultCaseIndex - 1; i >= 0; i--) {
-                J.Case aCase = cases.get(i);
-                if (aCase.getStatements().isEmpty() && !foundNonEmptyCase) {
-                    casesGroupedWithDefault.add(0, aCase);
-                } else {
-                    foundNonEmptyCase = true;
-                    fixedCases.add(0, aCase);
+                if (changed) {
+                    s = s.withCases(s.getCases().withStatements(casesWithDefaultLast.stream().map(Statement.class::cast).collect(toList())));
                 }
             }
+        }
+        return s;
+    }
+
+    private @Nullable List<J.Case> maybeReorderCases(List<J.Case> cases, P p) {
path: src/main/java/org/openrewrite/staticanalysis/DefaultComesLastVisitor.java
user: login: JohannisK
body: It's a bit ugly, but null has special meaning. It means we found a switch case with a body, and should return the original (t's completely ignored)
created_at: 2025-04-18T08:00:15Z
author_association: CONTRIBUTOR
diff_hunk: @@ -17,6 +17,8 @@
 
 import lombok.EqualsAndHashCode;
 import lombok.Value;
+import org.jspecify.annotations.NonNull;
+import org.jspecify.annotations.Nullable;
path: src/main/java/org/openrewrite/staticanalysis/DefaultComesLastVisitor.java
user: login: github-actions[bot]
body: ```suggestion
import org.jspecify.annotations.Nullable;
```
created_at: 2025-04-18T08:00:28Z
author_association: CONTRIBUTOR
diff_hunk: @@ -320,4 +353,148 @@ static int coverage(Object obj) {
           )
         );
     }
+
+    @Issue("https://github.com/openrewrite/rewrite-static-analysis/issues/4")
+    @Test
+    void moveDefaultToLastWithFallThrough() {
+        rewriteRun(
+          //language=java
+          java(
+            """
+              class Test {
+                  int n;
+                  {
+                      switch (n) {
+                          case 1:
+                              break;
+                          case 2:
+                              break;
+                          default:
+                          case 3:
+                              System.out.println("case3");
+                      }
+                  }
+              }
+              """,
+            """
+              class Test {
+                  int n;
+                  {
+                      switch (n) {
+                          case 1:
+                              break;
+                          case 2:
+                              break;
+                          case 3:
+                          default:
+                              System.out.println("case3");
+                      }
+                  }
+              }
+              """
+          )
+        );
+    }
+
+    @Issue("https://github.com/openrewrite/rewrite-static-analysis/issues/4")
+    @Test
+    void moveDefaultToLastWithFallThroughAndStatements() {
path: src/test/java/org/openrewrite/staticanalysis/DefaultComesLastTest.java
user: login: timtebeek
body: hmm; I'm not so sure; imagine there's 15 lines of code instead of one print statement; then you'd be duplicating a lot of logic, as part of a recipe that should only reorder, or make no change at all.
created_at: 2025-04-18T11:33:42Z
author_association: MEMBER
diff_hunk: @@ -320,4 +353,148 @@ static int coverage(Object obj) {
           )
         );
     }
+
+    @Issue("https://github.com/openrewrite/rewrite-static-analysis/issues/4")
+    @Test
+    void moveDefaultToLastWithFallThrough() {
+        rewriteRun(
+          //language=java
+          java(
+            """
+              class Test {
+                  int n;
+                  {
+                      switch (n) {
+                          case 1:
+                              break;
+                          case 2:
+                              break;
+                          default:
+                          case 3:
+                              System.out.println("case3");
+                      }
+                  }
+              }
+              """,
+            """
+              class Test {
+                  int n;
+                  {
+                      switch (n) {
+                          case 1:
+                              break;
+                          case 2:
+                              break;
+                          case 3:
+                          default:
+                              System.out.println("case3");
+                      }
+                  }
+              }
+              """
+          )
+        );
+    }
+
+    @Issue("https://github.com/openrewrite/rewrite-static-analysis/issues/4")
+    @Test
+    void moveDefaultToLastWithFallThroughAndStatements() {
path: src/test/java/org/openrewrite/staticanalysis/DefaultComesLastTest.java
user: login: greg-at-moderne
body: My way of thinking was that the user explicitly asked for this when they called 'DefaultComesLast'.

(no strong feelings, feel free to ignore my comments)
created_at: 2025-04-18T14:04:43Z
author_association: CONTRIBUTOR
diff_hunk: @@ -320,4 +353,148 @@ static int coverage(Object obj) {
           )
         );
     }
+
+    @Issue("https://github.com/openrewrite/rewrite-static-analysis/issues/4")
+    @Test
+    void moveDefaultToLastWithFallThrough() {
+        rewriteRun(
+          //language=java
+          java(
+            """
+              class Test {
+                  int n;
+                  {
+                      switch (n) {
+                          case 1:
+                              break;
+                          case 2:
+                              break;
+                          default:
+                          case 3:
+                              System.out.println("case3");
+                      }
+                  }
+              }
+              """,
+            """
+              class Test {
+                  int n;
+                  {
+                      switch (n) {
+                          case 1:
+                              break;
+                          case 2:
+                              break;
+                          case 3:
+                          default:
+                              System.out.println("case3");
+                      }
+                  }
+              }
+              """
+          )
+        );
+    }
+
+    @Issue("https://github.com/openrewrite/rewrite-static-analysis/issues/4")
+    @Test
+    void moveDefaultToLastWithFallThroughAndStatements() {
path: src/test/java/org/openrewrite/staticanalysis/DefaultComesLastTest.java
user: login: JohannisK
body: I've changed the logic, and added tests to show the behavior.
Now it should:
- Move entire fallthrough blocks to the bottom if they contain a default statement
- If the default case can be moved down within it's fallthrough block without copying or reordering statements the default case is moved down (it checks this by verifying the default case and all cases following the default case (except for the last one) do not contain statements themselves. If that is true statements of the last case are moved to the default case, and the default case is moved down.
created_at: 2025-04-18T14:18:24Z
author_association: CONTRIBUTOR

PR comments:
user: login: JohannisK
created_at: 2025-04-18T14:34:25Z
body: @timtebeek 

> I've discussed this with Johan, he's going to explore two options
> 
> 1. Either move the whole block of fall through cases that include `default` to the end
> 2. Or not make any change at all when there's fallthrough we can't handle well.

I've adjusted the recipe: https://github.com/openrewrite/rewrite-static-analysis/pull/520#discussion_r2050695648

I think it might be a little bit better then your proposal here.
It will move the entire block down.
If the default case can be moved without reordering or duplicating statements it will move the default case down.
author_association: CONTRIBUTOR
user: login: timtebeek
created_at: 2025-04-22T11:54:26Z
body: As discussed might be good to test this against some well known projects still before we merge; `apache/poi` could be interesting as they have a convention of default first, so that gives quite some cases to ealuate.
author_association: MEMBER
user: login: sambsnyd
created_at: 2025-04-22T18:30:57Z
body: There may well be further improvements possible but this looks like it's clearly an improvement over the existing form of the recipe. Thanks @JohannisK !
author_association: MEMBER