Repository: oxc-project/oxc
Pull request #9660: feat(linter): Add `eslint/no-lonely-if`
Author: login: therewillbecode
Labels: name: C-enhancement
description: Category - New feature or request
name: A-linter
description: Area - Linter
Created: 2025-03-10T22:36:25Z | Merged: 2025-03-13T13:20:42Z

Relates to #479 .

Rule details: https://eslint.org/docs/latest/rules/no-lonely-if


Diff:
diff --git a/crates/oxc_linter/src/rules.rs b/crates/oxc_linter/src/rules.rs
index f09dd7e8f3635..c989a758f51b6 100644
--- a/crates/oxc_linter/src/rules.rs
+++ b/crates/oxc_linter/src/rules.rs
@@ -102,6 +102,7 @@ mod eslint {
     pub mod no_label_var;
     pub mod no_labels;
     pub mod no_lone_blocks;
+    pub mod no_lonely_if;
     pub mod no_loss_of_precision;
     pub mod no_magic_numbers;
     pub mod no_multi_assign;
@@ -571,6 +572,7 @@ oxc_macros::declare_all_lint_rules! {
     eslint::max_lines,
     eslint::max_params,
     eslint::new_cap,
+    eslint::no_lonely_if,
     eslint::no_useless_call,
     eslint::no_unneeded_ternary,
     eslint::no_extra_label,
diff --git a/crates/oxc_linter/src/rules/eslint/no_lonely_if.rs b/crates/oxc_linter/src/rules/eslint/no_lonely_if.rs
new file mode 100644
index 0000000000000..a85837c299697
--- /dev/null
+++ b/crates/oxc_linter/src/rules/eslint/no_lonely_if.rs
@@ -0,0 +1,290 @@
+use crate::{AstNode, context::LintContext, rule::Rule};
+use oxc_ast::AstKind;
+use oxc_ast::ast::{Statement, Statement::BlockStatement};
+use oxc_diagnostics::OxcDiagnostic;
+use oxc_macros::declare_oxc_lint;
+use oxc_span::Span;
+
+fn no_lonely_if_diagnostic(span: Span) -> OxcDiagnostic {
+    OxcDiagnostic::warn("Unexpected `if` as the only statement in an `else` block")
+        .with_help("Consider using `else if` instead.")
+        .with_label(span)
+}
+
+#[derive(Debug, Default, Clone)]
+pub struct NoLonelyIf;
+
+declare_oxc_lint!(
+    /// ### What it does
+    ///
+    /// Disallow `if` statements as the only statement in `else` blocks
+    ///
+    /// ### Why is this bad?
+    ///
+    /// When an `if` statement is the only statement in an `else` block, it is often clearer to use
+    /// an `else if` instead.
+    ///
+    /// ### Examples
+    ///
+    /// Examples of **incorrect** code for this rule:
+    /// ```js
+    /// if (condition) {
+    ///   // ...
+    /// } else {
+    ///   if (anotherCondition) {
+    ///     // ...
+    ///   }
+    /// }
+    /// ```
+    ///
+    /// ```js
+    /// if (condition) {
+    ///   // ...
+    /// } else {
+    ///   if (anotherCondition) {
+    ///     // ...
+    ///   } else {
+    ///       // ...
+    ///   }
+    /// }
+    /// ```
+    ///
+    /// Examples of **correct** code for this rule:
+    /// ```js
+    /// if (condition) {
+    ///   // ...
+    /// } else if (anotherCondition) {
+    ///   // ...
+    /// }
+    /// ```
+    ///
+    /// ```js
+    /// if (condition) {
+    ///   // ...
+    /// } else if (anotherCondition) {
+    ///   // ...
+    /// } else {
+    ///   // ...
+    /// }
+    /// ```
+    ///
+    /// ```js
+    /// if (condition) {
+    ///   // ...
+    /// } else {
+    ///   if (anotherCondition) {
+    ///     // ...
+    ///   }
+    ///   doSomething();
+    /// }
+    /// ```
+    NoLonelyIf,
+    eslint,
+    pedantic,
+    pending
+);
+
+impl Rule for NoLonelyIf {
+    fn run<'a>(&self, node: &AstNode<'a>, ctx: &LintContext<'a>) {
+        let AstKind::IfStatement(if_stmt) = node.kind() else {
+            return;
+        };
+
+        let Some(ref alternate) = if_stmt.alternate else {
+            return;
+        };
+
+        let BlockStatement(b) = alternate else {
+            return;
+        };
+
+        if b.body.len() != 1 {
+            return;
+        };
+
+        let Some(only_stmt) = b.body.first() else {
+            return;
+        };
+
+        if let Some(AstKind::IfStatement(_)) = ctx.nodes().parent_kind(node.id()) {
+            return;
+        };
+
+        if let Statement::BlockStatement(inner_block) = only_stmt {
+            if inner_block.body.len() == 1 {
+                if let Some(Statement::IfStatement(lonely_if)) = inner_block.body.first() {
+                    ctx.diagnostic(no_lonely_if_diagnostic(Span::sized(lonely_if.span.start, 2)));
+                };
+            }
+        } else if let Statement::IfStatement(lonely_if) = only_stmt {
+            ctx.diagnostic(no_lonely_if_diagnostic(Span::sized(lonely_if.span.start, 2)));
+        };
+    }
+}
+
+#[test]
+fn test() {
+    use crate::tester::Tester;
+
+    let pass = vec![
+        "if (a) {;} else if (b) {;}",
+        "if (a) {;} else { if (b) {;} ; }",
+        "if (a) if (a) {} else { if (b) {} } else {}",
+        "if (a) {
+           if (b) {} else { }
+         } else {
+           if (c) {  }
+           if (d) {  }
+          }",
+    ];
+
+    let fail = vec![
+        "if (a) {;} else { if (b) {;} }",
+        "if (foo) {} else { if (bar) baz(); }",
+        "if (foo) {} else { if (bar) baz() } qux();",
+        "if (foo) {} else { if (bar) baz(); } qux();",
+        "if (a) {
+           foo();
+         } else {
+           /* otherwise, do the other thing */ if (b) {
+             bar();
+           }
+         }",
+        "if (a) {
+           foo();
+         } else {
+           if (b) {
+             bar();
+           } /* this comment will prevent this test case from being autofixed. */
+         }",
+        // No fix; removing the braces would cause a SyntaxError.
+        "if (foo) {
+         } else {
+           if (bar) baz();
+         }
+         qux();",
+        // Not fixed; removing the braces would change the semantics due to ASI.
+        "if (foo) {
+         } else {
+           if (bar) baz();
+         }
+         [1, 2, 3].forEach(foo);",
+        // Not fixed; removing the braces would change the semantics due to ASI.
+        "if (foo) { } else {
+           if (bar) baz++;
+         }
+         foo;",
+        // Not fixed; bar() would be interpreted as a template literal tag
+        "if (a) {
+           foo();
+         } else {
+           if (b) bar();
+         }
+         `template literal`;",
+    ];
+
+    /* Pending
+    let fix = vec![
+        (
+            "if (a) {
+               foo();
+             } else {
+               if (b) {
+                 bar();
+               }
+             }",
+            "if (a) {
+               foo();
+             } else if (b) {
+               bar();
+             }",
+            None,
+        ),
+        (
+        "if (a) {
+           foo();
+         } /* comment */
+ else {
+           if (b) {
+             bar();
+           }
+         }",
+        "if (a) {
+           foo();
+    } /* comment */
+ else {
+           if (b) {
+             bar();
+           }
+         }",
+            None,
+        ),
+        (
+        "if (a) {
+           foo();
+         } else {
+    if ( /* this comment is ok */
+ b) {
+             bar();
+           }
+         }",
+        "if (a) {
+           foo();
+    } else if ( /* this comment is ok */
+ b) {
+           bar();
+         }",
+            None,
+        ),
+        (
+        "if (foo) {} else { if (bar) baz(); }",
+        "if (foo) {} else if (bar) baz();",
+            None,
+        ),
+        (
+        "if (foo) { } else { if (bar) baz(); } qux();",
+        "if (foo) { } else if (bar) baz(); qux();",
+            None,
+        ),
+        (
+            "if (foo) { } else { if (bar) baz++; } foo;",
+            "if (foo) { } else if (bar) baz++; foo;",
+            None,
+        ),
+        (
+        "if (a) {
+           foo();
+         } else {
+           if (b) {
+             bar();
+           } else if (c) {
+             baz();
+           } else {
+             qux();
+           }
+         }",
+        "if (a) {
+           foo();
+         } else if (b) {
+           bar();
+         } else if (c) {
+           baz();
+         } else {
+           qux();
+         }",
+            None,
+        ),
+        ("if (a) {;} else { if (b) {;} }", "if (a) {;} else if (b) {;}", None),
+        ("if (foo) {} else { if (bar) baz(); }", "if (foo) {} else if (bar) baz();", None),
+        (
+            "if (foo) {} else { if (bar) baz(); } qux();",
+            "if (foo) {} else if (bar) baz(); qux();",
+            None,
+        ),
+    ];
+    */
+
+    Tester::new(NoLonelyIf::NAME, NoLonelyIf::PLUGIN, pass, fail)
+        //  .expect_fix(fix)
+        .test_and_snapshot();
+}
diff --git a/crates/oxc_linter/src/snapshots/eslint_no_lonely_if.snap b/crates/oxc_linter/src/snapshots/eslint_no_lonely_if.snap
new file mode 100644
index 0000000000000..cc0559da12e79
--- /dev/null
+++ b/crates/oxc_linter/src/snapshots/eslint_no_lonely_if.snap
@@ -0,0 +1,84 @@
+---
+source: crates/oxc_linter/src/tester.rs
+---
+  ⚠ eslint(no-lonely-if): Unexpected `if` as the only statement in an `else` block
+   ╭─[no_lonely_if.tsx:1:19]
+ 1 │ if (a) {;} else { if (b) {;} }
+   ·                   ──
+   ╰────
+  help: Consider using `else if` instead.
+
+  ⚠ eslint(no-lonely-if): Unexpected `if` as the only statement in an `else` block
+   ╭─[no_lonely_if.tsx:1:20]
+ 1 │ if (foo) {} else { if (bar) baz(); }
+   ·                    ──
+   ╰────
+  help: Consider using `else if` instead.
+
+  ⚠ eslint(no-lonely-if): Unexpected `if` as the only statement in an `else` block
+   ╭─[no_lonely_if.tsx:1:20]
+ 1 │ if (foo) {} else { if (bar) baz() } qux();
+   ·                    ──
+   ╰────
+  help: Consider using `else if` instead.
+
+  ⚠ eslint(no-lonely-if): Unexpected `if` as the only statement in an `else` block
+   ╭─[no_lonely_if.tsx:1:20]
+ 1 │ if (foo) {} else { if (bar) baz(); } qux();
+   ·                    ──
+   ╰────
+  help: Consider using `else if` instead.
+
+  ⚠ eslint(no-lonely-if): Unexpected `if` as the only statement in an `else` block
+   ╭─[no_lonely_if.tsx:4:48]
+ 3 │          } else {
+ 4 │            /* otherwise, do the other thing */ if (b) {
+   ·                                                ──
+ 5 │              bar();
+   ╰────
+  help: Consider using `else if` instead.
+
+  ⚠ eslint(no-lonely-if): Unexpected `if` as the only statement in an `else` block
+   ╭─[no_lonely_if.tsx:4:12]
+ 3 │          } else {
+ 4 │            if (b) {
+   ·            ──
+ 5 │              bar();
+   ╰────
+  help: Consider using `else if` instead.
+
+  ⚠ eslint(no-lonely-if): Unexpected `if` as the only statement in an `else` block
+   ╭─[no_lonely_if.tsx:3:12]
+ 2 │          } else {
+ 3 │            if (bar) baz();
+   ·            ──
+ 4 │          }
+   ╰────
+  help: Consider using `else if` instead.
+
+  ⚠ eslint(no-lonely-if): Unexpected `if` as the only statement in an `else` block
+   ╭─[no_lonely_if.tsx:3:12]
+ 2 │          } else {
+ 3 │            if (bar) baz();
+   ·            ──
+ 4 │          }
+   ╰────
+  help: Consider using `else if` instead.
+
+  ⚠ eslint(no-lonely-if): Unexpected `if` as the only statement in an `else` block
+   ╭─[no_lonely_if.tsx:2:12]
+ 1 │ if (foo) { } else {
+ 2 │            if (bar) baz++;
+   ·            ──
+ 3 │          }
+   ╰────
+  help: Consider using `else if` instead.
+
+  ⚠ eslint(no-lonely-if): Unexpected `if` as the only statement in an `else` block
+   ╭─[no_lonely_if.tsx:4:12]
+ 3 │          } else {
+ 4 │            if (b) bar();
+   ·            ──
+ 5 │          }
+   ╰────
+  help: Consider using `else if` instead.


