Skip to content

Commit 4637f05

Browse files
committed
A new lint for shared code in if blocks
* WIP working front and and detection * A working lint * Fixed the last test * Cleaning up some code * Fixed another test * Ret renamed the lint * Formatting
1 parent c93d8d2 commit 4637f05

14 files changed

+436
-190
lines changed

CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -2049,6 +2049,7 @@ Released 2018-09-13
20492049
[`shadow_reuse`]: https://rust-lang.github.io/rust-clippy/master/index.html#shadow_reuse
20502050
[`shadow_same`]: https://rust-lang.github.io/rust-clippy/master/index.html#shadow_same
20512051
[`shadow_unrelated`]: https://rust-lang.github.io/rust-clippy/master/index.html#shadow_unrelated
2052+
[`shared_code_in_if_blocks`]: https://rust-lang.github.io/rust-clippy/master/index.html#shared_code_in_if_blocks
20522053
[`short_circuit_statement`]: https://rust-lang.github.io/rust-clippy/master/index.html#short_circuit_statement
20532054
[`should_assert_eq`]: https://rust-lang.github.io/rust-clippy/master/index.html#should_assert_eq
20542055
[`should_implement_trait`]: https://rust-lang.github.io/rust-clippy/master/index.html#should_implement_trait

clippy_lints/src/copies.rs

+133-27
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
1-
use crate::utils::{eq_expr_value, in_macro, search_same, SpanlessEq, SpanlessHash};
2-
use crate::utils::{get_parent_expr, higher, if_sequence, span_lint_and_note};
1+
use crate::utils::{both, count_eq, eq_expr_value, in_macro, search_same, SpanlessEq, SpanlessHash};
2+
use crate::utils::{get_parent_expr, higher, if_sequence, span_lint_and_help, span_lint_and_note};
33
use rustc_hir::{Block, Expr};
44
use rustc_lint::{LateContext, LateLintPass};
55
use rustc_session::{declare_lint_pass, declare_tool_lint};
6+
use rustc_span::source_map::Span;
67

78
declare_clippy_lint! {
89
/// **What it does:** Checks for consecutive `if`s with the same condition.
@@ -103,7 +104,40 @@ declare_clippy_lint! {
103104
"`if` with the same `then` and `else` blocks"
104105
}
105106

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

108142
impl<'tcx> LateLintPass<'tcx> for CopyAndPaste {
109143
fn check_expr(&mut self, cx: &LateContext<'tcx>, expr: &'tcx Expr<'_>) {
@@ -118,28 +152,112 @@ impl<'tcx> LateLintPass<'tcx> for CopyAndPaste {
118152
}
119153

120154
let (conds, blocks) = if_sequence(expr);
121-
lint_same_then_else(cx, &blocks);
155+
// Conditions
122156
lint_same_cond(cx, &conds);
123157
lint_same_fns_in_if_cond(cx, &conds);
158+
// Block duplication
159+
lint_same_then_else(cx, &blocks, conds.len() != blocks.len());
124160
}
125161
}
126162
}
127163

