]>
Commit | Line | Data |
---|---|---|
a2a8927a XL |
1 | use clippy_utils::diagnostics::span_lint_and_then; |
2 | use clippy_utils::path_to_local; | |
3 | use clippy_utils::source::snippet_opt; | |
04454e1e | 4 | use clippy_utils::ty::needs_ordered_drop; |
2b03887a FG |
5 | use clippy_utils::visitors::{for_each_expr, for_each_expr_with_closures, is_local_used}; |
6 | use core::ops::ControlFlow; | |
04454e1e | 7 | use rustc_errors::{Applicability, MultiSpan}; |
04454e1e FG |
8 | use rustc_hir::{ |
9 | BindingAnnotation, Block, Expr, ExprKind, HirId, Local, LocalSource, MatchSource, Node, Pat, PatKind, Stmt, | |
10 | StmtKind, | |
11 | }; | |
a2a8927a | 12 | use rustc_lint::{LateContext, LateLintPass}; |
4b012472 | 13 | use rustc_session::declare_lint_pass; |
a2a8927a XL |
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 | |
ed00b5ec | 25 | /// ```no_run |
a2a8927a XL |
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: | |
ed00b5ec | 44 | /// ```no_run |
a2a8927a XL |
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 | /// ``` | |
923072b8 | 59 | #[clippy::version = "1.59.0"] |
a2a8927a XL |
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 { | |
2b03887a FG |
67 | for_each_expr_with_closures(cx, stmt, |e| { |
68 | if matches!(e.kind, ExprKind::Assign(..)) { | |
69 | ControlFlow::Break(()) | |
70 | } else { | |
71 | ControlFlow::Continue(()) | |
a2a8927a | 72 | } |
a2a8927a | 73 | }) |
2b03887a | 74 | .is_some() |
a2a8927a XL |
75 | } |
76 | ||
04454e1e | 77 | fn contains_let(cond: &Expr<'_>) -> bool { |
2b03887a FG |
78 | for_each_expr(cond, |e| { |
79 | if matches!(e.kind, ExprKind::Let(_)) { | |
80 | ControlFlow::Break(()) | |
81 | } else { | |
82 | ControlFlow::Continue(()) | |
04454e1e | 83 | } |
04454e1e | 84 | }) |
2b03887a | 85 | .is_some() |
04454e1e FG |
86 | } |
87 | ||
88 | fn stmt_needs_ordered_drop(cx: &LateContext<'_>, stmt: &Stmt<'_>) -> bool { | |
c620b35d | 89 | let StmtKind::Let(local) = stmt.kind else { |
add651ee FG |
90 | return false; |
91 | }; | |
04454e1e FG |
92 | !local.pat.walk_short(|pat| { |
93 | if let PatKind::Binding(.., None) = pat.kind { | |
94 | !needs_ordered_drop(cx, cx.typeck_results().pat_ty(pat)) | |
95 | } else { | |
96 | true | |
97 | } | |
98 | }) | |
99 | } | |
100 | ||
a2a8927a XL |
101 | #[derive(Debug)] |
102 | struct LocalAssign { | |
103 | lhs_id: HirId, | |
104 | lhs_span: Span, | |
105 | rhs_span: Span, | |
106 | span: Span, | |
107 | } | |
108 | ||
109 | impl LocalAssign { | |
110 | fn from_expr(expr: &Expr<'_>, span: Span) -> Option<Self> { | |
111 | if let ExprKind::Assign(lhs, rhs, _) = expr.kind { | |
112 | if lhs.span.from_expansion() { | |
113 | return None; | |
114 | } | |
115 | ||
116 | Some(Self { | |
117 | lhs_id: path_to_local(lhs)?, | |
118 | lhs_span: lhs.span, | |
119 | rhs_span: rhs.span.source_callsite(), | |
120 | span, | |
121 | }) | |
122 | } else { | |
123 | None | |
124 | } | |
125 | } | |
126 | ||
127 | fn new<'tcx>(cx: &LateContext<'tcx>, expr: &'tcx Expr<'tcx>, binding_id: HirId) -> Option<LocalAssign> { | |
128 | let assign = match expr.kind { | |
129 | ExprKind::Block(Block { expr: Some(expr), .. }, _) => Self::from_expr(expr, expr.span), | |
130 | ExprKind::Block(block, _) => { | |
4b012472 FG |
131 | if let Some((last, other_stmts)) = block.stmts.split_last() |
132 | && let StmtKind::Expr(expr) | StmtKind::Semi(expr) = last.kind | |
a2a8927a | 133 | |
4b012472 | 134 | && let assign = Self::from_expr(expr, last.span)? |
a2a8927a XL |
135 | |
136 | // avoid visiting if not needed | |
4b012472 FG |
137 | && assign.lhs_id == binding_id |
138 | && other_stmts.iter().all(|stmt| !contains_assign_expr(cx, stmt)) | |
139 | { | |
140 | Some(assign) | |
141 | } else { | |
142 | None | |
a2a8927a XL |
143 | } |
144 | }, | |
145 | ExprKind::Assign(..) => Self::from_expr(expr, expr.span), | |
146 | _ => None, | |
147 | }?; | |
148 | ||
149 | if assign.lhs_id == binding_id { | |
150 | Some(assign) | |
151 | } else { | |
152 | None | |
153 | } | |
154 | } | |
155 | } | |
156 | ||
157 | fn assignment_suggestions<'tcx>( | |
158 | cx: &LateContext<'tcx>, | |
159 | binding_id: HirId, | |
160 | exprs: impl IntoIterator<Item = &'tcx Expr<'tcx>>, | |
161 | ) -> Option<(Applicability, Vec<(Span, String)>)> { | |
162 | let mut assignments = Vec::new(); | |
163 | ||
164 | for expr in exprs { | |
165 | let ty = cx.typeck_results().expr_ty(expr); | |
166 | ||
167 | if ty.is_never() { | |
168 | continue; | |
169 | } | |
170 | if !ty.is_unit() { | |
171 | return None; | |
172 | } | |
173 | ||
174 | let assign = LocalAssign::new(cx, expr, binding_id)?; | |
175 | ||
176 | assignments.push(assign); | |
177 | } | |
178 | ||
179 | let suggestions = assignments | |
180 | .iter() | |
923072b8 | 181 | .flat_map(|assignment| { |
2b03887a FG |
182 | let mut spans = vec![assignment.span.until(assignment.rhs_span)]; |
183 | ||
184 | if assignment.rhs_span.hi() != assignment.span.hi() { | |
185 | spans.push(assignment.rhs_span.shrink_to_hi().with_hi(assignment.span.hi())); | |
186 | } | |
187 | ||
188 | spans | |
923072b8 FG |
189 | }) |
190 | .map(|span| (span, String::new())) | |
191 | .collect::<Vec<(Span, String)>>(); | |
192 | ||
193 | match suggestions.len() { | |
194 | // All of `exprs` are never types | |
195 | // https://github.com/rust-lang/rust-clippy/issues/8911 | |
196 | 0 => None, | |
197 | 1 => Some((Applicability::MachineApplicable, suggestions)), | |
a2a8927a XL |
198 | // multiple suggestions don't work with rustfix in multipart_suggest |
199 | // https://github.com/rust-lang/rustfix/issues/141 | |
923072b8 FG |
200 | _ => Some((Applicability::Unspecified, suggestions)), |
201 | } | |
a2a8927a XL |
202 | } |
203 | ||
204 | struct Usage<'tcx> { | |
205 | stmt: &'tcx Stmt<'tcx>, | |
206 | expr: &'tcx Expr<'tcx>, | |
207 | needs_semi: bool, | |
208 | } | |
209 | ||
210 | fn first_usage<'tcx>( | |
211 | cx: &LateContext<'tcx>, | |
212 | binding_id: HirId, | |
213 | local_stmt_id: HirId, | |
214 | block: &'tcx Block<'tcx>, | |
215 | ) -> Option<Usage<'tcx>> { | |
04454e1e FG |
216 | let significant_drop = needs_ordered_drop(cx, cx.typeck_results().node_type(binding_id)); |
217 | ||
a2a8927a XL |
218 | block |
219 | .stmts | |
220 | .iter() | |
221 | .skip_while(|stmt| stmt.hir_id != local_stmt_id) | |
222 | .skip(1) | |
04454e1e | 223 | .take_while(|stmt| !significant_drop || !stmt_needs_ordered_drop(cx, stmt)) |
a2a8927a XL |
224 | .find(|&stmt| is_local_used(cx, stmt, binding_id)) |
225 | .and_then(|stmt| match stmt.kind { | |
226 | StmtKind::Expr(expr) => Some(Usage { | |
227 | stmt, | |
228 | expr, | |
229 | needs_semi: true, | |
230 | }), | |
231 | StmtKind::Semi(expr) => Some(Usage { | |
232 | stmt, | |
233 | expr, | |
234 | needs_semi: false, | |
235 | }), | |
236 | _ => None, | |
237 | }) | |
238 | } | |
239 | ||
240 | fn local_snippet_without_semicolon(cx: &LateContext<'_>, local: &Local<'_>) -> Option<String> { | |
241 | let span = local.span.with_hi(match local.ty { | |
242 | // let <pat>: <ty>; | |
243 | // ~~~~~~~~~~~~~~~ | |
244 | Some(ty) => ty.span.hi(), | |
245 | // let <pat>; | |
246 | // ~~~~~~~~~ | |
247 | None => local.pat.span.hi(), | |
248 | }); | |
249 | ||
250 | snippet_opt(cx, span) | |
251 | } | |
252 | ||
253 | fn check<'tcx>( | |
254 | cx: &LateContext<'tcx>, | |
255 | local: &'tcx Local<'tcx>, | |
256 | local_stmt: &'tcx Stmt<'tcx>, | |
257 | block: &'tcx Block<'tcx>, | |
258 | binding_id: HirId, | |
259 | ) -> Option<()> { | |
260 | let usage = first_usage(cx, binding_id, local_stmt.hir_id, block)?; | |
261 | let binding_name = cx.tcx.hir().opt_name(binding_id)?; | |
262 | let let_snippet = local_snippet_without_semicolon(cx, local)?; | |
263 | ||
264 | match usage.expr.kind { | |
265 | ExprKind::Assign(..) => { | |
266 | let assign = LocalAssign::new(cx, usage.expr, binding_id)?; | |
04454e1e FG |
267 | let mut msg_span = MultiSpan::from_spans(vec![local_stmt.span, assign.span]); |
268 | msg_span.push_span_label(local_stmt.span, "created here"); | |
269 | msg_span.push_span_label(assign.span, "initialised here"); | |
a2a8927a XL |
270 | |
271 | span_lint_and_then( | |
272 | cx, | |
273 | NEEDLESS_LATE_INIT, | |
04454e1e FG |
274 | msg_span, |
275 | "unneeded late initialization", | |
a2a8927a XL |
276 | |diag| { |
277 | diag.tool_only_span_suggestion( | |
278 | local_stmt.span, | |
279 | "remove the local", | |
923072b8 | 280 | "", |
a2a8927a XL |
281 | Applicability::MachineApplicable, |
282 | ); | |
283 | ||
284 | diag.span_suggestion( | |
285 | assign.lhs_span, | |
9c376795 | 286 | format!("declare `{binding_name}` here"), |
a2a8927a XL |
287 | let_snippet, |
288 | Applicability::MachineApplicable, | |
289 | ); | |
290 | }, | |
291 | ); | |
292 | }, | |
04454e1e | 293 | ExprKind::If(cond, then_expr, Some(else_expr)) if !contains_let(cond) => { |
a2a8927a XL |
294 | let (applicability, suggestions) = assignment_suggestions(cx, binding_id, [then_expr, else_expr])?; |
295 | ||
296 | span_lint_and_then( | |
297 | cx, | |
298 | NEEDLESS_LATE_INIT, | |
299 | local_stmt.span, | |
04454e1e | 300 | "unneeded late initialization", |
a2a8927a XL |
301 | |diag| { |
302 | diag.tool_only_span_suggestion(local_stmt.span, "remove the local", String::new(), applicability); | |
303 | ||
304 | diag.span_suggestion_verbose( | |
305 | usage.stmt.span.shrink_to_lo(), | |
9c376795 | 306 | format!("declare `{binding_name}` here"), |
2b03887a | 307 | format!("{let_snippet} = "), |
a2a8927a XL |
308 | applicability, |
309 | ); | |
310 | ||
311 | diag.multipart_suggestion("remove the assignments from the branches", suggestions, applicability); | |
312 | ||
313 | if usage.needs_semi { | |
314 | diag.span_suggestion( | |
315 | usage.stmt.span.shrink_to_hi(), | |
316 | "add a semicolon after the `if` expression", | |
923072b8 | 317 | ";", |
a2a8927a XL |
318 | applicability, |
319 | ); | |
320 | } | |
321 | }, | |
322 | ); | |
323 | }, | |
324 | ExprKind::Match(_, arms, MatchSource::Normal) => { | |
325 | let (applicability, suggestions) = assignment_suggestions(cx, binding_id, arms.iter().map(|arm| arm.body))?; | |
326 | ||
327 | span_lint_and_then( | |
328 | cx, | |
329 | NEEDLESS_LATE_INIT, | |
330 | local_stmt.span, | |
04454e1e | 331 | "unneeded late initialization", |
a2a8927a XL |
332 | |diag| { |
333 | diag.tool_only_span_suggestion(local_stmt.span, "remove the local", String::new(), applicability); | |
334 | ||
335 | diag.span_suggestion_verbose( | |
336 | usage.stmt.span.shrink_to_lo(), | |
9c376795 | 337 | format!("declare `{binding_name}` here"), |
2b03887a | 338 | format!("{let_snippet} = "), |
a2a8927a XL |
339 | applicability, |
340 | ); | |
341 | ||
342 | diag.multipart_suggestion( | |
343 | "remove the assignments from the `match` arms", | |
344 | suggestions, | |
345 | applicability, | |
346 | ); | |
347 | ||
348 | if usage.needs_semi { | |
349 | diag.span_suggestion( | |
350 | usage.stmt.span.shrink_to_hi(), | |
351 | "add a semicolon after the `match` expression", | |
923072b8 | 352 | ";", |
a2a8927a XL |
353 | applicability, |
354 | ); | |
355 | } | |
356 | }, | |
357 | ); | |
358 | }, | |
359 | _ => {}, | |
360 | }; | |
361 | ||
362 | Some(()) | |
363 | } | |
364 | ||
5099ac24 | 365 | impl<'tcx> LateLintPass<'tcx> for NeedlessLateInit { |
a2a8927a XL |
366 | fn check_local(&mut self, cx: &LateContext<'tcx>, local: &'tcx Local<'tcx>) { |
367 | let mut parents = cx.tcx.hir().parent_iter(local.hir_id); | |
4b012472 FG |
368 | if let Local { |
369 | init: None, | |
370 | pat: | |
371 | &Pat { | |
f2b60f7d | 372 | kind: PatKind::Binding(BindingAnnotation::NONE, binding_id, _, None), |
a2a8927a XL |
373 | .. |
374 | }, | |
4b012472 FG |
375 | source: LocalSource::Normal, |
376 | .. | |
377 | } = local | |
378 | && let Some((_, Node::Stmt(local_stmt))) = parents.next() | |
379 | && let Some((_, Node::Block(block))) = parents.next() | |
380 | { | |
381 | check(cx, local, local_stmt, block, binding_id); | |
a2a8927a XL |
382 | } |
383 | } | |
384 | } |