Skip to content

Commit d1df732

Browse files
committed
A new lint for shared code in if blocks
* Added expression check for shared_code_in_if_blocks * Finishing touches for the shared_code_in_if_blocks lint * Applying PR suggestions * Update lints yay * Moved test into subfolder
1 parent 232e2b7 commit d1df732

17 files changed

+737
-164
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2455,6 +2455,7 @@ Released 2018-09-13
24552455
[`shadow_reuse`]: https://rust-lang.github.io/rust-clippy/master/index.html#shadow_reuse
24562456
[`shadow_same`]: https://rust-lang.github.io/rust-clippy/master/index.html#shadow_same
24572457
[`shadow_unrelated`]: https://rust-lang.github.io/rust-clippy/master/index.html#shadow_unrelated
2458+
[`shared_code_in_if_blocks`]: https://rust-lang.github.io/rust-clippy/master/index.html#shared_code_in_if_blocks
24582459
[`short_circuit_statement`]: https://rust-lang.github.io/rust-clippy/master/index.html#short_circuit_statement
24592460
[`should_assert_eq`]: https://rust-lang.github.io/rust-clippy/master/index.html#should_assert_eq
24602461
[`should_implement_trait`]: https://rust-lang.github.io/rust-clippy/master/index.html#should_implement_trait

clippy_lints/src/copies.rs