128-
/// Implementation of `IF_SAME_THEN_ELSE`.
129-
fn lint_same_then_else(cx: &LateContext<'_>, blocks: &[&Block<'_>]) {
130-
let eq: &dyn Fn(&&Block<'_>, &&Block<'_>) -> bool =
131-
&|&lhs, &rhs| -> bool { SpanlessEq::new(cx).eq_block(lhs, rhs) };
164+
/// Implementation of `SHARED_CODE_IN_IF_BLOCKS` and `IF_SAME_THEN_ELSE` if the blocks are equal.
165+
fn lint_same_then_else(cx: &LateContext<'_>, blocks: &[&Block<'_>], has_unconditional_else: bool) {
166+
/// This retrieves the span of the actual call site.
167+
fn get_source_span(span: Span) -> Span {
168+
if span.from_expansion() {
169+
span.source_callee().unwrap().call_site
170+
} else {
171+
span
172+
}
173+
}
132174

133-
if let Some((i, j)) = search_same_sequenced(blocks, eq) {
134-
span_lint_and_note(
175+
fn min(a: usize, b: usize) -> usize {
176+
if a < b {
177+
a
178+
} else {
179+
b
180+
}
181+
}
182+
183+
fn lint_duplicate_code(cx: &LateContext<'_>, position: &str, lint_span: Span) {
184+
span_lint_and_help(
135185
cx,
136-
IF_SAME_THEN_ELSE,
137-
j.span,
138-
"this `if` has identical blocks",
139-
Some(i.span),
140-
"same as this",
186+
SHARED_CODE_IN_IF_BLOCKS,
187+
lint_span,
188+
format!("All if blocks contain the same code at the {}", position).as_str(),
189+
None,
190+
"Consider moving the code out of the if statement to prevent code duplication",
141191
);
142192
}
193+
194+
// We only lint ifs with multiple blocks
195+
if blocks.len() < 2 {
196+
return;
197+
}
198+
199+
// Check if each block has shared code
200+
let mut start_eq = usize::MAX;
201+
let mut end_eq = usize::MAX;
202+
for (index, win) in blocks.windows(2).enumerate() {
203+
let l_stmts = win[0].stmts;
204+
let r_stmts = win[1].stmts;
205+
206+
let mut evaluator = SpanlessEq::new(cx);
207+
let current_start_eq = count_eq(&mut l_stmts.iter(), &mut r_stmts.iter(), |l, r| evaluator.eq_stmt(l, r));
208+
let current_end_eq = count_eq(&mut l_stmts.iter().rev(), &mut r_stmts.iter().rev(), |l, r| {
209+
evaluator.eq_stmt(l, r)
210+
});
211+
let current_block_expr_eq = both(&win[0].expr, &win[1].expr, |l, r| evaluator.eq_expr(l, r));
212+
213+
// IF_SAME_THEN_ELSE
214+
// We only lint the first two blocks (index == 0). Further blocks will be linted when that if
215+
// statement is checked
216+
if index == 0 && current_block_expr_eq && l_stmts.len() == r_stmts.len() && l_stmts.len() == current_start_eq {
217+
span_lint_and_note(
218+
cx,
219+
IF_SAME_THEN_ELSE,
220+
win[0].span,
221+
"this `if` has identical blocks",
222+
Some(win[1].span),
223+
"same as this",
224+
);
225+
226+
return;
227+
}
228+
229+
start_eq = min(start_eq, current_start_eq);
230+
end_eq = min(end_eq, current_end_eq);
231+
232+
// We can return if the eq count is 0 from both sides or if it has no unconditional else case
233+
if !has_unconditional_else || start_eq == 0 && end_eq == 0 {
234+
return;
235+
};
236+
}
237+
238+
let first_block = blocks[0];
239+
240+
// prevent double lint if the `start_eq` and `end_eq` cover the entire block
241+
if start_eq == first_block.stmts.len() {
242+
end_eq = 0;
243+
}
244+
245+
if start_eq != 0 {
246+
let start = first_block.span.shrink_to_lo();
247+
let end = get_source_span(first_block.stmts[start_eq - 1].span);
248+
let lint_span = start.to(end);
249+
250+
lint_duplicate_code(cx, "start", lint_span);
251+
}
252+
253+
if end_eq != 0 {
254+
let index = first_block.stmts.len() - end_eq;
255+
let start = get_source_span(first_block.stmts[index].span);
256+
let end = first_block.span.shrink_to_hi();
257+
let lint_span = start.to(end);
258+
259+
lint_duplicate_code(cx, "end", lint_span);
260+
}
143261
}
144262

145263
/// Implementation of `IFS_SAME_COND`.
@@ -195,15 +313,3 @@ fn lint_same_fns_in_if_cond(cx: &LateContext<'_>, conds: &[&Expr<'_>]) {
195313
);
196314
}
197315
}
198-
199-
fn search_same_sequenced<T, Eq>(exprs: &[T], eq: Eq) -> Option<(&T, &T)>
200-
where
201-
Eq: Fn(&T, &T) -> bool,
202-
{
203-
for win in exprs.windows(2) {
204-
if eq(&win[0], &win[1]) {
205-
return Some((&win[0], &win[1]));
206-
}
207-
}
208-
None
209-
}

clippy_lints/src/lib.rs

