Skip to content

Commit 511c888

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
1 parent c93d8d2 commit 511c888

14 files changed

+421
-189
lines changed

CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -1850,6 +1850,7 @@ Released 2018-09-13
18501850
[`if_let_some_result`]: https://rust-lang.github.io/rust-clippy/master/index.html#if_let_some_result
18511851
[`if_not_else`]: https://rust-lang.github.io/rust-clippy/master/index.html#if_not_else
18521852
[`if_same_then_else`]: https://rust-lang.github.io/rust-clippy/master/index.html#if_same_then_else
1853+
[`if_shares_code_with_else`]: https://rust-lang.github.io/rust-clippy/master/index.html#if_shares_code_with_else
18531854
[`ifs_same_cond`]: https://rust-lang.github.io/rust-clippy/master/index.html#ifs_same_cond
18541855
[`implicit_hasher`]: https://rust-lang.github.io/rust-clippy/master/index.html#implicit_hasher
18551856
[`implicit_return`]: https://rust-lang.github.io/rust-clippy/master/index.html#implicit_return

clippy_lints/src/copies.rs

+131-26
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,39 @@ 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.
109+
///
110+
/// **Why is this bad?** Duplicate code is less maintainable.
111+
///
112+
/// **Known problems:** Hopefully none.
113+
///
114+
/// **Example:**
115+
/// ```ignore
116+
/// let foo = if … {
117+
/// println!("Hello World");
118+
/// 13
119+
/// } else {
120+
/// println!("Hello World");
121+
/// 42
122+
/// };
123+
/// ```
124+
///
125+
/// Could be written as:
126+
/// ```ignore
127+
/// println!("Hello World");
128+
/// let foo = if … {
129+
/// 13
130+
/// } else {
131+
/// 42
132+
/// };
133+
/// ```
134+
pub IF_SHARES_CODE_WITH_ELSE,
135+
complexity,
136+
"`if` statement with shared code in all blocks"
137+
}
138+
139+
declare_lint_pass!(CopyAndPaste => [IFS_SAME_COND, SAME_FUNCTIONS_IN_IF_CONDITION, IF_SAME_THEN_ELSE, IF_SHARES_CODE_WITH_ELSE]);
107140

108141
impl<'tcx> LateLintPass<'tcx> for CopyAndPaste {
109142
fn check_expr(&mut self, cx: &LateContext<'tcx>, expr: &'tcx Expr<'_>) {
@@ -118,28 +151,112 @@ impl<'tcx> LateLintPass<'tcx> for CopyAndPaste {
118151
}
119152

120153
let (conds, blocks) = if_sequence(expr);
121-
lint_same_then_else(cx, &blocks);
154+
// Conditions
122155
lint_same_cond(cx, &conds);
123156
lint_same_fns_in_if_cond(cx, &conds);
157+
// Block duplication
158+
lint_same_then_else(cx, &blocks);
124159
}
125160
}
126161
}
127162

128-
/// Implementation of `IF_SAME_THEN_ELSE`.
163+
/// Implementation of `IF_SHARES_CODE_WITH_ELSE` and `IF_SAME_THEN_ELSE` if the blocks are equal.
129164
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) };
165+
/// This retrieves the span of the actual call site.
166+
fn get_source_span(span: Span) -> Span {
167+
if span.from_expansion() {
168+
span.source_callee().unwrap().call_site
169+
} else {
170+
span
171+
}
172+
}
132173

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

145262
/// Implementation of `IFS_SAME_COND`.
@@ -195,15 +312,3 @@ fn lint_same_fns_in_if_cond(cx: &LateContext<'_>, conds: &[&Expr<'_>]) {
195312
);
196313
}
197314
}
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
@@ -557,6 +557,7 @@ pub fn register_plugins(store: &mut rustc_lint::LintStore, sess: &Session, conf:
557557
&comparison_chain::COMPARISON_CHAIN,
558558
&copies::IFS_SAME_COND,
559559
&copies::IF_SAME_THEN_ELSE,
560+
&copies::IF_SHARES_CODE_WITH_ELSE,
560561
&copies::SAME_FUNCTIONS_IN_IF_CONDITION,
561562
&copy_iterator::COPY_ITERATOR,
562563
&create_dir::CREATE_DIR,
@@ -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::IF_SHARES_CODE_WITH_ELSE),
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::IF_SHARES_CODE_WITH_ELSE),
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::if_shares_code_with_else)]
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::if_shares_code_with_else,
910
)]
1011

1112
struct Foo {

0 commit comments

Comments
 (0)