]> git.lizzy.rs Git - rust.git/blob - src/tools/clippy/clippy_lints/src/needless_late_init.rs
Rollup merge of #103104 - SUPERCILEX:sep-ref, r=dtolnay
[rust.git] / src / tools / clippy / clippy_lints / src / needless_late_init.rs
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};
8 use rustc_hir::{
9     BindingAnnotation, Block, Expr, ExprKind, HirId, Local, LocalSource, MatchSource, Node, Pat, PatKind, Stmt,
10     StmtKind,
11 };
12 use rustc_lint::{LateContext, LateLintPass};
13 use rustc_session::{declare_lint_pass, declare_tool_lint};
14 use rustc_span::Span;
15
16 declare_clippy_lint! {
17     /// ### What it does
18     /// Checks for late initializations that can be replaced by a `let` statement
19     /// with an initializer.
20     ///
21     /// ### Why is this bad?
22     /// Assigning in the `let` statement is less repetitive.
23     ///
24     /// ### Example
25     /// ```rust
26     /// let a;
27     /// a = 1;
28     ///
29     /// let b;
30     /// match 3 {
31     ///     0 => b = "zero",
32     ///     1 => b = "one",
33     ///     _ => b = "many",
34     /// }
35     ///
36     /// let c;
37     /// if true {
38     ///     c = 1;
39     /// } else {
40     ///     c = -1;
41     /// }
42     /// ```
43     /// Use instead:
44     /// ```rust
45     /// let a = 1;
46     ///
47     /// let b = match 3 {
48     ///     0 => "zero",
49     ///     1 => "one",
50     ///     _ => "many",
51     /// };
52     ///
53     /// let c = if true {
54     ///     1
55     /// } else {
56     ///     -1
57     /// };
58     /// ```
59     #[clippy::version = "1.59.0"]
60     pub NEEDLESS_LATE_INIT,
61     style,
62     "late initializations that can be replaced by a `let` statement with an initializer"
63 }
64 declare_lint_pass!(NeedlessLateInit => [NEEDLESS_LATE_INIT]);
65
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(())
70         } else {
71             ControlFlow::Continue(())
72         }
73     })
74     .is_some()
75 }
76
77 fn contains_let(cond: &Expr<'_>) -> bool {
78     for_each_expr(cond, |e| {
79         if matches!(e.kind, ExprKind::Let(_)) {
80             ControlFlow::Break(())
81         } else {
82             ControlFlow::Continue(())
83         }
84     })
85     .is_some()
86 }
87
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))
93         } else {
94             true
95         }
96     })
97 }
98
99 #[derive(Debug)]
100 struct LocalAssign {
101     lhs_id: HirId,
102     lhs_span: Span,
103     rhs_span: Span,
104     span: Span,
105 }
106
107 impl LocalAssign {
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() {
111                 return None;
112             }
113
114             Some(Self {
115                 lhs_id: path_to_local(lhs)?,
116                 lhs_span: lhs.span,
117                 rhs_span: rhs.span.source_callsite(),
118                 span,
119             })
120         } else {
121             None
122         }
123     }
124
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, _) => {
129                 if_chain! {
130                     if let Some((last, other_stmts)) = block.stmts.split_last();
131                     if let StmtKind::Expr(expr) | StmtKind::Semi(expr) = last.kind;
132
133                     let assign = Self::from_expr(expr, last.span)?;
134
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));
138
139                     then {
140                         Some(assign)
141                     } else {
142                         None
143                     }
144                 }
145             },
146             ExprKind::Assign(..) => Self::from_expr(expr, expr.span),
147             _ => None,
148         }?;
149
150         if assign.lhs_id == binding_id {
151             Some(assign)
152         } else {
153             None
154         }
155     }
156 }
157
158 fn assignment_suggestions<'tcx>(
159     cx: &LateContext<'tcx>,
160     binding_id: HirId,
161     exprs: impl IntoIterator<Item = &'tcx Expr<'tcx>>,
162 ) -> Option<(Applicability, Vec<(Span, String)>)> {
163     let mut assignments = Vec::new();
164
165     for expr in exprs {
166         let ty = cx.typeck_results().expr_ty(expr);
167
168         if ty.is_never() {
169             continue;
170         }
171         if !ty.is_unit() {
172             return None;
173         }
174
175         let assign = LocalAssign::new(cx, expr, binding_id)?;
176
177         assignments.push(assign);
178     }
179
180     let suggestions = assignments
181         .iter()
182         .flat_map(|assignment| {
183             let mut spans = vec![assignment.span.until(assignment.rhs_span)];
184
185             if assignment.rhs_span.hi() != assignment.span.hi() {
186                 spans.push(assignment.rhs_span.shrink_to_hi().with_hi(assignment.span.hi()));
187             }
188
189             spans
190         })
191         .map(|span| (span, String::new()))
192         .collect::<Vec<(Span, String)>>();
193
194     match suggestions.len() {
195         // All of `exprs` are never types
196         // https://github.com/rust-lang/rust-clippy/issues/8911
197         0 => None,
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)),
202     }
203 }
204
205 struct Usage<'tcx> {
206     stmt: &'tcx Stmt<'tcx>,
207     expr: &'tcx Expr<'tcx>,
208     needs_semi: bool,
209 }
210
211 fn first_usage<'tcx>(
212     cx: &LateContext<'tcx>,
213     binding_id: HirId,
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));
218
219     block
220         .stmts
221         .iter()
222         .skip_while(|stmt| stmt.hir_id != local_stmt_id)
223         .skip(1)
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 {
228                 stmt,
229                 expr,
230                 needs_semi: true,
231             }),
232             StmtKind::Semi(expr) => Some(Usage {
233                 stmt,
234                 expr,
235                 needs_semi: false,
236             }),
237             _ => None,
238         })
239 }
240
241 fn local_snippet_without_semicolon(cx: &LateContext<'_>, local: &Local<'_>) -> Option<String> {
242     let span = local.span.with_hi(match local.ty {
243         // let <pat>: <ty>;
244         // ~~~~~~~~~~~~~~~
245         Some(ty) => ty.span.hi(),
246         // let <pat>;
247         // ~~~~~~~~~
248         None => local.pat.span.hi(),
249     });
250
251     snippet_opt(cx, span)
252 }
253
254 fn check<'tcx>(
255     cx: &LateContext<'tcx>,
256     local: &'tcx Local<'tcx>,
257     local_stmt: &'tcx Stmt<'tcx>,
258     block: &'tcx Block<'tcx>,
259     binding_id: HirId,
260 ) -> Option<()> {
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)?;
264
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");
271
272             span_lint_and_then(
273                 cx,
274                 NEEDLESS_LATE_INIT,
275                 msg_span,
276                 "unneeded late initialization",
277                 |diag| {
278                     diag.tool_only_span_suggestion(
279                         local_stmt.span,
280                         "remove the local",
281                         "",
282                         Applicability::MachineApplicable,
283                     );
284
285                     diag.span_suggestion(
286                         assign.lhs_span,
287                         format!("declare `{binding_name}` here"),
288                         let_snippet,
289                         Applicability::MachineApplicable,
290                     );
291                 },
292             );
293         },
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])?;
296
297             span_lint_and_then(
298                 cx,
299                 NEEDLESS_LATE_INIT,
300                 local_stmt.span,
301                 "unneeded late initialization",
302                 |diag| {
303                     diag.tool_only_span_suggestion(local_stmt.span, "remove the local", String::new(), applicability);
304
305                     diag.span_suggestion_verbose(
306                         usage.stmt.span.shrink_to_lo(),
307                         format!("declare `{binding_name}` here"),
308                         format!("{let_snippet} = "),
309                         applicability,
310                     );
311
312                     diag.multipart_suggestion("remove the assignments from the branches", suggestions, applicability);
313
314                     if usage.needs_semi {
315                         diag.span_suggestion(
316                             usage.stmt.span.shrink_to_hi(),
317                             "add a semicolon after the `if` expression",
318                             ";",
319                             applicability,
320                         );
321                     }
322                 },
323             );
324         },
325         ExprKind::Match(_, arms, MatchSource::Normal) => {
326             let (applicability, suggestions) = assignment_suggestions(cx, binding_id, arms.iter().map(|arm| arm.body))?;
327
328             span_lint_and_then(
329                 cx,
330                 NEEDLESS_LATE_INIT,
331                 local_stmt.span,
332                 "unneeded late initialization",
333                 |diag| {
334                     diag.tool_only_span_suggestion(local_stmt.span, "remove the local", String::new(), applicability);
335
336                     diag.span_suggestion_verbose(
337                         usage.stmt.span.shrink_to_lo(),
338                         format!("declare `{binding_name}` here"),
339                         format!("{let_snippet} = "),
340                         applicability,
341                     );
342
343                     diag.multipart_suggestion(
344                         "remove the assignments from the `match` arms",
345                         suggestions,
346                         applicability,
347                     );
348
349                     if usage.needs_semi {
350                         diag.span_suggestion(
351                             usage.stmt.span.shrink_to_hi(),
352                             "add a semicolon after the `match` expression",
353                             ";",
354                             applicability,
355                         );
356                     }
357                 },
358             );
359         },
360         _ => {},
361     };
362
363     Some(())
364 }
365
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);
369         if_chain! {
370             if let Local {
371                 init: None,
372                 pat: &Pat {
373                     kind: PatKind::Binding(BindingAnnotation::NONE, binding_id, _, None),
374                     ..
375                 },
376                 source: LocalSource::Normal,
377                 ..
378             } = local;
379             if let Some((_, Node::Stmt(local_stmt))) = parents.next();
380             if let Some((_, Node::Block(block))) = parents.next();
381
382             then {
383                 check(cx, local, local_stmt, block, binding_id);
384             }
385         }
386     }
387 }