diff options
Diffstat (limited to 'src/tools/clippy/clippy_lints/src/needless_late_init.rs')
-rw-r--r-- | src/tools/clippy/clippy_lints/src/needless_late_init.rs | 390 |
1 files changed, 390 insertions, 0 deletions
diff --git a/src/tools/clippy/clippy_lints/src/needless_late_init.rs b/src/tools/clippy/clippy_lints/src/needless_late_init.rs new file mode 100644 index 000000000..ff2999b1f --- /dev/null +++ b/src/tools/clippy/clippy_lints/src/needless_late_init.rs @@ -0,0 +1,390 @@ +use clippy_utils::diagnostics::span_lint_and_then; +use clippy_utils::path_to_local; +use clippy_utils::source::snippet_opt; +use clippy_utils::ty::needs_ordered_drop; +use clippy_utils::visitors::{expr_visitor, expr_visitor_no_bodies, is_local_used}; +use rustc_errors::{Applicability, MultiSpan}; +use rustc_hir::intravisit::Visitor; +use rustc_hir::{ + BindingAnnotation, Block, Expr, ExprKind, HirId, Local, LocalSource, MatchSource, Node, Pat, PatKind, Stmt, + StmtKind, +}; +use rustc_lint::{LateContext, LateLintPass}; +use rustc_session::{declare_lint_pass, declare_tool_lint}; +use rustc_span::Span; + +declare_clippy_lint! { + /// ### What it does + /// Checks for late initializations that can be replaced by a `let` statement + /// with an initializer. + /// + /// ### Why is this bad? + /// Assigning in the `let` statement is less repetitive. + /// + /// ### Example + /// ```rust + /// let a; + /// a = 1; + /// + /// let b; + /// match 3 { + /// 0 => b = "zero", + /// 1 => b = "one", + /// _ => b = "many", + /// } + /// + /// let c; + /// if true { + /// c = 1; + /// } else { + /// c = -1; + /// } + /// ``` + /// Use instead: + /// ```rust + /// let a = 1; + /// + /// let b = match 3 { + /// 0 => "zero", + /// 1 => "one", + /// _ => "many", + /// }; + /// + /// let c = if true { + /// 1 + /// } else { + /// -1 + /// }; + /// ``` + #[clippy::version = "1.59.0"] + pub NEEDLESS_LATE_INIT, + style, + "late initializations that can be replaced by a `let` statement with an initializer" +} +declare_lint_pass!(NeedlessLateInit => [NEEDLESS_LATE_INIT]); + +fn contains_assign_expr<'tcx>(cx: &LateContext<'tcx>, stmt: &'tcx Stmt<'tcx>) -> bool { + let mut seen = false; + expr_visitor(cx, |expr| { + if let ExprKind::Assign(..) = expr.kind { + seen = true; + } + + !seen + }) + .visit_stmt(stmt); + + seen +} + +fn contains_let(cond: &Expr<'_>) -> bool { + let mut seen = false; + expr_visitor_no_bodies(|expr| { + if let ExprKind::Let(_) = expr.kind { + seen = true; + } + + !seen + }) + .visit_expr(cond); + + seen +} + +fn stmt_needs_ordered_drop(cx: &LateContext<'_>, stmt: &Stmt<'_>) -> bool { + let StmtKind::Local(local) = stmt.kind else { return false }; + !local.pat.walk_short(|pat| { + if let PatKind::Binding(.., None) = pat.kind { + !needs_ordered_drop(cx, cx.typeck_results().pat_ty(pat)) + } else { + true + } + }) +} + +#[derive(Debug)] +struct LocalAssign { + lhs_id: HirId, + lhs_span: Span, + rhs_span: Span, + span: Span, +} + +impl LocalAssign { + fn from_expr(expr: &Expr<'_>, span: Span) -> Option<Self> { + if let ExprKind::Assign(lhs, rhs, _) = expr.kind { + if lhs.span.from_expansion() { + return None; + } + + Some(Self { + lhs_id: path_to_local(lhs)?, + lhs_span: lhs.span, + rhs_span: rhs.span.source_callsite(), + span, + }) + } else { + None + } + } + + fn new<'tcx>(cx: &LateContext<'tcx>, expr: &'tcx Expr<'tcx>, binding_id: HirId) -> Option<LocalAssign> { + let assign = match expr.kind { + ExprKind::Block(Block { expr: Some(expr), .. }, _) => Self::from_expr(expr, expr.span), + ExprKind::Block(block, _) => { + if_chain! { + if let Some((last, other_stmts)) = block.stmts.split_last(); + if let StmtKind::Expr(expr) | StmtKind::Semi(expr) = last.kind; + + let assign = Self::from_expr(expr, last.span)?; + + // avoid visiting if not needed + if assign.lhs_id == binding_id; + if other_stmts.iter().all(|stmt| !contains_assign_expr(cx, stmt)); + + then { + Some(assign) + } else { + None + } + } + }, + ExprKind::Assign(..) => Self::from_expr(expr, expr.span), + _ => None, + }?; + + if assign.lhs_id == binding_id { + Some(assign) + } else { + None + } + } +} + +fn assignment_suggestions<'tcx>( + cx: &LateContext<'tcx>, + binding_id: HirId, + exprs: impl IntoIterator<Item = &'tcx Expr<'tcx>>, +) -> Option<(Applicability, Vec<(Span, String)>)> { + let mut assignments = Vec::new(); + + for expr in exprs { + let ty = cx.typeck_results().expr_ty(expr); + + if ty.is_never() { + continue; + } + if !ty.is_unit() { + return None; + } + + let assign = LocalAssign::new(cx, expr, binding_id)?; + + assignments.push(assign); + } + + let suggestions = assignments + .iter() + .flat_map(|assignment| { + [ + assignment.span.until(assignment.rhs_span), + assignment.rhs_span.shrink_to_hi().with_hi(assignment.span.hi()), + ] + }) + .map(|span| (span, String::new())) + .collect::<Vec<(Span, String)>>(); + + match suggestions.len() { + // All of `exprs` are never types + // https://github.com/rust-lang/rust-clippy/issues/8911 + 0 => None, + 1 => Some((Applicability::MachineApplicable, suggestions)), + // multiple suggestions don't work with rustfix in multipart_suggest + // https://github.com/rust-lang/rustfix/issues/141 + _ => Some((Applicability::Unspecified, suggestions)), + } +} + +struct Usage<'tcx> { + stmt: &'tcx Stmt<'tcx>, + expr: &'tcx Expr<'tcx>, + needs_semi: bool, +} + +fn first_usage<'tcx>( + cx: &LateContext<'tcx>, + binding_id: HirId, + local_stmt_id: HirId, + block: &'tcx Block<'tcx>, +) -> Option<Usage<'tcx>> { + let significant_drop = needs_ordered_drop(cx, cx.typeck_results().node_type(binding_id)); + + block + .stmts + .iter() + .skip_while(|stmt| stmt.hir_id != local_stmt_id) + .skip(1) + .take_while(|stmt| !significant_drop || !stmt_needs_ordered_drop(cx, stmt)) + .find(|&stmt| is_local_used(cx, stmt, binding_id)) + .and_then(|stmt| match stmt.kind { + StmtKind::Expr(expr) => Some(Usage { + stmt, + expr, + needs_semi: true, + }), + StmtKind::Semi(expr) => Some(Usage { + stmt, + expr, + needs_semi: false, + }), + _ => None, + }) +} + +fn local_snippet_without_semicolon(cx: &LateContext<'_>, local: &Local<'_>) -> Option<String> { + let span = local.span.with_hi(match local.ty { + // let <pat>: <ty>; + // ~~~~~~~~~~~~~~~ + Some(ty) => ty.span.hi(), + // let <pat>; + // ~~~~~~~~~ + None => local.pat.span.hi(), + }); + + snippet_opt(cx, span) +} + +fn check<'tcx>( + cx: &LateContext<'tcx>, + local: &'tcx Local<'tcx>, + local_stmt: &'tcx Stmt<'tcx>, + block: &'tcx Block<'tcx>, + binding_id: HirId, +) -> Option<()> { + let usage = first_usage(cx, binding_id, local_stmt.hir_id, block)?; + let binding_name = cx.tcx.hir().opt_name(binding_id)?; + let let_snippet = local_snippet_without_semicolon(cx, local)?; + + match usage.expr.kind { + ExprKind::Assign(..) => { + let assign = LocalAssign::new(cx, usage.expr, binding_id)?; + let mut msg_span = MultiSpan::from_spans(vec![local_stmt.span, assign.span]); + msg_span.push_span_label(local_stmt.span, "created here"); + msg_span.push_span_label(assign.span, "initialised here"); + + span_lint_and_then( + cx, + NEEDLESS_LATE_INIT, + msg_span, + "unneeded late initialization", + |diag| { + diag.tool_only_span_suggestion( + local_stmt.span, + "remove the local", + "", + Applicability::MachineApplicable, + ); + + diag.span_suggestion( + assign.lhs_span, + &format!("declare `{}` here", binding_name), + let_snippet, + Applicability::MachineApplicable, + ); + }, + ); + }, + ExprKind::If(cond, then_expr, Some(else_expr)) if !contains_let(cond) => { + let (applicability, suggestions) = assignment_suggestions(cx, binding_id, [then_expr, else_expr])?; + + span_lint_and_then( + cx, + NEEDLESS_LATE_INIT, + local_stmt.span, + "unneeded late initialization", + |diag| { + diag.tool_only_span_suggestion(local_stmt.span, "remove the local", String::new(), applicability); + + diag.span_suggestion_verbose( + usage.stmt.span.shrink_to_lo(), + &format!("declare `{}` here", binding_name), + format!("{} = ", let_snippet), + applicability, + ); + + diag.multipart_suggestion("remove the assignments from the branches", suggestions, applicability); + + if usage.needs_semi { + diag.span_suggestion( + usage.stmt.span.shrink_to_hi(), + "add a semicolon after the `if` expression", + ";", + applicability, + ); + } + }, + ); + }, + ExprKind::Match(_, arms, MatchSource::Normal) => { + let (applicability, suggestions) = assignment_suggestions(cx, binding_id, arms.iter().map(|arm| arm.body))?; + + span_lint_and_then( + cx, + NEEDLESS_LATE_INIT, + local_stmt.span, + "unneeded late initialization", + |diag| { + diag.tool_only_span_suggestion(local_stmt.span, "remove the local", String::new(), applicability); + + diag.span_suggestion_verbose( + usage.stmt.span.shrink_to_lo(), + &format!("declare `{}` here", binding_name), + format!("{} = ", let_snippet), + applicability, + ); + + diag.multipart_suggestion( + "remove the assignments from the `match` arms", + suggestions, + applicability, + ); + + if usage.needs_semi { + diag.span_suggestion( + usage.stmt.span.shrink_to_hi(), + "add a semicolon after the `match` expression", + ";", + applicability, + ); + } + }, + ); + }, + _ => {}, + }; + + Some(()) +} + +impl<'tcx> LateLintPass<'tcx> for NeedlessLateInit { + fn check_local(&mut self, cx: &LateContext<'tcx>, local: &'tcx Local<'tcx>) { + let mut parents = cx.tcx.hir().parent_iter(local.hir_id); + if_chain! { + if let Local { + init: None, + pat: &Pat { + kind: PatKind::Binding(BindingAnnotation::Unannotated, binding_id, _, None), + .. + }, + source: LocalSource::Normal, + .. + } = local; + if let Some((_, Node::Stmt(local_stmt))) = parents.next(); + if let Some((_, Node::Block(block))) = parents.next(); + + then { + check(cx, local, local_stmt, block, binding_id); + } + } + } +} |