Reviews:
user: login: camchenry
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-12T03:11:01Z
user: login: therewillbecode
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-12T22:12:10Z
user: login: camchenry
body: looks good! I had a small recommendation for the span creation as well and the check order, but otherwise looks good to merge to me
state: APPROVED
author_association: MEMBER
submitted_at: 2025-03-13T03:34:25Z
user: login: camchenry
state: APPROVED
author_association: MEMBER
submitted_at: 2025-03-13T13:20:28Z

Review comments:
diff_hunk: @@ -0,0 +1,168 @@
+use crate::{AstNode, context::LintContext, rule::Rule};
+use oxc_ast::AstKind;
+use oxc_ast::ast::{Statement, Statement::BlockStatement};
+use oxc_diagnostics::OxcDiagnostic;
+use oxc_macros::declare_oxc_lint;
+use oxc_span::Span;
+
+fn no_lonely_if_diagnostic(span: Span) -> OxcDiagnostic {
+    OxcDiagnostic::warn("Unexpected `if` as the only statement in an `else` block")
+        .with_help("Consider using `else if` instead.")
+        .with_label(span)
+}
+
+#[derive(Debug, Default, Clone)]
+pub struct NoLonelyIf;
+
+declare_oxc_lint!(
+    /// ### What it does
+    ///
+    /// Disallow `if` statements as the only statement in `else` blocks
+    ///
+    /// ### Why is this bad?
+    ///
+    /// When an `if` statement is the only statement in an `else` block, it is often clearer to use
+    /// an `else if` instead.
+    ///
+    /// ### Examples
+    ///
+    /// Examples of **incorrect** code for this rule:
+    /// ```js
+    /// if (condition) {
+    ///   // ...
+    /// } else {
+    ///   if (anotherCondition) {
+    ///     // ...
+    ///   }
+    /// }
+    /// ```
+    ///
+    /// ```js
+    /// if (condition) {
+    ///   // ...
+    /// } else {
+    ///   if (anotherCondition) {
+    ///     // ...
+    ///   } else {
+    ///       // ...
+    ///   }
+    /// }
+    /// ```
+    ///
+    /// Examples of **correct** code for this rule:
+    /// ```js
+    /// if (condition) {
+    ///   // ...
+    /// } else if (anotherCondition) {
+    ///   // ...
+    /// }
+    /// ```
+    ///
+    /// ```js
+    /// if (condition) {
+    ///   // ...
+    /// } else if (anotherCondition) {
+    ///   // ...
+    /// } else {
+    ///   // ...
+    /// }
+    /// ```
+    ///
+    /// ```js
+    /// if (condition) {
+    ///   // ...
+    /// } else {
+    ///   if (anotherCondition) {
+    ///     // ...
+    ///   }
+    ///   doSomething();
+    /// }
+    /// ```
+    NoLonelyIf,
+    eslint,
+    pedantic,
+    pending
+);
+
+impl Rule for NoLonelyIf {
+    fn run<'a>(&self, node: &AstNode<'a>, ctx: &LintContext<'a>) {
+        let AstKind::IfStatement(if_stmt) = node.kind() else {
+            return;
+        };
+
+        let Some(ref alternate) = if_stmt.alternate else {
+            return;
+        };
+
+        let BlockStatement(b) = alternate else {
+            return;
+        };
+
+        if let Some(AstKind::IfStatement(_)) = ctx.nodes().parent_node(node.id()).map(AstNode::kind)
+        {
+            return;
+        };
path: crates/oxc_linter/src/rules/eslint/no_lonely_if.rs
user: login: camchenry
body: use `parent_kind` instead:
```suggestion
        if let Some(AstKind::IfStatement(_)) = ctx.nodes().parent_kind(node.id())
        {
            return;
        };
```
created_at: 2025-03-12T03:03:58Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,168 @@
+use crate::{AstNode, context::LintContext, rule::Rule};
+use oxc_ast::AstKind;
+use oxc_ast::ast::{Statement, Statement::BlockStatement};
+use oxc_diagnostics::OxcDiagnostic;
+use oxc_macros::declare_oxc_lint;
+use oxc_span::Span;
+
+fn no_lonely_if_diagnostic(span: Span) -> OxcDiagnostic {
+    OxcDiagnostic::warn("Unexpected `if` as the only statement in an `else` block")
+        .with_help("Consider using `else if` instead.")
+        .with_label(span)
+}
+
+#[derive(Debug, Default, Clone)]
+pub struct NoLonelyIf;
+
+declare_oxc_lint!(
+    /// ### What it does
+    ///
+    /// Disallow `if` statements as the only statement in `else` blocks
+    ///
+    /// ### Why is this bad?
+    ///
+    /// When an `if` statement is the only statement in an `else` block, it is often clearer to use
+    /// an `else if` instead.
+    ///
+    /// ### Examples
+    ///
+    /// Examples of **incorrect** code for this rule:
+    /// ```js
+    /// if (condition) {
+    ///   // ...
+    /// } else {
+    ///   if (anotherCondition) {
+    ///     // ...
+    ///   }
+    /// }
+    /// ```
+    ///
+    /// ```js
+    /// if (condition) {
+    ///   // ...
+    /// } else {
+    ///   if (anotherCondition) {
+    ///     // ...
+    ///   } else {
+    ///       // ...
+    ///   }
+    /// }
+    /// ```
+    ///
+    /// Examples of **correct** code for this rule:
+    /// ```js
+    /// if (condition) {
+    ///   // ...
+    /// } else if (anotherCondition) {
+    ///   // ...
+    /// }
+    /// ```
+    ///
+    /// ```js
+    /// if (condition) {
+    ///   // ...
+    /// } else if (anotherCondition) {
+    ///   // ...
+    /// } else {
+    ///   // ...
+    /// }
+    /// ```
+    ///
+    /// ```js
+    /// if (condition) {
+    ///   // ...
+    /// } else {
+    ///   if (anotherCondition) {
+    ///     // ...
+    ///   }
+    ///   doSomething();
+    /// }
+    /// ```
+    NoLonelyIf,
+    eslint,
+    pedantic,
+    pending
+);
+
+impl Rule for NoLonelyIf {
+    fn run<'a>(&self, node: &AstNode<'a>, ctx: &LintContext<'a>) {
+        let AstKind::IfStatement(if_stmt) = node.kind() else {
+            return;
+        };
+
+        let Some(ref alternate) = if_stmt.alternate else {
+            return;
+        };
+
+        let BlockStatement(b) = alternate else {
+            return;
+        };
+
+        if let Some(AstKind::IfStatement(_)) = ctx.nodes().parent_node(node.id()).map(AstNode::kind)
+        {
+            return;
+        };
+
+        if b.body.len() == 1 {
path: crates/oxc_linter/src/rules/eslint/no_lonely_if.rs
user: login: camchenry
body: early return here?
```suggestion
        if b.body.len() != 1 {
            return;
        }
```
created_at: 2025-03-12T03:04:55Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,168 @@
+use crate::{AstNode, context::LintContext, rule::Rule};
+use oxc_ast::AstKind;
+use oxc_ast::ast::{Statement, Statement::BlockStatement};
+use oxc_diagnostics::OxcDiagnostic;
+use oxc_macros::declare_oxc_lint;
+use oxc_span::Span;
+
+fn no_lonely_if_diagnostic(span: Span) -> OxcDiagnostic {
+    OxcDiagnostic::warn("Unexpected `if` as the only statement in an `else` block")
+        .with_help("Consider using `else if` instead.")
+        .with_label(span)
+}
+
+#[derive(Debug, Default, Clone)]
+pub struct NoLonelyIf;
+
+declare_oxc_lint!(
+    /// ### What it does
+    ///
+    /// Disallow `if` statements as the only statement in `else` blocks
+    ///
+    /// ### Why is this bad?
+    ///
+    /// When an `if` statement is the only statement in an `else` block, it is often clearer to use
+    /// an `else if` instead.
+    ///
+    /// ### Examples
+    ///
+    /// Examples of **incorrect** code for this rule:
+    /// ```js
+    /// if (condition) {
+    ///   // ...
+    /// } else {
+    ///   if (anotherCondition) {
+    ///     // ...
+    ///   }
+    /// }
+    /// ```
+    ///
+    /// ```js
+    /// if (condition) {
+    ///   // ...
+    /// } else {
+    ///   if (anotherCondition) {
+    ///     // ...
+    ///   } else {
+    ///       // ...
+    ///   }
+    /// }
+    /// ```
+    ///
+    /// Examples of **correct** code for this rule:
+    /// ```js
+    /// if (condition) {
+    ///   // ...
+    /// } else if (anotherCondition) {
+    ///   // ...
+    /// }
+    /// ```
+    ///
+    /// ```js
+    /// if (condition) {
+    ///   // ...
+    /// } else if (anotherCondition) {
+    ///   // ...
+    /// } else {
+    ///   // ...
+    /// }
+    /// ```
+    ///
+    /// ```js
+    /// if (condition) {
+    ///   // ...
+    /// } else {
+    ///   if (anotherCondition) {
+    ///     // ...
+    ///   }
+    ///   doSomething();
+    /// }
+    /// ```
+    NoLonelyIf,
+    eslint,
+    pedantic,
+    pending
+);
+
+impl Rule for NoLonelyIf {
+    fn run<'a>(&self, node: &AstNode<'a>, ctx: &LintContext<'a>) {
+        let AstKind::IfStatement(if_stmt) = node.kind() else {
+            return;
+        };
+
+        let Some(ref alternate) = if_stmt.alternate else {
+            return;
+        };
+
+        let BlockStatement(b) = alternate else {
+            return;
+        };
+
+        if let Some(AstKind::IfStatement(_)) = ctx.nodes().parent_node(node.id()).map(AstNode::kind)
+        {
+            return;
+        };
+
+        if b.body.len() == 1 {
+            let Some(only_stmt) = b.body.first() else {
+                return;
+            };
+
+            if let Statement::BlockStatement(inner_block) = only_stmt {
+                if inner_block.body.len() == 1 {
+                    if let Some(Statement::IfStatement(lonely_if)) = inner_block.body.first() {
+                        ctx.diagnostic(no_lonely_if_diagnostic(Span::new(
+                            lonely_if.span.start,
+                            lonely_if.span.start + 2,
+                        )));
+                    };
+                }
+            } else if let Statement::IfStatement(lonely_if) = only_stmt {
+                ctx.diagnostic(no_lonely_if_diagnostic(Span::new(
+                    lonely_if.span.start,
+                    lonely_if.span.start + 2,
+                )));
+            };
+        }
+    }
+}
+
+#[test]
+fn test() {
+    use crate::tester::Tester;
+
+    let pass = vec![
+        "if (a) {;} else if (b) {;}",
+        "if (a) {;} else { if (b) {;} ; }",
+        "if (a) if (a) {} else { if (b) {} } else {}",
+        "if (a) {
+           if (b) {} else { }
+         } else {
+           if (c) {  } 
+           if (d) {  } 
+          }",
+    ];
+
+    let fail = vec![
path: crates/oxc_linter/src/rules/eslint/no_lonely_if.rs
user: login: camchenry
body: It looks like we are missing some test cases from eslint. could you port those as well? e.g.:

```
if (foo) {
} else {
  if (bar) baz()
}
[1, 2, 3].forEach(foo);
```
created_at: 2025-03-12T03:09:26Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,168 @@
+use crate::{AstNode, context::LintContext, rule::Rule};
+use oxc_ast::AstKind;
+use oxc_ast::ast::{Statement, Statement::BlockStatement};
+use oxc_diagnostics::OxcDiagnostic;
+use oxc_macros::declare_oxc_lint;
+use oxc_span::Span;
+
+fn no_lonely_if_diagnostic(span: Span) -> OxcDiagnostic {
+    OxcDiagnostic::warn("Unexpected `if` as the only statement in an `else` block")
+        .with_help("Consider using `else if` instead.")
+        .with_label(span)
+}
+
+#[derive(Debug, Default, Clone)]
+pub struct NoLonelyIf;
+
+declare_oxc_lint!(
+    /// ### What it does
+    ///
+    /// Disallow `if` statements as the only statement in `else` blocks
+    ///
+    /// ### Why is this bad?
+    ///
+    /// When an `if` statement is the only statement in an `else` block, it is often clearer to use
+    /// an `else if` instead.
+    ///
+    /// ### Examples
+    ///
+    /// Examples of **incorrect** code for this rule:
+    /// ```js
+    /// if (condition) {
+    ///   // ...
+    /// } else {
+    ///   if (anotherCondition) {
+    ///     // ...
+    ///   }
+    /// }
+    /// ```
+    ///
+    /// ```js
+    /// if (condition) {
+    ///   // ...
+    /// } else {
+    ///   if (anotherCondition) {
+    ///     // ...
+    ///   } else {
+    ///       // ...
+    ///   }
+    /// }
+    /// ```
+    ///
+    /// Examples of **correct** code for this rule:
+    /// ```js
+    /// if (condition) {
+    ///   // ...
+    /// } else if (anotherCondition) {
+    ///   // ...
+    /// }
+    /// ```
+    ///
+    /// ```js
+    /// if (condition) {
+    ///   // ...
+    /// } else if (anotherCondition) {
+    ///   // ...
+    /// } else {
+    ///   // ...
+    /// }
+    /// ```
+    ///
+    /// ```js
+    /// if (condition) {
+    ///   // ...
+    /// } else {
+    ///   if (anotherCondition) {
+    ///     // ...
+    ///   }
+    ///   doSomething();
+    /// }
+    /// ```
+    NoLonelyIf,
+    eslint,
+    pedantic,
+    pending
+);
+
+impl Rule for NoLonelyIf {
+    fn run<'a>(&self, node: &AstNode<'a>, ctx: &LintContext<'a>) {
+        let AstKind::IfStatement(if_stmt) = node.kind() else {
+            return;
+        };
+
+        let Some(ref alternate) = if_stmt.alternate else {
+            return;
+        };
+
+        let BlockStatement(b) = alternate else {
+            return;
+        };
+
+        if let Some(AstKind::IfStatement(_)) = ctx.nodes().parent_node(node.id()).map(AstNode::kind)
+        {
+            return;
+        };
+
+        if b.body.len() == 1 {
path: crates/oxc_linter/src/rules/eslint/no_lonely_if.rs
user: login: camchenry
body: can we move this check above `parent_kind` check? this is a cheaper check to do first
created_at: 2025-03-12T03:10:47Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,168 @@
+use crate::{AstNode, context::LintContext, rule::Rule};
+use oxc_ast::AstKind;
+use oxc_ast::ast::{Statement, Statement::BlockStatement};
+use oxc_diagnostics::OxcDiagnostic;
+use oxc_macros::declare_oxc_lint;
+use oxc_span::Span;
+
+fn no_lonely_if_diagnostic(span: Span) -> OxcDiagnostic {
+    OxcDiagnostic::warn("Unexpected `if` as the only statement in an `else` block")
+        .with_help("Consider using `else if` instead.")
+        .with_label(span)
+}
+
+#[derive(Debug, Default, Clone)]
+pub struct NoLonelyIf;
+
+declare_oxc_lint!(
+    /// ### What it does
+    ///
+    /// Disallow `if` statements as the only statement in `else` blocks
+    ///
+    /// ### Why is this bad?
+    ///
+    /// When an `if` statement is the only statement in an `else` block, it is often clearer to use
+    /// an `else if` instead.
+    ///
+    /// ### Examples
+    ///
+    /// Examples of **incorrect** code for this rule:
+    /// ```js
+    /// if (condition) {
+    ///   // ...
+    /// } else {
+    ///   if (anotherCondition) {
+    ///     // ...
+    ///   }
+    /// }
+    /// ```
+    ///
+    /// ```js
+    /// if (condition) {
+    ///   // ...
+    /// } else {
+    ///   if (anotherCondition) {
+    ///     // ...
+    ///   } else {
+    ///       // ...
+    ///   }
+    /// }
+    /// ```
+    ///
+    /// Examples of **correct** code for this rule:
+    /// ```js
+    /// if (condition) {
+    ///   // ...
+    /// } else if (anotherCondition) {
+    ///   // ...
+    /// }
+    /// ```
+    ///
+    /// ```js
+    /// if (condition) {
+    ///   // ...
+    /// } else if (anotherCondition) {
+    ///   // ...
+    /// } else {
+    ///   // ...
+    /// }
+    /// ```
+    ///
+    /// ```js
+    /// if (condition) {
+    ///   // ...
+    /// } else {
+    ///   if (anotherCondition) {
+    ///     // ...
+    ///   }
+    ///   doSomething();
+    /// }
+    /// ```
+    NoLonelyIf,
+    eslint,
+    pedantic,
+    pending
+);
+
+impl Rule for NoLonelyIf {
+    fn run<'a>(&self, node: &AstNode<'a>, ctx: &LintContext<'a>) {
+        let AstKind::IfStatement(if_stmt) = node.kind() else {
+            return;
+        };
+
+        let Some(ref alternate) = if_stmt.alternate else {
+            return;
+        };
+
+        let BlockStatement(b) = alternate else {
+            return;
+        };
+
+        if let Some(AstKind::IfStatement(_)) = ctx.nodes().parent_node(node.id()).map(AstNode::kind)
+        {
+            return;
+        };
+
+        if b.body.len() == 1 {
+            let Some(only_stmt) = b.body.first() else {
+                return;
+            };
+
+            if let Statement::BlockStatement(inner_block) = only_stmt {
+                if inner_block.body.len() == 1 {
+                    if let Some(Statement::IfStatement(lonely_if)) = inner_block.body.first() {
+                        ctx.diagnostic(no_lonely_if_diagnostic(Span::new(
+                            lonely_if.span.start,
+                            lonely_if.span.start + 2,
+                        )));
+                    };
+                }
+            } else if let Statement::IfStatement(lonely_if) = only_stmt {
+                ctx.diagnostic(no_lonely_if_diagnostic(Span::new(
+                    lonely_if.span.start,
+                    lonely_if.span.start + 2,
+                )));
+            };
+        }
+    }
+}
+
+#[test]
+fn test() {
+    use crate::tester::Tester;
+
+    let pass = vec![
+        "if (a) {;} else if (b) {;}",
+        "if (a) {;} else { if (b) {;} ; }",
+        "if (a) if (a) {} else { if (b) {} } else {}",
+        "if (a) {
+           if (b) {} else { }
+         } else {
+           if (c) {  } 
+           if (d) {  } 
+          }",
+    ];
+
+    let fail = vec![
path: crates/oxc_linter/src/rules/eslint/no_lonely_if.rs
user: login: therewillbecode
body: Sure thing, added them!
created_at: 2025-03-12T22:12:10Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,296 @@
+use crate::{AstNode, context::LintContext, rule::Rule};
+use oxc_ast::AstKind;
+use oxc_ast::ast::{Statement, Statement::BlockStatement};
+use oxc_diagnostics::OxcDiagnostic;
+use oxc_macros::declare_oxc_lint;
+use oxc_span::Span;
+
+fn no_lonely_if_diagnostic(span: Span) -> OxcDiagnostic {
+    OxcDiagnostic::warn("Unexpected `if` as the only statement in an `else` block")
+        .with_help("Consider using `else if` instead.")
+        .with_label(span)
+}
+
+#[derive(Debug, Default, Clone)]
+pub struct NoLonelyIf;
+
+declare_oxc_lint!(
+    /// ### What it does
+    ///
+    /// Disallow `if` statements as the only statement in `else` blocks
+    ///
+    /// ### Why is this bad?
+    ///
+    /// When an `if` statement is the only statement in an `else` block, it is often clearer to use
+    /// an `else if` instead.
+    ///
+    /// ### Examples
+    ///
+    /// Examples of **incorrect** code for this rule:
+    /// ```js
+    /// if (condition) {
+    ///   // ...
+    /// } else {
+    ///   if (anotherCondition) {
+    ///     // ...
+    ///   }
+    /// }
+    /// ```
+    ///
+    /// ```js
+    /// if (condition) {
+    ///   // ...
+    /// } else {
+    ///   if (anotherCondition) {
+    ///     // ...
+    ///   } else {
+    ///       // ...
+    ///   }
+    /// }
+    /// ```
+    ///
+    /// Examples of **correct** code for this rule:
+    /// ```js
+    /// if (condition) {
+    ///   // ...
+    /// } else if (anotherCondition) {
+    ///   // ...
+    /// }
+    /// ```
+    ///
+    /// ```js
+    /// if (condition) {
+    ///   // ...
+    /// } else if (anotherCondition) {
+    ///   // ...
+    /// } else {
+    ///   // ...
+    /// }
+    /// ```
+    ///
+    /// ```js
+    /// if (condition) {
+    ///   // ...
+    /// } else {
+    ///   if (anotherCondition) {
+    ///     // ...
+    ///   }
+    ///   doSomething();
+    /// }
+    /// ```
+    NoLonelyIf,
+    eslint,
+    pedantic,
+    pending
+);
+
+impl Rule for NoLonelyIf {
+    fn run<'a>(&self, node: &AstNode<'a>, ctx: &LintContext<'a>) {
+        let AstKind::IfStatement(if_stmt) = node.kind() else {
+            return;
+        };
+
+        let Some(ref alternate) = if_stmt.alternate else {
+            return;
+        };
+
+        let BlockStatement(b) = alternate else {
+            return;
+        };
+
+        let Some(only_stmt) = b.body.first() else {
+            return;
+        };
+
+        if let Some(AstKind::IfStatement(_)) = ctx.nodes().parent_kind(node.id()) {
+            return;
+        };
+
+        if b.body.len() != 1 {
+            return;
+        };
path: crates/oxc_linter/src/rules/eslint/no_lonely_if.rs
user: login: camchenry
body: these are still mis-ordered I think, more expensive checks should happen as far down as possible:
```suggestion
        if b.body.len() != 1 {
            return;
        };
        
        let Some(only_stmt) = b.body.first() else {
            return;
        };

        if let Some(AstKind::IfStatement(_)) = ctx.nodes().parent_kind(node.id()) {
            return;
        };        
```
created_at: 2025-03-13T03:32:24Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,296 @@
+use crate::{AstNode, context::LintContext, rule::Rule};
+use oxc_ast::AstKind;
+use oxc_ast::ast::{Statement, Statement::BlockStatement};
+use oxc_diagnostics::OxcDiagnostic;
+use oxc_macros::declare_oxc_lint;
+use oxc_span::Span;
+
+fn no_lonely_if_diagnostic(span: Span) -> OxcDiagnostic {
+    OxcDiagnostic::warn("Unexpected `if` as the only statement in an `else` block")
+        .with_help("Consider using `else if` instead.")
+        .with_label(span)
+}
+
+#[derive(Debug, Default, Clone)]
+pub struct NoLonelyIf;
+
+declare_oxc_lint!(
+    /// ### What it does
+    ///
+    /// Disallow `if` statements as the only statement in `else` blocks
+    ///
+    /// ### Why is this bad?
+    ///
+    /// When an `if` statement is the only statement in an `else` block, it is often clearer to use
+    /// an `else if` instead.
+    ///
+    /// ### Examples
+    ///
+    /// Examples of **incorrect** code for this rule:
+    /// ```js
+    /// if (condition) {
+    ///   // ...
+    /// } else {
+    ///   if (anotherCondition) {
+    ///     // ...
+    ///   }
+    /// }
+    /// ```
+    ///
+    /// ```js
+    /// if (condition) {
+    ///   // ...
+    /// } else {
+    ///   if (anotherCondition) {
+    ///     // ...
+    ///   } else {
+    ///       // ...
+    ///   }
+    /// }
+    /// ```
+    ///
+    /// Examples of **correct** code for this rule:
+    /// ```js
+    /// if (condition) {
+    ///   // ...
+    /// } else if (anotherCondition) {
+    ///   // ...
+    /// }
+    /// ```
+    ///
+    /// ```js
+    /// if (condition) {
+    ///   // ...
+    /// } else if (anotherCondition) {
+    ///   // ...
+    /// } else {
+    ///   // ...
+    /// }
+    /// ```
+    ///
+    /// ```js
+    /// if (condition) {
+    ///   // ...
+    /// } else {
+    ///   if (anotherCondition) {
+    ///     // ...
+    ///   }
+    ///   doSomething();
+    /// }
+    /// ```
+    NoLonelyIf,
+    eslint,
+    pedantic,
+    pending
+);
+
+impl Rule for NoLonelyIf {
+    fn run<'a>(&self, node: &AstNode<'a>, ctx: &LintContext<'a>) {
+        let AstKind::IfStatement(if_stmt) = node.kind() else {
+            return;
+        };
+
+        let Some(ref alternate) = if_stmt.alternate else {
+            return;
+        };
+
+        let BlockStatement(b) = alternate else {
+            return;
+        };
+
+        let Some(only_stmt) = b.body.first() else {
+            return;
+        };
+
+        if let Some(AstKind::IfStatement(_)) = ctx.nodes().parent_kind(node.id()) {
+            return;
+        };
+
+        if b.body.len() != 1 {
+            return;
+        };
+
+        if let Statement::BlockStatement(inner_block) = only_stmt {
+            if inner_block.body.len() == 1 {
+                if let Some(Statement::IfStatement(lonely_if)) = inner_block.body.first() {
+                    ctx.diagnostic(no_lonely_if_diagnostic(Span::new(
+                        lonely_if.span.start,
+                        lonely_if.span.start + 2,
+                    )));
path: crates/oxc_linter/src/rules/eslint/no_lonely_if.rs
user: login: camchenry
body: can use `Span::sized` to make this shorter:
```suggestion
                    ctx.diagnostic(no_lonely_if_diagnostic(Span::sized(
                        lonely_if.span.start,
                        2,
                    )));
```
created_at: 2025-03-13T03:33:18Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,296 @@
+use crate::{AstNode, context::LintContext, rule::Rule};
+use oxc_ast::AstKind;
+use oxc_ast::ast::{Statement, Statement::BlockStatement};
+use oxc_diagnostics::OxcDiagnostic;
+use oxc_macros::declare_oxc_lint;
+use oxc_span::Span;
+
+fn no_lonely_if_diagnostic(span: Span) -> OxcDiagnostic {
+    OxcDiagnostic::warn("Unexpected `if` as the only statement in an `else` block")
+        .with_help("Consider using `else if` instead.")
+        .with_label(span)
+}
+
+#[derive(Debug, Default, Clone)]
+pub struct NoLonelyIf;
+
+declare_oxc_lint!(
+    /// ### What it does
+    ///
+    /// Disallow `if` statements as the only statement in `else` blocks
+    ///
+    /// ### Why is this bad?
+    ///
+    /// When an `if` statement is the only statement in an `else` block, it is often clearer to use
+    /// an `else if` instead.
+    ///
+    /// ### Examples
+    ///
+    /// Examples of **incorrect** code for this rule:
+    /// ```js
+    /// if (condition) {
+    ///   // ...
+    /// } else {
+    ///   if (anotherCondition) {
+    ///     // ...
+    ///   }
+    /// }
+    /// ```
+    ///
+    /// ```js
+    /// if (condition) {
+    ///   // ...
+    /// } else {
+    ///   if (anotherCondition) {
+    ///     // ...
+    ///   } else {
+    ///       // ...
+    ///   }
+    /// }
+    /// ```
+    ///
+    /// Examples of **correct** code for this rule:
+    /// ```js
+    /// if (condition) {
+    ///   // ...
+    /// } else if (anotherCondition) {
+    ///   // ...
+    /// }
+    /// ```
+    ///
+    /// ```js
+    /// if (condition) {
+    ///   // ...
+    /// } else if (anotherCondition) {
+    ///   // ...
+    /// } else {
+    ///   // ...
+    /// }
+    /// ```
+    ///
+    /// ```js
+    /// if (condition) {
+    ///   // ...
+    /// } else {
+    ///   if (anotherCondition) {
+    ///     // ...
+    ///   }
+    ///   doSomething();
+    /// }
+    /// ```
+    NoLonelyIf,
+    eslint,
+    pedantic,
+    pending
+);
+
+impl Rule for NoLonelyIf {
+    fn run<'a>(&self, node: &AstNode<'a>, ctx: &LintContext<'a>) {
+        let AstKind::IfStatement(if_stmt) = node.kind() else {
+            return;
+        };
+
+        let Some(ref alternate) = if_stmt.alternate else {
+            return;
+        };
+
+        let BlockStatement(b) = alternate else {
+            return;
+        };
+
+        let Some(only_stmt) = b.body.first() else {
+            return;
+        };
+
+        if let Some(AstKind::IfStatement(_)) = ctx.nodes().parent_kind(node.id()) {
+            return;
+        };
+
+        if b.body.len() != 1 {
+            return;
+        };
+
+        if let Statement::BlockStatement(inner_block) = only_stmt {
+            if inner_block.body.len() == 1 {
+                if let Some(Statement::IfStatement(lonely_if)) = inner_block.body.first() {
+                    ctx.diagnostic(no_lonely_if_diagnostic(Span::new(
+                        lonely_if.span.start,
+                        lonely_if.span.start + 2,
+                    )));
+                };
+            }
+        } else if let Statement::IfStatement(lonely_if) = only_stmt {
+            ctx.diagnostic(no_lonely_if_diagnostic(Span::new(
+                lonely_if.span.start,
+                lonely_if.span.start + 2,
+            )));
path: crates/oxc_linter/src/rules/eslint/no_lonely_if.rs
user: login: camchenry
body: ```suggestion
            ctx.diagnostic(no_lonely_if_diagnostic(Span::sized(
                lonely_if.span.start,
                2,
            )));
```
created_at: 2025-03-13T03:33:35Z
author_association: MEMBER

PR comments:
user: login: graphite-app[bot]
created_at: 2025-03-10T22:36:30Z
body: ## How to use the Graphite Merge Queue

Add either label to this PR to merge it via the merge queue:
- <kbd>0-merge</kbd> - adds this PR to the back of the merge queue
- <kbd>hotfix</kbd> - for urgent hot fixes, skip the queue and merge this PR next

You must have a Graphite account in order to use the merge queue. Sign up using [this link](https://app.graphite.dev/invite/github/oxc-project?ref=merge-queue-instructions-comment&prId=5980231250).

<h2></h2>
<sub>

An organization admin has enabled the [Graphite Merge Queue](https://graphite.dev/docs/graphite-merge-queue) in this repository.

</sub>

<sub>

Please do not merge from GitHub as this will restart CI on PRs being processed by the merge queue.

</sub>
author_association: CONTRIBUTOR
user: login: codspeed-hq[bot]
created_at: 2025-03-10T22:45:54Z
body: <!-- __CODSPEED_PERFORMANCE_REPORT_COMMENT__ -->
<!-- __CODSPEED_INSTRUMENTATION_PERFORMANCE_REPORT_COMMENT__ -->

## [CodSpeed Performance Report](https://codspeed.io/oxc-project/oxc/branches/therewillbecode%3Afeat%2Feslint-no-lonely-if)

### Merging #9660 will **not alter performance**

<sub>Comparing <code>therewillbecode:feat/eslint-no-lonely-if</code> (b4f380d) with <code>main</code> (a6c9b09)</sub>



### Summary

`✅ 33` untouched benchmarks  




author_association: NONE
user: login: therewillbecode
created_at: 2025-03-12T22:16:17Z
body: Thanks so much for the review @camchenry.

I have implemented all the feedback so far.
author_association: CONTRIBUTOR
user: login: camchenry
created_at: 2025-03-13T13:20:38Z
body: Thanks for the contribution!
author_association: MEMBER
user: login: therewillbecode
created_at: 2025-03-13T13:55:30Z
body: > Thanks for the contribution!

My pleasure. Thanks again for the helpful review. @camchenry 
author_association: CONTRIBUTOR