Repository: biomejs/biome (language: rust)

Issue:
📎 Implement `useCollapsedIf` - `clippy/collapsible_if`, `unicorn/no-lonely-if`
### Description

Implement [clippy/collapsible_if](https://rust-lang.github.io/rust-clippy/master/#/collapsible_if) and [unicorn/no-lonely-if](https://github.com/sindresorhus/eslint-plugin-unicorn/blob/main/docs/rules/no-lonely-if.md).

**Want to contribute?** Lets you know you are interested! We will assign you to the issue to prevent several people to work on the same issue. Don't worry, we can unassign you later if you are no longer interested in the issue! Read our [contributing guide](https://github.com/biomejs/biome/blob/main/CONTRIBUTING.md) and [analyzer contributing guide](https://github.com/biomejs/biome/blob/main/crates/biome_analyze/CONTRIBUTING.md).

The implementer could take some inspirations from the implementation of existing rules such as `useCollapsedElseIf`.


Discussion / hints:

Fix patch:
diff --git a/crates/biome_cli/src/execute/migrate/eslint_any_rule_to_biome.rs b/crates/biome_cli/src/execute/migrate/eslint_any_rule_to_biome.rs
index 439041656c45..b5c5dd51781f 100644
--- a/crates/biome_cli/src/execute/migrate/eslint_any_rule_to_biome.rs
+++ b/crates/biome_cli/src/execute/migrate/eslint_any_rule_to_biome.rs
@@ -1543,6 +1543,14 @@ pub(crate) fn migrate_eslint_any_rule(
             let rule = group.use_is_array.get_or_insert(Default::default());
             rule.set_level(rule_severity.into());
         }
+        "unicorn/no-lonely-if" => {
+            if !options.include_nursery {
+                return false;
+            }
+            let group = rules.nursery.get_or_insert_with(Default::default);
+            let rule = group.use_collapsed_if.get_or_insert(Default::default());
+            rule.set_level(rule_severity.into());
+        }
         "unicorn/no-static-only-class" => {
             let group = rules.complexity.get_or_insert_with(Default::default);
             let rule = group.no_static_only_class.get_or_insert(Default::default());
diff --git a/crates/biome_configuration/src/analyzer/linter/rules.rs b/crates/biome_configuration/src/analyzer/linter/rules.rs
index f57c2e14820a..76de7ff26e0a 100644
--- a/crates/biome_configuration/src/analyzer/linter/rules.rs
+++ b/crates/biome_configuration/src/analyzer/linter/rules.rs
@@ -3395,6 +3395,9 @@ pub struct Nursery {
     #[doc = "Use at() instead of integer index access."]
     #[serde(skip_serializing_if = "Option::is_none")]
     pub use_at_index: Option<RuleFixConfiguration<biome_js_analyze::options::UseAtIndex>>,
+    #[doc = "Enforce using single if instead of nested if clauses."]
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub use_collapsed_if: Option<RuleFixConfiguration<biome_js_analyze::options::UseCollapsedIf>>,
     #[doc = "Enforce declaring components only within modules that export React Components exclusively."]
     #[serde(skip_serializing_if = "Option::is_none")]
     pub use_component_export_only_modules:
@@ -3488,6 +3491,7 @@ impl Nursery {
         "useAdjacentOverloadSignatures",
         "useAriaPropsSupportedByRole",
         "useAtIndex",
+        "useCollapsedIf",
         "useComponentExportOnlyModules",
         "useConsistentCurlyBraces",
         "useConsistentMemberAccessibility",
@@ -3528,9 +3532,9 @@ impl Nursery {
         RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[27]),
         RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[28]),
         RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[32]),
-        RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[36]),
         RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[37]),
-        RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[42]),
+        RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[38]),
+        RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[43]),
     ];
     const ALL_RULES_AS_FILTERS: &'static [RuleFilter<'static>] = &[
         RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[0]),
@@ -3578,6 +3582,7 @@ impl Nursery {
         RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[42]),
         RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[43]),
         RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[44]),
+        RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[45]),
     ];
     #[doc = r" Retrieves the recommended rules"]
     pub(crate) fn is_recommended_true(&self) -> bool {
@@ -3764,61 +3769,66 @@ impl Nursery {
                 index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[33]));
             }
         }
-        if let Some(rule) = self.use_component_export_only_modules.as_ref() {
+        if let Some(rule) = self.use_collapsed_if.as_ref() {
             if rule.is_enabled() {
                 index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[34]));
             }
         }
-        if let Some(rule) = self.use_consistent_curly_braces.as_ref() {
+        if let Some(rule) = self.use_component_export_only_modules.as_ref() {
             if rule.is_enabled() {
                 index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[35]));
             }
         }
-        if let Some(rule) = self.use_consistent_member_accessibility.as_ref() {
+        if let Some(rule) = self.use_consistent_curly_braces.as_ref() {
             if rule.is_enabled() {
                 index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[36]));
             }
         }
-        if let Some(rule) = self.use_deprecated_reason.as_ref() {
+        if let Some(rule) = self.use_consistent_member_accessibility.as_ref() {
             if rule.is_enabled() {
                 index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[37]));
             }
         }
-        if let Some(rule) = self.use_explicit_type.as_ref() {
+        if let Some(rule) = self.use_deprecated_reason.as_ref() {
             if rule.is_enabled() {
                 index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[38]));
             }
         }
-        if let Some(rule) = self.use_guard_for_in.as_ref() {
+        if let Some(rule) = self.use_explicit_type.as_ref() {
             if rule.is_enabled() {
                 index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[39]));
             }
         }
-        if let Some(rule) = self.use_import_restrictions.as_ref() {
+        if let Some(rule) = self.use_guard_for_in.as_ref() {
             if rule.is_enabled() {
                 index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[40]));
             }
         }
-        if let Some(rule) = self.use_sorted_classes.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[41]));
             }
         }
-        if let Some(rule) = self.use_strict_mode.as_ref() {
+        if let Some(rule) = self.use_sorted_classes.as_ref() {
             if rule.is_enabled() {
                 index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[42]));
             }
         }
-        if let Some(rule) = self.use_trim_start_end.as_ref() {
+        if let Some(rule) = self.use_strict_mode.as_ref() {
             if rule.is_enabled() {
                 index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[43]));
             }
         }
-        if let Some(rule) = self.use_valid_autocomplete.as_ref() {
+        if let Some(rule) = self.use_trim_start_end.as_ref() {
             if rule.is_enabled() {
                 index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[44]));
             }
         }
+        if let Some(rule) = self.use_valid_autocomplete.as_ref() {
+            if rule.is_enabled() {
+                index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[45]));
+            }
+        }
         index_set
     }
     pub(crate) fn get_disabled_rules(&self) -> FxHashSet<RuleFilter<'static>> {
@@ -3993,61 +4003,66 @@ impl Nursery {
                 index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[33]));
             }
         }
-        if let Some(rule) = self.use_component_export_only_modules.as_ref() {
+        if let Some(rule) = self.use_collapsed_if.as_ref() {
             if rule.is_disabled() {
                 index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[34]));
             }
         }
