Skip to content

Commit 604f409

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 12212db commit 604f409

18 files changed

+747
-182
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2180,6 +2180,7 @@ Released 2018-09-13
21802180
[`shadow_reuse`]: https://rust-lang.github.io/rust-clippy/master/index.html#shadow_reuse
21812181
[`shadow_same`]: https://rust-lang.github.io/rust-clippy/master/index.html#shadow_same
21822182
[`shadow_unrelated`]: https://rust-lang.github.io/rust-clippy/master/index.html#shadow_unrelated
2183+
[`shared_code_in_if_blocks`]: https://rust-lang.github.io/rust-clippy/master/index.html#shared_code_in_if_blocks
21832184
[`short_circuit_statement`]: https://rust-lang.github.io/rust-clippy/master/index.html#short_circuit_statement
21842185
[`should_assert_eq`]: https://rust-lang.github.io/rust-clippy/master/index.html#should_assert_eq
21852186
[`should_implement_trait`]: https://rust-lang.github.io/rust-clippy/master/index.html#should_implement_trait

clippy_lints/src/copies.rs

Lines changed: 289 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,17 @@
1-
use crate::utils::{eq_expr_value, in_macro, search_same, SpanlessEq, SpanlessHash};
2-
use crate::utils::{get_parent_expr, if_sequence, span_lint_and_note};
3-
use rustc_hir::{Block, Expr, ExprKind};
1+
use crate::utils::{both, count_eq, eq_expr_value, in_macro, search_same, SpanlessEq, SpanlessHash};
2+
use crate::utils::{
3+
first_line_of_span, get_parent_expr, higher, if_sequence, indent_of, reindent_multiline, snippet,
4+
span_lint_and_note, span_lint_and_sugg, span_lint_and_then,
5+
};
6+
use rustc_data_structures::fx::FxHashSet;
7+
use rustc_errors::Applicability;
8+
use rustc_hir::intravisit::{self, NestedVisitorMap, Visitor};
9+
use rustc_hir::{Block, Expr, HirId};
410
use rustc_lint::{LateContext, LateLintPass};
11+
use rustc_middle::hir::map::Map;
512
use rustc_session::{declare_lint_pass, declare_tool_lint};
13+
use rustc_span::source_map::Span;
14+
use std::borrow::Cow;
615

716
declare_clippy_lint! {
817
/// **What it does:** Checks for consecutive `if`s with the same condition.
@@ -103,7 +112,45 @@ declare_clippy_lint! {
103112
"`if` with the same `then` and `else` blocks"
104113
}
105114

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

108155
impl<'tcx> LateLintPass<'tcx> for CopyAndPaste {
109156
fn check_expr(&mut self, cx: &LateContext<'tcx>, expr: &'tcx Expr<'_>) {
@@ -120,30 +167,256 @@ impl<'tcx> LateLintPass<'tcx> for CopyAndPaste {
120167
}
121168

122169
let (conds, blocks) = if_sequence(expr);
123-
lint_same_then_else(cx, &blocks);
170+
// Conditions
124171
lint_same_cond(cx, &conds);
125172
lint_same_fns_in_if_cond(cx, &conds);
173+
// Block duplication
174+
lint_same_then_else(cx, &blocks, conds.len() != blocks.len(), expr);
126175
}
127176
}
128177
}
129178

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

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

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

clippy_lints/src/lib.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -567,6 +567,7 @@ pub fn register_plugins(store: &mut rustc_lint::LintStore, sess: &Session, conf:
567567
&copies::IFS_SAME_COND,
568568
&copies::IF_SAME_THEN_ELSE,
569569
&copies::SAME_FUNCTIONS_IN_IF_CONDITION,
570+
&copies::SHARED_CODE_IN_IF_BLOCKS,
570571
&copy_iterator::COPY_ITERATOR,
571572
&create_dir::CREATE_DIR,
572573
&dbg_macro::DBG_MACRO,
@@ -1285,6 +1286,7 @@ pub fn register_plugins(store: &mut rustc_lint::LintStore, sess: &Session, conf:
12851286
LintId::of(&bit_mask::VERBOSE_BIT_MASK),
12861287
LintId::of(&checked_conversions::CHECKED_CONVERSIONS),
12871288
LintId::of(&copies::SAME_FUNCTIONS_IN_IF_CONDITION),
1289+
LintId::of(&copies::SHARED_CODE_IN_IF_BLOCKS),
12881290
LintId::of(&copy_iterator::COPY_ITERATOR),
12891291
LintId::of(&default::DEFAULT_TRAIT_ACCESS),
12901292
LintId::of(&dereference::EXPLICIT_DEREF_METHODS),

0 commit comments

Comments
 (0)