Skip to content

Commit

Permalink
A new lint for shared code in if blocks
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
xFrednet committed Jan 11, 2021
1 parent 6423711 commit 6b13418
Show file tree
Hide file tree
Showing 19 changed files with 1,177 additions and 182 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2180,6 +2180,7 @@ Released 2018-09-13
[`shadow_reuse`]: https://rust-lang.github.io/rust-clippy/master/index.html#shadow_reuse
[`shadow_same`]: https://rust-lang.github.io/rust-clippy/master/index.html#shadow_same
[`shadow_unrelated`]: https://rust-lang.github.io/rust-clippy/master/index.html#shadow_unrelated
[`shared_code_in_if_blocks`]: https://rust-lang.github.io/rust-clippy/master/index.html#shared_code_in_if_blocks
[`short_circuit_statement`]: https://rust-lang.github.io/rust-clippy/master/index.html#short_circuit_statement
[`should_assert_eq`]: https://rust-lang.github.io/rust-clippy/master/index.html#should_assert_eq
[`should_implement_trait`]: https://rust-lang.github.io/rust-clippy/master/index.html#should_implement_trait
Expand Down
320 changes: 292 additions & 28 deletions clippy_lints/src/copies.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,20 @@
use crate::utils::{eq_expr_value, in_macro, search_same, SpanlessEq, SpanlessHash};
use crate::utils::{get_parent_expr, higher, if_sequence, span_lint_and_note};
use rustc_hir::{Block, Expr};
use crate::utils::ast_utils::IdentIter;
use crate::utils::{both, count_eq, eq_expr_value, in_macro, search_same, SpanlessEq, SpanlessHash};
use crate::utils::{
first_line_of_span, get_parent_expr, higher, if_sequence, indent_of, multispan_sugg_with_applicability,
reindent_multiline, snippet, span_lint_and_help, span_lint_and_note, span_lint_and_sugg, span_lint_and_then,
};
use rustc_data_structures::fx::FxHashSet;
use rustc_errors::Applicability;
use rustc_hir::intravisit::{self, NestedVisitorMap, Visitor};
use rustc_hir::StmtKind;
use rustc_hir::{Block, Expr, ExprKind, HirId};
use rustc_lint::{LateContext, LateLintPass};
use rustc_middle::hir::map::Map;
use rustc_session::{declare_lint_pass, declare_tool_lint};
use rustc_span::source_map::Span;
use rustc_span::symbol::Ident;
use std::borrow::Cow;

declare_clippy_lint! {
/// **What it does:** Checks for consecutive `if`s with the same condition.
Expand Down Expand Up @@ -103,7 +115,45 @@ declare_clippy_lint! {
"`if` with the same `then` and `else` blocks"
}

declare_lint_pass!(CopyAndPaste => [IFS_SAME_COND, SAME_FUNCTIONS_IN_IF_CONDITION, IF_SAME_THEN_ELSE]);
declare_clippy_lint! {
/// **What it does:** Checks if the `if` and `else` block contain shared code that can be
/// moved out of the blocks.
///
/// **Why is this bad?** Duplicate code is less maintainable.
///
/// **Known problems:** Hopefully none.
///
/// **Example:**
/// ```ignore
/// let foo = if … {
/// println!("Hello World");
/// 13
/// } else {
/// println!("Hello World");
/// 42
/// };
/// ```
///
/// Could be written as:
/// ```ignore
/// println!("Hello World");
/// let foo = if … {
/// 13
/// } else {
/// 42
/// };
/// ```
pub SHARED_CODE_IN_IF_BLOCKS,
pedantic,
"`if` statement with shared code in all blocks"
}

declare_lint_pass!(CopyAndPaste => [
IFS_SAME_COND,
SAME_FUNCTIONS_IN_IF_CONDITION,
IF_SAME_THEN_ELSE,
SHARED_CODE_IN_IF_BLOCKS
]);

impl<'tcx> LateLintPass<'tcx> for CopyAndPaste {
fn check_expr(&mut self, cx: &LateContext<'tcx>, expr: &'tcx Expr<'_>) {
Expand All @@ -118,30 +168,256 @@ impl<'tcx> LateLintPass<'tcx> for CopyAndPaste {
}

let (conds, blocks) = if_sequence(expr);
lint_same_then_else(cx, &blocks);
// Conditions
lint_same_cond(cx, &conds);
lint_same_fns_in_if_cond(cx, &conds);
// Block duplication
lint_same_then_else(cx, &blocks, conds.len() != blocks.len(), expr);
}
}
}