-        if let Some(rule) = self.use_consistent_curly_braces.as_ref() {
+        if let Some(rule) = self.use_component_export_only_modules.as_ref() {
             if rule.is_disabled() {
                 index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[35]));
             }
         }
-        if let Some(rule) = self.use_consistent_member_accessibility.as_ref() {
+        if let Some(rule) = self.use_consistent_curly_braces.as_ref() {
             if rule.is_disabled() {
                 index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[36]));
             }
         }
-        if let Some(rule) = self.use_deprecated_reason.as_ref() {
+        if let Some(rule) = self.use_consistent_member_accessibility.as_ref() {
             if rule.is_disabled() {
                 index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[37]));
             }
         }
-        if let Some(rule) = self.use_explicit_type.as_ref() {
+        if let Some(rule) = self.use_deprecated_reason.as_ref() {
             if rule.is_disabled() {
                 index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[38]));
             }
         }
-        if let Some(rule) = self.use_guard_for_in.as_ref() {
+        if let Some(rule) = self.use_explicit_type.as_ref() {
             if rule.is_disabled() {
                 index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[39]));
             }
         }
-        if let Some(rule) = self.use_import_restrictions.as_ref() {
+        if let Some(rule) = self.use_guard_for_in.as_ref() {
             if rule.is_disabled() {
                 index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[40]));
             }
         }
-        if let Some(rule) = self.use_sorted_classes.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[41]));
             }
         }
-        if let Some(rule) = self.use_strict_mode.as_ref() {
+        if let Some(rule) = self.use_sorted_classes.as_ref() {
             if rule.is_disabled() {
                 index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[42]));
             }
         }
-        if let Some(rule) = self.use_trim_start_end.as_ref() {
+        if let Some(rule) = self.use_strict_mode.as_ref() {
             if rule.is_disabled() {
                 index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[43]));
             }
         }
-        if let Some(rule) = self.use_valid_autocomplete.as_ref() {
+        if let Some(rule) = self.use_trim_start_end.as_ref() {
             if rule.is_disabled() {
                 index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[44]));
             }
         }
+        if let Some(rule) = self.use_valid_autocomplete.as_ref() {
+            if rule.is_disabled() {
+                index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[45]));
+            }
+        }
         index_set
     }
     #[doc = r" Checks if, given a rule name, matches one of the rules contained in this category"]
