]> git.lizzy.rs Git - rust.git/blob - clippy_lints/src/needless_late_init.rs
Fix rebase and clippy tests
[rust.git] / 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, Clone)]
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.clone()
158         .into_iter()
159         .map(|assignment| Some((assignment.span.until(assignment.rhs_span), String::new())))
160         .chain(
161             assignments
162                 .into_iter()
163                 .map(|assignment| Some((assignment.rhs_span.shrink_to_hi().with_hi(assignment.span.hi()), String::new())))
164         )
165         .collect::<Option<Vec<(Span, String)>>>()?;
166
167     let applicability = if suggestions.len() > 1 {
168         // multiple suggestions don't work with rustfix in multipart_suggest
169         // https://github.com/rust-lang/rustfix/issues/141
170         Applicability::Unspecified
171     } else {
172         Applicability::MachineApplicable
173     };
174     Some((applicability, suggestions))
175 }
176
177 struct Usage<'tcx> {
178     stmt: &'tcx Stmt<'tcx>,
179     expr: &'tcx Expr<'tcx>,
180     needs_semi: bool,
181 }
182
183 fn first_usage<'tcx>(
184     cx: &LateContext<'tcx>,
185     binding_id: HirId,
186     local_stmt_id: HirId,
187     block: &'tcx Block<'tcx>,
188 ) -> Option<Usage<'tcx>> {
189     block
190         .stmts
191         .iter()
192         .skip_while(|stmt| stmt.hir_id != local_stmt_id)
193         .skip(1)
194         .find(|&stmt| is_local_used(cx, stmt, binding_id))
195         .and_then(|stmt| match stmt.kind {
196             StmtKind::Expr(expr) => Some(Usage {
197                 stmt,
198                 expr,
199                 needs_semi: true,
200             }),
201             StmtKind::Semi(expr) => Some(Usage {
202                 stmt,
203                 expr,
204                 needs_semi: false,
205             }),
206             _ => None,
207         })
208 }
209
210 fn local_snippet_without_semicolon(cx: &LateContext<'_>, local: &Local<'_>) -> Option<String> {
211     let span = local.span.with_hi(match local.ty {
212         // let <pat>: <ty>;
213         // ~~~~~~~~~~~~~~~
214         Some(ty) => ty.span.hi(),
215         // let <pat>;
216         // ~~~~~~~~~
217         None => local.pat.span.hi(),
218     });
219
220     snippet_opt(cx, span)
221 }
222
223 fn check<'tcx>(
224     cx: &LateContext<'tcx>,
225     local: &'tcx Local<'tcx>,
226     local_stmt: &'tcx Stmt<'tcx>,
227     block: &'tcx Block<'tcx>,
228     binding_id: HirId,
229 ) -> Option<()> {
230     let usage = first_usage(cx, binding_id, local_stmt.hir_id, block)?;
231     let binding_name = cx.tcx.hir().opt_name(binding_id)?;
232     let let_snippet = local_snippet_without_semicolon(cx, local)?;
233
234     match usage.expr.kind {
235         ExprKind::Assign(..) => {
236             let assign = LocalAssign::new(cx, usage.expr, binding_id)?;
237
238             span_lint_and_then(
239                 cx,
240                 NEEDLESS_LATE_INIT,
241                 local_stmt.span,
242                 "unneeded late initalization",
243                 |diag| {
244                     diag.tool_only_span_suggestion(
245                         local_stmt.span,
246                         "remove the local",
247                         String::new(),
248                         Applicability::MachineApplicable,
249                     );
250
251                     diag.span_suggestion(
252                         assign.lhs_span,
253                         &format!("declare `{}` here", binding_name),
254                         let_snippet,
255                         Applicability::MachineApplicable,
256                     );
257                 },
258             );
259         },
260         ExprKind::If(_, then_expr, Some(else_expr)) => {
261             let (applicability, suggestions) = assignment_suggestions(cx, binding_id, [then_expr, else_expr])?;
262
263             span_lint_and_then(
264                 cx,
265                 NEEDLESS_LATE_INIT,
266                 local_stmt.span,
267                 "unneeded late initalization",
268                 |diag| {
269                     diag.tool_only_span_suggestion(local_stmt.span, "remove the local", String::new(), applicability);
270
271                     diag.span_suggestion_verbose(
272                         usage.stmt.span.shrink_to_lo(),
273                         &format!("declare `{}` here", binding_name),
274                         format!("{} = ", let_snippet),
275                         applicability,
276                     );
277
278                     diag.multipart_suggestion("remove the assignments from the branches", suggestions, applicability);
279
280                     if usage.needs_semi {
281                         diag.span_suggestion(
282                             usage.stmt.span.shrink_to_hi(),
283                             "add a semicolon after the `if` expression",
284                             ";".to_string(),
285                             applicability,
286                         );
287                     }
288                 },
289             );
290         },
291         ExprKind::Match(_, arms, MatchSource::Normal) => {
292             let (applicability, suggestions) = assignment_suggestions(cx, binding_id, arms.iter().map(|arm| arm.body))?;
293
294             span_lint_and_then(
295                 cx,
296                 NEEDLESS_LATE_INIT,
297                 local_stmt.span,
298                 "unneeded late initalization",
299                 |diag| {
300                     diag.tool_only_span_suggestion(local_stmt.span, "remove the local", String::new(), applicability);
301
302                     diag.span_suggestion_verbose(
303                         usage.stmt.span.shrink_to_lo(),
304                         &format!("declare `{}` here", binding_name),
305                         format!("{} = ", let_snippet),
306                         applicability,
307                     );
308
309                     diag.multipart_suggestion(
310                         "remove the assignments from the `match` arms",
311                         suggestions,
312                         applicability,
313                     );
314
315                     if usage.needs_semi {
316                         diag.span_suggestion(
317                             usage.stmt.span.shrink_to_hi(),
318                             "add a semicolon after the `match` expression",
319                             ";".to_string(),
320                             applicability,
321                         );
322                     }
323                 },
324             );
325         },
326         _ => {},
327     };
328
329     Some(())
330 }
331
332 impl LateLintPass<'tcx> for NeedlessLateInit {
333     fn check_local(&mut self, cx: &LateContext<'tcx>, local: &'tcx Local<'tcx>) {
334         let mut parents = cx.tcx.hir().parent_iter(local.hir_id);
335
336         if_chain! {
337             if let Local {
338                 init: None,
339                 pat: &Pat {
340                     kind: PatKind::Binding(_, binding_id, _, None),
341                     ..
342                 },
343                 source: LocalSource::Normal,
344                 ..
345             } = local;
346             if let Some((_, Node::Stmt(local_stmt))) = parents.next();
347             if let Some((_, Node::Block(block))) = parents.next();
348
349             then {
350                 check(cx, local, local_stmt, block, binding_id);
351             }
352         }
353     }
354 }