Lines changed: 280 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,10 @@ use clippy_utils::{eq_expr_value, in_macro, search_same, SpanlessEq, SpanlessHas
33
use clippy_utils::{get_parent_expr, if_sequence};
44
use rustc_hir::{Block, Expr, ExprKind};
55
use rustc_lint::{LateContext, LateLintPass};
6+
use rustc_middle::hir::map::Map;
67
use rustc_session::{declare_lint_pass, declare_tool_lint};
8+
use rustc_span::source_map::Span;
9+
use std::borrow::Cow;
710

811
declare_clippy_lint! {
912
/// **What it does:** Checks for consecutive `if`s with the same condition.
@@ -104,7 +107,45 @@ declare_clippy_lint! {
104107
"`if` with the same `then` and `else` blocks"
105108
}
106109

107-
declare_lint_pass!(CopyAndPaste => [IFS_SAME_COND, SAME_FUNCTIONS_IN_IF_CONDITION, IF_SAME_THEN_ELSE]);
110+
declare_clippy_lint! {
111+
/// **What it does:** Checks if the `if` and `else` block contain shared code that can be
112+
/// moved out of the blocks.
113+
///
114+
/// **Why is this bad?** Duplicate code is less maintainable.
115+
///
116+
/// **Known problems:** Hopefully none.
117+
///
118+
/// **Example:**
119+
/// ```ignore
120+
/// let foo = if … {
121+
/// println!("Hello World");
122+
/// 13
123+
/// } else {
124+
/// println!("Hello World");
125+
/// 42
126+
/// };
127+
/// ```
128+
///
129+
/// Could be written as:
130+
/// ```ignore
131+
/// println!("Hello World");
132+
/// let foo = if … {
133+
/// 13
134+
/// } else {
135+
/// 42
136+
/// };
137+
/// ```
138+
pub SHARED_CODE_IN_IF_BLOCKS,
139+
pedantic,
140+
"`if` statement with shared code in all blocks"
141+
}
142+
143+
declare_lint_pass!(CopyAndPaste => [
144+
IFS_SAME_COND,
145+
SAME_FUNCTIONS_IN_IF_CONDITION,
146+
IF_SAME_THEN_ELSE,
147+
SHARED_CODE_IN_IF_BLOCKS
148+
]);
108149

109150
impl<'tcx> LateLintPass<'tcx> for CopyAndPaste {
110151
fn check_expr(&mut self, cx: &LateContext<'tcx>, expr: &'tcx Expr<'_>) {
@@ -121,30 +162,256 @@ impl<'tcx> LateLintPass<'tcx> for CopyAndPaste {
121162
}
122163

123164
let (conds, blocks) = if_sequence(expr);
124-
lint_same_then_else(cx, &blocks);
165+
// Conditions
125166
lint_same_cond(cx, &conds);
126167
lint_same_fns_in_if_cond(cx, &conds);
168+
// Block duplication
169+
lint_same_then_else(cx, &blocks, conds.len() != blocks.len(), expr);
127170
}
128171
}
129172
}
130173

131-
/// Implementation of `IF_SAME_THEN_ELSE`.
132-
fn lint_same_then_else(cx: &LateContext<'_>, blocks: &[&Block<'_>]) {
133-
let eq: &dyn Fn(&&Block<'_>, &&Block<'_>) -> bool =
134-
&|&lhs, &rhs| -> bool { SpanlessEq::new(cx).eq_block(lhs, rhs) };
174+
/// Implementation of `SHARED_CODE_IN_IF_BLOCKS` and `IF_SAME_THEN_ELSE` if the blocks are equal.
175+
fn lint_same_then_else<'tcx>(
176+
cx: &LateContext<'tcx>,
177+
blocks: &[&Block<'tcx>],
178+
has_unconditional_else: bool,
179+
expr: &'tcx Expr<'_>,
180+
) {
181+
// We only lint ifs with multiple blocks
182+
// TODO xFrednet 2021-01-01: Check if it's an else if block
183+
if blocks.len() < 2 {
184+
return;
185+
}
135186

136-
if let Some((i, j)) = search_same_sequenced(blocks, eq) {
137-
span_lint_and_note(
187+
let has_expr = blocks[0].expr.is_some();
188+
189+
// Check if each block has shared code
190+
let mut start_eq = usize::MAX;
191+
let mut end_eq = usize::MAX;
192+
let mut expr_eq = true;
193+
for (index, win) in blocks.windows(2).enumerate() {
194+
let l_stmts = win[0].stmts;
195+
let r_stmts = win[1].stmts;
196+
197+
let mut evaluator = SpanlessEq::new(cx);
198+
let current_start_eq = count_eq(&mut l_stmts.iter(), &mut r_stmts.iter(), |l, r| evaluator.eq_stmt(l, r));
199+
let current_end_eq = count_eq(&mut l_stmts.iter().rev(), &mut r_stmts.iter().rev(), |l, r| {
200+
evaluator.eq_stmt(l, r)
201+
});
202+
let block_expr_eq = both(&win[0].expr, &win[1].expr, |l, r| evaluator.eq_expr(l, r));
203+
204+
// IF_SAME_THEN_ELSE
205+
// We only lint the first two blocks (index == 0). Further blocks will be linted when that if
206+
// statement is checked
207+
if index == 0 && block_expr_eq && l_stmts.len() == r_stmts.len() && l_stmts.len() == current_start_eq {
208+
span_lint_and_note(
209+
cx,
210+
IF_SAME_THEN_ELSE,
211+
win[0].span,
212+
"this `if` has identical blocks",
213+
Some(win[1].span),
214+
"same as this",
215+
);
216+
217+
return;
218+
}
219+
220+
start_eq = start_eq.min(current_start_eq);
221+
end_eq = end_eq.min(current_end_eq);
222+
expr_eq &= block_expr_eq;
223+
224+
// We can return if the eq count is 0 from both sides or if it has no unconditional else case
225+
if !has_unconditional_else || (start_eq == 0 && end_eq == 0 && (has_expr && !expr_eq)) {
226+
return;
227+
}
228+
}
229+
230+
if has_expr && !expr_eq {
231+
end_eq = 0;
232+
}
233+
234+
// Check if the regions are overlapping. Set `end_eq` to prevent the overlap
235+
let min_block_size = blocks.iter().map(|x| x.stmts.len()).min().unwrap();
236+
if (start_eq + end_eq) > min_block_size {
237+
end_eq = min_block_size - start_eq;
238+
}
239+
240+
// Only the start is the same
241+
if start_eq != 0 && end_eq == 0 && (!has_expr || !expr_eq) {
242+
emit_shared_code_in_if_blocks_lint(cx, start_eq, 0, false, blocks, expr);
243+
} else if end_eq != 0 && (!has_expr || !expr_eq) {
244+
let block = blocks[blocks.len() - 1];
245+
let stmts = block.stmts.split_at(start_eq).1;
246+
let (block_stmts, moved_stmts) = stmts.split_at(stmts.len() - end_eq);
247+
248+
// Scan block
249+
let mut walker = SymbolFinderVisitor::new(cx);
250+
for stmt in block_stmts {
251+
intravisit::walk_stmt(&mut walker, stmt);
252+
}
253+
let mut block_defs = walker.defs;
254+
255+
// Scan moved stmts
256+
let mut moved_start: Option<usize> = None;
257+
let mut walker = SymbolFinderVisitor::new(cx);
258+
for (index, stmt) in moved_stmts.iter().enumerate() {
259+
intravisit::walk_stmt(&mut walker, stmt);
260+
261+
for value in &walker.uses {
262+
// Well we can't move this and all prev statements. So reset
263+
if block_defs.contains(&value) {
264+
moved_start = Some(index + 1);
265+
walker.defs.drain().for_each(|x| {
266+
block_defs.insert(x);
267+
});
268+
}
269+
}
270+
271+
walker.uses.clear();
272+
}
273+
274+
if let Some(moved_start) = moved_start {
275+
end_eq -= moved_start;
276+
}
277+
278+
let mut end_linable = true;
279+
if let Some(expr) = block.expr {
280+
intravisit::walk_expr(&mut walker, expr);
281+
end_linable = walker.uses.iter().any(|x| !block_defs.contains(x));
282+
}
283+
284+
emit_shared_code_in_if_blocks_lint(cx, start_eq, end_eq, end_linable, blocks, expr);
285+
}
286+
}
287+
288+
fn emit_shared_code_in_if_blocks_lint(
289+
cx: &LateContext<'tcx>,
290+
start_stmts: usize,
291+
end_stmts: usize,
292+
lint_end: bool,
293+
blocks: &[&Block<'tcx>],
294+
if_expr: &'tcx Expr<'_>,
295+
) {
296+
if start_stmts == 0 && !lint_end {
297+
return;
298+
}
299+
300+
// (help, span, suggestion)
301+
let mut suggestions: Vec<(&str, Span, String)> = vec![];
302+
303+
if start_stmts > 0 {
304+
let block = blocks[0];
305+
let span_start = first_line_of_span(cx, if_expr.span).shrink_to_lo();
306+
let span_end = block.stmts[start_stmts - 1].span.source_callsite();
307+
308+
let cond_span = first_line_of_span(cx, if_expr.span).until(block.span);
309+
let cond_snippet = reindent_multiline(snippet(cx, cond_span, "_"), false, None);
310+
let cond_indent = indent_of(cx, cond_span);
311+
let moved_span = block.stmts[0].span.source_callsite().to(span_end);
312+
let moved_snippet = reindent_multiline(snippet(cx, moved_span, "_"), true, None);
313+
let suggestion = moved_snippet.to_string() + "\n" + &cond_snippet + "{";
314+
let suggestion = reindent_multiline(Cow::Borrowed(&suggestion), true, cond_indent);
315+
316+
let span = span_start.to(span_end);
317+
suggestions.push(("START HELP", span, suggestion.to_string()));
318+
}
319+
320+
if lint_end {
321+
let block = blocks[blocks.len() - 1];
322+
let span_end = block.span.shrink_to_hi();
323+
324+
let moved_start = if end_stmts == 0 && block.expr.is_some() {
325+
block.expr.unwrap().span
326+
} else {
327+
block.stmts[block.stmts.len() - end_stmts].span
328+
}
329+
.source_callsite();
330+
let moved_end = if let Some(expr) = block.expr {
331+
expr.span
332+
} else {
333+
block.stmts[block.stmts.len() - 1].span
334+
}
335+
.source_callsite();
336+
337+
let moved_span = moved_start.to(moved_end);
338+
let moved_snipped = reindent_multiline(snippet(cx, moved_span, "_"), true, None);
339+
let indent = indent_of(cx, if_expr.span.shrink_to_hi());
340+
let suggestion = "}\n".to_string() + &moved_snipped;
341+
let suggestion = reindent_multiline(Cow::Borrowed(&suggestion), true, indent);
342+
343+
let span = moved_start.to(span_end);
344+
suggestions.push(("END_RANGE", span, suggestion.to_string()));
345+
}
346+
347+
if suggestions.len() == 1 {
348+
let (_, span, sugg) = &suggestions[0];
349+
span_lint_and_sugg(
138350
cx,
139-
IF_SAME_THEN_ELSE,
140-
j.span,
141-
"this `if` has identical blocks",
142-
Some(i.span),
143-
"same as this",
351+
SHARED_CODE_IN_IF_BLOCKS,
352+
*span,
353+
"All code blocks contain the same code",
354+
"Consider moving the code out like this",
355+
sugg.clone(),
356+
Applicability::Unspecified,
357+
);
358+
} else {
359+
span_lint_and_then(
360+
cx,
361+
SHARED_CODE_IN_IF_BLOCKS,
362+
if_expr.span,
363+
"All if blocks contain the same code",
364+
move |diag| {
365+
for (help, span, sugg) in suggestions {
366+
diag.span_suggestion(span, help, sugg, Applicability::Unspecified);
367+
}
368+
},
144369
);
145370
}
146371
}
147372

373+
pub struct SymbolFinderVisitor<'a, 'tcx> {
374+
cx: &'a LateContext<'tcx>,
375+
defs: FxHashSet<HirId>,
376+
uses: FxHashSet<HirId>,
377+
}
378+
379+
impl<'a, 'tcx> SymbolFinderVisitor<'a, 'tcx> {
380+
fn new(cx: &'a LateContext<'tcx>) -> Self {
381+
SymbolFinderVisitor {
382+
cx,
383+
defs: FxHashSet::default(),
384+
uses: FxHashSet::default(),
385+
}
386+
}
387+
}
388+
389+
impl<'a, 'tcx> Visitor<'tcx> for SymbolFinderVisitor<'a, 'tcx> {
390+
type Map = Map<'tcx>;
391+
392+
fn nested_visit_map(&mut self) -> NestedVisitorMap<Self::Map> {
393+
NestedVisitorMap::All(self.cx.tcx.hir())
394+
}
395+
396+
fn visit_local(&mut self, l: &'tcx rustc_hir::Local<'tcx>) {
397+
let local_id = l.pat.hir_id;
398+
self.defs.insert(local_id);
399+
if let Some(expr) = l.init {
400+
intravisit::walk_expr(self, expr);
401+
}
402+
}
403+
404+
fn visit_qpath(&mut self, qpath: &'tcx rustc_hir::QPath<'tcx>, id: HirId, _span: rustc_span::Span) {
405+
if let rustc_hir::QPath::Resolved(_, ref path) = *qpath {
406+
if path.segments.len() == 1 {
407+
if let rustc_hir::def::Res::Local(var) = self.cx.qpath_res(qpath, id) {
408+
self.uses.insert(var);
409+
}
410+
}
411+
}
412+
}
413+
}
414+
148415
/// Implementation of `IFS_SAME_COND`.
149416
fn lint_same_cond(cx: &LateContext<'_>, conds: &[&Expr<'_>]) {
150417
let hash: &dyn Fn(&&Expr<'_>) -> u64 = &|expr| -> u64 {
@@ -198,15 +465,3 @@ fn lint_same_fns_in_if_cond(cx: &LateContext<'_>, conds: &[&Expr<'_>]) {
198465
);
199466
}
200467
}
201-
202-
fn search_same_sequenced<T, Eq>(exprs: &[T], eq: Eq) -> Option<(&T, &T)>
203-
where
204-
Eq: Fn(&T, &T) -> bool,
205-
{
206-
for win in exprs.windows(2) {
207-
if eq(&win[0], &win[1]) {
208-
return Some((&win[0], &win[1]));
209-
}
210-
}
211-
None
212-
}

clippy_lints/src/lib.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -616,6 +616,7 @@ pub fn register_plugins(store: &mut rustc_lint::LintStore, sess: &Session, conf:
616616
&copies::IFS_SAME_COND,
617617
&copies::IF_SAME_THEN_ELSE,
618618
&copies::SAME_FUNCTIONS_IN_IF_CONDITION,
619+
&copies::SHARED_CODE_IN_IF_BLOCKS,
619620
&copy_iterator::COPY_ITERATOR,
620621
&create_dir::CREATE_DIR,
621622
&dbg_macro::DBG_MACRO,
@@ -1365,6 +1366,7 @@ pub fn register_plugins(store: &mut rustc_lint::LintStore, sess: &Session, conf:
13651366
LintId::of(&casts::PTR_AS_PTR),
13661367
LintId::of(&checked_conversions::CHECKED_CONVERSIONS),
13671368
LintId::of(&copies::SAME_FUNCTIONS_IN_IF_CONDITION),
1369+
LintId::of(&copies::SHARED_CODE_IN_IF_BLOCKS),
13681370
LintId::of(&copy_iterator::COPY_ITERATOR),
13691371
LintId::of(&default::DEFAULT_TRAIT_ACCESS),
13701372
LintId::of(&dereference::EXPLICIT_DEREF_METHODS),

0 commit comments

Comments
 (0)