+3
Original file line numberDiff line numberDiff line change
@@ -558,6 +558,7 @@ pub fn register_plugins(store: &mut rustc_lint::LintStore, sess: &Session, conf:
558558
&copies::IFS_SAME_COND,
559559
&copies::IF_SAME_THEN_ELSE,
560560
&copies::SAME_FUNCTIONS_IN_IF_CONDITION,
561+
&copies::SHARED_CODE_IN_IF_BLOCKS,
561562
&copy_iterator::COPY_ITERATOR,
562563
&create_dir::CREATE_DIR,
563564
&dbg_macro::DBG_MACRO,
@@ -1381,6 +1382,7 @@ pub fn register_plugins(store: &mut rustc_lint::LintStore, sess: &Session, conf:
13811382
LintId::of(&comparison_chain::COMPARISON_CHAIN),
13821383
LintId::of(&copies::IFS_SAME_COND),
13831384
LintId::of(&copies::IF_SAME_THEN_ELSE),
1385+
LintId::of(&copies::SHARED_CODE_IN_IF_BLOCKS),
13841386
LintId::of(&default::FIELD_REASSIGN_WITH_DEFAULT),
13851387
LintId::of(&derive::DERIVE_HASH_XOR_EQ),
13861388
LintId::of(&derive::DERIVE_ORD_XOR_PARTIAL_ORD),
@@ -1751,6 +1753,7 @@ pub fn register_plugins(store: &mut rustc_lint::LintStore, sess: &Session, conf:
17511753
LintId::of(&assign_ops::MISREFACTORED_ASSIGN_OP),
17521754
LintId::of(&attrs::DEPRECATED_CFG_ATTR),
17531755
LintId::of(&booleans::NONMINIMAL_BOOL),
1756+
LintId::of(&copies::SHARED_CODE_IN_IF_BLOCKS),
17541757
LintId::of(&double_comparison::DOUBLE_COMPARISONS),
17551758
LintId::of(&double_parens::DOUBLE_PARENS),
17561759
LintId::of(&duration_subsec::DURATION_SUBSEC),

clippy_lints/src/utils/hir_utils.rs

+9
Original file line numberDiff line numberDiff line change
@@ -368,6 +368,15 @@ pub fn over<X>(left: &[X], right: &[X], mut eq_fn: impl FnMut(&X, &X) -> bool) -
368368
left.len() == right.len() && left.iter().zip(right).all(|(x, y)| eq_fn(x, y))
369369
}
370370

371+
/// Counts how many elements of the slices are equal as per `eq_fn`.
372+
pub fn count_eq<X: Sized>(
373+
left: &mut dyn Iterator<Item = X>,
374+
right: &mut dyn Iterator<Item = X>,
375+
mut eq_fn: impl FnMut(&X, &X) -> bool,
376+
) -> usize {
377+
left.zip(right).take_while(|(l, r)| eq_fn(l, r)).count()
378+
}
379+
371380
/// Checks if two expressions evaluate to the same value, and don't contain any side effects.
372381
pub fn eq_expr_value(cx: &LateContext<'_>, left: &Expr<'_>, right: &Expr<'_>) -> bool {
373382
SpanlessEq::new(cx).deny_side_effects().eq_expr(left, right)

clippy_lints/src/utils/mod.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ pub mod visitors;
2626

2727
pub use self::attrs::*;
2828
pub use self::diagnostics::*;
29-
pub use self::hir_utils::{both, eq_expr_value, over, SpanlessEq, SpanlessHash};
29+
pub use self::hir_utils::{both, count_eq, eq_expr_value, over, SpanlessEq, SpanlessHash};
3030

3131
use std::borrow::Cow;
3232
use std::collections::hash_map::Entry;

tests/ui/checked_unwrap/complex_conditionals.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
#![deny(clippy::panicking_unwrap, clippy::unnecessary_unwrap)]
2-
#![allow(clippy::if_same_then_else)]
2+
#![allow(clippy::if_same_then_else, clippy::shared_code_in_if_blocks)]
33

44
fn test_complex_conditions() {
55
let x: Result<(), ()> = Ok(());

tests/ui/if_same_then_else.rs

+2-1
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,8 @@
55
clippy::never_loop,
66
clippy::no_effect,
77
clippy::unused_unit,
8-
clippy::zero_divided_by_zero
8+
clippy::zero_divided_by_zero,
9+
clippy::shared_code_in_if_blocks
910
)]
1011

1112
struct Foo {

0 commit comments

Comments
 (0)