1 use clippy_utils::diagnostics::span_lint_and_then;
2 use clippy_utils::path_to_local;
3 use clippy_utils::source::snippet_opt;
4 use clippy_utils::ty::needs_ordered_drop;
5 use clippy_utils::visitors::{for_each_expr, for_each_expr_with_closures, is_local_used};
6 use core::ops::ControlFlow;
7 use rustc_errors::{Applicability, MultiSpan};
9 BindingAnnotation, Block, Expr, ExprKind, HirId, Local, LocalSource, MatchSource, Node, Pat, PatKind, Stmt,
12 use rustc_lint::{LateContext, LateLintPass};
13 use rustc_session::{declare_lint_pass, declare_tool_lint};
16 declare_clippy_lint! {
18 /// Checks for late initializations that can be replaced by a `let` statement
19 /// with an initializer.
21 /// ### Why is this bad?
22 /// Assigning in the `let` statement is less repetitive.
59 #[clippy::version = "1.59.0"]
60 pub NEEDLESS_LATE_INIT,
62 "late initializations that can be replaced by a `let` statement with an initializer"
64 declare_lint_pass!(NeedlessLateInit => [NEEDLESS_LATE_INIT]);
66 fn contains_assign_expr<'tcx>(cx: &LateContext<'tcx>, stmt: &'tcx Stmt<'tcx>) -> bool {
67 for_each_expr_with_closures(cx, stmt, |e| {
68 if matches!(e.kind, ExprKind::Assign(..)) {
69 ControlFlow::Break(())
71 ControlFlow::Continue(())
77 fn contains_let(cond: &Expr<'_>) -> bool {
78 for_each_expr(cond, |e| {
79 if matches!(e.kind, ExprKind::Let(_)) {
80 ControlFlow::Break(())
82 ControlFlow::Continue(())
88 fn stmt_needs_ordered_drop(cx: &LateContext<'_>, stmt: &Stmt<'_>) -> bool {
89 let StmtKind::Local(local) = stmt.kind else { return false };
90 !local.pat.walk_short(|pat| {
91 if let PatKind::Binding(.., None) = pat.kind {
92 !needs_ordered_drop(cx, cx.typeck_results().pat_ty(pat))
108 fn from_expr(expr: &Expr<'_>, span: Span) -> Option<Self> {
109 if let ExprKind::Assign(lhs, rhs, _) = expr.kind {
110 if lhs.span.from_expansion() {
115 lhs_id: path_to_local(lhs)?,
117 rhs_span: rhs.span.source_callsite(),
125 fn new<'tcx>(cx: &LateContext<'tcx>, expr: &'tcx Expr<'tcx>, binding_id: HirId) -> Option<LocalAssign> {
126 let assign = match expr.kind {
127 ExprKind::Block(Block { expr: Some(expr), .. }, _) => Self::from_expr(expr, expr.span),
128 ExprKind::Block(block, _) => {
130 if let Some((last, other_stmts)) = block.stmts.split_last();
131 if let StmtKind::Expr(expr) | StmtKind::Semi(expr) = last.kind;
133 let assign = Self::from_expr(expr, last.span)?;
135 // avoid visiting if not needed
136 if assign.lhs_id == binding_id;
137 if other_stmts.iter().all(|stmt| !contains_assign_expr(cx, stmt));
146 ExprKind::Assign(..) => Self::from_expr(expr, expr.span),
150 if assign.lhs_id == binding_id {
158 fn assignment_suggestions<'tcx>(
159 cx: &LateContext<'tcx>,
161 exprs: impl IntoIterator<Item = &'tcx Expr<'tcx>>,
162 ) -> Option<(Applicability, Vec<(Span, String)>)> {
163 let mut assignments = Vec::new();
166 let ty = cx.typeck_results().expr_ty(expr);
175 let assign = LocalAssign::new(cx, expr, binding_id)?;
177 assignments.push(assign);
180 let suggestions = assignments
182 .flat_map(|assignment| {
183 let mut spans = vec![assignment.span.until(assignment.rhs_span)];
185 if assignment.rhs_span.hi() != assignment.span.hi() {
186 spans.push(assignment.rhs_span.shrink_to_hi().with_hi(assignment.span.hi()));
191 .map(|span| (span, String::new()))
192 .collect::<Vec<(Span, String)>>();
194 match suggestions.len() {
195 // All of `exprs` are never types
196 // https://github.com/rust-lang/rust-clippy/issues/8911
198 1 => Some((Applicability::MachineApplicable, suggestions)),
199 // multiple suggestions don't work with rustfix in multipart_suggest
200 // https://github.com/rust-lang/rustfix/issues/141
201 _ => Some((Applicability::Unspecified, suggestions)),
206 stmt: &'tcx Stmt<'tcx>,
207 expr: &'tcx Expr<'tcx>,
211 fn first_usage<'tcx>(
212 cx: &LateContext<'tcx>,
214 local_stmt_id: HirId,
215 block: &'tcx Block<'tcx>,
216 ) -> Option<Usage<'tcx>> {
217 let significant_drop = needs_ordered_drop(cx, cx.typeck_results().node_type(binding_id));
222 .skip_while(|stmt| stmt.hir_id != local_stmt_id)
224 .take_while(|stmt| !significant_drop || !stmt_needs_ordered_drop(cx, stmt))
225 .find(|&stmt| is_local_used(cx, stmt, binding_id))
226 .and_then(|stmt| match stmt.kind {
227 StmtKind::Expr(expr) => Some(Usage {
232 StmtKind::Semi(expr) => Some(Usage {
241 fn local_snippet_without_semicolon(cx: &LateContext<'_>, local: &Local<'_>) -> Option<String> {
242 let span = local.span.with_hi(match local.ty {
245 Some(ty) => ty.span.hi(),
248 None => local.pat.span.hi(),
251 snippet_opt(cx, span)
255 cx: &LateContext<'tcx>,
256 local: &'tcx Local<'tcx>,
257 local_stmt: &'tcx Stmt<'tcx>,
258 block: &'tcx Block<'tcx>,
261 let usage = first_usage(cx, binding_id, local_stmt.hir_id, block)?;
262 let binding_name = cx.tcx.hir().opt_name(binding_id)?;
263 let let_snippet = local_snippet_without_semicolon(cx, local)?;
265 match usage.expr.kind {
266 ExprKind::Assign(..) => {
267 let assign = LocalAssign::new(cx, usage.expr, binding_id)?;
268 let mut msg_span = MultiSpan::from_spans(vec![local_stmt.span, assign.span]);
269 msg_span.push_span_label(local_stmt.span, "created here");
270 msg_span.push_span_label(assign.span, "initialised here");
276 "unneeded late initialization",
278 diag.tool_only_span_suggestion(
282 Applicability::MachineApplicable,
285 diag.span_suggestion(
287 format!("declare `{binding_name}` here"),
289 Applicability::MachineApplicable,
294 ExprKind::If(cond, then_expr, Some(else_expr)) if !contains_let(cond) => {
295 let (applicability, suggestions) = assignment_suggestions(cx, binding_id, [then_expr, else_expr])?;
301 "unneeded late initialization",
303 diag.tool_only_span_suggestion(local_stmt.span, "remove the local", String::new(), applicability);
305 diag.span_suggestion_verbose(
306 usage.stmt.span.shrink_to_lo(),
307 format!("declare `{binding_name}` here"),
308 format!("{let_snippet} = "),
312 diag.multipart_suggestion("remove the assignments from the branches", suggestions, applicability);
314 if usage.needs_semi {
315 diag.span_suggestion(
316 usage.stmt.span.shrink_to_hi(),
317 "add a semicolon after the `if` expression",
325 ExprKind::Match(_, arms, MatchSource::Normal) => {
326 let (applicability, suggestions) = assignment_suggestions(cx, binding_id, arms.iter().map(|arm| arm.body))?;
332 "unneeded late initialization",
334 diag.tool_only_span_suggestion(local_stmt.span, "remove the local", String::new(), applicability);
336 diag.span_suggestion_verbose(
337 usage.stmt.span.shrink_to_lo(),
338 format!("declare `{binding_name}` here"),
339 format!("{let_snippet} = "),
343 diag.multipart_suggestion(
344 "remove the assignments from the `match` arms",
349 if usage.needs_semi {
350 diag.span_suggestion(
351 usage.stmt.span.shrink_to_hi(),
352 "add a semicolon after the `match` expression",
366 impl<'tcx> LateLintPass<'tcx> for NeedlessLateInit {
367 fn check_local(&mut self, cx: &LateContext<'tcx>, local: &'tcx Local<'tcx>) {
368 let mut parents = cx.tcx.hir().parent_iter(local.hir_id);
373 kind: PatKind::Binding(BindingAnnotation::NONE, binding_id, _, None),
376 source: LocalSource::Normal,
379 if let Some((_, Node::Stmt(local_stmt))) = parents.next();
380 if let Some((_, Node::Block(block))) = parents.next();
383 check(cx, local, local_stmt, block, binding_id);