]> git.lizzy.rs Git - rust.git/blob - src/tools/clippy/clippy_lints/src/needless_late_init.rs
Merge commit 'a5d597637dcb78dc73f93561ce474f23d4177c35' into clippyup
[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::visitors::{expr_visitor, is_local_used};
5 use rustc_errors::Applicability;
6 use rustc_hir::intravisit::Visitor;
7 use rustc_hir::{Block, Expr, ExprKind, HirId, Local, LocalSource, MatchSource, Node, Pat, PatKind, Stmt, StmtKind};
8 use rustc_lint::{LateContext, LateLintPass};
9 use rustc_session::{declare_lint_pass, declare_tool_lint};
10 use rustc_span::Span;
11
12 declare_clippy_lint! {
13     /// ### What it does
14     /// Checks for late initializations that can be replaced by a `let` statement
15     /// with an initializer.
16     ///
17     /// ### Why is this bad?
18     /// Assigning in the `let` statement is less repetitive.
19     ///
20     /// ### Example
21     /// ```rust
22     /// let a;
23     /// a = 1;
24     ///
25     /// let b;
26     /// match 3 {
27     ///     0 => b = "zero",
28     ///     1 => b = "one",
29     ///     _ => b = "many",
30     /// }
31     ///
32     /// let c;
33     /// if true {
34     ///     c = 1;
35     /// } else {
36     ///     c = -1;
37     /// }
38     /// ```
39     /// Use instead:
40     /// ```rust
41     /// let a = 1;
42     ///
43     /// let b = match 3 {
44     ///     0 => "zero",
45     ///     1 => "one",
46     ///     _ => "many",
47     /// };
48     ///
49     /// let c = if true {
50     ///     1
51     /// } else {
52     ///     -1
53     /// };
54     /// ```
55     #[clippy::version = "1.58.0"]
56     pub NEEDLESS_LATE_INIT,
57     style,
58     "late initializations that can be replaced by a `let` statement with an initializer"
59 }
60 declare_lint_pass!(NeedlessLateInit => [NEEDLESS_LATE_INIT]);
61
62 fn contains_assign_expr<'tcx>(cx: &LateContext<'tcx>, stmt: &'tcx Stmt<'tcx>) -> bool {
63     let mut seen = false;
64     expr_visitor(cx, |expr| {
65         if let ExprKind::Assign(..) = expr.kind {
66             seen = true;
67         }
68
69         !seen
70     })
71     .visit_stmt(stmt);
72
73     seen
74 }
75
76 #[derive(Debug)]
77 struct LocalAssign {
78     lhs_id: HirId,
79     lhs_span: Span,
80     rhs_span: Span,
81     span: Span,
82 }
83
84 impl LocalAssign {
85     fn from_expr(expr: &Expr<'_>, span: Span) -> Option<Self> {
86         if let ExprKind::Assign(lhs, rhs, _) = expr.kind {
87             if lhs.span.from_expansion() {
88                 return None;
89             }
90
91             Some(Self {
92                 lhs_id: path_to_local(lhs)?,
93                 lhs_span: lhs.span,
94                 rhs_span: rhs.span.source_callsite(),
95                 span,
96             })
97         } else {
98             None
99         }
100     }
101
102     fn new<'tcx>(cx: &LateContext<'tcx>, expr: &'tcx Expr<'tcx>, binding_id: HirId) -> Option<LocalAssign> {
103         let assign = match expr.kind {
104             ExprKind::Block(Block { expr: Some(expr), .. }, _) => Self::from_expr(expr, expr.span),
105             ExprKind::Block(block, _) => {
106                 if_chain! {
107                     if let Some((last, other_stmts)) = block.stmts.split_last();
108                     if let StmtKind::Expr(expr) | StmtKind::Semi(expr) = last.kind;
109
110                     let assign = Self::from_expr(expr, last.span)?;
111
112                     // avoid visiting if not needed
113                     if assign.lhs_id == binding_id;
114                     if other_stmts.iter().all(|stmt| !contains_assign_expr(cx, stmt));
115
116                     then {
117                         Some(assign)
118                     } else {
119                         None
120                     }
121                 }
122             },
123             ExprKind::Assign(..) => Self::from_expr(expr, expr.span),
124             _ => None,
125         }?;
126
127         if assign.lhs_id == binding_id {
128             Some(assign)
129         } else {
130             None
131         }
132     }
133 }
134
135 fn assignment_suggestions<'tcx>(
136     cx: &LateContext<'tcx>,
137     binding_id: HirId,
138     exprs: impl IntoIterator<Item = &'tcx Expr<'tcx>>,
139 ) -> Option<(Applicability, Vec<(Span, String)>)> {
140     let mut assignments = Vec::new();
141
142     for expr in exprs {
143         let ty = cx.typeck_results().expr_ty(expr);
144
145         if ty.is_never() {
146             continue;
147         }
148         if !ty.is_unit() {
149             return None;
150         }
151
152         let assign = LocalAssign::new(cx, expr, binding_id)?;
153
154         assignments.push(assign);
155     }
156
157     let suggestions = assignments
158         .into_iter()
159         .map(|assignment| Some((assignment.span, snippet_opt(cx, assignment.rhs_span)?)))
160         .collect::<Option<Vec<(Span, String)>>>()?;
161
162     let applicability = if suggestions.len() > 1 {
163         // multiple suggestions don't work with rustfix in multipart_suggest
164         // https://github.com/rust-lang/rustfix/issues/141
165         Applicability::Unspecified
166     } else {
167         Applicability::MachineApplicable
168     };
169     Some((applicability, suggestions))
170 }
171
172 struct Usage<'tcx> {
173     stmt: &'tcx Stmt<'tcx>,
174     expr: &'tcx Expr<'tcx>,
175     needs_semi: bool,
176 }
177
178 fn first_usage<'tcx>(
179     cx: &LateContext<'tcx>,
180     binding_id: HirId,
181     local_stmt_id: HirId,
182     block: &'tcx Block<'tcx>,
183 ) -> Option<Usage<'tcx>> {
184     block
185         .stmts
186         .iter()
187         .skip_while(|stmt| stmt.hir_id != local_stmt_id)
188         .skip(1)
189         .find(|&stmt| is_local_used(cx, stmt, binding_id))
190         .and_then(|stmt| match stmt.kind {
191             StmtKind::Expr(expr) => Some(Usage {
192                 stmt,
193                 expr,
194                 needs_semi: true,
195             }),
196             StmtKind::Semi(expr) => Some(Usage {
197                 stmt,
198                 expr,
199                 needs_semi: false,
200             }),
201             _ => None,
202         })
203 }
204
205 fn local_snippet_without_semicolon(cx: &LateContext<'_>, local: &Local<'_>) -> Option<String> {
206     let span = local.span.with_hi(match local.ty {
207         // let <pat>: <ty>;
208         // ~~~~~~~~~~~~~~~
209         Some(ty) => ty.span.hi(),
210         // let <pat>;
211         // ~~~~~~~~~
212         None => local.pat.span.hi(),
213     });
214
215     snippet_opt(cx, span)
216 }
217
218 fn check<'tcx>(
219     cx: &LateContext<'tcx>,
220     local: &'tcx Local<'tcx>,
221     local_stmt: &'tcx Stmt<'tcx>,
222     block: &'tcx Block<'tcx>,
223     binding_id: HirId,
224 ) -> Option<()> {
225     let usage = first_usage(cx, binding_id, local_stmt.hir_id, block)?;
226     let binding_name = cx.tcx.hir().opt_name(binding_id)?;
227     let let_snippet = local_snippet_without_semicolon(cx, local)?;
228
229     match usage.expr.kind {
230         ExprKind::Assign(..) => {
231             let assign = LocalAssign::new(cx, usage.expr, binding_id)?;
232
233             span_lint_and_then(
234                 cx,
235                 NEEDLESS_LATE_INIT,
236                 local_stmt.span,
237                 "unneeded late initalization",
238                 |diag| {
239                     diag.tool_only_span_suggestion(
240                         local_stmt.span,
241                         "remove the local",
242                         String::new(),
243                         Applicability::MachineApplicable,
244                     );
245
246                     diag.span_suggestion(
247                         assign.lhs_span,
248                         &format!("declare `{}` here", binding_name),
249                         let_snippet,
250                         Applicability::MachineApplicable,
251                     );
252                 },
253             );
254         },
255         ExprKind::If(_, then_expr, Some(else_expr)) => {
256             let (applicability, suggestions) = assignment_suggestions(cx, binding_id, [then_expr, else_expr])?;
257
258             span_lint_and_then(
259                 cx,
260                 NEEDLESS_LATE_INIT,
261                 local_stmt.span,
262                 "unneeded late initalization",
263                 |diag| {
264                     diag.tool_only_span_suggestion(local_stmt.span, "remove the local", String::new(), applicability);
265
266                     diag.span_suggestion_verbose(
267                         usage.stmt.span.shrink_to_lo(),
268                         &format!("declare `{}` here", binding_name),
269                         format!("{} = ", let_snippet),
270                         applicability,
271                     );
272
273                     diag.multipart_suggestion("remove the assignments from the branches", suggestions, applicability);
274
275                     if usage.needs_semi {
276                         diag.span_suggestion(
277                             usage.stmt.span.shrink_to_hi(),
278                             "add a semicolon after the `if` expression",
279                             ";".to_string(),
280                             applicability,
281                         );
282                     }
283                 },
284             );
285         },
286         ExprKind::Match(_, arms, MatchSource::Normal) => {
287             let (applicability, suggestions) = assignment_suggestions(cx, binding_id, arms.iter().map(|arm| arm.body))?;
288
289             span_lint_and_then(
290                 cx,
291                 NEEDLESS_LATE_INIT,
292                 local_stmt.span,
293                 "unneeded late initalization",
294                 |diag| {
295                     diag.tool_only_span_suggestion(local_stmt.span, "remove the local", String::new(), applicability);
296
297                     diag.span_suggestion_verbose(
298                         usage.stmt.span.shrink_to_lo(),
299                         &format!("declare `{}` here", binding_name),
300                         format!("{} = ", let_snippet),
301                         applicability,
302                     );
303
304                     diag.multipart_suggestion(
305                         "remove the assignments from the `match` arms",
306                         suggestions,
307                         applicability,
308                     );
309
310                     if usage.needs_semi {
311                         diag.span_suggestion(
312                             usage.stmt.span.shrink_to_hi(),
313                             "add a semicolon after the `match` expression",
314                             ";".to_string(),
315                             applicability,
316                         );
317                     }
318                 },
319             );
320         },
321         _ => {},
322     };
323
324     Some(())
325 }
326
327 impl LateLintPass<'tcx> for NeedlessLateInit {
328     fn check_local(&mut self, cx: &LateContext<'tcx>, local: &'tcx Local<'tcx>) {
329         let mut parents = cx.tcx.hir().parent_iter(local.hir_id);
330
331         if_chain! {
332             if let Local {
333                 init: None,
334                 pat: &Pat {
335                     kind: PatKind::Binding(_, binding_id, _, None),
336                     ..
337                 },
338                 source: LocalSource::Normal,
339                 ..
340             } = local;
341             if let Some((_, Node::Stmt(local_stmt))) = parents.next();
342             if let Some((_, Node::Block(block))) = parents.next();
343
344             then {
345                 check(cx, local, local_stmt, block, binding_id);
346             }
347         }
348     }
349 }