/// Implementation of `IF_SAME_THEN_ELSE`.
fn lint_same_then_else(cx: &LateContext<'_>, blocks: &[&Block<'_>]) {
let eq: &dyn Fn(&&Block<'_>, &&Block<'_>) -> bool =
&|&lhs, &rhs| -> bool { SpanlessEq::new(cx).eq_block(lhs, rhs) };
/// Implementation of `SHARED_CODE_IN_IF_BLOCKS` and `IF_SAME_THEN_ELSE` if the blocks are equal.
fn lint_same_then_else<'tcx>(
cx: &LateContext<'tcx>,
blocks: &[&Block<'tcx>],
has_unconditional_else: bool,
expr: &'tcx Expr<'_>,
) {
// We only lint ifs with multiple blocks
// TODO xFrednet 2021-01-01: Check if it's an else if block
if blocks.len() < 2 {
return;
}

if let Some((i, j)) = search_same_sequenced(blocks, eq) {
span_lint_and_note(
let has_expr = blocks[0].expr.is_some();

// Check if each block has shared code
let mut start_eq = usize::MAX;
let mut end_eq = usize::MAX;
let mut expr_eq = true;
for (index, win) in blocks.windows(2).enumerate() {
let l_stmts = win[0].stmts;
let r_stmts = win[1].stmts;

let mut evaluator = SpanlessEq::new(cx);
let current_start_eq = count_eq(&mut l_stmts.iter(), &mut r_stmts.iter(), |l, r| evaluator.eq_stmt(l, r));
let current_end_eq = count_eq(&mut l_stmts.iter().rev(), &mut r_stmts.iter().rev(), |l, r| {
evaluator.eq_stmt(l, r)
});
let block_expr_eq = both(&win[0].expr, &win[1].expr, |l, r| evaluator.eq_expr(l, r));

// IF_SAME_THEN_ELSE
// We only lint the first two blocks (index == 0). Further blocks will be linted when that if
// statement is checked
if index == 0 && block_expr_eq && l_stmts.len() == r_stmts.len() && l_stmts.len() == current_start_eq {
span_lint_and_note(
cx,
IF_SAME_THEN_ELSE,
win[0].span,
"this `if` has identical blocks",
Some(win[1].span),
"same as this",
);

return;
}

start_eq = start_eq.min(current_start_eq);
end_eq = end_eq.min(current_end_eq);
expr_eq &= block_expr_eq;

// We can return if the eq count is 0 from both sides or if it has no unconditional else case
if !has_unconditional_else || (start_eq == 0 && end_eq == 0 && (has_expr && !expr_eq)) {
return;
}
}

if has_expr && !expr_eq {
end_eq = 0;
}

// Check if the regions are overlapping. Set `end_eq` to prevent the overlap
let min_block_size = blocks.iter().map(|x| x.stmts.len()).min().unwrap();
if (start_eq + end_eq) > min_block_size {
end_eq = min_block_size - start_eq;
}

// Only the start is the same
if start_eq != 0 && end_eq == 0 && (!has_expr || !expr_eq) {
emit_shared_code_in_if_blocks_lint(cx, start_eq, 0, false, blocks, expr);
} else if end_eq != 0 && (!has_expr || !expr_eq) {
let block = blocks[blocks.len() - 1];
let stmts = block.stmts.split_at(start_eq).1;
let (block_stmts, moved_stmts) = stmts.split_at(stmts.len() - end_eq);

// Scan block
let mut walker = SymbolFinderVisitor::new(cx);
for stmt in block_stmts {
intravisit::walk_stmt(&mut walker, stmt);
}
let mut block_defs = walker.defs;

// Scan moved stmts
let mut moved_start: Option<usize> = None;
let mut walker = SymbolFinderVisitor::new(cx);
for (index, stmt) in moved_stmts.iter().enumerate() {
intravisit::walk_stmt(&mut walker, stmt);

for value in &walker.uses {
// Well we can't move this and all prev statements. So reset
if block_defs.contains(&value) {
moved_start = Some(index + 1);
walker.defs.drain().for_each(|x| {
block_defs.insert(x);
});
}
}

walker.uses.clear();
}

if let Some(moved_start) = moved_start {
end_eq -= moved_start;
}

let mut end_linable = true;
if let Some(expr) = block.expr {
intravisit::walk_expr(&mut walker, expr);
end_linable = walker.uses.iter().any(|x| !block_defs.contains(x));
}

emit_shared_code_in_if_blocks_lint(cx, start_eq, end_eq, end_linable, blocks, expr);
}
}

fn emit_shared_code_in_if_blocks_lint(
cx: &LateContext<'tcx>,
start_stmts: usize,
end_stmts: usize,
lint_end: bool,
blocks: &[&Block<'tcx>],
if_expr: &'tcx Expr<'_>,
) {
if start_stmts == 0 && !lint_end {
return;
}

// (help, span, suggestion)
let mut suggestions: Vec<(&str, Span, String)> = vec![];

if start_stmts > 0 {
let block = blocks[0];
let span_start = first_line_of_span(cx, if_expr.span).shrink_to_lo();
let span_end = block.stmts[start_stmts - 1].span.source_callsite();

let cond_span = first_line_of_span(cx, if_expr.span).until(block.span);
let cond_snippet = reindent_multiline(snippet(cx, cond_span, "_"), false, None);
let cond_indent = indent_of(cx, cond_span);
let moved_span = block.stmts[0].span.source_callsite().to(span_end);
let moved_snippet = reindent_multiline(snippet(cx, moved_span, "_"), true, None);
let suggestion = moved_snippet.to_string() + "\n" + &cond_snippet + "{";
let suggestion = reindent_multiline(Cow::Borrowed(&suggestion), true, cond_indent);

let span = span_start.to(span_end);
suggestions.push(("START HELP", span, suggestion.to_string()));
}

if lint_end {
let block = blocks[blocks.len() - 1];
let span_end = block.span.shrink_to_hi();

let moved_start = if end_stmts == 0 && block.expr.is_some() {
block.expr.unwrap().span
} else {
block.stmts[block.stmts.len() - end_stmts].span
}
.source_callsite();
let moved_end = if let Some(expr) = block.expr {
expr.span
} else {
block.stmts[block.stmts.len() - 1].span
}
.source_callsite();

let moved_span = moved_start.to(moved_end);
let moved_snipped = reindent_multiline(snippet(cx, moved_span, "_"), true, None);
let indent = indent_of(cx, if_expr.span.shrink_to_hi());
let suggestion = "}\n".to_string() + &moved_snipped;
let suggestion = reindent_multiline(Cow::Borrowed(&suggestion), true, indent);

let span = moved_start.to(span_end);
suggestions.push(("END_RANGE", span, suggestion.to_string()));
}

if suggestions.len() == 1 {
let (_, span, sugg) = &suggestions[0];
span_lint_and_sugg(
cx,
IF_SAME_THEN_ELSE,
j.span,
"this `if` has identical blocks",
Some(i.span),
"same as this",
SHARED_CODE_IN_IF_BLOCKS,
*span,
"All code blocks contain the same code",
"Consider moving the code out like this",
sugg.clone(),
Applicability::Unspecified,
);
} else {
span_lint_and_then(
cx,
SHARED_CODE_IN_IF_BLOCKS,
if_expr.span,
"All if blocks contain the same code",
move |diag| {
for (help, span, sugg) in suggestions {
diag.span_suggestion(span, help, sugg, Applicability::Unspecified);
}
},
);
}
}

pub struct SymbolFinderVisitor<'a, 'tcx> {
cx: &'a LateContext<'tcx>,
defs: FxHashSet<HirId>,
uses: FxHashSet<HirId>,
}

impl<'a, 'tcx> SymbolFinderVisitor<'a, 'tcx> {
fn new(cx: &'a LateContext<'tcx>) -> Self {
SymbolFinderVisitor {
cx,
defs: FxHashSet::default(),
uses: FxHashSet::default(),
}
}
}

impl<'a, 'tcx> Visitor<'tcx> for SymbolFinderVisitor<'a, 'tcx> {
type Map = Map<'tcx>;

fn nested_visit_map(&mut self) -> NestedVisitorMap<Self::Map> {
NestedVisitorMap::All(self.cx.tcx.hir())
}

fn visit_local(&mut self, l: &'tcx rustc_hir::Local<'tcx>) {
let local_id = l.pat.hir_id;
self.defs.insert(local_id);
if let Some(expr) = l.init {
intravisit::walk_expr(self, expr);
}
}

fn visit_qpath(&mut self, qpath: &'tcx rustc_hir::QPath<'tcx>, id: HirId, _span: rustc_span::Span) {
if let rustc_hir::QPath::Resolved(_, ref path) = *qpath {
if path.segments.len() == 1 {
if let rustc_hir::def::Res::Local(var) = self.cx.qpath_res(qpath, id) {
self.uses.insert(var);
}
}
}
}
}

/// Implementation of `IFS_SAME_COND`.
fn lint_same_cond(cx: &LateContext<'_>, conds: &[&Expr<'_>]) {
let hash: &dyn Fn(&&Expr<'_>) -> u64 = &|expr| -> u64 {
Expand Down Expand Up @@ -195,15 +471,3 @@ fn lint_same_fns_in_if_cond(cx: &LateContext<'_>, conds: &[&Expr<'_>]) {
);
}
}

fn search_same_sequenced<T, Eq>(exprs: &[T], eq: Eq) -> Option<(&T, &T)>
where
Eq: Fn(&T, &T) -> bool,
{
for win in exprs.windows(2) {
if eq(&win[0], &win[1]) {
return Some((&win[0], &win[1]));
}
}
None
}
2 changes: 2 additions & 0 deletions clippy_lints/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -567,6 +567,7 @@ pub fn register_plugins(store: &mut rustc_lint::LintStore, sess: &Session, conf:
&copies::IFS_SAME_COND,
&copies::IF_SAME_THEN_ELSE,
&copies::SAME_FUNCTIONS_IN_IF_CONDITION,
&copies::SHARED_CODE_IN_IF_BLOCKS,
&copy_iterator::COPY_ITERATOR,
&create_dir::CREATE_DIR,
&dbg_macro::DBG_MACRO,
Expand Down Expand Up @@ -1285,6 +1286,7 @@ pub fn register_plugins(store: &mut rustc_lint::LintStore, sess: &Session, conf:
LintId::of(&bit_mask::VERBOSE_BIT_MASK),
LintId::of(&checked_conversions::CHECKED_CONVERSIONS),
LintId::of(&copies::SAME_FUNCTIONS_IN_IF_CONDITION),
LintId::of(&copies::SHARED_CODE_IN_IF_BLOCKS),
LintId::of(&copy_iterator::COPY_ITERATOR),
LintId::of(&default::DEFAULT_TRAIT_ACCESS),
LintId::of(&dereference::EXPLICIT_DEREF_METHODS),
Expand Down
Loading

0 comments on commit 6b13418

Please sign in to comment.