]> git.lizzy.rs Git - rust.git/blobdiff - clippy_lints/src/copies.rs
Auto merge of #7460 - camsteffen:run-from-source, r=Manishearth
[rust.git] / clippy_lints / src / copies.rs
index 089f8c3ab0defb4093b5a6afc92199c28510b5b2..9cbcde597686e8c212c45ce95d4e28ad954624cf 100644 (file)
@@ -1,16 +1,18 @@
-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, if_sequence, indent_of, parent_node_is_if_expr, reindent_multiline, snippet,
-    snippet_opt, span_lint_and_note, span_lint_and_sugg, span_lint_and_then,
+use clippy_utils::diagnostics::{span_lint_and_note, span_lint_and_then};
+use clippy_utils::source::{first_line_of_span, indent_of, reindent_multiline, snippet, snippet_opt};
+use clippy_utils::{
+    both, count_eq, eq_expr_value, get_enclosing_block, get_parent_expr, if_sequence, in_macro, is_else_clause,
+    is_lint_allowed, search_same, ContainsName, SpanlessEq, SpanlessHash,
 };
+use if_chain::if_chain;
 use rustc_data_structures::fx::FxHashSet;
-use rustc_errors::Applicability;
+use rustc_errors::{Applicability, DiagnosticBuilder};
 use rustc_hir::intravisit::{self, NestedVisitorMap, Visitor};
 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, BytePos};
+use rustc_span::{source_map::Span, symbol::Symbol, BytePos};
 use std::borrow::Cow;
 
 declare_clippy_lint! {
     ///
     /// **Why is this bad?** Duplicate code is less maintainable.
     ///
-    /// **Known problems:** Hopefully none.
+    /// **Known problems:**
+    /// * The lint doesn't check if the moved expressions modify values that are beeing used in
+    ///   the if condition. The suggestion can in that case modify the behavior of the program.
+    ///   See [rust-clippy#7452](https://github.com/rust-lang/rust-clippy/issues/7452)
     ///
     /// **Example:**
     /// ```ignore
     ///     42
     /// };
     /// ```
-    pub SHARED_CODE_IN_IF_BLOCKS,
-    nursery,
+    pub BRANCHES_SHARING_CODE,
+    complexity,
     "`if` statement with shared code in all blocks"
 }
 
     IFS_SAME_COND,
     SAME_FUNCTIONS_IN_IF_CONDITION,
     IF_SAME_THEN_ELSE,
