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