From 4afe0af186e034575b1e1a2ec0e447472e0b261b Mon Sep 17 00:00:00 2001
From: Victorien Elvinger <victorien@elvinger.fr>
Date: Thu, 21 Sep 2023 16:37:14 +0200
Subject: [PATCH] feat(lint/noUselessElse): add rule (#331)

---
 CHANGELOG.md                                  |   4 +
 .../src/categories.rs                         |   3 +-
 .../biome_js_analyze/src/analyzers/nursery.rs |   2 +
 .../src/analyzers/nursery/no_useless_else.rs  | 195 +++++++
 .../nursery/use_is_array.rs                   |   2 +-
 .../specs/nursery/noUselessElse/invalid.js    |  94 ++++
 .../nursery/noUselessElse/invalid.js.snap     | 501 ++++++++++++++++++
 .../specs/nursery/noUselessElse/missed.js     |  53 ++
 .../nursery/noUselessElse/missed.js.snap      |  63 +++
 .../specs/nursery/noUselessElse/valid.js      |  77 +++
 .../specs/nursery/noUselessElse/valid.js.snap |  87 +++
 crates/biome_rowan/src/ast/mod.rs             |   6 +-
 .../src/configuration/linter/rules.rs         |  69 ++-
 .../src/configuration/parse/json/rules.rs     |  24 +
 editors/vscode/configuration_schema.json      |   7 +
 editors/vscode/src/main.ts                    |  23 +-
 .../@biomejs/backend-jsonrpc/src/workspace.ts |   7 +-
 .../@biomejs/biome/configuration_schema.json  |   7 +
 website/astro.config.ts                       |   3 +-
 .../components/generated/NumberOfRules.astro  |   2 +-
 .../src/content/docs/internals/changelog.mdx  |   4 +
 .../src/content/docs/linter/rules/index.mdx   |   2 +
 .../docs/linter/rules/no-useless-else.md      | 185 +++++++
 website/src/frontend-scripts/toc.ts           |   5 +-
 website/src/playground/Playground.tsx         |  13 +-
 .../src/playground/tabs/FormatterCodeTab.tsx  |   3 +-
 website/src/playground/utils.ts               |   5 +-
 .../src/playground/workers/prettierWorker.ts  |  15 +-
 28 files changed, 1392 insertions(+), 69 deletions(-)
 create mode 100644 crates/biome_js_analyze/src/analyzers/nursery/no_useless_else.rs
 create mode 100644 crates/biome_js_analyze/tests/specs/nursery/noUselessElse/invalid.js
 create mode 100644 crates/biome_js_analyze/tests/specs/nursery/noUselessElse/invalid.js.snap
 create mode 100644 crates/biome_js_analyze/tests/specs/nursery/noUselessElse/missed.js
 create mode 100644 crates/biome_js_analyze/tests/specs/nursery/noUselessElse/missed.js.snap
 create mode 100644 crates/biome_js_analyze/tests/specs/nursery/noUselessElse/valid.js
 create mode 100644 crates/biome_js_analyze/tests/specs/nursery/noUselessElse/valid.js.snap
 create mode 100644 website/src/content/docs/linter/rules/no-useless-else.md

diff --git a/CHANGELOG.md b/CHANGELOG.md
index c59971b14b3b..b1882085b313 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -36,6 +36,10 @@ Read our [guidelines for writing a good changelog entry](https://github.com/biom
 
 - Add [noMisleadingInstantiator](https://biomejs.dev/linter/rules/no-mileading-instantiator) rule. The rule reports the misleading use of the `new` and `constructor` methods. Contributed by @unvalley
 
+- Add [noUselessElse](https://biomejs.dev/linter/rules/no-useless-else) rule.
+  The rule reports `else` clauses that can be omitted because their `if` branches break.
+  Contributed by @Conaclos
+
 #### Bug fixes
 
 - Fix [#294](https://github.com/biomejs/biome/issues/294). [noConfusingVoidType](https://biomejs.dev/linter/rules/no-confusing-void-type/) no longer reports false positives for return types. Contributed by @b4s36t4
diff --git a/crates/biome_diagnostics_categories/src/categories.rs b/crates/biome_diagnostics_categories/src/categories.rs
index a2ef48c296e2..62a255622556 100644
--- a/crates/biome_diagnostics_categories/src/categories.rs
+++ b/crates/biome_diagnostics_categories/src/categories.rs
@@ -27,7 +27,6 @@ define_categories! {
     "lint/a11y/noSvgWithoutTitle": "https://biomejs.dev/linter/rules/no-svg-without-title",
     "lint/a11y/useAltText": "https://biomejs.dev/linter/rules/use-alt-text",
     "lint/a11y/useAnchorContent": "https://biomejs.dev/linter/rules/use-anchor-content",
-    "lint/a11y/useValidAriaValues": "https://biomejs.dev/linter/rules/use-valid-aria-values",
     "lint/a11y/useAriaPropsForRole": "https://biomejs.dev/linter/rules/use-aria-props-for-role",
     "lint/a11y/useButtonType": "https://biomejs.dev/linter/rules/use-button-type",
     "lint/a11y/useHeadingContent": "https://biomejs.dev/linter/rules/use-heading-content",
@@ -38,6 +37,7 @@ define_categories! {
     "lint/a11y/useMediaCaption": "https://biomejs.dev/linter/rules/use-media-caption",
     "lint/a11y/useValidAnchor": "https://biomejs.dev/linter/rules/use-valid-anchor",
     "lint/a11y/useValidAriaProps": "https://biomejs.dev/linter/rules/use-valid-aria-props",
+    "lint/a11y/useValidAriaValues": "https://biomejs.dev/linter/rules/use-valid-aria-values",
     "lint/a11y/useValidLang": "https://biomejs.dev/linter/rules/use-valid-lang",
     "lint/complexity/noBannedTypes": "https://biomejs.dev/linter/rules/no-banned-types",
     "lint/complexity/noExtraBooleanCast": "https://biomejs.dev/linter/rules/no-extra-boolean-cast",
@@ -96,6 +96,7 @@ define_categories! {
     "lint/nursery/noGlobalIsFinite": "https://biomejs.dev/linter/rules/no-global-is-finite",
     "lint/nursery/noGlobalIsNan": "https://biomejs.dev/linter/rules/no-global-is-nan",
     "lint/nursery/noMisleadingInstantiator": "https://biomejs.dev/linter/rules/no-misleading-instantiator",
+    "lint/nursery/noUselessElse": "https://biomejs.dev/lint/rules/no-useless-else",
     "lint/nursery/noVoid": "https://biomejs.dev/linter/rules/no-void",
     "lint/nursery/useArrowFunction": "https://biomejs.dev/linter/rules/use-arrow-function",
     "lint/nursery/useBiomeSuppressionComment": "https://biomejs.dev/lint/rules/use-biome-suppression-comment",
diff --git a/crates/biome_js_analyze/src/analyzers/nursery.rs b/crates/biome_js_analyze/src/analyzers/nursery.rs
index 63dbe9e17163..75c63bc88608 100644
--- a/crates/biome_js_analyze/src/analyzers/nursery.rs
+++ b/crates/biome_js_analyze/src/analyzers/nursery.rs
@@ -6,6 +6,7 @@ pub(crate) mod no_confusing_void_type;
 pub(crate) mod no_excessive_complexity;
 pub(crate) mod no_fallthrough_switch_clause;
 pub(crate) mod no_misleading_instantiator;
+pub(crate) mod no_useless_else;
 pub(crate) mod no_void;
 pub(crate) mod use_arrow_function;
 pub(crate) mod use_collapsed_else_if;
@@ -20,6 +21,7 @@ declare_group! {
             self :: no_excessive_complexity :: NoExcessiveComplexity ,
             self :: no_fallthrough_switch_clause :: NoFallthroughSwitchClause ,
             self :: no_misleading_instantiator :: NoMisleadingInstantiator ,
+            self :: no_useless_else :: NoUselessElse ,
             self :: no_void :: NoVoid ,
             self :: use_arrow_function :: UseArrowFunction ,
             self :: use_collapsed_else_if :: UseCollapsedElseIf ,
diff --git a/crates/biome_js_analyze/src/analyzers/nursery/no_useless_else.rs b/crates/biome_js_analyze/src/analyzers/nursery/no_useless_else.rs
new file mode 100644
index 000000000000..b5717c8c8010
--- /dev/null
+++ b/crates/biome_js_analyze/src/analyzers/nursery/no_useless_else.rs
@@ -0,0 +1,195 @@
+use biome_analyze::{
+    context::RuleContext, declare_rule, ActionCategory, Ast, Rule, RuleDiagnostic,
+};
+use biome_console::markup;
+use biome_diagnostics::Applicability;
+use biome_js_factory::make;
+use biome_js_syntax::{AnyJsStatement, JsElseClause, JsIfStatement, JsStatementList};
+use biome_rowan::{AstNode, AstNodeList, BatchMutationExt};
+
+use crate::JsRuleAction;
+
+declare_rule! {
+    /// Disallow `else` block when the `if` block breaks early.
+    ///
+    /// If an `if` block breaks early using a breaking statement (`return`, `break`, `continue`, or `throw`),
+    /// then the `else` block becomes useless.
+    /// Its contents can be placed outside of the block.
+    ///
+    /// ## Examples
+    ///
+    /// ### Invalid
+    ///
+    /// ```js,expect_diagnostic
+    /// while (x > 0) {
+    ///     if (f(x)) {
+    ///         break;
+    ///     } else {
+    ///         x++
+    ///     }
+    /// }
+    /// ```
+    ///
+    /// ```js,expect_diagnostic
+    /// function f() {
+    ///     if (x < 0) {
+    ///         return 0;
+    ///     } else {
+    ///         return x;
+    ///     }
+    /// }
+    /// ```
+    ///
+    /// ```js,expect_diagnostic
+    /// function f() {
+    ///     if (x < 0) {
+    ///         throw new RangeError();
+    ///     } else {
+    ///         return x;
+    ///     }
+    /// }
+    /// ```
+    ///
+    /// ## Valid
+    ///
+    /// ```js
+    /// function f() {
+    ///     if (x < 0) {
+    ///         return 0;
+    ///     }
+    ///     return x;
+    /// }
+    /// ```
+    pub(crate) NoUselessElse {
+        version: "next",
+        name: "noUselessElse",
+        recommended: true,
+    }
+}
+
+impl Rule for NoUselessElse {
+    type Query = Ast<JsElseClause>;
+    type State = ();
+    type Signals = Option<Self::State>;
+    type Options = ();
+
+    fn run(ctx: &RuleContext<Self>) -> Self::Signals {
+        let else_clause = ctx.query();
+        let if_stmt = else_clause.parent::<JsIfStatement>()?;
+        let mut stmt_stack = vec![(if_stmt.consequent().ok()?, ScopeMetadata::default())];
+        while let Some((stmt, metadata)) = stmt_stack.pop() {
+            match stmt {
+                AnyJsStatement::JsBlockStatement(block_stmt) => {
+                    let Some(last) = block_stmt.statements().iter().last() else {
+                        // empty statement block
+                        return None;
+                    };
+                    stmt_stack.push((last, metadata));
+                }
+                AnyJsStatement::JsBreakStatement(_) => {
+                    if metadata.is_breakable {
+                        // We are inside a breakable structure (switch statement)
+                        // that we saw in a previous iteration.
+                        return None;
+                    }
+                }
+                AnyJsStatement::JsContinueStatement(_)
+                | AnyJsStatement::JsReturnStatement(_)
+                | AnyJsStatement::JsThrowStatement(_) => {}
+                AnyJsStatement::JsIfStatement(if_stmt) => {
+                    let Some(else_clause) = if_stmt.else_clause() else {
+                        // No else clause
+                        return None;
+                    };
+                    stmt_stack.push((if_stmt.consequent().ok()?, metadata));
+                    stmt_stack.push((else_clause.alternate().ok()?, metadata));
+                }
+                AnyJsStatement::JsSwitchStatement(switch_stmt) => {
+                    // To simplify, We do not take fallthoughs into account.
+                    // Thus, this can miss some useless else.
+                    let cases = switch_stmt.cases();
+                    let Some(last_case) = cases.last() else {
+                        // Empty switch
+                        return None;
+                    };
+                    if last_case.consequent().is_empty() {
+                        return None;
+                    }
+                    for switch_clause in cases.iter() {
+                        if let Some(last) = switch_clause.consequent().last() {
+                            stmt_stack.push((last, ScopeMetadata { is_breakable: true }));
+                        }
+                    }
+                }
+                _ => {
+                    // labeled statements, loops, try-catch, with statement, and others
+                    return None;
+                }
+            }
+        }
+        Some(())
+    }
+
+    fn diagnostic(ctx: &RuleContext<Self>, _: &Self::State) -> Option<RuleDiagnostic> {
+        let else_clause = ctx.query();
+        Some(
+            RuleDiagnostic::new(
+                rule_category!(),
+                else_clause.range(),
+                markup! {
+                    "This "<Emphasis>"else"</Emphasis>" clause can be omitted."
+                },
+            )
+            .detail(
+                else_clause.syntax().parent()?.text_trimmed_range(),
+                markup! {
+                    "This "<Emphasis>"if"</Emphasis>" statement uses an early breaking statement."
+                },
+            ),
+        )
+    }
+
+    fn action(ctx: &RuleContext<Self>, _: &Self::State) -> Option<JsRuleAction> {
+        let else_clause = ctx.query();
+        let if_stmt = else_clause.parent::<JsIfStatement>()?;
+        if let Some(stmts_list) = JsStatementList::cast(if_stmt.syntax().parent()?) {
+            let else_alternative = else_clause.alternate().ok()?;
+            let if_pos = stmts_list
+                .iter()
+                .position(|x| x.syntax() == if_stmt.syntax())?;
+            let new_if_stmt = AnyJsStatement::JsIfStatement(if_stmt.clone().with_else_clause(None))
+                .with_trailing_trivia_pieces([])?;
+            let prev_stmts = stmts_list.iter().take(if_pos).chain([new_if_stmt]);
+            let next_stmts = stmts_list.iter().skip(if_pos + 1);
+            // We collect the statements because `chain` is not able to produce an `ExactSizeIterator`.
+            let new_stmts: Vec<_> =
+                if let AnyJsStatement::JsBlockStatement(else_block_stmts) = else_alternative {
+                    prev_stmts
+                        .chain(else_block_stmts.statements().iter())
+                        .chain(next_stmts)
+                        .collect()
+                } else {
+                    prev_stmts
+                        .chain([else_alternative])
+                        .chain(next_stmts)
+                        .collect()
+                };
+            let new_stmts_list = make::js_statement_list(new_stmts);
+            let mut mutation = ctx.root().begin();
+            mutation.replace_node_discard_trivia(stmts_list, new_stmts_list);
+            return Some(JsRuleAction {
+                category: ActionCategory::QuickFix,
+                applicability: Applicability::MaybeIncorrect,
+                message: markup! { "Omit the "<Emphasis>"else"</Emphasis>" clause." }.to_owned(),
+                mutation,
+            });
+        }
+        None
+    }
+}
+
+#[derive(Debug, Copy, Clone, Default)]
+struct ScopeMetadata {
+    // We are inside a breakable structure
+    is_breakable: bool,
+}
diff --git a/crates/biome_js_analyze/src/semantic_analyzers/nursery/use_is_array.rs b/crates/biome_js_analyze/src/semantic_analyzers/nursery/use_is_array.rs
index 798722d0ad00..4fb92bdc1d96 100644
--- a/crates/biome_js_analyze/src/semantic_analyzers/nursery/use_is_array.rs
+++ b/crates/biome_js_analyze/src/semantic_analyzers/nursery/use_is_array.rs
@@ -85,7 +85,7 @@ impl Rule for UseIsArray {
             make::token(T![.]),
             make::js_name(make::ident("isArray")).into(),
         );
-        let arg = AnyJsCallArgument::AnyJsExpression(node.left().ok()?.trim()?);
+        let arg = AnyJsCallArgument::AnyJsExpression(node.left().ok()?.trim_trivia()?);
         let instanceof_trailing_trivia = node.instanceof_token().ok()?.trailing_trivia().pieces();
         let args = make::js_call_arguments(
             make::token(T!['(']).with_trailing_trivia_pieces(trim_leading_trivia_pieces(
diff --git a/crates/biome_js_analyze/tests/specs/nursery/noUselessElse/invalid.js b/crates/biome_js_analyze/tests/specs/nursery/noUselessElse/invalid.js
new file mode 100644
index 000000000000..764661f0ddf3
--- /dev/null
+++ b/crates/biome_js_analyze/tests/specs/nursery/noUselessElse/invalid.js
@@ -0,0 +1,94 @@
+function f (x) {
+    if (x < 0) {
+        throw new RangeError();
+    } else {
+        return x;
+    }
+}
+
+function f (x) {
+    f();
+    if (x < 0) {
+        throw new RangeError();
+    } else {
+        g();
+    }
+    h();
+}
+
+function f (x) {
+    if (x < 0) {
+        throw new RangeError();
+    } else return x;
+}
+
+function f (x) {
+    if (x < 0)
+        throw new RangeError();
+    else
+        return x;
+}
+
+function f (x) {
+    if (x < 0) {
+        throw new RangeError();
+    } else if (x === 0) {
+        return 1;
+    } else {
+        return x;
+    }
+}
+
+function f (x) {
+    while (true) {
+        if (x < 0) {
+            break;
+        } else {
+            x -= g(x)
+        }
+    }
+    return x;
+}
+
+function f (x) {
+    while (true) {
+        if (x < 0) {
+            break;
+        } else {
+            x -= g(x)
+        }
+    }
+    return x;
+}
+
+function f (x) {
+    if (x > 0 && x < 5) {
+        switch (x) {
+            case 0:
+            case 1:
+                return 0;
+            default:
+                return x;
+        }
+    } else {
+        return x;
+    }
+}
+
+function f (x) { // 0
+    // 1
+    if (x > 0 && x < 5) {
+        switch (x) {
+            case 0:
+            case 1:
+                return 0;
+            default:
+                return x;
+        }
+    } /*a*/ else /*b*/ { // c
+        // d
+        return x; // e
+        // f
+    } // g
+    // 2
+} // 3
diff --git a/crates/biome_js_analyze/tests/specs/nursery/noUselessElse/invalid.js.snap b/crates/biome_js_analyze/tests/specs/nursery/noUselessElse/invalid.js.snap
new file mode 100644
index 000000000000..b200db6a85e9
--- /dev/null
+++ b/crates/biome_js_analyze/tests/specs/nursery/noUselessElse/invalid.js.snap
@@ -0,0 +1,501 @@
+---
+source: crates/biome_js_analyze/tests/spec_tests.rs
+expression: invalid.js
+---
+# Input
+```js
+function f (x) {
+    if (x < 0) {
+        throw new RangeError();
+    } else {
+        return x;
+    }
+}
+
+function f (x) {
+    f();
+    if (x < 0) {
+        throw new RangeError();
+    } else {
+        g();
+    }
+    h();
+}
+
+function f (x) {
+    if (x < 0) {
+        throw new RangeError();
+    } else return x;
+}
+
+function f (x) {
+    if (x < 0)
+        throw new RangeError();
+    else
+        return x;
+}
+
+function f (x) {
+    if (x < 0) {
+        throw new RangeError();
+    } else if (x === 0) {
+        return 1;
+    } else {
+        return x;
+    }
+}
+
+function f (x) {
+    while (true) {
+        if (x < 0) {
+            break;
+        } else {
+            x -= g(x)
+        }
+    }
+    return x;
+}
+
+function f (x) {
+    while (true) {
+        if (x < 0) {
+            break;
+        } else {
+            x -= g(x)
+        }
+    }
+    return x;
+}
+
+function f (x) {
+    if (x > 0 && x < 5) {
+        switch (x) {
+            case 0:
+            case 1:
+                return 0;
+            default:
+                return x;
+        }
+    } else {
+        return x;
+    }
+}
+
+function f (x) { // 0
+    // 1
+    if (x > 0 && x < 5) {
+        switch (x) {
+            case 0:
+            case 1:
+                return 0;
+            default:
+                return x;
+        }
+    } /*a*/ else /*b*/ { // c
+        // d
+        return x; // e
+        // f
+    } // g
+    // 2
+} // 3
+
+```
+
+# Diagnostics
+```
+invalid.js:4:7 lint/nursery/noUselessElse  FIXABLE  ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+
+  ! This else clause can be omitted.
+  
+    2 │     if (x < 0) {
+    3 │         throw new RangeError();
+  > 4 │     } else {
+      │       ^^^^^^
+  > 5 │         return x;
+  > 6 │     }
+      │     ^
+    7 │ }
+    8 │ 
+  
+  i This if statement uses an early breaking statement.
+  
+    1 │ function f (x) {
+  > 2 │     if (x < 0) {
+      │     ^^^^^^^^^^^^
+  > 3 │         throw new RangeError();
+  > 4 │     } else {
+  > 5 │         return x;
+  > 6 │     }
+      │     ^
+    7 │ }
+    8 │ 
+  
+  i Suggested fix: Omit the else clause.
+  
+     2  2 │       if (x < 0) {
+     3  3 │           throw new RangeError();
+     4    │ - ····}·else·{
+        4 │ + ····}
+     5  5 │           return x;
+     6    │ - ····}
+     7  6 │   }
+     8  7 │   
+  
+
+```
+
+```
+invalid.js:13:7 lint/nursery/noUselessElse  FIXABLE  ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+
+  ! This else clause can be omitted.
+  
+    11 │     if (x < 0) {
+    12 │         throw new RangeError();
+  > 13 │     } else {
+       │       ^^^^^^
+  > 14 │         g();
+  > 15 │     }
+       │     ^
+    16 │     h();
+    17 │ }
+  
+  i This if statement uses an early breaking statement.
+  
+     9 │ function f (x) {
+    10 │     f();
+  > 11 │     if (x < 0) {
+       │     ^^^^^^^^^^^^
+  > 12 │         throw new RangeError();
+  > 13 │     } else {
+  > 14 │         g();
+  > 15 │     }
+       │     ^
+    16 │     h();
+    17 │ }
+  
+  i Suggested fix: Omit the else clause.
+  
+    11 11 │       if (x < 0) {
+    12 12 │           throw new RangeError();
+    13    │ - ····}·else·{
+       13 │ + ····}
+    14 14 │           g();
+    15    │ - ····}
+    16 15 │       h();
+    17 16 │   }
+  
+
+```
+
+```
+invalid.js:22:7 lint/nursery/noUselessElse  FIXABLE  ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+
+  ! This else clause can be omitted.
+  
+    20 │     if (x < 0) {
+    21 │         throw new RangeError();
+  > 22 │     } else return x;
+       │       ^^^^^^^^^^^^^^
+    23 │ }
+    24 │ 
+  
+  i This if statement uses an early breaking statement.
+  
+    19 │ function f (x) {
+  > 20 │     if (x < 0) {
+       │     ^^^^^^^^^^^^
+  > 21 │         throw new RangeError();
+  > 22 │     } else return x;
+       │     ^^^^^^^^^^^^^^^^
+    23 │ }
+    24 │ 
+  
+  i Suggested fix: Omit the else clause.
+  
+    22 │ ····}·else·return·x;
+       │      ------         
+
+```
+
+```
+invalid.js:28:5 lint/nursery/noUselessElse  FIXABLE  ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+
+  ! This else clause can be omitted.
+  
+    26 │     if (x < 0)
+    27 │         throw new RangeError();
+  > 28 │     else
+       │     ^^^^
+  > 29 │         return x;
+       │         ^^^^^^^^^
+    30 │ }
+    31 │ 
+  
+  i This if statement uses an early breaking statement.
+  
+    25 │ function f (x) {
+  > 26 │     if (x < 0)
+       │     ^^^^^^^^^^
+  > 27 │         throw new RangeError();
+  > 28 │     else
+  > 29 │         return x;
+       │         ^^^^^^^^^
+    30 │ }
+    31 │ 
+  
+  i Suggested fix: Omit the else clause.
+  
+    26 26 │       if (x < 0)
+    27 27 │           throw new RangeError();
+    28    │ - ····else
+    29 28 │           return x;
+    30 29 │   }
+  
+
+```
+
+```
+invalid.js:35:7 lint/nursery/noUselessElse  FIXABLE  ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+
+  ! This else clause can be omitted.
+  
+    33 │     if (x < 0) {
+    34 │         throw new RangeError();
+  > 35 │     } else if (x === 0) {
+       │       ^^^^^^^^^^^^^^^^^^^
+  > 36 │         return 1;
+  > 37 │     } else {
+  > 38 │         return x;
+  > 39 │     }
+       │     ^
+    40 │ }
+    41 │ 
+  
+  i This if statement uses an early breaking statement.
+  
+    32 │ function f (x) {
+  > 33 │     if (x < 0) {
+       │     ^^^^^^^^^^^^
+  > 34 │         throw new RangeError();
+        ...
+  > 38 │         return x;
+  > 39 │     }
+       │     ^
+    40 │ }
+    41 │ 
+  
+  i Suggested fix: Omit the else clause.
+  
+    35 │ ····}·else·if·(x·===·0)·{
+       │      ------              
+
+```
+
+```
+invalid.js:37:7 lint/nursery/noUselessElse ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+
+  ! This else clause can be omitted.
+  
+    35 │     } else if (x === 0) {
+    36 │         return 1;
+  > 37 │     } else {
+       │       ^^^^^^
+  > 38 │         return x;
+  > 39 │     }
+       │     ^
+    40 │ }
+    41 │ 
+  
+  i This if statement uses an early breaking statement.
+  
+    33 │     if (x < 0) {
+    34 │         throw new RangeError();
+  > 35 │     } else if (x === 0) {
+       │            ^^^^^^^^^^^^^^
+  > 36 │         return 1;
+  > 37 │     } else {
+  > 38 │         return x;
+  > 39 │     }
+       │     ^
+    40 │ }
+    41 │ 
+  
+
+```
+
+```
+invalid.js:46:11 lint/nursery/noUselessElse  FIXABLE  ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+
+  ! This else clause can be omitted.
+  
+    44 │         if (x < 0) {
+    45 │             break;
+  > 46 │         } else {
+       │           ^^^^^^
+  > 47 │             x -= g(x)
+  > 48 │         }
+       │         ^
+    49 │     }
+    50 │     return x;
+  
+  i This if statement uses an early breaking statement.
+  
+    42 │ function f (x) {
+    43 │     while (true) {
+  > 44 │         if (x < 0) {
+       │         ^^^^^^^^^^^^
+  > 45 │             break;
+  > 46 │         } else {
+  > 47 │             x -= g(x)
+  > 48 │         }
+       │         ^
+    49 │     }
+    50 │     return x;
+  
+  i Suggested fix: Omit the else clause.
+  
+    44 44 │           if (x < 0) {
+    45 45 │               break;
+    46    │ - ········}·else·{
+       46 │ + ········}
+    47 47 │               x -= g(x)
+    48    │ - ········}
+    49 48 │       }
+    50 49 │       return x;
+  
+
+```
+
+```
+invalid.js:57:11 lint/nursery/noUselessElse  FIXABLE  ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+
+  ! This else clause can be omitted.
+  
+    55 │         if (x < 0) {
+    56 │             break;
+  > 57 │         } else {
+       │           ^^^^^^
+  > 58 │             x -= g(x)
+  > 59 │         }
+       │         ^
+    60 │     }
+    61 │     return x;
+  
+  i This if statement uses an early breaking statement.
+  
+    53 │ function f (x) {
+    54 │     while (true) {
+  > 55 │         if (x < 0) {
+       │         ^^^^^^^^^^^^
+  > 56 │             break;
+  > 57 │         } else {
+  > 58 │             x -= g(x)
+  > 59 │         }
+       │         ^
+    60 │     }
+    61 │     return x;
+  
+  i Suggested fix: Omit the else clause.
+  
+    55 55 │           if (x < 0) {
+    56 56 │               break;
+    57    │ - ········}·else·{
+       57 │ + ········}
+    58 58 │               x -= g(x)
+    59    │ - ········}
+    60 59 │       }
+    61 60 │       return x;
+  
+
+```
+
+```
+invalid.js:73:7 lint/nursery/noUselessElse  FIXABLE  ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+
+  ! This else clause can be omitted.
+  
+    71 │                 return x;
+    72 │         }
+  > 73 │     } else {
+       │       ^^^^^^
+  > 74 │         return x;
+  > 75 │     }
+       │     ^
+    76 │ }
+    77 │ 
+  
+  i This if statement uses an early breaking statement.
+  
+    64 │ function f (x) {
+  > 65 │     if (x > 0 && x < 5) {
+       │     ^^^^^^^^^^^^^^^^^^^^^
+  > 66 │         switch (x) {
+        ...
+  > 74 │         return x;
+  > 75 │     }
+       │     ^
+    76 │ }
+    77 │ 
+  
+  i Suggested fix: Omit the else clause.
+  
+    71 71 │                   return x;
+    72 72 │           }
+    73    │ - ····}·else·{
+       73 │ + ····}
+    74 74 │           return x;
+    75    │ - ····}
+    76 75 │   }
+    77 76 │   
+  
+
+```
+
+```
+invalid.js:88:13 lint/nursery/noUselessElse  FIXABLE  ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+
+  ! This else clause can be omitted.
+  
+    86 │                 return x;
+    87 │         }
+  > 88 │     } /*a*/ else /*b*/ { // c
+       │             ^^^^^^^^^^^^^^^^^
+  > 89 │         // d
+  > 90 │         return x; // e
+  > 91 │         // f
+  > 92 │     } // g
+       │     ^
+    93 │     // 2
+    94 │ } // 3
+  
+  i This if statement uses an early breaking statement.
+  
+    78 │ function f (x) { // 0
+    79 │     // 1
+  > 80 │     if (x > 0 && x < 5) {
+       │     ^^^^^^^^^^^^^^^^^^^^^
+  > 81 │         switch (x) {
+        ...
+  > 91 │         // f
+  > 92 │     } // g
+       │     ^
+    93 │     // 2
+    94 │ } // 3
+  
+  i Suggested fix: Omit the else clause.
+  
+    86 86 │                   return x;
+    87 87 │           }
+    88    │ - ····}·/*a*/·else·/*b*/·{·//·c
+       88 │ + ····}
+    89 89 │           // d
+    90 90 │           return x; // e
+    91    │ - ········//·f
+    92    │ - ····}·//·g
+    93 91 │       // 2
+    94 92 │   } // 3
+  
+
+```
+
+
diff --git a/crates/biome_js_analyze/tests/specs/nursery/noUselessElse/missed.js b/crates/biome_js_analyze/tests/specs/nursery/noUselessElse/missed.js
new file mode 100644
index 000000000000..d1094a14f586
--- /dev/null
+++ b/crates/biome_js_analyze/tests/specs/nursery/noUselessElse/missed.js
@@ -0,0 +1,53 @@
+// Examples which are not reported by the rule (false negative cases)
+
+function f (x) {
+    if (x < 0) {
+        while(true) {
+            return 0;
+        }
+    } else {
+        return x;
+    }
+}
+
+function f (x) {
+    if (x > 0 && x < 5) {
+        switch (x) {
+            case 0:
+                f(); // fallthough
+            default:
+                return 0;
+        }
+    } else {
+        return x;
+    }
+}
+
+
+function f (x) {
+    if (x > 0 && x < 5) {
+        try {
+            g(x);
+        } catch {
+            g(0);
+        } finally {
+            return 0;
+        }
+    } else {
+        return x;
+    }
+}
+
+
+function f (x) {
+    if (x > 0 && x < 5) {
+        try {
+            g(x);
+            return x;
+        } catch {
+            return 0;
+        }
+    } else {
+        return x;
+    }
+}
diff --git a/crates/biome_js_analyze/tests/specs/nursery/noUselessElse/missed.js.snap b/crates/biome_js_analyze/tests/specs/nursery/noUselessElse/missed.js.snap
new file mode 100644
index 000000000000..815b49732e11
--- /dev/null
+++ b/crates/biome_js_analyze/tests/specs/nursery/noUselessElse/missed.js.snap
@@ -0,0 +1,63 @@
+---
+source: crates/biome_js_analyze/tests/spec_tests.rs
+expression: missed.js
+---
+# Input
+```js
+// Examples which are not reported by the rule (false negative cases)
+
+function f (x) {
+    if (x < 0) {
+        while(true) {
+            return 0;
+        }
+    } else {
+        return x;
+    }
+}
+
+function f (x) {
+    if (x > 0 && x < 5) {
+        switch (x) {
+            case 0:
+                f(); // fallthough
+            default:
+                return 0;
+        }
+    } else {
+        return x;
+    }
+}
+
+
+function f (x) {
+    if (x > 0 && x < 5) {
+        try {
+            g(x);
+        } catch {
+            g(0);
+        } finally {
+            return 0;
+        }
+    } else {
+        return x;
+    }
+}
+
+
+function f (x) {
+    if (x > 0 && x < 5) {
+        try {
+            g(x);
+            return x;
+        } catch {
+            return 0;
+        }
+    } else {
+        return x;
+    }
+}
+
+```
+
+
diff --git a/crates/biome_js_analyze/tests/specs/nursery/noUselessElse/valid.js b/crates/biome_js_analyze/tests/specs/nursery/noUselessElse/valid.js
new file mode 100644
index 000000000000..18527e4e9e76
--- /dev/null
+++ b/crates/biome_js_analyze/tests/specs/nursery/noUselessElse/valid.js
@@ -0,0 +1,77 @@
+function f(x) {
+    if (x < 0) {
+
+    } else {
+        return 0
+    }
+}
+
+function f(x) {
+    if (x < 0) {
+        g();
+    } else {
+        return 0
+    }
+}
+
+function f(x) {
+    if (x < 0) {
+        g();
+        if(x === -1) {
+            return 1;
+        }
+    } else {
+        return 0
+    }
+}
+
+function f(x) {
+    if (x < 0) {
+        throw new RangeError();
+    }
+    return x;
+}
+
+function f(x) {
+    if (x < 0) {
+        while(x < 0) {
+            x += f(x);
+        }
+    } else {
+        return 0
+    }
+}
+
+function f (x) {
+    if (x > 0 && x < 5) {
+        switch (x) {}
+    } else {
+        return x;
+    }
+}
+
+function f (x) {
+    if (x > 0 && x < 5) {
+        switch (x) {
+            case 0:
+            case 1:
+                return 0;
+            default:
+        }
+    } else {
+        return x;
+    }
+}
+
+function f (x) {
+    if (x > 0 && x < 5) {
+        switch (x) {
+            case 0:
+                break;
+            default:
+                break;
+        }
+    } else {
+        return x;
+    }
+}
diff --git a/crates/biome_js_analyze/tests/specs/nursery/noUselessElse/valid.js.snap b/crates/biome_js_analyze/tests/specs/nursery/noUselessElse/valid.js.snap
new file mode 100644
index 000000000000..7ac1f81620a4
--- /dev/null
+++ b/crates/biome_js_analyze/tests/specs/nursery/noUselessElse/valid.js.snap
@@ -0,0 +1,87 @@
+---
+source: crates/biome_js_analyze/tests/spec_tests.rs
+expression: valid.js
+---
+# Input
+```js
+function f(x) {
+    if (x < 0) {
+
+    } else {
+        return 0
+    }
+}
+
+function f(x) {
+    if (x < 0) {
+        g();
+    } else {
+        return 0
+    }
+}
+
+function f(x) {
+    if (x < 0) {
+        g();
+        if(x === -1) {
+            return 1;
+        }
+    } else {
+        return 0
+    }
+}
+
+function f(x) {
+    if (x < 0) {
+        throw new RangeError();
+    }
+    return x;
+}
+
+function f(x) {
+    if (x < 0) {
+        while(x < 0) {
+            x += f(x);
+        }
+    } else {
+        return 0
+    }
+}
+
+function f (x) {
+    if (x > 0 && x < 5) {
+        switch (x) {}
+    } else {
+        return x;
+    }
+}
+
+function f (x) {
+    if (x > 0 && x < 5) {
+        switch (x) {
+            case 0:
+            case 1:
+                return 0;
+            default:
+        }
+    } else {
+        return x;
+    }
+}
+
+function f (x) {
+    if (x > 0 && x < 5) {
+        switch (x) {
+            case 0:
+                break;
+            default:
+                break;
+        }
+    } else {
+        return x;
+    }
+}
+
+```
+
+
diff --git a/crates/biome_rowan/src/ast/mod.rs b/crates/biome_rowan/src/ast/mod.rs
index 04375935095f..6def1b4306bc 100644
--- a/crates/biome_rowan/src/ast/mod.rs
+++ b/crates/biome_rowan/src/ast/mod.rs
@@ -292,7 +292,7 @@ pub trait AstNode: Clone {
     }
 
     /// Return a new version of this node without leading and trailing newlines and whitespaces.
-    fn trim(self) -> Option<Self> {
+    fn trim_trivia(self) -> Option<Self> {
         Self::cast(
             self.into_syntax()
                 .trim_leading_trivia()?
@@ -301,12 +301,12 @@ pub trait AstNode: Clone {
     }
 
     /// Return a new version of this node without leading newlines and whitespaces.
-    fn trim_start(self) -> Option<Self> {
+    fn trim_leading_trivia(self) -> Option<Self> {
         Self::cast(self.into_syntax().trim_leading_trivia()?)
     }
 
     /// Return a new version of this node without trailing newlines and whitespaces.
-    fn trim_end(self) -> Option<Self> {
+    fn trim_trailing_trivia(self) -> Option<Self> {
         Self::cast(self.into_syntax().trim_trailing_trivia()?)
     }
 }
diff --git a/crates/biome_service/src/configuration/linter/rules.rs b/crates/biome_service/src/configuration/linter/rules.rs
index b2137b9a56c7..8478a53a25d7 100644
--- a/crates/biome_service/src/configuration/linter/rules.rs
+++ b/crates/biome_service/src/configuration/linter/rules.rs
@@ -2210,6 +2210,10 @@ pub struct Nursery {
     )]
     #[serde(skip_serializing_if = "Option::is_none")]
     pub no_misleading_instantiator: Option<RuleConfiguration>,
+    #[doc = "Disallow else block when the if block breaks early."]
+    #[bpaf(long("no-useless-else"), argument("on|off|warn"), optional, hide)]
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub no_useless_else: Option<RuleConfiguration>,
     #[doc = "Disallow the use of void operators, which is not a familiar operator."]
     #[bpaf(long("no-void"), argument("on|off|warn"), optional, hide)]
     #[serde(skip_serializing_if = "Option::is_none")]
@@ -2260,7 +2264,7 @@ pub struct Nursery {
 }
 impl Nursery {
     const GROUP_NAME: &'static str = "nursery";
-    pub(crate) const GROUP_RULES: [&'static str; 16] = [
+    pub(crate) const GROUP_RULES: [&'static str; 17] = [
         "noAccumulatingSpread",
         "noConfusingVoidType",
         "noDuplicateJsonKeys",
@@ -2269,6 +2273,7 @@ impl Nursery {
         "noGlobalIsFinite",
         "noGlobalIsNan",
         "noMisleadingInstantiator",
+        "noUselessElse",
         "noVoid",
         "useArrowFunction",
         "useCollapsedElseIf",
@@ -2278,27 +2283,29 @@ impl Nursery {
         "useImportRestrictions",
         "useIsArray",
     ];
-    const RECOMMENDED_RULES: [&'static str; 8] = [
+    const RECOMMENDED_RULES: [&'static str; 9] = [
         "noDuplicateJsonKeys",
         "noGlobalIsFinite",
         "noGlobalIsNan",
         "noMisleadingInstantiator",
+        "noUselessElse",
         "useArrowFunction",
         "useExhaustiveDependencies",
         "useGroupedTypeImport",
         "useIsArray",
     ];
-    const RECOMMENDED_RULES_AS_FILTERS: [RuleFilter<'static>; 8] = [
+    const RECOMMENDED_RULES_AS_FILTERS: [RuleFilter<'static>; 9] = [
         RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[2]),
         RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[5]),
         RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[6]),
         RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[7]),
-        RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[9]),
-        RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[11]),
+        RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[8]),
+        RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[10]),
         RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[12]),
-        RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[15]),
+        RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[13]),
+        RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[16]),
     ];
-    const ALL_RULES_AS_FILTERS: [RuleFilter<'static>; 16] = [
+    const ALL_RULES_AS_FILTERS: [RuleFilter<'static>; 17] = [
         RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[0]),
         RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[1]),
         RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[2]),
@@ -2315,6 +2322,7 @@ impl Nursery {
         RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[13]),
         RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[14]),
         RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[15]),
+        RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[16]),
     ];
     #[doc = r" Retrieves the recommended rules"]
     pub(crate) fn is_recommended(&self) -> bool {
@@ -2371,46 +2379,51 @@ impl Nursery {
                 index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[7]));
             }
         }
-        if let Some(rule) = self.no_void.as_ref() {
+        if let Some(rule) = self.no_useless_else.as_ref() {
             if rule.is_enabled() {
                 index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[8]));
             }
         }
-        if let Some(rule) = self.use_arrow_function.as_ref() {
+        if let Some(rule) = self.no_void.as_ref() {
             if rule.is_enabled() {
                 index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[9]));
             }
         }
