rustc_lint/
if_let_rescope.rs

1use std::iter::repeat;
2use std::ops::ControlFlow;
3
4use hir::intravisit::{self, Visitor};
5use rustc_ast::Recovered;
6use rustc_errors::{
7    Applicability, Diag, EmissionGuarantee, SubdiagMessageOp, Subdiagnostic, SuggestionStyle,
8};
9use rustc_hir::{self as hir, HirIdSet};
10use rustc_macros::LintDiagnostic;
11use rustc_middle::ty::TyCtxt;
12use rustc_middle::ty::adjustment::Adjust;
13use rustc_session::lint::{FutureIncompatibilityReason, LintId};
14use rustc_session::{declare_lint, impl_lint_pass};
15use rustc_span::Span;
16use rustc_span::edition::Edition;
17
18use crate::{LateContext, LateLintPass};
19
20declare_lint! {
21    /// The `if_let_rescope` lint detects cases where a temporary value with
22    /// significant drop is generated on the right hand side of `if let`
23    /// and suggests a rewrite into `match` when possible.
24    ///
25    /// ### Example
26    ///
27    /// ```rust,edition2021
28    /// #![warn(if_let_rescope)]
29    /// #![allow(unused_variables)]
30    ///
31    /// struct Droppy;
32    /// impl Drop for Droppy {
33    ///     fn drop(&mut self) {
34    ///         // Custom destructor, including this `drop` implementation, is considered
35    ///         // significant.
36    ///         // Rust does not check whether this destructor emits side-effects that can
37    ///         // lead to observable change in program semantics, when the drop order changes.
38    ///         // Rust biases to be on the safe side, so that you can apply discretion whether
39    ///         // this change indeed breaches any contract or specification that your code needs
40    ///         // to honour.
41    ///         println!("dropped");
42    ///     }
43    /// }
44    /// impl Droppy {
45    ///     fn get(&self) -> Option<u8> {
46    ///         None
47    ///     }
48    /// }
49    ///
50    /// fn main() {
51    ///     if let Some(value) = Droppy.get() {
52    ///         // do something
53    ///     } else {
54    ///         // do something else
55    ///     }
56    /// }
57    /// ```
58    ///
59    /// {{produces}}
60    ///
61    /// ### Explanation
62    ///
63    /// With Edition 2024, temporaries generated while evaluating `if let`s
64    /// will be dropped before the `else` block.
65    /// This lint captures a possible change in runtime behaviour due to
66    /// a change in sequence of calls to significant `Drop::drop` destructors.
67    ///
68    /// A significant [`Drop::drop`](https://doc.rust-lang.org/std/ops/trait.Drop.html)
69    /// destructor here refers to an explicit, arbitrary implementation of the `Drop` trait on the type
70    /// with exceptions including `Vec`, `Box`, `Rc`, `BTreeMap` and `HashMap`
71    /// that are marked by the compiler otherwise so long that the generic types have
72    /// no significant destructor recursively.
73    /// In other words, a type has a significant drop destructor when it has a `Drop` implementation
74    /// or its destructor invokes a significant destructor on a type.
75    /// Since we cannot completely reason about the change by just inspecting the existence of
76    /// a significant destructor, this lint remains only a suggestion and is set to `allow` by default.
77    ///
78    /// Whenever possible, a rewrite into an equivalent `match` expression that
79    /// observe the same order of calls to such destructors is proposed by this lint.
80    /// Authors may take their own discretion whether the rewrite suggestion shall be
81    /// accepted, or rejected to continue the use of the `if let` expression.
82    pub IF_LET_RESCOPE,
83    Allow,
84    "`if let` assigns a shorter lifetime to temporary values being pattern-matched against in Edition 2024 and \
85    rewriting in `match` is an option to preserve the semantics up to Edition 2021",
86    @future_incompatible = FutureIncompatibleInfo {
87        reason: FutureIncompatibilityReason::EditionSemanticsChange(Edition::Edition2024),
88        reference: "<https://doc.rust-lang.org/nightly/edition-guide/rust-2024/temporary-if-let-scope.html>",
89    };
90}
91
92/// Lint for potential change in program semantics of `if let`s
93#[derive(Default)]
94pub(crate) struct IfLetRescope {
95    skip: HirIdSet,
96}
97
98fn expr_parent_is_else(tcx: TyCtxt<'_>, hir_id: hir::HirId) -> bool {
99    let Some((_, hir::Node::Expr(expr))) = tcx.hir().parent_iter(hir_id).next() else {
100        return false;
101    };
102    let hir::ExprKind::If(_cond, _conseq, Some(alt)) = expr.kind else { return false };
103    alt.hir_id == hir_id
104}
105
106fn expr_parent_is_stmt(tcx: TyCtxt<'_>, hir_id: hir::HirId) -> bool {
107    let mut parents = tcx.hir().parent_iter(hir_id);
108    let stmt = match parents.next() {
109        Some((_, hir::Node::Stmt(stmt))) => stmt,
110        Some((_, hir::Node::Block(_) | hir::Node::Arm(_))) => return true,
111        _ => return false,
112    };
113    let (hir::StmtKind::Semi(expr) | hir::StmtKind::Expr(expr)) = stmt.kind else { return false };
114    expr.hir_id == hir_id
115}
116
117fn match_head_needs_bracket(tcx: TyCtxt<'_>, expr: &hir::Expr<'_>) -> bool {
118    expr_parent_is_else(tcx, expr.hir_id) && matches!(expr.kind, hir::ExprKind::If(..))
119}
120
121impl IfLetRescope {
122    fn probe_if_cascade<'tcx>(&mut self, cx: &LateContext<'tcx>, mut expr: &'tcx hir::Expr<'tcx>) {
123        if self.skip.contains(&expr.hir_id) {
124            return;
125        }
126        let tcx = cx.tcx;
127        let source_map = tcx.sess.source_map();
128        let expr_end = match expr.kind {
129            hir::ExprKind::If(_cond, conseq, None) => conseq.span.shrink_to_hi(),
130            hir::ExprKind::If(_cond, _conseq, Some(alt)) => alt.span.shrink_to_hi(),
131            _ => return,
132        };
133        let mut add_bracket_to_match_head = match_head_needs_bracket(tcx, expr);
134        let mut significant_droppers = vec![];
135        let mut lifetime_ends = vec![];
136        let mut closing_brackets = 0;
137        let mut alt_heads = vec![];
138        let mut match_heads = vec![];
139        let mut consequent_heads = vec![];
140        let mut first_if_to_lint = None;
141        let mut first_if_to_rewrite = false;
142        let mut empty_alt = false;
143        while let hir::ExprKind::If(cond, conseq, alt) = expr.kind {
144            self.skip.insert(expr.hir_id);
145            // We are interested in `let` fragment of the condition.
146            // Otherwise, we probe into the `else` fragment.
147            if let hir::ExprKind::Let(&hir::LetExpr {
148                span,
149                pat,
150                init,
151                ty: ty_ascription,
152                recovered: Recovered::No,
153            }) = cond.kind
154            {
155                // Peel off round braces
156                let if_let_pat = source_map
157                    .span_take_while(expr.span, |&ch| ch == '(' || ch.is_whitespace())
158                    .between(init.span);
159                // The consequent fragment is always a block.
160                let before_conseq = conseq.span.shrink_to_lo();
161                let lifetime_end = source_map.end_point(conseq.span);
162
163                if let ControlFlow::Break(significant_dropper) =
164                    (FindSignificantDropper { cx }).check_if_let_scrutinee(init)
165                {
166                    first_if_to_lint = first_if_to_lint.or_else(|| Some((span, expr.hir_id)));
167                    significant_droppers.push(significant_dropper);
168                    lifetime_ends.push(lifetime_end);
169                    if ty_ascription.is_some()
170                        || !expr.span.can_be_used_for_suggestions()
171                        || !pat.span.can_be_used_for_suggestions()
172                        || !if_let_pat.can_be_used_for_suggestions()
173                        || !before_conseq.can_be_used_for_suggestions()
174                    {
175                        // Our `match` rewrites does not support type ascription,
176                        // so we just bail.
177                        // Alternatively when the span comes from proc macro expansion,
178                        // we will also bail.
179                        // FIXME(#101728): change this when type ascription syntax is stabilized again
180                    } else if let Ok(pat) = source_map.span_to_snippet(pat.span) {
181                        let emit_suggestion = |alt_span| {
182                            first_if_to_rewrite = true;
183                            if add_bracket_to_match_head {
184                                closing_brackets += 2;
185                                match_heads.push(SingleArmMatchBegin::WithOpenBracket(if_let_pat));
186                            } else {
187                                // Sometimes, wrapping `match` into a block is undesirable,
188                                // because the scrutinee temporary lifetime is shortened and
189                                // the proposed fix will not work.
190                                closing_brackets += 1;
191                                match_heads
192                                    .push(SingleArmMatchBegin::WithoutOpenBracket(if_let_pat));
193                            }
194                            consequent_heads.push(ConsequentRewrite { span: before_conseq, pat });
195                            if let Some(alt_span) = alt_span {
196                                alt_heads.push(AltHead(alt_span));
197                            }
198                        };
199                        if let Some(alt) = alt {
200                            let alt_head = conseq.span.between(alt.span);
201                            if alt_head.can_be_used_for_suggestions() {
202                                // We lint only when the `else` span is user code, too.
203                                emit_suggestion(Some(alt_head));
204                            }
205                        } else {
206                            // This is the end of the `if .. else ..` cascade.
207                            // We can stop here.
208                            emit_suggestion(None);
209                            empty_alt = true;
210                            break;
211                        }
212                    }
213                }
214            }
215            // At this point, any `if let` fragment in the cascade is definitely preceeded by `else`,
216            // so a opening bracket is mandatory before each `match`.
217            add_bracket_to_match_head = true;
218            if let Some(alt) = alt {
219                expr = alt;
220            } else {
221                break;
222            }
223        }
224        if let Some((span, hir_id)) = first_if_to_lint {
225            tcx.emit_node_span_lint(
226                IF_LET_RESCOPE,
227                hir_id,
228                span,
229                IfLetRescopeLint {
230                    significant_droppers,
231                    lifetime_ends,
232                    rewrite: first_if_to_rewrite.then_some(IfLetRescopeRewrite {
233                        match_heads,
234                        consequent_heads,
235                        closing_brackets: ClosingBrackets {
236                            span: expr_end,
237                            count: closing_brackets,
238                            empty_alt,
239                        },
240                        alt_heads,
241                    }),
242                },
243            );
244        }
245    }
246}
247
248impl_lint_pass!(
249    IfLetRescope => [IF_LET_RESCOPE]
250);
251
252impl<'tcx> LateLintPass<'tcx> for IfLetRescope {
253    fn check_expr(&mut self, cx: &LateContext<'tcx>, expr: &'tcx hir::Expr<'tcx>) {
254        if expr.span.edition().at_least_rust_2024()
255            || cx.tcx.lints_that_dont_need_to_run(()).contains(&LintId::of(IF_LET_RESCOPE))
256        {
257            return;
258        }
259
260        if let hir::ExprKind::Loop(block, _label, hir::LoopSource::While, _span) = expr.kind
261            && let Some(value) = block.expr
262            && let hir::ExprKind::If(cond, _conseq, _alt) = value.kind
263            && let hir::ExprKind::Let(..) = cond.kind
264        {
265            // Recall that `while let` is lowered into this:
266            // ```
267            // loop {
268            //     if let .. { body } else { break; }
269            // }
270            // ```
271            // There is no observable change in drop order on the overall `if let` expression
272            // given that the `{ break; }` block is trivial so the edition change
273            // means nothing substantial to this `while` statement.
274            self.skip.insert(value.hir_id);
275            return;
276        }
277        if expr_parent_is_stmt(cx.tcx, expr.hir_id)
278            && matches!(expr.kind, hir::ExprKind::If(_cond, _conseq, None))
279        {
280            // `if let` statement without an `else` branch has no observable change
281            // so we can skip linting it
282            return;
283        }
284        self.probe_if_cascade(cx, expr);
285    }
286}
287
288#[derive(LintDiagnostic)]
289#[diag(lint_if_let_rescope)]
290struct IfLetRescopeLint {
291    #[label]
292    significant_droppers: Vec<Span>,
293    #[help]
294    lifetime_ends: Vec<Span>,
295    #[subdiagnostic]
296    rewrite: Option<IfLetRescopeRewrite>,
297}
298
299struct IfLetRescopeRewrite {
300    match_heads: Vec<SingleArmMatchBegin>,
301    consequent_heads: Vec<ConsequentRewrite>,
302    closing_brackets: ClosingBrackets,
303    alt_heads: Vec<AltHead>,
304}
305
306impl Subdiagnostic for IfLetRescopeRewrite {
307    fn add_to_diag_with<G: EmissionGuarantee, F: SubdiagMessageOp<G>>(
308        self,
309        diag: &mut Diag<'_, G>,
310        f: &F,
311    ) {
312        let mut suggestions = vec![];
313        for match_head in self.match_heads {
314            match match_head {
315                SingleArmMatchBegin::WithOpenBracket(span) => {
316                    suggestions.push((span, "{ match ".into()))
317                }
318                SingleArmMatchBegin::WithoutOpenBracket(span) => {
319                    suggestions.push((span, "match ".into()))
320                }
321            }
322        }
323        for ConsequentRewrite { span, pat } in self.consequent_heads {
324            suggestions.push((span, format!("{{ {pat} => ")));
325        }
326        for AltHead(span) in self.alt_heads {
327            suggestions.push((span, " _ => ".into()));
328        }
329        let closing_brackets = self.closing_brackets;
330        suggestions.push((
331            closing_brackets.span,
332            closing_brackets
333                .empty_alt
334                .then_some(" _ => {}".chars())
335                .into_iter()
336                .flatten()
337                .chain(repeat('}').take(closing_brackets.count))
338                .collect(),
339        ));
340        let msg = f(diag, crate::fluent_generated::lint_suggestion);
341        diag.multipart_suggestion_with_style(
342            msg,
343            suggestions,
344            Applicability::MachineApplicable,
345            SuggestionStyle::ShowCode,
346        );
347    }
348}
349
350struct AltHead(Span);
351
352struct ConsequentRewrite {
353    span: Span,
354    pat: String,
355}
356
357struct ClosingBrackets {
358    span: Span,
359    count: usize,
360    empty_alt: bool,
361}
362enum SingleArmMatchBegin {
363    WithOpenBracket(Span),
364    WithoutOpenBracket(Span),
365}
366
367struct FindSignificantDropper<'a, 'tcx> {
368    cx: &'a LateContext<'tcx>,
369}
370
371impl<'tcx> FindSignificantDropper<'_, 'tcx> {
372    /// Check the scrutinee of an `if let` to see if it promotes any temporary values
373    /// that would change drop order in edition 2024. Specifically, it checks the value
374    /// of the scrutinee itself, and also recurses into the expression to find any ref
375    /// exprs (or autoref) which would promote temporaries that would be scoped to the
376    /// end of this `if`.
377    fn check_if_let_scrutinee(&mut self, init: &'tcx hir::Expr<'tcx>) -> ControlFlow<Span> {
378        self.check_promoted_temp_with_drop(init)?;
379        self.visit_expr(init)
380    }
381
382    /// Check that an expression is not a promoted temporary with a significant
383    /// drop impl.
384    ///
385    /// An expression is a promoted temporary if it has an addr taken (i.e. `&expr` or autoref)
386    /// or is the scrutinee of the `if let`, *and* the expression is not a place
387    /// expr, and it has a significant drop.
388    fn check_promoted_temp_with_drop(&self, expr: &'tcx hir::Expr<'tcx>) -> ControlFlow<Span> {
389        if !expr.is_place_expr(|base| {
390            self.cx
391                .typeck_results()
392                .adjustments()
393                .get(base.hir_id)
394                .is_some_and(|x| x.iter().any(|adj| matches!(adj.kind, Adjust::Deref(_))))
395        }) && self
396            .cx
397            .typeck_results()
398            .expr_ty(expr)
399            .has_significant_drop(self.cx.tcx, self.cx.typing_env())
400        {
401            ControlFlow::Break(expr.span)
402        } else {
403            ControlFlow::Continue(())
404        }
405    }
406}
407
408impl<'tcx> Visitor<'tcx> for FindSignificantDropper<'_, 'tcx> {
409    type Result = ControlFlow<Span>;
410
411    fn visit_block(&mut self, b: &'tcx hir::Block<'tcx>) -> Self::Result {
412        // Blocks introduce temporary terminating scope for all of its
413        // statements, so just visit the tail expr, skipping over any
414        // statements. This prevents false positives like `{ let x = &Drop; }`.
415        if let Some(expr) = b.expr { self.visit_expr(expr) } else { ControlFlow::Continue(()) }
416    }
417
418    fn visit_expr(&mut self, expr: &'tcx hir::Expr<'tcx>) -> Self::Result {
419        // Check for promoted temporaries from autoref, e.g.
420        // `if let None = TypeWithDrop.as_ref() {} else {}`
421        // where `fn as_ref(&self) -> Option<...>`.
422        for adj in self.cx.typeck_results().expr_adjustments(expr) {
423            match adj.kind {
424                // Skip when we hit the first deref expr.
425                Adjust::Deref(_) => break,
426                Adjust::Borrow(_) => {
427                    self.check_promoted_temp_with_drop(expr)?;
428                }
429                _ => {}
430            }
431        }
432
433        match expr.kind {
434            // Account for cases like `if let None = Some(&Drop) {} else {}`.
435            hir::ExprKind::AddrOf(_, _, expr) => {
436                self.check_promoted_temp_with_drop(expr)?;
437                intravisit::walk_expr(self, expr)
438            }
439            // `(Drop, ()).1` introduces a temporary and then moves out of
440            // part of it, therefore we should check it for temporaries.
441            // FIXME: This may have false positives if we move the part
442            // that actually has drop, but oh well.
443            hir::ExprKind::Index(expr, _, _) | hir::ExprKind::Field(expr, _) => {
444                self.check_promoted_temp_with_drop(expr)?;
445                intravisit::walk_expr(self, expr)
446            }
447            // If always introduces a temporary terminating scope for its cond and arms,
448            // so don't visit them.
449            hir::ExprKind::If(..) => ControlFlow::Continue(()),
450            // Match introduces temporary terminating scopes for arms, so don't visit
451            // them, and only visit the scrutinee to account for cases like:
452            // `if let None = match &Drop { _ => Some(1) } {} else {}`.
453            hir::ExprKind::Match(scrut, _, _) => self.visit_expr(scrut),
454            // Self explanatory.
455            hir::ExprKind::DropTemps(_) => ControlFlow::Continue(()),
456            // Otherwise, walk into the expr's parts.
457            _ => intravisit::walk_expr(self, expr),
458        }
459    }
460}