Skip to content

Commit 8dd6809

Browse files
authoredMar 13, 2025··
feat(linter): Add eslint/no-lonely-if (#9660)
Relates to #479 . Rule details: https://eslint.org/docs/latest/rules/no-lonely-if
1 parent 7856a0b commit 8dd6809

File tree

3 files changed

+376
-0
lines changed

3 files changed

+376
-0
lines changed
 

‎crates/oxc_linter/src/rules.rs

+2
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,7 @@ mod eslint {
104104
pub mod no_label_var;
105105
pub mod no_labels;
106106
pub mod no_lone_blocks;
107+
pub mod no_lonely_if;
107108
pub mod no_loss_of_precision;
108109
pub mod no_magic_numbers;
109110
pub mod no_multi_assign;
@@ -577,6 +578,7 @@ oxc_macros::declare_all_lint_rules! {
577578
eslint::max_lines,
578579
eslint::max_params,
579580
eslint::new_cap,
581+
eslint::no_lonely_if,
580582
eslint::no_useless_call,
581583
eslint::no_unneeded_ternary,
582584
eslint::no_extra_label,
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,290 @@
1+
use crate::{AstNode, context::LintContext, rule::Rule};
2+
use oxc_ast::AstKind;
3+
use oxc_ast::ast::{Statement, Statement::BlockStatement};
4+
use oxc_diagnostics::OxcDiagnostic;
5+
use oxc_macros::declare_oxc_lint;
6+
use oxc_span::Span;
7+
8+
fn no_lonely_if_diagnostic(span: Span) -> OxcDiagnostic {
9+
OxcDiagnostic::warn("Unexpected `if` as the only statement in an `else` block")
10+
.with_help("Consider using `else if` instead.")
11+
.with_label(span)
12+
}
13+
14+
#[derive(Debug, Default, Clone)]
15+
pub struct NoLonelyIf;
16+
17+
declare_oxc_lint!(
18+
/// ### What it does
19+
///
20+
/// Disallow `if` statements as the only statement in `else` blocks
21+
///
22+
/// ### Why is this bad?
23+
///
24+
/// When an `if` statement is the only statement in an `else` block, it is often clearer to use
25+
/// an `else if` instead.
26+
///
27+
/// ### Examples
28+
///
29+
/// Examples of **incorrect** code for this rule:
30+
/// ```js
31+
/// if (condition) {
32+
/// // ...
33+
/// } else {
34+
/// if (anotherCondition) {
35+
/// // ...
36+
/// }
37+
/// }
38+
/// ```
39+
///
40+
/// ```js
41+
/// if (condition) {
42+
/// // ...
43+
/// } else {
44+
/// if (anotherCondition) {
45+
/// // ...
46+
/// } else {
47+
/// // ...
48+
/// }
49+
/// }
50+
/// ```
51+
///
52+
/// Examples of **correct** code for this rule:
53+
/// ```js
54+
/// if (condition) {
55+
/// // ...
56+
/// } else if (anotherCondition) {
57+
/// // ...
58+
/// }
59+
/// ```
60+
///
61+
/// ```js
62+
/// if (condition) {
63+
/// // ...
64+
/// } else if (anotherCondition) {
65+
/// // ...
66+
/// } else {
67+
/// // ...
68+
/// }
69+
/// ```
70+
///
71+
/// ```js
72+
/// if (condition) {
73+
/// // ...
74+
/// } else {
75+
/// if (anotherCondition) {
76+
/// // ...
77+
/// }
78+
/// doSomething();
79+
/// }
80+
/// ```
81+
NoLonelyIf,
82+
eslint,
83+
pedantic,
84+
pending
85+
);
86+
87+
impl Rule for NoLonelyIf {
88+
fn run<'a>(&self, node: &AstNode<'a>, ctx: &LintContext<'a>) {
89+
let AstKind::IfStatement(if_stmt) = node.kind() else {
90+
return;
91+
};
92+
93+
let Some(ref alternate) = if_stmt.alternate else {
94+
return;
95+
};
96+
97+
let BlockStatement(b) = alternate else {
98+
return;
99+
};
100+
101+
if b.body.len() != 1 {
102+
return;
103+
};
104+
105+
let Some(only_stmt) = b.body.first() else {
106+
return;
107+
};
108+
109+
if let Some(AstKind::IfStatement(_)) = ctx.nodes().parent_kind(node.id()) {
110+
return;
111+
};
112+
113+
if let Statement::BlockStatement(inner_block) = only_stmt {
114+
if inner_block.body.len() == 1 {
115+
if let Some(Statement::IfStatement(lonely_if)) = inner_block.body.first() {
116+
ctx.diagnostic(no_lonely_if_diagnostic(Span::sized(lonely_if.span.start, 2)));
117+
};
118+
}
119+
} else if let Statement::IfStatement(lonely_if) = only_stmt {
120+
ctx.diagnostic(no_lonely_if_diagnostic(Span::sized(lonely_if.span.start, 2)));
121+
};
122+
}
123+
}
124+
125+
#[test]
126+
fn test() {
127+
use crate::tester::Tester;
128+
129+
let pass = vec![
130+
"if (a) {;} else if (b) {;}",
131+
"if (a) {;} else { if (b) {;} ; }",
132+
"if (a) if (a) {} else { if (b) {} } else {}",
133+
"if (a) {
134+
if (b) {} else { }
135+
} else {
136+
if (c) { }
137+
if (d) { }
138+
}",
139+
];
140+
141+
let fail = vec![
142+
"if (a) {;} else { if (b) {;} }",
143+
"if (foo) {} else { if (bar) baz(); }",
144+
"if (foo) {} else { if (bar) baz() } qux();",
145+
"if (foo) {} else { if (bar) baz(); } qux();",
146+
"if (a) {
147+
foo();
148+
} else {
149+
/* otherwise, do the other thing */ if (b) {
150+
bar();
151+
}
152+
}",
153+
"if (a) {
154+
foo();
155+
} else {
156+
if (b) {
157+
bar();
158+
} /* this comment will prevent this test case from being autofixed. */
159+
}",
160+
// No fix; removing the braces would cause a SyntaxError.
161+
"if (foo) {
162+
} else {
163+
if (bar) baz();
164+
}
165+
qux();",
166+
// Not fixed; removing the braces would change the semantics due to ASI.
167+
"if (foo) {
168+
} else {
169+
if (bar) baz();
170+
}
171+
[1, 2, 3].forEach(foo);",
172+
// Not fixed; removing the braces would change the semantics due to ASI.
173+
"if (foo) { } else {
174+
if (bar) baz++;
175+
}
176+
foo;",
177+
// Not fixed; bar() would be interpreted as a template literal tag
178+
"if (a) {
179+
foo();
180+
} else {
181+
if (b) bar();
182+
}
183+
`template literal`;",
184+
];
185+
186+
/* Pending
187+
let fix = vec![
188+
(
189+
"if (a) {
190+
foo();
191+
} else {
192+
if (b) {
193+
bar();
194+
}
195+
}",
196+
"if (a) {
197+
foo();
198+
} else if (b) {
199+
bar();
200+
}",
201+
None,
202+
),
203+
(
204+
"if (a) {
205+
foo();
206+
} /* comment */
207+
else {
208+
if (b) {
209+
bar();
210+
}
211+
}",
212+
"if (a) {
213+
foo();
214+
} /* comment */
215+
else {
216+
if (b) {
217+
bar();
218+
}
219+
}",
220+
None,
221+
),
222+
(
223+
"if (a) {
224+
foo();
225+
} else {
226+
if ( /* this comment is ok */
227+
b) {
228+
bar();
229+
}
230+
}",
231+
"if (a) {
232+
foo();
233+
} else if ( /* this comment is ok */
234+
b) {
235+
bar();
236+
}",
237+
None,
238+
),
239+
(
240+
"if (foo) {} else { if (bar) baz(); }",
241+
"if (foo) {} else if (bar) baz();",
242+
None,
243+
),
244+
(
245+
"if (foo) { } else { if (bar) baz(); } qux();",
246+
"if (foo) { } else if (bar) baz(); qux();",
247+
None,
248+
),
249+
(
250+
"if (foo) { } else { if (bar) baz++; } foo;",
251+
"if (foo) { } else if (bar) baz++; foo;",
252+
None,
253+
),
254+
(
255+
"if (a) {
256+
foo();
257+
} else {
258+
if (b) {
259+
bar();
260+
} else if (c) {
261+
baz();
262+
} else {
263+
qux();
264+
}
265+
}",
266+
"if (a) {
267+
foo();
268+
} else if (b) {
269+
bar();
270+
} else if (c) {
271+
baz();
272+
} else {
273+
qux();
274+
}",
275+
None,
276+
),
277+
("if (a) {;} else { if (b) {;} }", "if (a) {;} else if (b) {;}", None),
278+
("if (foo) {} else { if (bar) baz(); }", "if (foo) {} else if (bar) baz();", None),
279+
(
280+
"if (foo) {} else { if (bar) baz(); } qux();",
281+
"if (foo) {} else if (bar) baz(); qux();",
282+
None,
283+
),
284+
];
285+
*/
286+
287+
Tester::new(NoLonelyIf::NAME, NoLonelyIf::PLUGIN, pass, fail)
288+
// .expect_fix(fix)
289+
.test_and_snapshot();
290+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
---
2+
source: crates/oxc_linter/src/tester.rs
3+
---
4+
eslint(no-lonely-if): Unexpected `if` as the only statement in an `else` block
5+
╭─[no_lonely_if.tsx:1:19]
6+
1if (a) {;} else { if (b) {;} }
7+
· ──
8+
╰────
9+
help: Consider using `else if` instead.
10+
11+
eslint(no-lonely-if): Unexpected `if` as the only statement in an `else` block
12+
╭─[no_lonely_if.tsx:1:20]
13+
1if (foo) {} else { if (bar) baz(); }
14+
· ──
15+
╰────
16+
help: Consider using `else if` instead.
17+
18+
eslint(no-lonely-if): Unexpected `if` as the only statement in an `else` block
19+
╭─[no_lonely_if.tsx:1:20]
20+
1if (foo) {} else { if (bar) baz() } qux();
21+
· ──
22+
╰────
23+
help: Consider using `else if` instead.
24+
25+
eslint(no-lonely-if): Unexpected `if` as the only statement in an `else` block
26+
╭─[no_lonely_if.tsx:1:20]
27+
1if (foo) {} else { if (bar) baz(); } qux();
28+
· ──
29+
╰────
30+
help: Consider using `else if` instead.
31+
32+
eslint(no-lonely-if): Unexpected `if` as the only statement in an `else` block
33+
╭─[no_lonely_if.tsx:4:48]
34+
3 │ } else {
35+
4/* otherwise, do the other thing */ if (b) {
36+
· ──
37+
5bar();
38+
╰────
39+
help: Consider using `else if` instead.
40+
41+
eslint(no-lonely-if): Unexpected `if` as the only statement in an `else` block
42+
╭─[no_lonely_if.tsx:4:12]
43+
3 │ } else {
44+
4if (b) {
45+
· ──
46+
5bar();
47+
╰────
48+
help: Consider using `else if` instead.
49+
50+
eslint(no-lonely-if): Unexpected `if` as the only statement in an `else` block
51+
╭─[no_lonely_if.tsx:3:12]
52+
2 │ } else {
53+
3if (bar) baz();
54+
· ──
55+
4 │ }
56+
╰────
57+
help: Consider using `else if` instead.
58+
59+
eslint(no-lonely-if): Unexpected `if` as the only statement in an `else` block
60+
╭─[no_lonely_if.tsx:3:12]
61+
2 │ } else {
62+
3if (bar) baz();
63+
· ──
64+
4 │ }
65+
╰────
66+
help: Consider using `else if` instead.
67+
68+
eslint(no-lonely-if): Unexpected `if` as the only statement in an `else` block
69+
╭─[no_lonely_if.tsx:2:12]
70+
1if (foo) { } else {
71+
2if (bar) baz++;
72+
· ──
73+
3 │ }
74+
╰────
75+
help: Consider using `else if` instead.
76+
77+
eslint(no-lonely-if): Unexpected `if` as the only statement in an `else` block
78+
╭─[no_lonely_if.tsx:4:12]
79+
3 │ } else {
80+
4if (b) bar();
81+
· ──
82+
5 │ }
83+
╰────
84+
help: Consider using `else if` instead.

0 commit comments

Comments
 (0)
Please sign in to comment.