-        if let Some(rule) = self.use_collapsed_else_if.as_ref() {
+        if let Some(rule) = self.use_arrow_function.as_ref() {
             if rule.is_enabled() {
                 index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[10]));
             }
         }
-        if let Some(rule) = self.use_exhaustive_dependencies.as_ref() {
+        if let Some(rule) = self.use_collapsed_else_if.as_ref() {
             if rule.is_enabled() {
                 index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[11]));
             }
         }
-        if let Some(rule) = self.use_grouped_type_import.as_ref() {
+        if let Some(rule) = self.use_exhaustive_dependencies.as_ref() {
             if rule.is_enabled() {
                 index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[12]));
             }
         }
-        if let Some(rule) = self.use_hook_at_top_level.as_ref() {
+        if let Some(rule) = self.use_grouped_type_import.as_ref() {
             if rule.is_enabled() {
                 index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[13]));
             }
         }
-        if let Some(rule) = self.use_import_restrictions.as_ref() {
+        if let Some(rule) = self.use_hook_at_top_level.as_ref() {
             if rule.is_enabled() {
                 index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[14]));
             }
         }
-        if let Some(rule) = self.use_is_array.as_ref() {
+        if let Some(rule) = self.use_import_restrictions.as_ref() {
             if rule.is_enabled() {
                 index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[15]));
             }
         }
