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