Skip to content

Commit 9f9d25f

Browse files
committed
feat(linter/oxc): implement branches-sharing-code (#14440)
1 parent 6e061f6 commit 9f9d25f

File tree

4 files changed

+584
-0
lines changed

4 files changed

+584
-0
lines changed

crates/oxc_linter/src/generated/rule_runner_impls.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1461,6 +1461,11 @@ impl RuleRunner for crate::rules::oxc::bad_replace_all_arg::BadReplaceAllArg {
14611461
Some(&AstTypesBitset::from_types(&[AstType::CallExpression]));
14621462
}
14631463

1464+
impl RuleRunner for crate::rules::oxc::branches_sharing_code::BranchesSharingCode {
1465+
const NODE_TYPES: Option<&AstTypesBitset> =
1466+
Some(&AstTypesBitset::from_types(&[AstType::IfStatement]));
1467+
}
1468+
14641469
impl RuleRunner for crate::rules::oxc::const_comparisons::ConstComparisons {
14651470
const NODE_TYPES: Option<&AstTypesBitset> = None;
14661471
}

crates/oxc_linter/src/rules.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -543,6 +543,7 @@ pub(crate) mod oxc {
543543
pub mod bad_min_max_func;
544544
pub mod bad_object_literal_comparison;
545545
pub mod bad_replace_all_arg;
546+
pub mod branches_sharing_code;
546547
pub mod const_comparisons;
547548
pub mod double_comparisons;
548549
pub mod erasing_op;
@@ -969,6 +970,7 @@ oxc_macros::declare_all_lint_rules! {
969970
oxc::bad_min_max_func,
970971
oxc::bad_object_literal_comparison,
971972
oxc::bad_replace_all_arg,
973+
oxc::branches_sharing_code,
972974
oxc::const_comparisons,
973975
oxc::double_comparisons,
974976
oxc::erasing_op,
Lines changed: 345 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,345 @@
1+
// Inspired by https://github.com/rust-lang/rust-clippy/blob/95f5a00d8628fba223c802251af3c42547927c5a/clippy_lints/src/ifs/branches_sharing_code.rs
2+
use oxc_ast::{
3+
AstKind,
4+
ast::{Expression, IfStatement, Statement},
5+
};
6+
use oxc_diagnostics::OxcDiagnostic;
7+
use oxc_macros::declare_oxc_lint;
8+
use oxc_span::{ContentEq, GetSpan, Span};
9+
10+
use crate::{AstNode, context::LintContext, rule::Rule};
11+
12+
fn branches_sharing_code_at_start_diagnostic(
13+
span: Span,
14+
duplicated_code_spans: impl Iterator<Item = Span>,
15+
) -> OxcDiagnostic {
16+
OxcDiagnostic::warn("All `if` blocks contain the same code at the start")
17+
.with_help("Move the shared code outside the `if` statement to reduce code duplication")
18+
.with_labels(
19+
std::iter::once(span.primary_label("`if` statement declared here"))
20+
.chain(duplicated_code_spans.map(Into::into)),
21+
)
22+
}
23+
24+
fn branches_sharing_code_at_end_diagnostic(
25+
span: Span,
26+
duplicated_code_spans: impl Iterator<Item = Span>,
27+
) -> OxcDiagnostic {
28+
OxcDiagnostic::warn("All `if` blocks contain the same code at the end")
29+
.with_help("Move the shared code outside the `if` statement to reduce code duplication")
30+
.with_labels(
31+
std::iter::once(span.primary_label("`if` statement declared here"))
32+
.chain(duplicated_code_spans.map(Into::into)),
33+
)
34+
}
35+
36+
#[derive(Debug, Default, Clone)]
37+
pub struct BranchesSharingCode;
38+
39+
declare_oxc_lint!(
40+
/// ### What it does
41+
///
42+
/// Checks if the `if` and `else` blocks contain shared code that can be moved out of the blocks.
43+
///
44+
/// ### Why is this bad?
45+
///
46+
/// Duplicate code is less maintainable. Extracting common code from branches makes the code more DRY (Don't Repeat Yourself)
47+
/// and easier to maintain.
48+
///
49+
/// ### Examples
50+
///
51+
/// Examples of **incorrect** code for this rule:
52+
/// ```javascript
53+
/// let foo = if (condition) {
54+
/// console.log("Hello");
55+
/// 13
56+
/// } else {
57+
/// console.log("Hello");
58+
/// 42
59+
/// };
60+
///
61+
/// if (condition) {
62+
/// doSomething();
63+
/// cleanup();
64+
/// } else {
65+
/// doSomethingElse();
66+
/// cleanup();
67+
/// }
68+
/// ```
69+
///
70+
/// Examples of **correct** code for this rule:
71+
/// ```javascript
72+
/// console.log("Hello");
73+
/// let foo = if (condition) {
74+
/// 13
75+
/// } else {
76+
/// 42
77+
/// };
78+
///
79+
/// if (condition) {
80+
/// doSomething();
81+
/// } else {
82+
/// doSomethingElse();
83+
/// }
84+
/// cleanup();
85+
/// ```
86+
BranchesSharingCode,
87+
oxc,
88+
nursery
89+
);
90+
91+
impl Rule for BranchesSharingCode {
92+
fn run<'a>(&self, node: &AstNode<'a>, ctx: &LintContext<'a>) {
93+
let AstKind::IfStatement(if_stmt) = node.kind() else {
94+
return;
95+
};
96+
97+
let (conditions, bodies) = extract_if_sequence(if_stmt);
98+
99+
if bodies.len() < 2 || bodies.len() == conditions.len() {
100+
return;
101+
}
102+
103+
let (start_eq, end_eq) = scan_blocks_for_eq(&bodies);
104+
105+
let if_span = Span::new(if_stmt.span.start, if_stmt.span.start + 2);
106+
107+
if let Some(start) = start_eq {
108+
let spans = bodies.iter().map(|body| get_duplicated_span(start, body, false));
109+
ctx.diagnostic(branches_sharing_code_at_start_diagnostic(if_span, spans));
110+
}
111+
112+
if let Some(end) = end_eq {
113+
let spans = bodies.iter().map(|body| get_duplicated_span(end, body, true));
114+
ctx.diagnostic(branches_sharing_code_at_end_diagnostic(if_span, spans));
115+
}
116+
}
117+
}
118+
119+
fn get_block_statements<'a>(stmt: &'a Statement<'a>) -> &'a [Statement<'a>] {
120+
match stmt {
121+
Statement::BlockStatement(block) => &block.body,
122+
_ => &[],
123+
}
124+
}
125+
126+
fn get_duplicated_span(count: usize, body: &Statement, reverse: bool) -> Span {
127+
let stmts = get_block_statements(body);
128+
let range = if reverse { &stmts[stmts.len() - count..] } else { &stmts[..count] };
129+
let start = range.first().map(|s| s.span().start).unwrap();
130+
let end = range.last().map(|s| s.span().end).unwrap();
131+
Span::new(start, end)
132+
}
133+
134+
fn scan_blocks_for_eq<'a>(bodies: &[&'a Statement<'a>]) -> (Option<usize>, Option<usize>) {
135+
let first_stmts = get_block_statements(bodies[0]);
136+
137+
let min_stmt_count =
138+
bodies.iter().map(|body| get_block_statements(body).len()).min().unwrap_or(0);
139+
140+
let start_end_eq = first_stmts
141+
.iter()
142+
.enumerate()
143+
.take_while(|(i, stmt)| {
144+
bodies[1..].iter().all(|body| {
145+
let stmts = get_block_statements(body);
146+
stmts.get(*i).is_some_and(|s| s.content_eq(stmt))
147+
})
148+
})
149+
.count();
150+
151+
if start_end_eq >= min_stmt_count {
152+
return (None, None);
153+
}
154+
155+
let max_end_search = min_stmt_count - start_end_eq;
156+
157+
let end_begin_eq = (1..=max_end_search)
158+
.take_while(|&offset| {
159+
bodies.iter().all(|body| {
160+
let stmts = get_block_statements(body);
161+
let idx = stmts.len() - offset;
162+
let stmt = &stmts[idx];
163+
let first_stmt = &first_stmts[first_stmts.len() - offset];
164+
stmt.content_eq(first_stmt)
165+
})
166+
})
167+
.count();
168+
169+
let has_remaining_code_for_start = start_end_eq > 0
170+
&& bodies.iter().any(|body| {
171+
let stmts = get_block_statements(body);
172+
stmts.len() > start_end_eq
173+
});
174+
175+
let has_remaining_code_for_end = end_begin_eq > 0
176+
&& bodies.iter().any(|body| {
177+
let stmts = get_block_statements(body);
178+
let start_idx = stmts.len().saturating_sub(end_begin_eq);
179+
start_idx > start_end_eq
180+
});
181+
182+
(
183+
if has_remaining_code_for_start { Some(start_end_eq) } else { None },
184+
if has_remaining_code_for_end { Some(end_begin_eq) } else { None },
185+
)
186+
}
187+
188+
fn extract_if_sequence<'a>(
189+
if_stmt: &'a IfStatement<'a>,
190+
) -> (Vec<&'a Expression<'a>>, Vec<&'a Statement<'a>>) {
191+
let mut conditions = Vec::new();
192+
let mut bodies = Vec::new();
193+
194+
let mut current_if = Some(if_stmt);
195+
while let Some(if_node) = current_if {
196+
conditions.push(&if_node.test);
197+
bodies.push(&if_node.consequent);
198+
199+
match &if_node.alternate {
200+
Some(Statement::IfStatement(else_if)) => {
201+
current_if = Some(else_if);
202+
}
203+
Some(else_stmt) => {
204+
bodies.push(else_stmt);
205+
current_if = None;
206+
}
207+
None => {
208+
current_if = None;
209+
}
210+
}
211+
}
212+
213+
(conditions, bodies)
214+
}
215+
216+
#[test]
217+
fn test() {
218+
use crate::tester::Tester;
219+
220+
let pass = vec![
221+
"if (condition) { foo(); } else { bar(); }",
222+
"if (condition) { foo(); bar(); } else { baz(); qux(); }",
223+
"if (condition) { foo(); }",
224+
"if (condition) { foo(); } else { foo(); bar(); }",
225+
"if (condition) { foo(); bar(); } else { foo(); }",
226+
"if (condition) { foo(); bar(); } else if (condition2) { foo(); }",
227+
r"
228+
if (condition) {
229+
doA();
230+
doB();
231+
} else {
232+
doC();
233+
doD();
234+
}
235+
",
236+
r"
237+
if (x > 0) {
238+
const a = 1;
239+
console.log('positive');
240+
} else {
241+
const a = 2;
242+
console.log('negative');
243+
}
244+
",
245+
"if (condition) {} else {}",
246+
];
247+
248+
let fail = vec![
249+
r"
250+
if (condition) {
251+
console.log('hello');
252+
doA();
253+
} else {
254+
console.log('hello');
255+
doB();
256+
}
257+
",
258+
r"
259+
if (condition) {
260+
doA();
261+
cleanup();
262+
} else {
263+
doB();
264+
cleanup();
265+
}
266+
",
267+
r"
268+
let foo;
269+
if (condition) {
270+
console.log('start');
271+
foo = 13;
272+
} else {
273+
console.log('start');
274+
foo = 42;
275+
}
276+
",
277+
r"
278+
if (x) {
279+
console.log('before');
280+
doX();
281+
console.log('after');
282+
} else {
283+
console.log('before');
284+
doY();
285+
console.log('after');
286+
}
287+
",
288+
r"
289+
if (flag) {
290+
initialize();
291+
processData();
292+
finalize();
293+
} else {
294+
initialize();
295+
processOtherData();
296+
finalize();
297+
}
298+
",
299+
r"
300+
if (test) {
301+
a++;
302+
b++;
303+
} else {
304+
a++;
305+
c++;
306+
}
307+
",
308+
r"
309+
if (x > 0) {
310+
const a = 1;
311+
console.log(a);
312+
} else {
313+
const a = 2;
314+
console.log(a);
315+
}
316+
",
317+
r"
318+
if (flag) {
319+
console.log('start');
320+
doA();
321+
} else if (otherFlag) {
322+
console.log('start');
323+
doB();
324+
} else {
325+
console.log('start');
326+
doC();
327+
}
328+
",
329+
r"
330+
if (x === 1) {
331+
setup();
332+
return 1;
333+
} else if (x === 2) {
334+
setup();
335+
return 2;
336+
} else {
337+
setup();
338+
return 3;
339+
}
340+
",
341+
];
342+
343+
Tester::new(BranchesSharingCode::NAME, BranchesSharingCode::PLUGIN, pass, fail)
344+
.test_and_snapshot();
345+
}

0 commit comments

Comments
 (0)