+        if let Some(rule) = self.use_is_array.as_ref() {
+            if rule.is_enabled() {
+                index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[16]));
+            }
+        }
         index_set
     }
     pub(crate) fn get_disabled_rules(&self) -> IndexSet<RuleFilter> {
@@ -2455,46 +2468,51 @@ impl Nursery {
                 index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[7]));
             }
         }
-        if let Some(rule) = self.no_void.as_ref() {
+        if let Some(rule) = self.no_useless_else.as_ref() {
             if rule.is_disabled() {
                 index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[8]));
             }
         }
-        if let Some(rule) = self.use_arrow_function.as_ref() {
+        if let Some(rule) = self.no_void.as_ref() {
             if rule.is_disabled() {
                 index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[9]));
             }
         }
-        if let Some(rule) = self.use_collapsed_else_if.as_ref() {
+        if let Some(rule) = self.use_arrow_function.as_ref() {
             if rule.is_disabled() {
                 index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[10]));
             }
         }
-        if let Some(rule) = self.use_exhaustive_dependencies.as_ref() {
+        if let Some(rule) = self.use_collapsed_else_if.as_ref() {
             if rule.is_disabled() {
                 index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[11]));
             }
         }
-        if let Some(rule) = self.use_grouped_type_import.as_ref() {
+        if let Some(rule) = self.use_exhaustive_dependencies.as_ref() {
             if rule.is_disabled() {
                 index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[12]));
             }
         }
