]> git.proxmox.com Git - rustc.git/blame - src/tools/clippy/clippy_lints/src/manual_let_else.rs
New upstream version 1.72.1+dfsg1
[rustc.git] / src / tools / clippy / clippy_lints / src / manual_let_else.rs
CommitLineData
487cf647
FG
1use clippy_utils::diagnostics::span_lint_and_then;
2use clippy_utils::higher::IfLetOrMatch;
3use clippy_utils::msrvs::{self, Msrv};
4use clippy_utils::peel_blocks;
5use clippy_utils::source::snippet_with_context;
6use clippy_utils::ty::is_type_diagnostic_item;
9ffffee4 7use clippy_utils::visitors::{Descend, Visitable};
487cf647 8use if_chain::if_chain;
fe692bf9 9use rustc_data_structures::fx::{FxHashMap, FxHashSet};
487cf647 10use rustc_errors::Applicability;
9ffffee4
FG
11use rustc_hir::intravisit::{walk_expr, Visitor};
12use rustc_hir::{Expr, ExprKind, HirId, ItemId, Local, MatchSource, Pat, PatKind, QPath, Stmt, StmtKind, Ty};
487cf647
FG
13use rustc_lint::{LateContext, LateLintPass, LintContext};
14use rustc_middle::lint::in_external_macro;
15use rustc_session::{declare_tool_lint, impl_lint_pass};
fe692bf9 16use rustc_span::symbol::{sym, Symbol};
487cf647
FG
17use rustc_span::Span;
18use serde::Deserialize;
19use std::ops::ControlFlow;
fe692bf9 20use std::slice;
487cf647
FG
21
22declare_clippy_lint! {
23 /// ### What it does
24 ///
25 /// Warn of cases where `let...else` could be used
26 ///
27 /// ### Why is this bad?
28 ///
29 /// `let...else` provides a standard construct for this pattern
30 /// that people can easily recognize. It's also more compact.
31 ///
32 /// ### Example
33 ///
34 /// ```rust
35 /// # let w = Some(0);
36 /// let v = if let Some(v) = w { v } else { return };
37 /// ```
38 ///
39 /// Could be written:
40 ///
41 /// ```rust
487cf647
FG
42 /// # fn main () {
43 /// # let w = Some(0);
44 /// let Some(v) = w else { return };
45 /// # }
46 /// ```
47 #[clippy::version = "1.67.0"]
48 pub MANUAL_LET_ELSE,
49 pedantic,
50 "manual implementation of a let...else statement"
51}
52
53pub struct ManualLetElse {
54 msrv: Msrv,
55 matches_behaviour: MatchLintBehaviour,
56}
57
58impl ManualLetElse {
59 #[must_use]
60 pub fn new(msrv: Msrv, matches_behaviour: MatchLintBehaviour) -> Self {
61 Self {
62 msrv,
63 matches_behaviour,
64 }
65 }
66}
67
68impl_lint_pass!(ManualLetElse => [MANUAL_LET_ELSE]);
69
70impl<'tcx> LateLintPass<'tcx> for ManualLetElse {
71 fn check_stmt(&mut self, cx: &LateContext<'_>, stmt: &'tcx Stmt<'tcx>) {
49aad941
FG
72 if !self.msrv.meets(msrvs::LET_ELSE) || in_external_macro(cx.sess(), stmt.span) {
73 return;
74 }
487cf647 75
49aad941
FG
76 if let StmtKind::Local(local) = stmt.kind &&
77 let Some(init) = local.init &&
78 local.els.is_none() &&
79 local.ty.is_none() &&
80 init.span.ctxt() == stmt.span.ctxt() &&
fe692bf9
FG
81 let Some(if_let_or_match) = IfLetOrMatch::parse(cx, init)
82 {
83 match if_let_or_match {
84 IfLetOrMatch::IfLet(if_let_expr, let_pat, if_then, if_else) => if_chain! {
85 if let Some(ident_map) = expr_simple_identity_map(local.pat, let_pat, if_then);
86 if let Some(if_else) = if_else;
87 if expr_diverges(cx, if_else);
88 then {
89 emit_manual_let_else(cx, stmt.span, if_let_expr, &ident_map, let_pat, if_else);
90 }
91 },
92 IfLetOrMatch::Match(match_expr, arms, source) => {
93 if self.matches_behaviour == MatchLintBehaviour::Never {
94 return;
95 }
96 if source != MatchSource::Normal {
97 return;
98 }
99 // Any other number than two arms doesn't (necessarily)
100 // have a trivial mapping to let else.
101 if arms.len() != 2 {
102 return;
103 }
104 // Guards don't give us an easy mapping either
105 if arms.iter().any(|arm| arm.guard.is_some()) {
106 return;
107 }
108 let check_types = self.matches_behaviour == MatchLintBehaviour::WellKnownTypes;
109 let diverging_arm_opt = arms
110 .iter()
111 .enumerate()
112 .find(|(_, arm)| expr_diverges(cx, arm.body) && pat_allowed_for_else(cx, arm.pat, check_types));
113 let Some((idx, diverging_arm)) = diverging_arm_opt else { return; };
114 // If the non-diverging arm is the first one, its pattern can be reused in a let/else statement.
115 // However, if it arrives in second position, its pattern may cover some cases already covered
116 // by the diverging one.
117 // TODO: accept the non-diverging arm as a second position if patterns are disjointed.
118 if idx == 0 {
119 return;
120 }
121 let pat_arm = &arms[1 - idx];
122 let Some(ident_map) = expr_simple_identity_map(local.pat, pat_arm.pat, pat_arm.body) else {
123 return
124 };
487cf647 125
fe692bf9
FG
126 emit_manual_let_else(cx, stmt.span, match_expr, &ident_map, pat_arm.pat, diverging_arm.body);
127 },
128 }
49aad941 129 };
487cf647
FG
130 }
131
132 extract_msrv_attr!(LateContext);
133}
134
49aad941
FG
135fn emit_manual_let_else(
136 cx: &LateContext<'_>,
137 span: Span,
138 expr: &Expr<'_>,
fe692bf9 139 ident_map: &FxHashMap<Symbol, &Pat<'_>>,
49aad941
FG
140 pat: &Pat<'_>,
141 else_body: &Expr<'_>,
142) {
487cf647
FG
143 span_lint_and_then(
144 cx,
145 MANUAL_LET_ELSE,
146 span,
147 "this could be rewritten as `let...else`",
148 |diag| {
149 // This is far from perfect, for example there needs to be:
fe692bf9
FG
150 // * renamings of the bindings for many `PatKind`s like slices, etc.
151 // * limitations in the existing replacement algorithms
487cf647 152 // * unused binding collision detection with existing ones
487cf647
FG
153 // for this to be machine applicable.
154 let mut app = Applicability::HasPlaceholders;
487cf647
FG
155 let (sn_expr, _) = snippet_with_context(cx, expr.span, span.ctxt(), "", &mut app);
156 let (sn_else, _) = snippet_with_context(cx, else_body.span, span.ctxt(), "", &mut app);
157
158 let else_bl = if matches!(else_body.kind, ExprKind::Block(..)) {
159 sn_else.into_owned()
160 } else {
161 format!("{{ {sn_else} }}")
162 };
fe692bf9 163 let sn_bl = replace_in_pattern(cx, span, ident_map, pat, &mut app, true);
9c376795 164 let sugg = format!("let {sn_bl} = {sn_expr} else {else_bl};");
487cf647
FG
165 diag.span_suggestion(span, "consider writing", sugg, app);
166 },
167 );
168}
169
fe692bf9
FG
170/// Replaces the locals in the pattern
171///
172/// For this example:
173///
174/// ```ignore
175/// let (a, FooBar { b, c }) = if let Bar { Some(a_i), b_i } = ex { (a_i, b_i) } else { return };
176/// ```
177///
178/// We have:
179///
180/// ```ignore
181/// pat: Bar { Some(a_i), b_i }
182/// ident_map: (a_i) -> (a), (b_i) -> (FooBar { b, c })
183/// ```
184///
185/// We return:
186///
187/// ```ignore
188/// Bar { Some(a), b_i: FooBar { b, c } }
189/// ```
190fn replace_in_pattern(
191 cx: &LateContext<'_>,
192 span: Span,
193 ident_map: &FxHashMap<Symbol, &Pat<'_>>,
194 pat: &Pat<'_>,
195 app: &mut Applicability,
196 top_level: bool,
197) -> String {
198 // We put a labeled block here so that we can implement the fallback in this function.
199 // As the function has multiple call sites, implementing the fallback via an Option<T>
200 // return type and unwrap_or_else would cause repetition. Similarly, the function also
201 // invokes the fall back multiple times.
202 'a: {
203 // If the ident map is empty, there is no replacement to do.
204 // The code following this if assumes a non-empty ident_map.
205 if ident_map.is_empty() {
206 break 'a;
207 }
208
209 match pat.kind {
210 PatKind::Binding(_ann, _id, binding_name, opt_subpt) => {
211 let Some(pat_to_put) = ident_map.get(&binding_name.name) else { break 'a };
212 let (sn_ptp, _) = snippet_with_context(cx, pat_to_put.span, span.ctxt(), "", app);
213 if let Some(subpt) = opt_subpt {
214 let subpt = replace_in_pattern(cx, span, ident_map, subpt, app, false);
215 return format!("{sn_ptp} @ {subpt}");
216 }
217 return sn_ptp.to_string();
218 },
219 PatKind::Or(pats) => {
220 let patterns = pats
221 .iter()
222 .map(|pat| replace_in_pattern(cx, span, ident_map, pat, app, false))
223 .collect::<Vec<_>>();
224 let or_pat = patterns.join(" | ");
225 if top_level {
226 return format!("({or_pat})");
227 }
228 return or_pat;
229 },
230 PatKind::Struct(path, fields, has_dot_dot) => {
231 let fields = fields
232 .iter()
233 .map(|fld| {
234 if let PatKind::Binding(_, _, name, None) = fld.pat.kind &&
235 let Some(pat_to_put) = ident_map.get(&name.name)
236 {
237 let (sn_fld_name, _) = snippet_with_context(cx, fld.ident.span, span.ctxt(), "", app);
238 let (sn_ptp, _) = snippet_with_context(cx, pat_to_put.span, span.ctxt(), "", app);
239 // TODO: this is a bit of a hack, but it does its job. Ideally, we'd check if pat_to_put is
240 // a PatKind::Binding but that is also hard to get right.
241 if sn_fld_name == sn_ptp {
242 // Field init shorthand
243 return format!("{sn_fld_name}");
244 }
245 return format!("{sn_fld_name}: {sn_ptp}");
246 }
247 let (sn_fld, _) = snippet_with_context(cx, fld.span, span.ctxt(), "", app);
248 sn_fld.into_owned()
249 })
250 .collect::<Vec<_>>();
251 let fields_string = fields.join(", ");
252
253 let dot_dot_str = if has_dot_dot { " .." } else { "" };
254 let (sn_pth, _) = snippet_with_context(cx, path.span(), span.ctxt(), "", app);
255 return format!("{sn_pth} {{ {fields_string}{dot_dot_str} }}");
256 },
257 // Replace the variable name iff `TupleStruct` has one argument like `Variant(v)`.
258 PatKind::TupleStruct(ref w, args, dot_dot_pos) => {
259 let mut args = args
260 .iter()
261 .map(|pat| replace_in_pattern(cx, span, ident_map, pat, app, false))
262 .collect::<Vec<_>>();
263 if let Some(pos) = dot_dot_pos.as_opt_usize() {
264 args.insert(pos, "..".to_owned());
265 }
266 let args = args.join(", ");
267 let sn_wrapper = cx.sess().source_map().span_to_snippet(w.span()).unwrap_or_default();
268 return format!("{sn_wrapper}({args})");
269 },
270 PatKind::Tuple(args, dot_dot_pos) => {
271 let mut args = args
272 .iter()
273 .map(|pat| replace_in_pattern(cx, span, ident_map, pat, app, false))
274 .collect::<Vec<_>>();
275 if let Some(pos) = dot_dot_pos.as_opt_usize() {
276 args.insert(pos, "..".to_owned());
277 }
278 let args = args.join(", ");
279 return format!("({args})");
280 },
281 _ => {},
282 }
283 }
284 let (sn_pat, _) = snippet_with_context(cx, pat.span, span.ctxt(), "", app);
285 sn_pat.into_owned()
286}
287
9ffffee4
FG
288/// Check whether an expression is divergent. May give false negatives.
289fn expr_diverges(cx: &LateContext<'_>, expr: &Expr<'_>) -> bool {
290 struct V<'cx, 'tcx> {
291 cx: &'cx LateContext<'tcx>,
292 res: ControlFlow<(), Descend>,
487cf647 293 }
9ffffee4
FG
294 impl<'tcx> Visitor<'tcx> for V<'_, '_> {
295 fn visit_expr(&mut self, e: &'tcx Expr<'tcx>) {
296 fn is_never(cx: &LateContext<'_>, expr: &'_ Expr<'_>) -> bool {
297 if let Some(ty) = cx.typeck_results().expr_ty_opt(expr) {
298 return ty.is_never();
487cf647 299 }
9ffffee4
FG
300 false
301 }
302
303 if self.res.is_break() {
304 return;
305 }
306
307 // We can't just call is_never on expr and be done, because the type system
308 // sometimes coerces the ! type to something different before we can get
309 // our hands on it. So instead, we do a manual search. We do fall back to
310 // is_never in some places when there is no better alternative.
311 self.res = match e.kind {
312 ExprKind::Continue(_) | ExprKind::Break(_, _) | ExprKind::Ret(_) => ControlFlow::Break(()),
313 ExprKind::Call(call, _) => {
314 if is_never(self.cx, e) || is_never(self.cx, call) {
315 ControlFlow::Break(())
316 } else {
317 ControlFlow::Continue(Descend::Yes)
318 }
319 },
320 ExprKind::MethodCall(..) => {
321 if is_never(self.cx, e) {
322 ControlFlow::Break(())
323 } else {
324 ControlFlow::Continue(Descend::Yes)
325 }
326 },
327 ExprKind::If(if_expr, if_then, if_else) => {
328 let else_diverges = if_else.map_or(false, |ex| expr_diverges(self.cx, ex));
329 let diverges =
330 expr_diverges(self.cx, if_expr) || (else_diverges && expr_diverges(self.cx, if_then));
331 if diverges {
332 ControlFlow::Break(())
333 } else {
334 ControlFlow::Continue(Descend::No)
335 }
336 },
337 ExprKind::Match(match_expr, match_arms, _) => {
338 let diverges = expr_diverges(self.cx, match_expr)
339 || match_arms.iter().all(|arm| {
340 let guard_diverges = arm.guard.as_ref().map_or(false, |g| expr_diverges(self.cx, g.body()));
341 guard_diverges || expr_diverges(self.cx, arm.body)
342 });
343 if diverges {
344 ControlFlow::Break(())
345 } else {
346 ControlFlow::Continue(Descend::No)
347 }
348 },
487cf647 349
9ffffee4
FG
350 // Don't continue into loops or labeled blocks, as they are breakable,
351 // and we'd have to start checking labels.
352 ExprKind::Block(_, Some(_)) | ExprKind::Loop(..) => ControlFlow::Continue(Descend::No),
353
354 // Default: descend
355 _ => ControlFlow::Continue(Descend::Yes),
356 };
357 if let ControlFlow::Continue(Descend::Yes) = self.res {
358 walk_expr(self, e);
359 }
360 }
487cf647 361
9ffffee4
FG
362 fn visit_local(&mut self, local: &'tcx Local<'_>) {
363 // Don't visit the else block of a let/else statement as it will not make
364 // the statement divergent even though the else block is divergent.
365 if let Some(init) = local.init {
366 self.visit_expr(init);
367 }
487cf647 368 }
9ffffee4
FG
369
370 // Avoid unnecessary `walk_*` calls.
371 fn visit_ty(&mut self, _: &'tcx Ty<'tcx>) {}
372 fn visit_pat(&mut self, _: &'tcx Pat<'tcx>) {}
373 fn visit_qpath(&mut self, _: &'tcx QPath<'tcx>, _: HirId, _: Span) {}
374 // Avoid monomorphising all `visit_*` functions.
375 fn visit_nested_item(&mut self, _: ItemId) {}
376 }
377
378 let mut v = V {
379 cx,
380 res: ControlFlow::Continue(Descend::Yes),
381 };
382 expr.visit(&mut v);
383 v.res.is_break()
487cf647
FG
384}
385
386fn pat_allowed_for_else(cx: &LateContext<'_>, pat: &'_ Pat<'_>, check_types: bool) -> bool {
387 // Check whether the pattern contains any bindings, as the
388 // binding might potentially be used in the body.
389 // TODO: only look for *used* bindings.
390 let mut has_bindings = false;
391 pat.each_binding_or_first(&mut |_, _, _, _| has_bindings = true);
392 if has_bindings {
393 return false;
394 }
395
396 // If we shouldn't check the types, exit early.
397 if !check_types {
398 return true;
399 }
400
401 // Check whether any possibly "unknown" patterns are included,
402 // because users might not know which values some enum has.
403 // Well-known enums are excepted, as we assume people know them.
404 // We do a deep check, to be able to disallow Err(En::Foo(_))
405 // for usage of the En::Foo variant, as we disallow En::Foo(_),
406 // but we allow Err(_).
407 let typeck_results = cx.typeck_results();
408 let mut has_disallowed = false;
409 pat.walk_always(|pat| {
410 // Only do the check if the type is "spelled out" in the pattern
411 if !matches!(
412 pat.kind,
413 PatKind::Struct(..) | PatKind::TupleStruct(..) | PatKind::Path(..)
414 ) {
415 return;
416 };
417 let ty = typeck_results.pat_ty(pat);
418 // Option and Result are allowed, everything else isn't.
419 if !(is_type_diagnostic_item(cx, ty, sym::Option) || is_type_diagnostic_item(cx, ty, sym::Result)) {
420 has_disallowed = true;
421 }
422 });
423 !has_disallowed
424}
425
fe692bf9
FG
426/// Checks if the passed block is a simple identity referring to bindings created by the pattern,
427/// and if yes, returns a mapping between the relevant sub-pattern and the identifier it corresponds
428/// to.
429///
430/// We support patterns with multiple bindings and tuples, e.g.:
431///
432/// ```ignore
433/// let (foo_o, bar_o) = if let (Some(foo), bar) = g() { (foo, bar) } else { ... }
434/// ```
435///
436/// The expected params would be:
437///
438/// ```ignore
439/// local_pat: (foo_o, bar_o)
440/// let_pat: (Some(foo), bar)
441/// expr: (foo, bar)
442/// ```
443///
444/// We build internal `sub_pats` so that it looks like `[foo_o, bar_o]` and `paths` so that it looks
445/// like `[foo, bar]`. Then we turn that into `FxHashMap [(foo) -> (foo_o), (bar) -> (bar_o)]` which
446/// we return.
447fn expr_simple_identity_map<'a, 'hir>(
448 local_pat: &'a Pat<'hir>,
449 let_pat: &'_ Pat<'hir>,
450 expr: &'_ Expr<'hir>,
451) -> Option<FxHashMap<Symbol, &'a Pat<'hir>>> {
487cf647 452 let peeled = peel_blocks(expr);
fe692bf9
FG
453 let (sub_pats, paths) = match (local_pat.kind, peeled.kind) {
454 (PatKind::Tuple(pats, _), ExprKind::Tup(exprs)) | (PatKind::Slice(pats, ..), ExprKind::Array(exprs)) => {
455 (pats, exprs)
456 },
457 (_, ExprKind::Path(_)) => (slice::from_ref(local_pat), slice::from_ref(peeled)),
458 _ => return None,
487cf647 459 };
fe692bf9
FG
460
461 // There is some length mismatch, which indicates usage of .. in the patterns above e.g.:
462 // let (a, ..) = if let [a, b, _c] = ex { (a, b) } else { ... };
463 // We bail in these cases as they should be rare.
464 if paths.len() != sub_pats.len() {
465 return None;
466 }
467
487cf647 468 let mut pat_bindings = FxHashSet::default();
fe692bf9 469 let_pat.each_binding_or_first(&mut |_ann, _hir_id, _sp, ident| {
487cf647
FG
470 pat_bindings.insert(ident);
471 });
472 if pat_bindings.len() < paths.len() {
fe692bf9
FG
473 // This rebinds some bindings from the outer scope, or it repeats some copy-able bindings multiple
474 // times. We don't support these cases so we bail here. E.g.:
475 // let foo = 0;
476 // let (new_foo, bar, bar_copied) = if let Some(bar) = Some(0) { (foo, bar, bar) } else { .. };
477 return None;
487cf647 478 }
fe692bf9
FG
479 let mut ident_map = FxHashMap::default();
480 for (sub_pat, path) in sub_pats.iter().zip(paths.iter()) {
481 if let ExprKind::Path(QPath::Resolved(_ty, path)) = path.kind &&
482 let [path_seg] = path.segments
483 {
484 let ident = path_seg.ident;
485 if !pat_bindings.remove(&ident) {
486 return None;
487cf647 487 }
fe692bf9
FG
488 ident_map.insert(ident.name, sub_pat);
489 } else {
490 return None;
487cf647
FG
491 }
492 }
fe692bf9 493 Some(ident_map)
487cf647
FG
494}
495
496#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Deserialize)]
497pub enum MatchLintBehaviour {
498 AllTypes,
499 WellKnownTypes,
500 Never,
501}