@@ -4220,6 +4235,10 @@ impl Nursery {
                 .use_at_index
                 .as_ref()
                 .map(|conf| (conf.level(), conf.get_options())),
+            "useCollapsedIf" => self
+                .use_collapsed_if
+                .as_ref()
+                .map(|conf| (conf.level(), conf.get_options())),
             "useComponentExportOnlyModules" => self
                 .use_component_export_only_modules
                 .as_ref()
diff --git a/crates/biome_diagnostics_categories/src/categories.rs b/crates/biome_diagnostics_categories/src/categories.rs
index 4d03ac67332e..7a5f6e5341dc 100644
--- a/crates/biome_diagnostics_categories/src/categories.rs
+++ b/crates/biome_diagnostics_categories/src/categories.rs
@@ -188,6 +188,7 @@ define_categories! {
     "lint/nursery/useAriaPropsSupportedByRole": "https://biomejs.dev/linter/rules/use-aria-props-supported-by-role",
     "lint/nursery/useAtIndex": "https://biomejs.dev/linter/rules/use-at-index",
     "lint/nursery/useBiomeSuppressionComment": "https://biomejs.dev/linter/rules/use-biome-suppression-comment",
+    "lint/nursery/useCollapsedIf": "https://biomejs.dev/linter/rules/use-collapsed-if",
     "lint/nursery/useComponentExportOnlyModules": "https://biomejs.dev/linter/rules/use-components-only-module",
     "lint/nursery/useConsistentCurlyBraces": "https://biomejs.dev/linter/rules/use-consistent-curly-braces",
     "lint/nursery/useConsistentMemberAccessibility": "https://biomejs.dev/linter/rules/use-consistent-member-accessibility",
diff --git a/crates/biome_js_analyze/src/lint/nursery.rs b/crates/biome_js_analyze/src/lint/nursery.rs
index 4a3341d01aca..4660799ace74 100644
--- a/crates/biome_js_analyze/src/lint/nursery.rs
+++ b/crates/biome_js_analyze/src/lint/nursery.rs
@@ -27,6 +27,7 @@ pub mod no_useless_string_raw;
 pub mod use_adjacent_overload_signatures;
 pub mod use_aria_props_supported_by_role;
 pub mod use_at_index;
+pub mod use_collapsed_if;
 pub mod use_component_export_only_modules;
 pub mod use_consistent_curly_braces;
 pub mod use_consistent_member_accessibility;
@@ -67,6 +68,7 @@ declare_lint_group! {
             self :: use_adjacent_overload_signatures :: UseAdjacentOverloadSignatures ,
             self :: use_aria_props_supported_by_role :: UseAriaPropsSupportedByRole ,
             self :: use_at_index :: UseAtIndex ,
+            self :: use_collapsed_if :: UseCollapsedIf ,
             self :: use_component_export_only_modules :: UseComponentExportOnlyModules ,
             self :: use_consistent_curly_braces :: UseConsistentCurlyBraces ,
             self :: use_consistent_member_accessibility :: UseConsistentMemberAccessibility ,
diff --git a/crates/biome_js_analyze/src/lint/nursery/use_collapsed_if.rs b/crates/biome_js_analyze/src/lint/nursery/use_collapsed_if.rs
new file mode 100644
index 000000000000..ced96dd5292e
--- /dev/null
+++ b/crates/biome_js_analyze/src/lint/nursery/use_collapsed_if.rs
@@ -0,0 +1,198 @@
+use biome_analyze::{
+    context::RuleContext, declare_lint_rule, ActionCategory, Ast, FixKind, Rule, RuleDiagnostic,
+    RuleSource,
+};
+use biome_console::markup;
+use biome_js_factory::make::{js_logical_expression, parenthesized, token};
+use biome_js_syntax::parentheses::NeedsParentheses;
+use biome_js_syntax::{AnyJsStatement, JsIfStatement, T};
+use biome_rowan::{AstNode, AstNodeList, BatchMutationExt};
+
+use crate::JsRuleAction;
+
+declare_lint_rule! {
+    /// Enforce using single `if` instead of nested `if` clauses.
+    ///
+    /// If an `if (b)` statement is the only statement in an `if (a)` block, it is often clearer to use an `if (a && b)` form.
+    ///
+    /// ## Examples
+    ///
+    /// ### Invalid
+    ///
+    /// ```js,expect_diagnostic
+    /// if (condition) {
+    ///     if (anotherCondition) {
+    ///         // ...
+    ///     }
+    /// }
+    /// ```
+    ///
+    /// ```js,expect_diagnostic
+    /// if (condition) {
+    ///     // Comment
+    ///     if (anotherCondition) {
+    ///         // ...
+    ///     }
+    /// }
+    /// ```
+    ///
+    /// ### Valid
+    ///
+    /// ```js
+    /// if (condition && anotherCondition) {
+    ///     // ...
+    /// }
+    /// ```
+    ///
+    /// ```js
+    /// if (condition) {
+    ///     if (anotherCondition) {
+    ///         // ...
+    ///     }
+    ///     doSomething();
+    /// }
+    /// ```
+    ///
+    /// ```js
+    /// if (condition) {
+    ///     if (anotherCondition) {
+    ///         // ...
+    ///     } else {
+    ///         // ...
+    ///     }
+    /// }
+    /// ```
+    ///
+    pub UseCollapsedIf {
+        version: "next",
+        name: "useCollapsedIf",
+        language: "js",
+        sources: &[
+            RuleSource::EslintUnicorn("no-lonely-if"),
+            RuleSource::Clippy("collapsible_if")
+        ],
+        recommended: false,
+        fix_kind: FixKind::Safe,
+    }
+}
+
+pub struct RuleState {
+    parent_if_statement: JsIfStatement,
+    child_if_statement: JsIfStatement,
+}
+
+impl Rule for UseCollapsedIf {
+    type Query = Ast<JsIfStatement>;
+    type State = RuleState;
+    type Signals = Option<Self::State>;
+    type Options = ();
+
+    fn run(ctx: &RuleContext<Self>) -> Self::Signals {
+        let if_stmt = ctx.query();
+        let consequent = if_stmt.consequent().ok()?;
+
+        let child_if_statement = match consequent {
+            // If `consequent` is a `JsBlockStatement` and the block contains only one
+            // `JsIfStatement`, the child `if` statement should be merged.
+            AnyJsStatement::JsBlockStatement(parent_block_statement) => {
+                let statements = parent_block_statement.statements();
+                if statements.len() != 1 {
+                    return None;
+                }
+
+                let AnyJsStatement::JsIfStatement(child_if_statement) = statements.first()? else {
+                    return None;
+                };
+
+                Some(child_if_statement)
+            }
+            // If `consequent` is a `JsIfStatement` without any block, it should be merged.
+            AnyJsStatement::JsIfStatement(child_if_statement) => Some(child_if_statement),
+            _ => None,
+        }?;
+
+        // It cannot be merged if the child `if` statement has any else clause(s).
+        if child_if_statement.else_clause().is_some() {
+            return None;
+        }
+
+        Some(RuleState {
+            parent_if_statement: if_stmt.clone(),
+            child_if_statement,
+        })
+    }
+
+    fn diagnostic(_ctx: &RuleContext<Self>, state: &Self::State) -> Option<RuleDiagnostic> {
+        Some(RuleDiagnostic::new(
+            rule_category!(),
+            state.child_if_statement.syntax().text_range(),
+            markup! {
+                "This "<Emphasis>"if"</Emphasis>" statement can be collapsed into another "<Emphasis>"if"</Emphasis>" statement."
+            },
+        ))
+    }
+
+    fn action(ctx: &RuleContext<Self>, state: &Self::State) -> Option<JsRuleAction> {
+        let RuleState {
+            parent_if_statement,
+            child_if_statement,
+        } = state;
+
+        let parent_consequent = parent_if_statement.consequent().ok()?;
+        let parent_test = parent_if_statement.test().ok()?;
+        let child_consequent = child_if_statement.consequent().ok()?;
+        let child_test = child_if_statement.test().ok()?;
+
+        let parent_has_comments = match &parent_consequent {
+            AnyJsStatement::JsBlockStatement(block_stmt) => {
+                block_stmt.l_curly_token().ok()?.has_trailing_comments()
+                    || block_stmt.r_curly_token().ok()?.has_leading_comments()
+            }
+            _ => false,
+        };
+
+        let has_comments = parent_has_comments
+            || child_if_statement.syntax().has_comments_direct()
+            || child_if_statement
+                .r_paren_token()
+                .ok()?
+                .has_trailing_comments();
+        if has_comments {
+            return None;
+        }
+
+        let mut expr =
+            js_logical_expression(parent_test.clone(), token(T![&&]), child_test.clone());
+
+        // Parenthesize arms of the `&&` expression if needed
+        let left = expr.left().ok()?;
+        if left.needs_parentheses() {
+            expr = expr.with_left(parenthesized(left).into());
+        }
+
+        let right = expr.right().ok()?;
+        if right.needs_parentheses() {
+            expr = expr.with_right(parenthesized(right).into());
+        }
+
+        // If the inner `if` statement has no block and the statement does not end with semicolon,
+        // it cannot be fixed automatically because that will break the ASI rule.
+        if !matches!(&child_consequent, AnyJsStatement::JsBlockStatement(_)) {
+            let last_token = child_consequent.syntax().last_token()?;
+            if last_token.kind() != T![;] {
+                return None;
+            }
+        }
+
+        let mut mutation = ctx.root().begin();
+        mutation.replace_node(parent_test, expr.into());
+        mutation.replace_node(parent_consequent, child_consequent);
+
+        Some(JsRuleAction::new(
+            ActionCategory::QuickFix,
+            ctx.metadata().applicability(),
+            markup! { "Use collapsed "<Emphasis>"if"</Emphasis>" instead." }.to_owned(),
+            mutation,
+        ))
+    }
+}
diff --git a/crates/biome_js_analyze/src/options.rs b/crates/biome_js_analyze/src/options.rs
index 93ab2a833591..fc0bd18366de 100644
--- a/crates/biome_js_analyze/src/options.rs
+++ b/crates/biome_js_analyze/src/options.rs
@@ -302,6 +302,8 @@ pub type UseButtonType =
     <lint::a11y::use_button_type::UseButtonType as biome_analyze::Rule>::Options;
 pub type UseCollapsedElseIf =
     <lint::style::use_collapsed_else_if::UseCollapsedElseIf as biome_analyze::Rule>::Options;
+pub type UseCollapsedIf =
+    <lint::nursery::use_collapsed_if::UseCollapsedIf as biome_analyze::Rule>::Options;
 pub type UseComponentExportOnlyModules = < lint :: nursery :: use_component_export_only_modules :: UseComponentExportOnlyModules as biome_analyze :: Rule > :: Options ;
 pub type UseConsistentArrayType = < lint :: style :: use_consistent_array_type :: UseConsistentArrayType as biome_analyze :: Rule > :: Options ;
 pub type UseConsistentBuiltinInstantiation = < lint :: style :: use_consistent_builtin_instantiation :: UseConsistentBuiltinInstantiation as biome_analyze :: Rule > :: Options ;
diff --git a/packages/@biomejs/backend-jsonrpc/src/workspace.ts b/packages/@biomejs/backend-jsonrpc/src/workspace.ts
index a38557656a37..a5030b84d2d0 100644
--- a/packages/@biomejs/backend-jsonrpc/src/workspace.ts
+++ b/packages/@biomejs/backend-jsonrpc/src/workspace.ts
@@ -1358,6 +1358,10 @@ export interface Nursery {
 	 * Use at() instead of integer index access.
 	 */
 	useAtIndex?: RuleFixConfiguration_for_Null;
+	/**
+	 * Enforce using single if instead of nested if clauses.
+	 */
+	useCollapsedIf?: RuleFixConfiguration_for_Null;
 	/**
 	 * Enforce declaring components only within modules that export React Components exclusively.
 	 */
@@ -2956,6 +2960,7 @@ export type Category =
 	| "lint/nursery/useAriaPropsSupportedByRole"
 	| "lint/nursery/useAtIndex"
 	| "lint/nursery/useBiomeSuppressionComment"
+	| "lint/nursery/useCollapsedIf"
 	| "lint/nursery/useComponentExportOnlyModules"
 	| "lint/nursery/useConsistentCurlyBraces"
 	| "lint/nursery/useConsistentMemberAccessibility"
diff --git a/packages/@biomejs/biome/configuration_schema.json b/packages/@biomejs/biome/configuration_schema.json
index 82f908c7d177..419e88edbaa9 100644
--- a/packages/@biomejs/biome/configuration_schema.json
+++ b/packages/@biomejs/biome/configuration_schema.json
@@ -2327,6 +2327,13 @@
 						{ "type": "null" }
 					]
 				},
+				"useCollapsedIf": {
+					"description": "Enforce using single if instead of nested if clauses.",
+					"anyOf": [
+						{ "$ref": "#/definitions/RuleFixConfiguration" },
+						{ "type": "null" }
+					]
+				},
 				"useComponentExportOnlyModules": {
 					"description": "Enforce declaring components only within modules that export React Components exclusively.",
 					"anyOf": [


Test patch:
diff --git a/crates/biome_js_analyze/tests/specs/nursery/useCollapsedIf/invalid.js b/crates/biome_js_analyze/tests/specs/nursery/useCollapsedIf/invalid.js
new file mode 100644
index 000000000000..055684561cb1
--- /dev/null
+++ b/crates/biome_js_analyze/tests/specs/nursery/useCollapsedIf/invalid.js
@@ -0,0 +1,156 @@
+/**
+ * Safe fixes:
+ */
+
+if (condition) {
+	if (anotherCondition) {
+		// ...
+	}
+}
+
+if (condition) {
+	if (anotherCondition) {
+		// ...
+	}
+} else {
+	// ...
+}
+
+if (condition) // Comment
+	if (anotherCondition)
+		doSomething();
+
+// Inner one is `JsBlockStatement`
+if (condition) if (anotherCondition) {
+	// ...
+}
+
+// Outer one is `JsBlockStatement`
+if (condition) {
+	if (anotherCondition) doSomething();
+}
+
+// No `JsBlockStatement`
+if (condition) if (anotherCondition) doSomething();
+
+// `JsEmptyStatement`
+if (condition) if (anotherCondition);
+
+// Nested
+if (a) {
+	if (b) {
+		// ...
+	}
+} else if (c) {
+	if (d) {
+		// ...
+	}
+}
+
+// Need parenthesis
+function* foo() {
+	if (a || b)
+		if (a ?? b)
+			if (a ? b : c)
+				if (a = b)
+					if (a += b)
+						if (a -= b)
+							if (a &&= b)
+								if (yield a)
+									if (a, b);
+}
+
+// Should not add parenthesis
+async function foo() {
+	if (a)
+		if (await a)
+			if (a.b)
+				if (a && b);
+}
+
+// Don't case parenthesis in outer test
+if (((a || b))) if (((c || d)));
+
+// Semicolon
+if (a)
+	if (b) foo()
+	;[].forEach(bar)
+
+if (a) {
+	if (b) foo()
+}
+;[].forEach(bar)
+
+/**
+ * Suggested fixes:
+ */
+
+if (condition) { // Comment
+	if (anotherCondition) {
+		// ...
+	}
+}
+
+if (condition) {
+	// Comment
+	if (anotherCondition) {
+		// ...
+	}
+}
+
+if (condition) {
+	if (anotherCondition) {
+		// ...
+	} // Comment
+}
+
+if (condition) {
+	if (anotherCondition) {
+		// ...
+	}
+	// Comment
+}
+
+if (condition) { // Comment
+	if (anotherCondition) {
+		// ...
+	}
+} else {
+	// ...
+}
+
+if (condition) {
+	// Comment
+	if (anotherCondition) {
+		// ...
+	}
+} else {
+	// ...
+}
+
+if (condition) {
+	if (anotherCondition) {
+		// ...
+	} // Comment
+} else {
+	// ...
+}
+
+if (condition) {
+	if (anotherCondition) {
+		// ...
+	}
+	// Comment
+} else {
+	// ...
+}
+
+if (condition)
+	if (anotherCondition) // Comment
+		doSomething();
+
+// Semicolon
+if (a) {
+	if (b) foo()
+}
+[].forEach(bar)
diff --git a/crates/biome_js_analyze/tests/specs/nursery/useCollapsedIf/invalid.js.snap b/crates/biome_js_analyze/tests/specs/nursery/useCollapsedIf/invalid.js.snap
new file mode 100644
index 000000000000..0e89ef746207
--- /dev/null
+++ b/crates/biome_js_analyze/tests/specs/nursery/useCollapsedIf/invalid.js.snap
@@ -0,0 +1,1024 @@
+---
+source: crates/biome_js_analyze/tests/spec_tests.rs
+expression: invalid.js
+---
+# Input
+```jsx
+/**
+ * Safe fixes:
+ */
+
+if (condition) {
+	if (anotherCondition) {
+		// ...
+	}
+}
+
+if (condition) {
+	if (anotherCondition) {
+		// ...
+	}
+} else {
+	// ...
+}
+
+if (condition) // Comment
+	if (anotherCondition)
+		doSomething();
+
+// Inner one is `JsBlockStatement`
+if (condition) if (anotherCondition) {
+	// ...
+}
+
+// Outer one is `JsBlockStatement`
+if (condition) {
+	if (anotherCondition) doSomething();
+}
+
+// No `JsBlockStatement`
+if (condition) if (anotherCondition) doSomething();
+
+// `JsEmptyStatement`
+if (condition) if (anotherCondition);
+
+// Nested
+if (a) {
+	if (b) {
+		// ...
+	}
+} else if (c) {
+	if (d) {
+		// ...
+	}
+}
+
+// Need parenthesis
+function* foo() {
+	if (a || b)
+		if (a ?? b)
+			if (a ? b : c)
+				if (a = b)
+					if (a += b)
+						if (a -= b)
+							if (a &&= b)
+								if (yield a)
+									if (a, b);
+}
+
+// Should not add parenthesis
+async function foo() {
+	if (a)
+		if (await a)
+			if (a.b)
+				if (a && b);
+}
+
+// Don't case parenthesis in outer test
+if (((a || b))) if (((c || d)));
+
+// Semicolon
+if (a)
+	if (b) foo()
+	;[].forEach(bar)
+
+if (a) {
+	if (b) foo()
+}
+;[].forEach(bar)
+
+/**
+ * Suggested fixes:
+ */
+
+if (condition) { // Comment
+	if (anotherCondition) {
+		// ...
+	}
+}
+
+if (condition) {
+	// Comment
+	if (anotherCondition) {
+		// ...
+	}
+}
+
+if (condition) {
+	if (anotherCondition) {
+		// ...
+	} // Comment
+}
+
+if (condition) {
+	if (anotherCondition) {
+		// ...
+	}
+	// Comment
+}
+
+if (condition) { // Comment
+	if (anotherCondition) {
+		// ...
+	}
+} else {
+	// ...
+}
+
+if (condition) {
+	// Comment
+	if (anotherCondition) {
+		// ...
+	}
+} else {
+	// ...
+}
+
+if (condition) {
+	if (anotherCondition) {
+		// ...
+	} // Comment
+} else {
+	// ...
+}
+
+if (condition) {
+	if (anotherCondition) {
+		// ...
+	}
+	// Comment
+} else {
+	// ...
+}
+
+if (condition)
+	if (anotherCondition) // Comment
+		doSomething();
+
+// Semicolon
+if (a) {
+	if (b) foo()
+}
+[].forEach(bar)
+
+```
+
+# Diagnostics
+```
+invalid.js:5:17 lint/nursery/useCollapsedIf  FIXABLE  ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+
+  ! This if statement can be collapsed into another if statement.
+  
+     3 │  */
+     4 │ 
+   > 5 │ if (condition) {
+       │                 
+   > 6 │ 	if (anotherCondition) {
+   > 7 │ 		// ...
+   > 8 │ 	}
+       │ 	^
+     9 │ }
+    10 │ 
+  
+  i Safe fix: Use collapsed if instead.
+  
+      3   3 │    */
+      4   4 │   
+      5     │ - if·(condition)·{
+      6     │ - → if·(anotherCondition)·{
+      7     │ - → → //·...
+      8     │ - → }
+      9     │ - }
+          5 │ + if·(condition&&anotherCondition)·{
+          6 │ + → → //·...
+          7 │ + → }
+     10   8 │   
+     11   9 │   if (condition) {
+  
+
+```
+
+```
+invalid.js:11:17 lint/nursery/useCollapsedIf  FIXABLE  ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+
+  ! This if statement can be collapsed into another if statement.
+  
+     9 │ }
+    10 │ 
+  > 11 │ if (condition) {
+       │                 
+  > 12 │ 	if (anotherCondition) {
+  > 13 │ 		// ...
+  > 14 │ 	}
+       │ 	^
+    15 │ } else {
+    16 │ 	// ...
+  
+  i Safe fix: Use collapsed if instead.
+  
+      9   9 │   }
+     10  10 │   
+     11     │ - if·(condition)·{
+     12     │ - → if·(anotherCondition)·{
+     13     │ - → → //·...
+     14     │ - → }
+     15     │ - }·else·{
+         11 │ + if·(condition&&anotherCondition)·{
+         12 │ + → → //·...
+         13 │ + → }·else·{
+     16  14 │   	// ...
+     17  15 │   }
+  
+
+```
+
+```
+invalid.js:19:26 lint/nursery/useCollapsedIf  FIXABLE  ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+
+  ! This if statement can be collapsed into another if statement.
+  
+    17 │ }
+    18 │ 
+  > 19 │ if (condition) // Comment
+       │                          
+  > 20 │ 	if (anotherCondition)
+  > 21 │ 		doSomething();
+       │ 		^^^^^^^^^^^^^^
+    22 │ 
+    23 │ // Inner one is `JsBlockStatement`
+  
+  i Safe fix: Use collapsed if instead.
+  
+     17  17 │   }
+     18  18 │   
+     19     │ - if·(condition)·//·Comment
+     20     │ - → if·(anotherCondition)
+     21     │ - → → doSomething();
+         19 │ + if·(condition&&anotherCondition)·//·Comment
+         20 │ + → doSomething();
+     22  21 │   
+     23  22 │   // Inner one is `JsBlockStatement`
+  
+
+```
+
+```
+invalid.js:24:16 lint/nursery/useCollapsedIf  FIXABLE  ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+
+  ! This if statement can be collapsed into another if statement.
+  
+    23 │ // Inner one is `JsBlockStatement`
+  > 24 │ if (condition) if (anotherCondition) {
+       │                ^^^^^^^^^^^^^^^^^^^^^^^
+  > 25 │ 	// ...
+  > 26 │ }
+       │ ^
+    27 │ 
+    28 │ // Outer one is `JsBlockStatement`
+  
+  i Safe fix: Use collapsed if instead.
+  
+     22  22 │   
+     23  23 │   // Inner one is `JsBlockStatement`
+     24     │ - if·(condition)·if·(anotherCondition)·{
+         24 │ + if·(condition&&anotherCondition)·{
+     25  25 │   	// ...
+     26  26 │   }
+  
+
+```
+
+```
+invalid.js:29:17 lint/nursery/useCollapsedIf  FIXABLE  ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+
+  ! This if statement can be collapsed into another if statement.
+  
+    28 │ // Outer one is `JsBlockStatement`
+  > 29 │ if (condition) {
+       │                 
+  > 30 │ 	if (anotherCondition) doSomething();
+       │ 	^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+    31 │ }
+    32 │ 
+  
+  i Safe fix: Use collapsed if instead.
+  
+     27  27 │   
+     28  28 │   // Outer one is `JsBlockStatement`
+     29     │ - if·(condition)·{
+     30     │ - → if·(anotherCondition)·doSomething();
+     31     │ - }
+         29 │ + if·(condition&&anotherCondition)·doSomething();
+     32  30 │   
+     33  31 │   // No `JsBlockStatement`
+  
+
+```
+
+```
+invalid.js:34:16 lint/nursery/useCollapsedIf  FIXABLE  ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+
+  ! This if statement can be collapsed into another if statement.
+  
+    33 │ // No `JsBlockStatement`
+  > 34 │ if (condition) if (anotherCondition) doSomething();
+       │                ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+    35 │ 
+    36 │ // `JsEmptyStatement`
+  
+  i Safe fix: Use collapsed if instead.
+  
+     32  32 │   
+     33  33 │   // No `JsBlockStatement`
+     34     │ - if·(condition)·if·(anotherCondition)·doSomething();
+         34 │ + if·(condition&&anotherCondition)·doSomething();
+     35  35 │   
+     36  36 │   // `JsEmptyStatement`
+  
+
+```
+
+```
+invalid.js:37:16 lint/nursery/useCollapsedIf  FIXABLE  ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+
+  ! This if statement can be collapsed into another if statement.
+  
+    36 │ // `JsEmptyStatement`
+  > 37 │ if (condition) if (anotherCondition);
+       │                ^^^^^^^^^^^^^^^^^^^^^^
+    38 │ 
+    39 │ // Nested
+  
+  i Safe fix: Use collapsed if instead.
+  
+     35  35 │   
+     36  36 │   // `JsEmptyStatement`
+     37     │ - if·(condition)·if·(anotherCondition);
+         37 │ + if·(condition&&anotherCondition)·;
+     38  38 │   
+     39  39 │   // Nested
+  
+
+```
+
+```
+invalid.js:40:9 lint/nursery/useCollapsedIf  FIXABLE  ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+
+  ! This if statement can be collapsed into another if statement.
+  
+    39 │ // Nested
+  > 40 │ if (a) {
+       │         
+  > 41 │ 	if (b) {
+  > 42 │ 		// ...
+  > 43 │ 	}
+       │ 	^
+    44 │ } else if (c) {
+    45 │ 	if (d) {
+  
+  i Safe fix: Use collapsed if instead.
+  
+     38  38 │   
+     39  39 │   // Nested
+     40     │ - if·(a)·{
+     41     │ - → if·(b)·{
+     42     │ - → → //·...
+     43     │ - → }
+     44     │ - }·else·if·(c)·{
+         40 │ + if·(a&&b)·{
+         41 │ + → → //·...
+         42 │ + → }·else·if·(c)·{
+     45  43 │   	if (d) {
+     46  44 │   		// ...
+  
+
+```
+
+```
+invalid.js:44:16 lint/nursery/useCollapsedIf  FIXABLE  ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+
+  ! This if statement can be collapsed into another if statement.
+  
+    42 │ 		// ...
+    43 │ 	}
+  > 44 │ } else if (c) {
+       │                
+  > 45 │ 	if (d) {
+  > 46 │ 		// ...
+  > 47 │ 	}
+       │ 	^
+    48 │ }
+    49 │ 
+  
+  i Safe fix: Use collapsed if instead.
+  
+     42  42 │   		// ...
+     43  43 │   	}
+     44     │ - }·else·if·(c)·{
+     45     │ - → if·(d)·{
+     46     │ - → → //·...
+     47     │ - → }
+     48     │ - }
+         44 │ + }·else·if·(c&&d)·{
+         45 │ + → → //·...
+         46 │ + → }
+     49  47 │   
+     50  48 │   // Need parenthesis
+  
+
+```
+
+```
+invalid.js:52:13 lint/nursery/useCollapsedIf  FIXABLE  ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+
+  ! This if statement can be collapsed into another if statement.
+  
+    50 │ // Need parenthesis
+    51 │ function* foo() {
+  > 52 │ 	if (a || b)
+       │ 	           
+  > 53 │ 		if (a ?? b)
+        ...
+  > 59 │ 								if (yield a)
+  > 60 │ 									if (a, b);
+       │ 									^^^^^^^^^^
+    61 │ }
+    62 │ 
+  
+  i Safe fix: Use collapsed if instead.
+  
+     50  50 │   // Need parenthesis
+     51  51 │   function* foo() {
+     52     │ - → if·(a·||·b)
+     53     │ - → → if·(a·??·b)
+     54     │ - → → → if·(a·?·b·:·c)
+         52 │ + → if·((a·||·b)&&(a·??·b))
+         53 │ + → → if·(a·?·b·:·c)
+     55  54 │   				if (a = b)
+     56  55 │   					if (a += b)
+  
+
+```
+
+```
+invalid.js:53:14 lint/nursery/useCollapsedIf  FIXABLE  ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+
+  ! This if statement can be collapsed into another if statement.
+  
+    51 │ function* foo() {
+    52 │ 	if (a || b)
+  > 53 │ 		if (a ?? b)
+       │ 		           
+  > 54 │ 			if (a ? b : c)
+        ...
+  > 59 │ 								if (yield a)
+  > 60 │ 									if (a, b);
+       │ 									^^^^^^^^^^
+    61 │ }
+    62 │ 
+  
+  i Safe fix: Use collapsed if instead.
+  
+     51  51 │   function* foo() {
+     52  52 │   	if (a || b)
+     53     │ - → → if·(a·??·b)
+     54     │ - → → → if·(a·?·b·:·c)
+     55     │ - → → → → if·(a·=·b)
+         53 │ + → → if·((a·??·b)&&(a·?·b·:·c))
+         54 │ + → → → if·(a·=·b)
+     56  55 │   					if (a += b)
+     57  56 │   						if (a -= b)
+  
+
+```
+
+```
+invalid.js:54:18 lint/nursery/useCollapsedIf  FIXABLE  ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+
+  ! This if statement can be collapsed into another if statement.
+  
+    52 │ 	if (a || b)
+    53 │ 		if (a ?? b)
+  > 54 │ 			if (a ? b : c)
+       │ 			              
+  > 55 │ 				if (a = b)
+        ...
+  > 59 │ 								if (yield a)
+  > 60 │ 									if (a, b);
+       │ 									^^^^^^^^^^
+    61 │ }
+    62 │ 
+  
+  i Safe fix: Use collapsed if instead.
+  
+     52  52 │   	if (a || b)
+     53  53 │   		if (a ?? b)
+     54     │ - → → → if·(a·?·b·:·c)
+     55     │ - → → → → if·(a·=·b)
+     56     │ - → → → → → if·(a·+=·b)
+         54 │ + → → → if·((a·?·b·:·c)&&(a·=·b))
+         55 │ + → → → → if·(a·+=·b)
+     57  56 │   						if (a -= b)
+     58  57 │   							if (a &&= b)
+  
+
+```
+
+```
+invalid.js:55:15 lint/nursery/useCollapsedIf  FIXABLE  ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+
+  ! This if statement can be collapsed into another if statement.
+  
+    53 │ 		if (a ?? b)
+    54 │ 			if (a ? b : c)
+  > 55 │ 				if (a = b)
+       │ 				          
+  > 56 │ 					if (a += b)
+  > 57 │ 						if (a -= b)
+  > 58 │ 							if (a &&= b)
+  > 59 │ 								if (yield a)
+  > 60 │ 									if (a, b);
+       │ 									^^^^^^^^^^
+    61 │ }
+    62 │ 
+  
+  i Safe fix: Use collapsed if instead.
+  
+     53  53 │   		if (a ?? b)
+     54  54 │   			if (a ? b : c)
+     55     │ - → → → → if·(a·=·b)
+     56     │ - → → → → → if·(a·+=·b)
+     57     │ - → → → → → → if·(a·-=·b)
+         55 │ + → → → → if·((a·=·b)&&(a·+=·b))
+         56 │ + → → → → → if·(a·-=·b)
+     58  57 │   							if (a &&= b)
+     59  58 │   								if (yield a)
+  
+
+```
+
+```
+invalid.js:56:17 lint/nursery/useCollapsedIf  FIXABLE  ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+
+  ! This if statement can be collapsed into another if statement.
+  
+    54 │ 			if (a ? b : c)
+    55 │ 				if (a = b)
+  > 56 │ 					if (a += b)
+       │ 					           
+  > 57 │ 						if (a -= b)
+  > 58 │ 							if (a &&= b)
+  > 59 │ 								if (yield a)
+  > 60 │ 									if (a, b);
+       │ 									^^^^^^^^^^
+    61 │ }
+    62 │ 
+  
+  i Safe fix: Use collapsed if instead.
+  
+     54  54 │   			if (a ? b : c)
+     55  55 │   				if (a = b)
+     56     │ - → → → → → if·(a·+=·b)
+     57     │ - → → → → → → if·(a·-=·b)
+     58     │ - → → → → → → → if·(a·&&=·b)
+         56 │ + → → → → → if·((a·+=·b)&&(a·-=·b))
+         57 │ + → → → → → → if·(a·&&=·b)
+     59  58 │   								if (yield a)
+     60  59 │   									if (a, b);
+  
+
+```
+
+```
+invalid.js:57:18 lint/nursery/useCollapsedIf  FIXABLE  ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+
+  ! This if statement can be collapsed into another if statement.
+  
+    55 │ 				if (a = b)
+    56 │ 					if (a += b)
+  > 57 │ 						if (a -= b)
+       │ 						           
+  > 58 │ 							if (a &&= b)
+  > 59 │ 								if (yield a)
+  > 60 │ 									if (a, b);
+       │ 									^^^^^^^^^^
+    61 │ }
+    62 │ 
+  
+  i Safe fix: Use collapsed if instead.
+  
+     55  55 │   				if (a = b)
+     56  56 │   					if (a += b)
+     57     │ - → → → → → → if·(a·-=·b)
+     58     │ - → → → → → → → if·(a·&&=·b)
+     59     │ - → → → → → → → → if·(yield·a)
+         57 │ + → → → → → → if·((a·-=·b)&&(a·&&=·b))
+         58 │ + → → → → → → → if·(yield·a)
+     60  59 │   									if (a, b);
+     61  60 │   }
+  
+
+```
+
+```
+invalid.js:58:20 lint/nursery/useCollapsedIf  FIXABLE  ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+
+  ! This if statement can be collapsed into another if statement.
+  
+    56 │ 					if (a += b)
+    57 │ 						if (a -= b)
+  > 58 │ 							if (a &&= b)
+       │ 							            
+  > 59 │ 								if (yield a)
+  > 60 │ 									if (a, b);
+       │ 									^^^^^^^^^^
+    61 │ }
+    62 │ 
+  
+  i Safe fix: Use collapsed if instead.
+  
+     56  56 │   					if (a += b)
+     57  57 │   						if (a -= b)
+     58     │ - → → → → → → → if·(a·&&=·b)
+     59     │ - → → → → → → → → if·(yield·a)
+     60     │ - → → → → → → → → → if·(a,·b);
+         58 │ + → → → → → → → if·((a·&&=·b)&&(yield·a))
+         59 │ + → → → → → → → → if·(a,·b);
+     61  60 │   }
+     62  61 │   
+  
+
+```
+
+```
+invalid.js:59:21 lint/nursery/useCollapsedIf  FIXABLE  ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+
+  ! This if statement can be collapsed into another if statement.
+  
+    57 │ 						if (a -= b)
+    58 │ 							if (a &&= b)
+  > 59 │ 								if (yield a)
+       │ 								            
+  > 60 │ 									if (a, b);
+       │ 									^^^^^^^^^^
+    61 │ }
+    62 │ 
+  
+  i Safe fix: Use collapsed if instead.
+  
+     57  57 │   						if (a -= b)
+     58  58 │   							if (a &&= b)
+     59     │ - → → → → → → → → if·(yield·a)
+     60     │ - → → → → → → → → → if·(a,·b);
+         59 │ + → → → → → → → → if·((yield·a)&&(a,·b))
+         60 │ + → → → → → → → → → ;
+     61  61 │   }
+     62  62 │   
+  
+
+```
+
+```
+invalid.js:65:8 lint/nursery/useCollapsedIf  FIXABLE  ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+
+  ! This if statement can be collapsed into another if statement.
+  
+    63 │ // Should not add parenthesis
+    64 │ async function foo() {
+  > 65 │ 	if (a)
+       │ 	      
+  > 66 │ 		if (await a)
+  > 67 │ 			if (a.b)
+  > 68 │ 				if (a && b);
+       │ 				^^^^^^^^^^^^
+    69 │ }
+    70 │ 
+  
+  i Safe fix: Use collapsed if instead.
+  
+     63  63 │   // Should not add parenthesis
+     64  64 │   async function foo() {
+     65     │ - → if·(a)
+     66     │ - → → if·(await·a)
+     67     │ - → → → if·(a.b)
+         65 │ + → if·(a&&(await·a))
+         66 │ + → → if·(a.b)
+     68  67 │   				if (a && b);
+     69  68 │   }
+  
+
+```
+
+```
+invalid.js:66:15 lint/nursery/useCollapsedIf  FIXABLE  ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+
+  ! This if statement can be collapsed into another if statement.
+  
+    64 │ async function foo() {
+    65 │ 	if (a)
+  > 66 │ 		if (await a)
+       │ 		            
+  > 67 │ 			if (a.b)
+  > 68 │ 				if (a && b);
+       │ 				^^^^^^^^^^^^
+    69 │ }
+    70 │ 
+  
+  i Safe fix: Use collapsed if instead.
+  
+     64  64 │   async function foo() {
+     65  65 │   	if (a)
+     66     │ - → → if·(await·a)
+     67     │ - → → → if·(a.b)
+     68     │ - → → → → if·(a·&&·b);
+         66 │ + → → if·((await·a)&&a.b)
+         67 │ + → → → if·(a·&&·b);
+     69  68 │   }
+     70  69 │   
+  
+
+```
+
+```
+invalid.js:67:12 lint/nursery/useCollapsedIf  FIXABLE  ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+
+  ! This if statement can be collapsed into another if statement.
+  
+    65 │ 	if (a)
+    66 │ 		if (await a)
+  > 67 │ 			if (a.b)
+       │ 			        
+  > 68 │ 				if (a && b);
+       │ 				^^^^^^^^^^^^
+    69 │ }
+    70 │ 
+  
+  i Safe fix: Use collapsed if instead.
+  
+     65  65 │   	if (a)
+     66  66 │   		if (await a)
+     67     │ - → → → if·(a.b)
+     68     │ - → → → → if·(a·&&·b);
+         67 │ + → → → if·(a.b&&a·&&·b)
+         68 │ + → → → → ;
+     69  69 │   }
+     70  70 │   
+  
+
+```
+
+```
+invalid.js:72:17 lint/nursery/useCollapsedIf  FIXABLE  ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+
+  ! This if statement can be collapsed into another if statement.
+  
+    71 │ // Don't case parenthesis in outer test
+  > 72 │ if (((a || b))) if (((c || d)));
+       │                 ^^^^^^^^^^^^^^^^
+    73 │ 
+    74 │ // Semicolon
+  
+  i Safe fix: Use collapsed if instead.
+  
+     70  70 │   
+     71  71 │   // Don't case parenthesis in outer test
+     72     │ - if·(((a·||·b)))·if·(((c·||·d)));
+         72 │ + if·(((a·||·b))&&((c·||·d)))·;
+     73  73 │   
+     74  74 │   // Semicolon
+  
+
+```
+
+```
+invalid.js:75:7 lint/nursery/useCollapsedIf  FIXABLE  ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+
+  ! This if statement can be collapsed into another if statement.
+  
+    74 │ // Semicolon
+  > 75 │ if (a)
+       │       
+  > 76 │ 	if (b) foo()
+  > 77 │ 	;[].forEach(bar)
+       │ 	^
+    78 │ 
+    79 │ if (a) {
+  
+  i Safe fix: Use collapsed if instead.
+  
+     73  73 │   
+     74  74 │   // Semicolon
+     75     │ - if·(a)
+     76     │ - → if·(b)·foo()
+         75 │ + if·(a&&b)
+         76 │ + → foo()
+     77  77 │   	;[].forEach(bar)
+     78  78 │   
+  
+
+```
+
+```
+invalid.js:79:9 lint/nursery/useCollapsedIf ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+
+  ! This if statement can be collapsed into another if statement.
+  
+    77 │ 	;[].forEach(bar)
+    78 │ 
+  > 79 │ if (a) {
+       │         
+  > 80 │ 	if (b) foo()
+       │ 	^^^^^^^^^^^^
+    81 │ }
+    82 │ ;[].forEach(bar)
+  
+
+```
+
+```
+invalid.js:88:28 lint/nursery/useCollapsedIf ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+
+  ! This if statement can be collapsed into another if statement.
+  
+    86 │  */
+    87 │ 
+  > 88 │ if (condition) { // Comment
+       │                            
+  > 89 │ 	if (anotherCondition) {
+  > 90 │ 		// ...
+  > 91 │ 	}
+       │ 	^
+    92 │ }
+    93 │ 
+  
+
+```
+
+```
+invalid.js:94:17 lint/nursery/useCollapsedIf ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+
+  ! This if statement can be collapsed into another if statement.
+  
+     92 │ }
+     93 │ 
+   > 94 │ if (condition) {
+        │                 
+   > 95 │ 	// Comment
+   > 96 │ 	if (anotherCondition) {
+   > 97 │ 		// ...
+   > 98 │ 	}
+        │ 	^
+     99 │ }
+    100 │ 
+  
+
+```
+
+```
+invalid.js:101:17 lint/nursery/useCollapsedIf ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+
+  ! This if statement can be collapsed into another if statement.
+  
+     99 │ }
+    100 │ 
+  > 101 │ if (condition) {
+        │                 
+  > 102 │ 	if (anotherCondition) {
+  > 103 │ 		// ...
+  > 104 │ 	} // Comment
+        │ 	^^^^^^^^^^^^
+    105 │ }
+    106 │ 
+  
+
+```
+
+```
+invalid.js:107:17 lint/nursery/useCollapsedIf ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+
+  ! This if statement can be collapsed into another if statement.
+  
+    105 │ }
+    106 │ 
+  > 107 │ if (condition) {
+        │                 
+  > 108 │ 	if (anotherCondition) {
+  > 109 │ 		// ...
+  > 110 │ 	}
+        │ 	^
+    111 │ 	// Comment
+    112 │ }
+  
+
+```
+
+```
+invalid.js:114:28 lint/nursery/useCollapsedIf ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+
+  ! This if statement can be collapsed into another if statement.
+  
+    112 │ }
+    113 │ 
+  > 114 │ if (condition) { // Comment
+        │                            
+  > 115 │ 	if (anotherCondition) {
+  > 116 │ 		// ...
+  > 117 │ 	}
+        │ 	^
+    118 │ } else {
+    119 │ 	// ...
+  
+
+```
+
+```
+invalid.js:122:17 lint/nursery/useCollapsedIf ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+
+  ! This if statement can be collapsed into another if statement.
+  
+    120 │ }
+    121 │ 
+  > 122 │ if (condition) {
+        │                 
+  > 123 │ 	// Comment
+  > 124 │ 	if (anotherCondition) {
+  > 125 │ 		// ...
+  > 126 │ 	}
+        │ 	^
+    127 │ } else {
+    128 │ 	// ...
+  
+
+```
+
+```
+invalid.js:131:17 lint/nursery/useCollapsedIf ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+
+  ! This if statement can be collapsed into another if statement.
+  
+    129 │ }
+    130 │ 
+  > 131 │ if (condition) {
+        │                 
+  > 132 │ 	if (anotherCondition) {
+  > 133 │ 		// ...
+  > 134 │ 	} // Comment
+        │ 	^^^^^^^^^^^^
+    135 │ } else {
+    136 │ 	// ...
+  
+
+```
+
+```
+invalid.js:139:17 lint/nursery/useCollapsedIf ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+
+  ! This if statement can be collapsed into another if statement.
+  
+    137 │ }
+    138 │ 
+  > 139 │ if (condition) {
+        │                 
+  > 140 │ 	if (anotherCondition) {
+  > 141 │ 		// ...
+  > 142 │ 	}
+        │ 	^
+    143 │ 	// Comment
+    144 │ } else {
+  
+
+```
+
+```
+invalid.js:148:15 lint/nursery/useCollapsedIf ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+
+  ! This if statement can be collapsed into another if statement.
+  
+    146 │ }
+    147 │ 
+  > 148 │ if (condition)
+        │               
+  > 149 │ 	if (anotherCondition) // Comment
+  > 150 │ 		doSomething();
+        │ 		^^^^^^^^^^^^^^
+    151 │ 
+    152 │ // Semicolon
+  
+
+```
+
+```
+invalid.js:153:9 lint/nursery/useCollapsedIf ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+
+  ! This if statement can be collapsed into another if statement.
+  
+    152 │ // Semicolon
+  > 153 │ if (a) {
+        │         
+  > 154 │ 	if (b) foo()
+        │ 	^^^^^^^^^^^^
+    155 │ }
+    156 │ [].forEach(bar)
+  
+
+```
diff --git a/crates/biome_js_analyze/tests/specs/nursery/useCollapsedIf/valid.js b/crates/biome_js_analyze/tests/specs/nursery/useCollapsedIf/valid.js
new file mode 100644
index 000000000000..f1fc08fc0488
--- /dev/null
+++ b/crates/biome_js_analyze/tests/specs/nursery/useCollapsedIf/valid.js
@@ -0,0 +1,40 @@
+if (condition && anotherCondition) {
+	// ...
+}
+
+if (condition) {
+  if (anotherCondition) {
+		// ...
+  }
+  doSomething();
+}
+
+if (condition) {
+	if (anotherCondition) {
+		// ...
+	} else {
+		// ...
+	}
+}
+
+if (condition) {
+	if (anotherCondition) {
+		// ...
+	}
+	doSomething();
+} else {
+	// ...
+}
+
+if (condition) {
+	anotherCondition ? c() : d()
+}
+
+// Covered by `useCollapsedElseIf`
+if (condition) {
+	// ...
+} else {
+	if (anotherCondition) {
+		// ...
+	}
+}
diff --git a/crates/biome_js_analyze/tests/specs/nursery/useCollapsedIf/valid.js.snap b/crates/biome_js_analyze/tests/specs/nursery/useCollapsedIf/valid.js.snap
new file mode 100644
index 000000000000..83648186b0f1
--- /dev/null
+++ b/crates/biome_js_analyze/tests/specs/nursery/useCollapsedIf/valid.js.snap
@@ -0,0 +1,48 @@
+---
+source: crates/biome_js_analyze/tests/spec_tests.rs
+expression: valid.js
+---
+# Input
+```jsx
+if (condition && anotherCondition) {
+	// ...
+}
+
+if (condition) {
+  if (anotherCondition) {
+		// ...
+  }
+  doSomething();
+}
+
+if (condition) {
+	if (anotherCondition) {
+		// ...
+	} else {
+		// ...
+	}
+}
+
+if (condition) {
+	if (anotherCondition) {
+		// ...
+	}
+	doSomething();
+} else {
+	// ...
+}
+
+if (condition) {
+	anotherCondition ? c() : d()
+}
+
+// Covered by `useCollapsedElseIf`
+if (condition) {
+	// ...
+} else {
+	if (anotherCondition) {
+		// ...
+	}
+}
+
+```