-        if let Some(rule) = self.use_hook_at_top_level.as_ref() {
+        if let Some(rule) = self.use_grouped_type_import.as_ref() {
             if rule.is_disabled() {
                 index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[13]));
             }
         }
-        if let Some(rule) = self.use_import_restrictions.as_ref() {
+        if let Some(rule) = self.use_hook_at_top_level.as_ref() {
             if rule.is_disabled() {
                 index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[14]));
             }
         }
-        if let Some(rule) = self.use_is_array.as_ref() {
+        if let Some(rule) = self.use_import_restrictions.as_ref() {
             if rule.is_disabled() {
                 index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[15]));
             }
         }
+        if let Some(rule) = self.use_is_array.as_ref() {
+            if rule.is_disabled() {
+                index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[16]));
+            }
+        }
         index_set
     }
     #[doc = r" Checks if, given a rule name, matches one of the rules contained in this category"]
@@ -2505,10 +2523,10 @@ impl Nursery {
     pub(crate) fn is_recommended_rule(rule_name: &str) -> bool {
         Self::RECOMMENDED_RULES.contains(&rule_name)
     }
-    pub(crate) fn recommended_rules_as_filters() -> [RuleFilter<'static>; 8] {
+    pub(crate) fn recommended_rules_as_filters() -> [RuleFilter<'static>; 9] {
         Self::RECOMMENDED_RULES_AS_FILTERS
     }
-    pub(crate) fn all_rules_as_filters() -> [RuleFilter<'static>; 16] {
+    pub(crate) fn all_rules_as_filters() -> [RuleFilter<'static>; 17] {
         Self::ALL_RULES_AS_FILTERS
     }
     #[doc = r" Select preset rules"]
@@ -2539,6 +2557,7 @@ impl Nursery {
             "noGlobalIsFinite" => self.no_global_is_finite.as_ref(),
             "noGlobalIsNan" => self.no_global_is_nan.as_ref(),
             "noMisleadingInstantiator" => self.no_misleading_instantiator.as_ref(),
+            "noUselessElse" => self.no_useless_else.as_ref(),
             "noVoid" => self.no_void.as_ref(),
             "useArrowFunction" => self.use_arrow_function.as_ref(),
             "useCollapsedElseIf" => self.use_collapsed_else_if.as_ref(),
diff --git a/crates/biome_service/src/configuration/parse/json/rules.rs b/crates/biome_service/src/configuration/parse/json/rules.rs
index 202831289098..1aaa62af3c0a 100644
--- a/crates/biome_service/src/configuration/parse/json/rules.rs
+++ b/crates/biome_service/src/configuration/parse/json/rules.rs
@@ -2031,6 +2031,7 @@ impl VisitNode<JsonLanguage> for Nursery {
                 "noGlobalIsFinite",
                 "noGlobalIsNan",
                 "noMisleadingInstantiator",
+                "noUselessElse",
                 "noVoid",
                 "useArrowFunction",
                 "useCollapsedElseIf",
@@ -2242,6 +2243,29 @@ impl VisitNode<JsonLanguage> for Nursery {
                     ));
                 }
             },
+            "noUselessElse" => match value {
+                AnyJsonValue::JsonStringValue(_) => {
+                    let mut configuration = RuleConfiguration::default();
+                    self.map_to_known_string(&value, name_text, &mut configuration, diagnostics)?;
+                    self.no_useless_else = Some(configuration);
+                }
+                AnyJsonValue::JsonObjectValue(_) => {
+                    let mut rule_configuration = RuleConfiguration::default();
+                    rule_configuration.map_rule_configuration(
+                        &value,
+                        name_text,
+                        "noUselessElse",
+                        diagnostics,
+                    )?;
+                    self.no_useless_else = Some(rule_configuration);
+                }
+                _ => {
+                    diagnostics.push(DeserializationDiagnostic::new_incorrect_type(
+                        "object or string",
+                        value.range(),
+                    ));
+                }
+            },
             "noVoid" => match value {
                 AnyJsonValue::JsonStringValue(_) => {
                     let mut configuration = RuleConfiguration::default();
diff --git a/editors/vscode/configuration_schema.json b/editors/vscode/configuration_schema.json
index 20edce3c8c2e..7aaa71cac330 100644
--- a/editors/vscode/configuration_schema.json
+++ b/editors/vscode/configuration_schema.json
@@ -1019,6 +1019,13 @@
 						{ "type": "null" }
 					]
 				},
+				"noUselessElse": {
+					"description": "Disallow else block when the if block breaks early.",
+					"anyOf": [
+						{ "$ref": "#/definitions/RuleConfiguration" },
+						{ "type": "null" }
+					]
+				},
 				"noVoid": {
 					"description": "Disallow the use of void operators, which is not a familiar operator.",
 					"anyOf": [
diff --git a/editors/vscode/src/main.ts b/editors/vscode/src/main.ts
index 5623c0530c88..ef86f698b103 100644
--- a/editors/vscode/src/main.ts
+++ b/editors/vscode/src/main.ts
@@ -227,16 +227,15 @@ async function getServerPath(
 async function getWorkspaceRelativePath(path: string) {
 	if (isAbsolute(path)) {
 		return path;
-	} else {
-		for (let i = 0; i < workspace.workspaceFolders.length; i++) {
-			const workspaceFolder = workspace.workspaceFolders[i];
-			const possiblePath = Uri.joinPath(workspaceFolder.uri, path);
-			if (await fileExists(possiblePath)) {
-				return possiblePath.fsPath;
-			}
+	}
+	for (let i = 0; i < workspace.workspaceFolders.length; i++) {
+		const workspaceFolder = workspace.workspaceFolders[i];
+		const possiblePath = Uri.joinPath(workspaceFolder.uri, path);
+		if (await fileExists(possiblePath)) {
+			return possiblePath.fsPath;
 		}
-		return undefined;
 	}
+	return undefined;
 }
 
 // Tries to resolve a path to `@biomejs/cli-*` binary package from the root of the workspace
@@ -298,9 +297,8 @@ async function fileExists(path: Uri) {
 	} catch (err) {
 		if (err.code === "ENOENT" || err.code === "FileNotFound") {
 			return false;
-		} else {
-			throw err;
 		}
+		throw err;
 	}
 }
 
@@ -390,10 +388,9 @@ async function getSocket(
 		}
 
 		throw new Error(message);
-	} else {
-		outputChannel.appendLine(`Connecting to "${pipeName}" ...`);
-		return pipeName;
 	}
+	outputChannel.appendLine(`Connecting to "${pipeName}" ...`);
+	return pipeName;
 }
 
 function wrapConnectionError(err: Error, path: string): Error {
diff --git a/packages/@biomejs/backend-jsonrpc/src/workspace.ts b/packages/@biomejs/backend-jsonrpc/src/workspace.ts
index b20c7f2b2dda..dfcf6152b6db 100644
--- a/packages/@biomejs/backend-jsonrpc/src/workspace.ts
+++ b/packages/@biomejs/backend-jsonrpc/src/workspace.ts
@@ -681,6 +681,10 @@ export interface Nursery {
 	 * Enforce proper usage of new and constructor.
 	 */
 	noMisleadingInstantiator?: RuleConfiguration;
+	/**
+	 * Disallow else block when the if block breaks early.
+	 */
+	noUselessElse?: RuleConfiguration;
 	/**
 	 * Disallow the use of void operators, which is not a familiar operator.
 	 */
@@ -1206,7 +1210,6 @@ export type Category =
 	| "lint/a11y/noSvgWithoutTitle"
 	| "lint/a11y/useAltText"
 	| "lint/a11y/useAnchorContent"
-	| "lint/a11y/useValidAriaValues"
 	| "lint/a11y/useAriaPropsForRole"
 	| "lint/a11y/useButtonType"
 	| "lint/a11y/useHeadingContent"
@@ -1217,6 +1220,7 @@ export type Category =
 	| "lint/a11y/useMediaCaption"
 	| "lint/a11y/useValidAnchor"
 	| "lint/a11y/useValidAriaProps"
+	| "lint/a11y/useValidAriaValues"
 	| "lint/a11y/useValidLang"
 	| "lint/complexity/noBannedTypes"
 	| "lint/complexity/noExtraBooleanCast"
@@ -1275,6 +1279,7 @@ export type Category =
 	| "lint/nursery/noGlobalIsFinite"
 	| "lint/nursery/noGlobalIsNan"
 	| "lint/nursery/noMisleadingInstantiator"
+	| "lint/nursery/noUselessElse"
 	| "lint/nursery/noVoid"
 	| "lint/nursery/useArrowFunction"
 	| "lint/nursery/useBiomeSuppressionComment"
diff --git a/packages/@biomejs/biome/configuration_schema.json b/packages/@biomejs/biome/configuration_schema.json
index 20edce3c8c2e..7aaa71cac330 100644
--- a/packages/@biomejs/biome/configuration_schema.json
+++ b/packages/@biomejs/biome/configuration_schema.json
@@ -1019,6 +1019,13 @@
 						{ "type": "null" }
 					]
 				},
+				"noUselessElse": {
+					"description": "Disallow else block when the if block breaks early.",
+					"anyOf": [
+						{ "$ref": "#/definitions/RuleConfiguration" },
+						{ "type": "null" }
+					]
+				},
 				"noVoid": {
 					"description": "Disallow the use of void operators, which is not a familiar operator.",
 					"anyOf": [
diff --git a/website/astro.config.ts b/website/astro.config.ts
index d9edbe29f7f0..73bfc8a7dea3 100644
--- a/website/astro.config.ts
+++ b/website/astro.config.ts
@@ -14,9 +14,8 @@ import remarkToc from "remark-toc";
 function resolveFile(relative: string, parent: string, root: string): string {
 	if (relative[0] === "/") {
 		return `${root}${relative}`;
-	} else {
-		return path.resolve(path.join(parent, relative));
 	}
+	return path.resolve(path.join(parent, relative));
 }
 
 const IMPORT_REGEX = /^import"(.*?)";?$/;
diff --git a/website/src/components/generated/NumberOfRules.astro b/website/src/components/generated/NumberOfRules.astro
index 79ddcc67dde3..162acf325203 100644
--- a/website/src/components/generated/NumberOfRules.astro
+++ b/website/src/components/generated/NumberOfRules.astro
@@ -1,2 +1,2 @@
 <!-- this file is auto generated, use `cargo lintdoc` to update it -->
- <p>Biome's linter has a total of <strong><a href='/linter/rules'>158 rules</a></strong><p>
\ No newline at end of file
+ <p>Biome's linter has a total of <strong><a href='/linter/rules'>159 rules</a></strong><p>
\ No newline at end of file
diff --git a/website/src/content/docs/internals/changelog.mdx b/website/src/content/docs/internals/changelog.mdx
index 363f0e7f683b..d4a9c7bc58c6 100644
--- a/website/src/content/docs/internals/changelog.mdx
+++ b/website/src/content/docs/internals/changelog.mdx
@@ -42,6 +42,10 @@ Read our [guidelines for writing a good changelog entry](https://github.com/biom
 
 - Add [noMisleadingInstantiator](https://biomejs.dev/linter/rules/no-mileading-instantiator) rule. The rule reports the misleading use of the `new` and `constructor` methods. Contributed by @unvalley
 
+- Add [noUselessElse](https://biomejs.dev/linter/rules/no-useless-else) rule.
+  The rule reports `else` clauses that can be omitted because their `if` branches break.
+  Contributed by @Conaclos
+
 #### Bug fixes
 
 - Fix [#294](https://github.com/biomejs/biome/issues/294). [noConfusingVoidType](https://biomejs.dev/linter/rules/no-confusing-void-type/) no longer reports false positives for return types. Contributed by @b4s36t4
diff --git a/website/src/content/docs/linter/rules/index.mdx b/website/src/content/docs/linter/rules/index.mdx
index cc1be0146d13..0e836335c99e 100644
--- a/website/src/content/docs/linter/rules/index.mdx
+++ b/website/src/content/docs/linter/rules/index.mdx
@@ -354,6 +354,8 @@ Use <code>Number.isFinite</code> instead of global <code>isFinite</code>.
 Use <code>Number.isNaN</code> instead of global <code>isNaN</code>.
 ### [noMisleadingInstantiator](/linter/rules/no-misleading-instantiator)
 Enforce proper usage of <code>new</code> and <code>constructor</code>.
+### [noUselessElse](/linter/rules/no-useless-else)
+Disallow <code>else</code> block when the <code>if</code> block breaks early.
 ### [noVoid](/linter/rules/no-void)
 Disallow the use of <code>void</code> operators, which is not a familiar operator.
 ### [useArrowFunction](/linter/rules/use-arrow-function)
diff --git a/website/src/content/docs/linter/rules/no-useless-else.md b/website/src/content/docs/linter/rules/no-useless-else.md
new file mode 100644
index 000000000000..b1b04873873d
--- /dev/null
+++ b/website/src/content/docs/linter/rules/no-useless-else.md
@@ -0,0 +1,185 @@
+---
+title: noUselessElse (since vnext)
+---
+
+**Diagnostic Category: `lint/nursery/noUselessElse`**
+
+:::caution
+This rule is part of the [nursery](/linter/rules/#nursery) group.
+:::
+
+Disallow `else` block when the `if` block breaks early.
+
+If an `if` block breaks early using a breaking statement (`return`, `break`, `continue`, or `throw`),
+then the `else` block becomes useless.
+Its contents can be placed outside of the block.
+
+## Examples
+
+### Invalid
+
+```jsx
+while (x > 0) {
+    if (f(x)) {
+        break;
+    } else {
+        x++
+    }
+}
+```
+
+<pre class="language-text"><code class="language-text">nursery/noUselessElse.js:4:7 <a href="https://biomejs.dev/lint/rules/no-useless-else">lint/nursery/noUselessElse</a> <span style="color: #000; background-color: #ddd;"> FIXABLE </span> ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+
+<strong><span style="color: Tomato;">  </span></strong><strong><span style="color: Tomato;">✖</span></strong> <span style="color: Tomato;">This </span><span style="color: Tomato;"><strong>else</strong></span><span style="color: Tomato;"> clause can be omitted.</span>
+  
+    <strong>2 │ </strong>    if (f(x)) {
+    <strong>3 │ </strong>        break;
+<strong><span style="color: Tomato;">  </span></strong><strong><span style="color: Tomato;">&gt;</span></strong> <strong>4 │ </strong>    } else {
+   <strong>   │ </strong>      <strong><span style="color: Tomato;">^</span></strong><strong><span style="color: Tomato;">^</span></strong><strong><span style="color: Tomato;">^</span></strong><strong><span style="color: Tomato;">^</span></strong><strong><span style="color: Tomato;">^</span></strong><strong><span style="color: Tomato;">^</span></strong>
+<strong><span style="color: Tomato;">  </span></strong><strong><span style="color: Tomato;">&gt;</span></strong> <strong>5 │ </strong>        x++
+<strong><span style="color: Tomato;">  </span></strong><strong><span style="color: Tomato;">&gt;</span></strong> <strong>6 │ </strong>    }
+   <strong>   │ </strong>    <strong><span style="color: Tomato;">^</span></strong>
+    <strong>7 │ </strong>}
+    <strong>8 │ </strong>
+  
+<strong><span style="color: rgb(38, 148, 255);">  </span></strong><strong><span style="color: rgb(38, 148, 255);">ℹ</span></strong> <span style="color: rgb(38, 148, 255);">This </span><span style="color: rgb(38, 148, 255);"><strong>if</strong></span><span style="color: rgb(38, 148, 255);"> statement uses an early breaking statement.</span>
+  
+    <strong>1 │ </strong>while (x &gt; 0) {
+<strong><span style="color: Tomato;">  </span></strong><strong><span style="color: Tomato;">&gt;</span></strong> <strong>2 │ </strong>    if (f(x)) {
+   <strong>   │ </strong>    <strong><span style="color: Tomato;">^</span></strong><strong><span style="color: Tomato;">^</span></strong><strong><span style="color: Tomato;">^</span></strong><strong><span style="color: Tomato;">^</span></strong><strong><span style="color: Tomato;">^</span></strong><strong><span style="color: Tomato;">^</span></strong><strong><span style="color: Tomato;">^</span></strong><strong><span style="color: Tomato;">^</span></strong><strong><span style="color: Tomato;">^</span></strong><strong><span style="color: Tomato;">^</span></strong><strong><span style="color: Tomato;">^</span></strong>
+<strong><span style="color: Tomato;">  </span></strong><strong><span style="color: Tomato;">&gt;</span></strong> <strong>3 │ </strong>        break;
+<strong><span style="color: Tomato;">  </span></strong><strong><span style="color: Tomato;">&gt;</span></strong> <strong>4 │ </strong>    } else {
+<strong><span style="color: Tomato;">  </span></strong><strong><span style="color: Tomato;">&gt;</span></strong> <strong>5 │ </strong>        x++
+<strong><span style="color: Tomato;">  </span></strong><strong><span style="color: Tomato;">&gt;</span></strong> <strong>6 │ </strong>    }
+   <strong>   │ </strong>    <strong><span style="color: Tomato;">^</span></strong>
+    <strong>7 │ </strong>}
+    <strong>8 │ </strong>
+  
+<strong><span style="color: rgb(38, 148, 255);">  </span></strong><strong><span style="color: rgb(38, 148, 255);">ℹ</span></strong> <span style="color: rgb(38, 148, 255);">Suggested fix</span><span style="color: rgb(38, 148, 255);">: </span><span style="color: rgb(38, 148, 255);">Omit the </span><span style="color: rgb(38, 148, 255);"><strong>else</strong></span><span style="color: rgb(38, 148, 255);"> clause.</span>
+  
+    <strong>2</strong> <strong>2</strong><strong> │ </strong>      if (f(x)) {
+    <strong>3</strong> <strong>3</strong><strong> │ </strong>          break;
+    <strong>4</strong>  <strong> │ </strong><span style="color: Tomato;">-</span> <span style="color: Tomato;"><span style="opacity: 0.8;">·</span></span><span style="color: Tomato;"><span style="opacity: 0.8;">·</span></span><span style="color: Tomato;"><span style="opacity: 0.8;">·</span></span><span style="color: Tomato;"><span style="opacity: 0.8;">·</span></span><span style="color: Tomato;">}</span><span style="color: Tomato;"><span style="opacity: 0.8;"><strong>·</strong></span></span><span style="color: Tomato;"><strong>e</strong></span><span style="color: Tomato;"><strong>l</strong></span><span style="color: Tomato;"><strong>s</strong></span><span style="color: Tomato;"><strong>e</strong></span><span style="color: Tomato;"><span style="opacity: 0.8;"><strong>·</strong></span></span><span style="color: Tomato;"><strong>{</strong></span>
+      <strong>4</strong><strong> │ </strong><span style="color: MediumSeaGreen;">+</span> <span style="color: MediumSeaGreen;"><span style="opacity: 0.8;">·</span></span><span style="color: MediumSeaGreen;"><span style="opacity: 0.8;">·</span></span><span style="color: MediumSeaGreen;"><span style="opacity: 0.8;">·</span></span><span style="color: MediumSeaGreen;"><span style="opacity: 0.8;">·</span></span><span style="color: MediumSeaGreen;">}</span>
+    <strong>5</strong> <strong>5</strong><strong> │ </strong>          x++
+    <strong>6</strong>  <strong> │ </strong><span style="color: Tomato;">-</span> <span style="color: Tomato;"><span style="opacity: 0.8;"><strong>·</strong></span></span><span style="color: Tomato;"><span style="opacity: 0.8;"><strong>·</strong></span></span><span style="color: Tomato;"><span style="opacity: 0.8;"><strong>·</strong></span></span><span style="color: Tomato;"><span style="opacity: 0.8;"><strong>·</strong></span></span><span style="color: Tomato;"><strong>}</strong></span>
+    <strong>7</strong> <strong>6</strong><strong> │ </strong>  }
+    <strong>8</strong> <strong>7</strong><strong> │ </strong>  
+  
+</code></pre>
+
+```jsx
+function f() {
+    if (x < 0) {
+        return 0;
+    } else {
+        return x;
+    }
+}
+```
+
+<pre class="language-text"><code class="language-text">nursery/noUselessElse.js:4:7 <a href="https://biomejs.dev/lint/rules/no-useless-else">lint/nursery/noUselessElse</a> <span style="color: #000; background-color: #ddd;"> FIXABLE </span> ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+
+<strong><span style="color: Tomato;">  </span></strong><strong><span style="color: Tomato;">✖</span></strong> <span style="color: Tomato;">This </span><span style="color: Tomato;"><strong>else</strong></span><span style="color: Tomato;"> clause can be omitted.</span>
+  
+    <strong>2 │ </strong>    if (x &lt; 0) {
+    <strong>3 │ </strong>        return 0;
+<strong><span style="color: Tomato;">  </span></strong><strong><span style="color: Tomato;">&gt;</span></strong> <strong>4 │ </strong>    } else {
+   <strong>   │ </strong>      <strong><span style="color: Tomato;">^</span></strong><strong><span style="color: Tomato;">^</span></strong><strong><span style="color: Tomato;">^</span></strong><strong><span style="color: Tomato;">^</span></strong><strong><span style="color: Tomato;">^</span></strong><strong><span style="color: Tomato;">^</span></strong>
+<strong><span style="color: Tomato;">  </span></strong><strong><span style="color: Tomato;">&gt;</span></strong> <strong>5 │ </strong>        return x;
+<strong><span style="color: Tomato;">  </span></strong><strong><span style="color: Tomato;">&gt;</span></strong> <strong>6 │ </strong>    }
+   <strong>   │ </strong>    <strong><span style="color: Tomato;">^</span></strong>
+    <strong>7 │ </strong>}
+    <strong>8 │ </strong>
+  
+<strong><span style="color: rgb(38, 148, 255);">  </span></strong><strong><span style="color: rgb(38, 148, 255);">ℹ</span></strong> <span style="color: rgb(38, 148, 255);">This </span><span style="color: rgb(38, 148, 255);"><strong>if</strong></span><span style="color: rgb(38, 148, 255);"> statement uses an early breaking statement.</span>
+  
+    <strong>1 │ </strong>function f() {
+<strong><span style="color: Tomato;">  </span></strong><strong><span style="color: Tomato;">&gt;</span></strong> <strong>2 │ </strong>    if (x &lt; 0) {
+   <strong>   │ </strong>    <strong><span style="color: Tomato;">^</span></strong><strong><span style="color: Tomato;">^</span></strong><strong><span style="color: Tomato;">^</span></strong><strong><span style="color: Tomato;">^</span></strong><strong><span style="color: Tomato;">^</span></strong><strong><span style="color: Tomato;">^</span></strong><strong><span style="color: Tomato;">^</span></strong><strong><span style="color: Tomato;">^</span></strong><strong><span style="color: Tomato;">^</span></strong><strong><span style="color: Tomato;">^</span></strong><strong><span style="color: Tomato;">^</span></strong><strong><span style="color: Tomato;">^</span></strong>
+<strong><span style="color: Tomato;">  </span></strong><strong><span style="color: Tomato;">&gt;</span></strong> <strong>3 │ </strong>        return 0;
+<strong><span style="color: Tomato;">  </span></strong><strong><span style="color: Tomato;">&gt;</span></strong> <strong>4 │ </strong>    } else {
+<strong><span style="color: Tomato;">  </span></strong><strong><span style="color: Tomato;">&gt;</span></strong> <strong>5 │ </strong>        return x;
+<strong><span style="color: Tomato;">  </span></strong><strong><span style="color: Tomato;">&gt;</span></strong> <strong>6 │ </strong>    }
+   <strong>   │ </strong>    <strong><span style="color: Tomato;">^</span></strong>
+    <strong>7 │ </strong>}
+    <strong>8 │ </strong>
+  
+<strong><span style="color: rgb(38, 148, 255);">  </span></strong><strong><span style="color: rgb(38, 148, 255);">ℹ</span></strong> <span style="color: rgb(38, 148, 255);">Suggested fix</span><span style="color: rgb(38, 148, 255);">: </span><span style="color: rgb(38, 148, 255);">Omit the </span><span style="color: rgb(38, 148, 255);"><strong>else</strong></span><span style="color: rgb(38, 148, 255);"> clause.</span>
+  
+    <strong>2</strong> <strong>2</strong><strong> │ </strong>      if (x &lt; 0) {
+    <strong>3</strong> <strong>3</strong><strong> │ </strong>          return 0;
+    <strong>4</strong>  <strong> │ </strong><span style="color: Tomato;">-</span> <span style="color: Tomato;"><span style="opacity: 0.8;">·</span></span><span style="color: Tomato;"><span style="opacity: 0.8;">·</span></span><span style="color: Tomato;"><span style="opacity: 0.8;">·</span></span><span style="color: Tomato;"><span style="opacity: 0.8;">·</span></span><span style="color: Tomato;">}</span><span style="color: Tomato;"><span style="opacity: 0.8;"><strong>·</strong></span></span><span style="color: Tomato;"><strong>e</strong></span><span style="color: Tomato;"><strong>l</strong></span><span style="color: Tomato;"><strong>s</strong></span><span style="color: Tomato;"><strong>e</strong></span><span style="color: Tomato;"><span style="opacity: 0.8;"><strong>·</strong></span></span><span style="color: Tomato;"><strong>{</strong></span>
+      <strong>4</strong><strong> │ </strong><span style="color: MediumSeaGreen;">+</span> <span style="color: MediumSeaGreen;"><span style="opacity: 0.8;">·</span></span><span style="color: MediumSeaGreen;"><span style="opacity: 0.8;">·</span></span><span style="color: MediumSeaGreen;"><span style="opacity: 0.8;">·</span></span><span style="color: MediumSeaGreen;"><span style="opacity: 0.8;">·</span></span><span style="color: MediumSeaGreen;">}</span>
+    <strong>5</strong> <strong>5</strong><strong> │ </strong>          return x;
+    <strong>6</strong>  <strong> │ </strong><span style="color: Tomato;">-</span> <span style="color: Tomato;"><span style="opacity: 0.8;"><strong>·</strong></span></span><span style="color: Tomato;"><span style="opacity: 0.8;"><strong>·</strong></span></span><span style="color: Tomato;"><span style="opacity: 0.8;"><strong>·</strong></span></span><span style="color: Tomato;"><span style="opacity: 0.8;"><strong>·</strong></span></span><span style="color: Tomato;"><strong>}</strong></span>
+    <strong>7</strong> <strong>6</strong><strong> │ </strong>  }
+    <strong>8</strong> <strong>7</strong><strong> │ </strong>  
+  
+</code></pre>
+
+```jsx
+function f() {
+    if (x < 0) {
+        throw new RangeError();
+    } else {
+        return x;
+    }
+}
+```
+
+<pre class="language-text"><code class="language-text">nursery/noUselessElse.js:4:7 <a href="https://biomejs.dev/lint/rules/no-useless-else">lint/nursery/noUselessElse</a> <span style="color: #000; background-color: #ddd;"> FIXABLE </span> ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+
+<strong><span style="color: Tomato;">  </span></strong><strong><span style="color: Tomato;">✖</span></strong> <span style="color: Tomato;">This </span><span style="color: Tomato;"><strong>else</strong></span><span style="color: Tomato;"> clause can be omitted.</span>
+  
+    <strong>2 │ </strong>    if (x &lt; 0) {
+    <strong>3 │ </strong>        throw new RangeError();
+<strong><span style="color: Tomato;">  </span></strong><strong><span style="color: Tomato;">&gt;</span></strong> <strong>4 │ </strong>    } else {
+   <strong>   │ </strong>      <strong><span style="color: Tomato;">^</span></strong><strong><span style="color: Tomato;">^</span></strong><strong><span style="color: Tomato;">^</span></strong><strong><span style="color: Tomato;">^</span></strong><strong><span style="color: Tomato;">^</span></strong><strong><span style="color: Tomato;">^</span></strong>
+<strong><span style="color: Tomato;">  </span></strong><strong><span style="color: Tomato;">&gt;</span></strong> <strong>5 │ </strong>        return x;
+<strong><span style="color: Tomato;">  </span></strong><strong><span style="color: Tomato;">&gt;</span></strong> <strong>6 │ </strong>    }
+   <strong>   │ </strong>    <strong><span style="color: Tomato;">^</span></strong>
+    <strong>7 │ </strong>}
+    <strong>8 │ </strong>
+  
+<strong><span style="color: rgb(38, 148, 255);">  </span></strong><strong><span style="color: rgb(38, 148, 255);">ℹ</span></strong> <span style="color: rgb(38, 148, 255);">This </span><span style="color: rgb(38, 148, 255);"><strong>if</strong></span><span style="color: rgb(38, 148, 255);"> statement uses an early breaking statement.</span>
+  
+    <strong>1 │ </strong>function f() {
+<strong><span style="color: Tomato;">  </span></strong><strong><span style="color: Tomato;">&gt;</span></strong> <strong>2 │ </strong>    if (x &lt; 0) {
+   <strong>   │ </strong>    <strong><span style="color: Tomato;">^</span></strong><strong><span style="color: Tomato;">^</span></strong><strong><span style="color: Tomato;">^</span></strong><strong><span style="color: Tomato;">^</span></strong><strong><span style="color: Tomato;">^</span></strong><strong><span style="color: Tomato;">^</span></strong><strong><span style="color: Tomato;">^</span></strong><strong><span style="color: Tomato;">^</span></strong><strong><span style="color: Tomato;">^</span></strong><strong><span style="color: Tomato;">^</span></strong><strong><span style="color: Tomato;">^</span></strong><strong><span style="color: Tomato;">^</span></strong>
+<strong><span style="color: Tomato;">  </span></strong><strong><span style="color: Tomato;">&gt;</span></strong> <strong>3 │ </strong>        throw new RangeError();
+<strong><span style="color: Tomato;">  </span></strong><strong><span style="color: Tomato;">&gt;</span></strong> <strong>4 │ </strong>    } else {
+<strong><span style="color: Tomato;">  </span></strong><strong><span style="color: Tomato;">&gt;</span></strong> <strong>5 │ </strong>        return x;
+<strong><span style="color: Tomato;">  </span></strong><strong><span style="color: Tomato;">&gt;</span></strong> <strong>6 │ </strong>    }
+   <strong>   │ </strong>    <strong><span style="color: Tomato;">^</span></strong>
+    <strong>7 │ </strong>}
+    <strong>8 │ </strong>
+  
+<strong><span style="color: rgb(38, 148, 255);">  </span></strong><strong><span style="color: rgb(38, 148, 255);">ℹ</span></strong> <span style="color: rgb(38, 148, 255);">Suggested fix</span><span style="color: rgb(38, 148, 255);">: </span><span style="color: rgb(38, 148, 255);">Omit the </span><span style="color: rgb(38, 148, 255);"><strong>else</strong></span><span style="color: rgb(38, 148, 255);"> clause.</span>
+  
+    <strong>2</strong> <strong>2</strong><strong> │ </strong>      if (x &lt; 0) {
+    <strong>3</strong> <strong>3</strong><strong> │ </strong>          throw new RangeError();
+    <strong>4</strong>  <strong> │ </strong><span style="color: Tomato;">-</span> <span style="color: Tomato;"><span style="opacity: 0.8;">·</span></span><span style="color: Tomato;"><span style="opacity: 0.8;">·</span></span><span style="color: Tomato;"><span style="opacity: 0.8;">·</span></span><span style="color: Tomato;"><span style="opacity: 0.8;">·</span></span><span style="color: Tomato;">}</span><span style="color: Tomato;"><span style="opacity: 0.8;"><strong>·</strong></span></span><span style="color: Tomato;"><strong>e</strong></span><span style="color: Tomato;"><strong>l</strong></span><span style="color: Tomato;"><strong>s</strong></span><span style="color: Tomato;"><strong>e</strong></span><span style="color: Tomato;"><span style="opacity: 0.8;"><strong>·</strong></span></span><span style="color: Tomato;"><strong>{</strong></span>
+      <strong>4</strong><strong> │ </strong><span style="color: MediumSeaGreen;">+</span> <span style="color: MediumSeaGreen;"><span style="opacity: 0.8;">·</span></span><span style="color: MediumSeaGreen;"><span style="opacity: 0.8;">·</span></span><span style="color: MediumSeaGreen;"><span style="opacity: 0.8;">·</span></span><span style="color: MediumSeaGreen;"><span style="opacity: 0.8;">·</span></span><span style="color: MediumSeaGreen;">}</span>
+    <strong>5</strong> <strong>5</strong><strong> │ </strong>          return x;
+    <strong>6</strong>  <strong> │ </strong><span style="color: Tomato;">-</span> <span style="color: Tomato;"><span style="opacity: 0.8;"><strong>·</strong></span></span><span style="color: Tomato;"><span style="opacity: 0.8;"><strong>·</strong></span></span><span style="color: Tomato;"><span style="opacity: 0.8;"><strong>·</strong></span></span><span style="color: Tomato;"><span style="opacity: 0.8;"><strong>·</strong></span></span><span style="color: Tomato;"><strong>}</strong></span>
+    <strong>7</strong> <strong>6</strong><strong> │ </strong>  }
+    <strong>8</strong> <strong>7</strong><strong> │ </strong>  
+  
+</code></pre>
+
+## Valid
+
+```jsx
+function f() {
+    if (x < 0) {
+        return 0;
+    }
+    return x;
+}
+```
+
+## Related links
+
+- [Disable a rule](/linter/#disable-a-lint-rule)
+- [Rule options](/linter/#rule-options)
diff --git a/website/src/frontend-scripts/toc.ts b/website/src/frontend-scripts/toc.ts
index d85c102b423f..e658991cbbed 100644
--- a/website/src/frontend-scripts/toc.ts
+++ b/website/src/frontend-scripts/toc.ts
@@ -205,10 +205,9 @@ class Manager {
 		if (this.lastActiveHeading !== undefined) {
 			if (this.isVisibleHeading(this.lastActiveHeading)) {
 				return;
-			} else {
-				this.toggleActiveHeading(this.lastActiveHeading, false);
-				this.lastActiveHeading = undefined;
 			}
+			this.toggleActiveHeading(this.lastActiveHeading, false);
+			this.lastActiveHeading = undefined;
 		}
 
 		this.ensureCalculatedHeadings();
diff --git a/website/src/playground/Playground.tsx b/website/src/playground/Playground.tsx
index 933a80b2b0c2..21219f5084f7 100644
--- a/website/src/playground/Playground.tsx
+++ b/website/src/playground/Playground.tsx
@@ -51,14 +51,13 @@ export default function PlaygroundLoader({
 	const codeMirrorExtensions = useMemo(() => {
 		if (isJsonFilename(playgroundState.currentFile)) {
 			return [json()];
-		} else {
-			return [
-				javascript({
-					jsx: isJsxFilename(playgroundState.currentFile),
-					typescript: isTypeScriptFilename(playgroundState.currentFile),
-				}),
-			];
 		}
+		return [
+			javascript({
+				jsx: isJsxFilename(playgroundState.currentFile),
+				typescript: isTypeScriptFilename(playgroundState.currentFile),
+			}),
+		];
 	}, [playgroundState.currentFile]);
 
 	const biomeAstSyntacticDataRef = useRef<BiomeAstSyntacticData | null>(null);
diff --git a/website/src/playground/tabs/FormatterCodeTab.tsx b/website/src/playground/tabs/FormatterCodeTab.tsx
index 0f1a97559734..2feb652bd340 100644
--- a/website/src/playground/tabs/FormatterCodeTab.tsx
+++ b/website/src/playground/tabs/FormatterCodeTab.tsx
@@ -18,7 +18,8 @@ function removeWhitespace(str: string): string {
 function calculateHint(a: string, b: string): string | JSX.Element {
 	if (a === b) {
 		return <strong>Exact match</strong>;
-	} else if (removeWhitespace(a) === removeWhitespace(b)) {
+	}
+	if (removeWhitespace(a) === removeWhitespace(b)) {
 		return <strong>Only whitespace differences</strong>;
 	}
 
diff --git a/website/src/playground/utils.ts b/website/src/playground/utils.ts
index bb071d469de7..275e5b6b808e 100644
--- a/website/src/playground/utils.ts
+++ b/website/src/playground/utils.ts
@@ -84,11 +84,10 @@ export function createLocalStorage(name: string): {
 		},
 		getNumber: () => {
 			const elem = localStorage.getItem(key);
-			if (elem == null) {
-				return undefined;
-			} else {
+			if (elem != null) {
 				return Number(elem);
 			}
+			return undefined;
 		},
 		getBoolean: () => {
 			return localStorage.getItem(key) === "true";
diff --git a/website/src/playground/workers/prettierWorker.ts b/website/src/playground/workers/prettierWorker.ts
index 038556366fd6..c63af9795216 100644
--- a/website/src/playground/workers/prettierWorker.ts
+++ b/website/src/playground/workers/prettierWorker.ts
@@ -125,21 +125,20 @@ async function formatWithPrettier(
 				type: "ERROR",
 				stack: err.message,
 			};
-		} else {
-			return {
-				type: "ERROR",
-				stack: (err as Error).stack ?? "",
-			};
 		}
+		return {
+			type: "ERROR",
+			stack: (err as Error).stack ?? "",
+		};
 	}
 }
 
 function getPrettierParser(filename: string): string {
 	if (isTypeScriptFilename(filename)) {
 		return "babel-ts";
-	} else if (isJsonFilename(filename)) {
+	}
+	if (isJsonFilename(filename)) {
 		return "json5";
-	} else {
-		return "babel";
 	}
+	return "babel";
 }