-    SHARED_CODE_IN_IF_BLOCKS
+    BRANCHES_SHARING_CODE
 ]);
 
 impl<'tcx> LateLintPass<'tcx> for CopyAndPaste {
     fn check_expr(&mut self, cx: &LateContext<'tcx>, expr: &'tcx Expr<'_>) {
         if !expr.span.from_expansion() {
-            // skip ifs directly in else, it will be checked in the parent if
-            if let Some(&Expr {
-                kind: ExprKind::If(_, _, Some(ref else_expr)),
-                ..
-            }) = get_parent_expr(cx, expr)
-            {
-                if else_expr.hir_id == expr.hir_id {
-                    return;
+            if let ExprKind::If(_, _, _) = expr.kind {
+                // skip ifs directly in else, it will be checked in the parent if
+                if let Some(&Expr {
+                    kind: ExprKind::If(_, _, Some(else_expr)),
+                    ..
+                }) = get_parent_expr(cx, expr)
+                {
+                    if else_expr.hir_id == expr.hir_id {
+                        return;
+                    }
                 }
-            }
 
-            let (conds, blocks) = if_sequence(expr);
-            // 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);
+                let (conds, blocks) = if_sequence(expr);
+                // 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 `SHARED_CODE_IN_IF_BLOCKS` and `IF_SAME_THEN_ELSE` if the blocks are equal.
+/// Implementation of `BRANCHES_SHARING_CODE` 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,
+    has_conditional_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 || parent_node_is_if_expr(expr, cx) {
+    if blocks.len() < 2 || is_else_clause(cx.tcx, expr) {
         return;
     }
 
-    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 win in blocks.windows(2) {
-        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
-        if 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;
-    }
+    let has_expr = blocks[0].expr.is_some();
 
-    // SHARED_CODE_IN_IF_BLOCKS prerequisites
-    if !has_unconditional_else || (start_eq == 0 && end_eq == 0 && (has_expr && !expr_eq)) {
+    let (start_eq, mut end_eq, expr_eq) = if let Some(block_eq) = scan_block_for_eq(cx, blocks) {
+        (block_eq.start_eq, block_eq.end_eq, block_eq.expr_eq)
+    } else {
         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;
+    // BRANCHES_SHARING_CODE prerequisites
+    if has_conditional_else || (start_eq == 0 && end_eq == 0 && (has_expr && !expr_eq)) {
+        return;
     }
 
     // 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[0];
+        let start_stmts = block.stmts.split_at(start_eq).0;
+
+        let mut start_walker = UsedValueFinderVisitor::new(cx);
+        for stmt in start_stmts {
+            intravisit::walk_stmt(&mut start_walker, stmt);
+        }
+
+        emit_branches_sharing_code_lint(
+            cx,
+            start_eq,
+            0,
+            false,
+            check_for_warn_of_moved_symbol(cx, &start_walker.def_symbols, expr),
+            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);
+        let (start_stmts, block_stmts) = block.stmts.split_at(start_eq);
+        let (block_stmts, end_stmts) = block_stmts.split_at(block_stmts.len() - end_eq);
+
+        // Scan start
+        let mut start_walker = UsedValueFinderVisitor::new(cx);
+        for stmt in start_stmts {
+            intravisit::walk_stmt(&mut start_walker, stmt);
+        }
+        let mut moved_syms = start_walker.def_symbols;
 
         // Scan block
-        let mut walker = SymbolFinderVisitor::new(cx);
+        let mut block_walker = UsedValueFinderVisitor::new(cx);
         for stmt in block_stmts {
-            intravisit::walk_stmt(&mut walker, stmt);
+            intravisit::walk_stmt(&mut block_walker, stmt);
         }
-        let mut block_defs = walker.defs;
+        let mut block_defs = block_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);
+        let mut end_walker = UsedValueFinderVisitor::new(cx);
+        for (index, stmt) in end_stmts.iter().enumerate() {
+            intravisit::walk_stmt(&mut end_walker, stmt);
 
-            for value in &walker.uses {
+            for value in &end_walker.uses {
                 // Well we can't move this and all prev statements. So reset
-                if block_defs.contains(&value) {
+                if block_defs.contains(value) {
                     moved_start = Some(index + 1);
-                    walker.defs.drain().for_each(|x| {
+                    end_walker.defs.drain().for_each(|x| {
                         block_defs.insert(x);
                     });
+
+                    end_walker.def_symbols.clear();
                 }
             }
 
-            walker.uses.clear();
+            end_walker.uses.clear();
         }
 
         if let Some(moved_start) = moved_start {
             end_eq -= moved_start;
         }
 
-        let end_linable = if let Some(expr) = block.expr {
-            intravisit::walk_expr(&mut walker, expr);
-            walker.uses.iter().any(|x| !block_defs.contains(x))
-        } else if end_eq == 0 {
-            false
-        } else {
-            true
+        let end_linable = block.expr.map_or_else(
+            || end_eq != 0,
+            |expr| {
+                intravisit::walk_expr(&mut end_walker, expr);
+                end_walker.uses.iter().any(|x| !block_defs.contains(x))
+            },
+        );
+
+        if end_linable {
+            end_walker.def_symbols.drain().for_each(|x| {
+                moved_syms.insert(x);
+            });
+        }
+
+        emit_branches_sharing_code_lint(
+            cx,
+            start_eq,
+            end_eq,
+            end_linable,
+            check_for_warn_of_moved_symbol(cx, &moved_syms, expr),
+            blocks,
+            expr,
+        );
+    }
+}
+
+struct BlockEqual {
+    /// The amount statements that are equal from the start
+    start_eq: usize,
+    /// The amount statements that are equal from the end
+    end_eq: usize,
+    ///  An indication if the block expressions are the same. This will also be true if both are
+    /// `None`
+    expr_eq: bool,
+}
+
+/// This function can also trigger the `IF_SAME_THEN_ELSE` in which case it'll return `None` to
+/// abort any further processing and avoid duplicate lint triggers.
+fn scan_block_for_eq(cx: &LateContext<'tcx>, blocks: &[&Block<'tcx>]) -> Option<BlockEqual> {
+    let mut start_eq = usize::MAX;
+    let mut end_eq = usize::MAX;
+    let mut expr_eq = true;
+    for win in blocks.windows(2) {
+        let l_stmts = win[0].stmts;
+        let r_stmts = win[1].stmts;
+
+        // `SpanlessEq` now keeps track of the locals and is therefore context sensitive clippy#6752.
+        // The comparison therefore needs to be done in a way that builds the correct context.
+        let mut evaluator = SpanlessEq::new(cx);
+        let mut evaluator = evaluator.inter_expr();
+
+        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 = {
+            // We skip the middle statements which can't be equal
+            let end_comparison_count = l_stmts.len().min(r_stmts.len()) - current_start_eq;
+            let it1 = l_stmts.iter().skip(l_stmts.len() - end_comparison_count);
+            let it2 = r_stmts.iter().skip(r_stmts.len() - end_comparison_count);
+            it1.zip(it2)
+                .fold(0, |acc, (l, r)| if evaluator.eq_stmt(l, r) { acc + 1 } else { 0 })
         };
+        let block_expr_eq = both(&win[0].expr, &win[1].expr, |l, r| evaluator.eq_expr(l, r));
+
+        // IF_SAME_THEN_ELSE
+        if_chain! {
+            if block_expr_eq;
+            if l_stmts.len() == r_stmts.len();
+            if l_stmts.len() == current_start_eq;
+            if !is_lint_allowed(cx, IF_SAME_THEN_ELSE, win[0].hir_id);
+            if !is_lint_allowed(cx, IF_SAME_THEN_ELSE, win[1].hir_id);
+            then {
+                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 None;
+            }
+        }
+
+        start_eq = start_eq.min(current_start_eq);
+        end_eq = end_eq.min(current_end_eq);
+        expr_eq &= block_expr_eq;
+    }
 
-        emit_shared_code_in_if_blocks_lint(cx, start_eq, end_eq, end_linable, blocks, expr);
+    if !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;
     }
+
+    Some(BlockEqual {
+        start_eq,
+        end_eq,
+        expr_eq,
+    })
+}
+
+fn check_for_warn_of_moved_symbol(
+    cx: &LateContext<'tcx>,
+    symbols: &FxHashSet<Symbol>,
+    if_expr: &'tcx Expr<'_>,
+) -> bool {
+    get_enclosing_block(cx, if_expr.hir_id).map_or(false, |block| {
+        let ignore_span = block.span.shrink_to_lo().to(if_expr.span);
+
+        symbols
+            .iter()
+            .filter(|sym| !sym.as_str().starts_with('_'))
+            .any(move |sym| {
+                let mut walker = ContainsName {
+                    name: *sym,
+                    result: false,
+                };
+
+                // Scan block
+                block
+                    .stmts
+                    .iter()
+                    .filter(|stmt| !ignore_span.overlaps(stmt.span))
+                    .for_each(|stmt| intravisit::walk_stmt(&mut walker, stmt));
+
+                if let Some(expr) = block.expr {
+                    intravisit::walk_expr(&mut walker, expr);
+                }
+
+                walker.result
+            })
+    })
 }
 
-fn emit_shared_code_in_if_blocks_lint(
+fn emit_branches_sharing_code_lint(
     cx: &LateContext<'tcx>,
     start_stmts: usize,
     end_stmts: usize,
     lint_end: bool,
+    warn_about_moved_symbol: bool,
     blocks: &[&Block<'tcx>],
     if_expr: &'tcx Expr<'_>,
 ) {
@@ -305,7 +426,9 @@ fn emit_shared_code_in_if_blocks_lint(
 
     // (help, span, suggestion)
     let mut suggestions: Vec<(&str, Span, String)> = vec![];
+    let mut add_expr_note = false;
 
+    // Construct suggestions
     if start_stmts > 0 {
         let block = blocks[0];
         let span_start = first_line_of_span(cx, if_expr.span).shrink_to_lo();
@@ -333,12 +456,10 @@ fn emit_shared_code_in_if_blocks_lint(
             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_end = block
+            .expr
+            .map_or_else(|| block.stmts[block.stmts.len() - 1].span, |expr| expr.span)
+            .source_callsite();
 
         let moved_span = moved_start.to(moved_end);
         let moved_snipped = reindent_multiline(snippet(cx, moved_span, "_"), true, None);
@@ -357,67 +478,86 @@ fn emit_shared_code_in_if_blocks_lint(
         }
 
         suggestions.push(("end", span, suggestion.to_string()));
+        add_expr_note = !cx.typeck_results().expr_ty(if_expr).is_unit();
     }
 
+    let add_optional_msgs = |diag: &mut DiagnosticBuilder<'_>| {
+        if add_expr_note {
+            diag.note("The end suggestion probably needs some adjustments to use the expression result correctly");
+        }
+
+        if warn_about_moved_symbol {
+            diag.warn("Some moved values might need to be renamed to avoid wrong references");
+        }
+    };
+
+    // Emit lint
     if suggestions.len() == 1 {
         let (place_str, span, sugg) = suggestions.pop().unwrap();
-        let msg = format!("All if blocks contain the same code at the {}", place_str);
-        let help = format!("Consider moving the {} statements out like this", place_str);
-        span_lint_and_sugg(
-            cx,
-            SHARED_CODE_IN_IF_BLOCKS,
-            span,
-            msg.as_str(),
-            help.as_str(),
-            sugg,
-            Applicability::Unspecified,
-        );
+        let msg = format!("all if blocks contain the same code at the {}", place_str);
+        let help = format!("consider moving the {} statements out like this", place_str);
+        span_lint_and_then(cx, BRANCHES_SHARING_CODE, span, msg.as_str(), |diag| {
+            diag.span_suggestion(span, help.as_str(), sugg, Applicability::Unspecified);
+
+            add_optional_msgs(diag);
+        });
     } else if suggestions.len() == 2 {
         let (_, end_span, end_sugg) = suggestions.pop().unwrap();
         let (_, start_span, start_sugg) = suggestions.pop().unwrap();
         span_lint_and_then(
             cx,
-            SHARED_CODE_IN_IF_BLOCKS,
+            BRANCHES_SHARING_CODE,
             start_span,
-            "All if blocks contain the same code at the start and the end. Here at the start:",
+            "all if blocks contain the same code at the start and the end. Here at the start",
             move |diag| {
-                diag.span_note(end_span, "And here at the end:");
+                diag.span_note(end_span, "and here at the end");
 
                 diag.span_suggestion(
                     start_span,
-                    "Consider moving the start statements out like this:",
+                    "consider moving the start statements out like this",
                     start_sugg,
                     Applicability::Unspecified,
                 );
 
                 diag.span_suggestion(
                     end_span,
-                    "And consider moving the end statements out like this:",
+                    "and consider moving the end statements out like this",
                     end_sugg,
                     Applicability::Unspecified,
                 );
+
+                add_optional_msgs(diag);
             },
         );
     }
 }
 
-pub struct SymbolFinderVisitor<'a, 'tcx> {
+/// This visitor collects `HirId`s and Symbols of defined symbols and `HirId`s of used values.
+struct UsedValueFinderVisitor<'a, 'tcx> {
     cx: &'a LateContext<'tcx>,
+
+    /// The `HirId`s of defined values in the scanned statements
     defs: FxHashSet<HirId>,
+
+    /// The Symbols of the defined symbols in the scanned statements
+    def_symbols: FxHashSet<Symbol>,
+
+    /// The `HirId`s of the used values
     uses: FxHashSet<HirId>,
 }
 
-impl<'a, 'tcx> SymbolFinderVisitor<'a, 'tcx> {
+impl<'a, 'tcx> UsedValueFinderVisitor<'a, 'tcx> {
     fn new(cx: &'a LateContext<'tcx>) -> Self {
-        SymbolFinderVisitor {
+        UsedValueFinderVisitor {
             cx,
             defs: FxHashSet::default(),
+            def_symbols: FxHashSet::default(),
             uses: FxHashSet::default(),
         }
     }
 }
 
-impl<'a, 'tcx> Visitor<'tcx> for SymbolFinderVisitor<'a, 'tcx> {
+impl<'a, 'tcx> Visitor<'tcx> for UsedValueFinderVisitor<'a, 'tcx> {
     type Map = Map<'tcx>;
 
     fn nested_visit_map(&mut self) -> NestedVisitorMap<Self::Map> {
@@ -427,13 +567,18 @@ fn nested_visit_map(&mut self) -> NestedVisitorMap<Self::Map> {
     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(sym) = l.pat.simple_ident() {
+            self.def_symbols.insert(sym.name);
+        }
+
         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 let rustc_hir::QPath::Resolved(_, 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);