use std::iter::repeat; use std::ops::ControlFlow; use hir::intravisit::{self, Visitor}; use rustc_ast::Recovered; use rustc_errors::{Applicability, Diag, EmissionGuarantee, Subdiagnostic, SuggestionStyle}; use rustc_hir::{self as hir, HirIdSet}; use rustc_macros::{LintDiagnostic, Subdiagnostic}; use rustc_middle::ty::adjustment::Adjust; use rustc_middle::ty::significant_drop_order::{ extract_component_with_significant_dtor, ty_dtor_span, }; use rustc_middle::ty::{self, Ty, TyCtxt}; use rustc_session::lint::{FutureIncompatibilityReason, LintId}; use rustc_session::{declare_lint, impl_lint_pass}; use rustc_span::edition::Edition; use rustc_span::{DUMMY_SP, Span}; use smallvec::SmallVec; use crate::{LateContext, LateLintPass}; declare_lint! { /// The `if_let_rescope` lint detects cases where a temporary value with /// significant drop is generated on the right hand side of `if let` /// and suggests a rewrite into `match` when possible. /// /// ### Example /// /// ```rust,edition2021 /// #![warn(if_let_rescope)] /// #![allow(unused_variables)] /// /// struct Droppy; /// impl Drop for Droppy { /// fn drop(&mut self) { /// // Custom destructor, including this `drop` implementation, is considered /// // significant. /// // Rust does not check whether this destructor emits side-effects that can /// // lead to observable change in program semantics, when the drop order changes. /// // Rust biases to be on the safe side, so that you can apply discretion whether /// // this change indeed breaches any contract or specification that your code needs /// // to honour. /// println!("dropped"); /// } /// } /// impl Droppy { /// fn get(&self) -> Option { /// None /// } /// } /// /// fn main() { /// if let Some(value) = Droppy.get() { /// // do something /// } else { /// // do something else /// } /// } /// ``` /// /// {{produces}} /// /// ### Explanation /// /// With Edition 2024, temporaries generated while evaluating `if let`s /// will be dropped before the `else` block. /// This lint captures a possible change in runtime behaviour due to /// a change in sequence of calls to significant `Drop::drop` destructors. /// /// A significant [`Drop::drop`](https://doc.rust-lang.org/std/ops/trait.Drop.html) /// destructor here refers to an explicit, arbitrary implementation of the `Drop` trait on the type /// with exceptions including `Vec`, `Box`, `Rc`, `BTreeMap` and `HashMap` /// that are marked by the compiler otherwise so long that the generic types have /// no significant destructor recursively. /// In other words, a type has a significant drop destructor when it has a `Drop` implementation /// or its destructor invokes a significant destructor on a type. /// Since we cannot completely reason about the change by just inspecting the existence of /// a significant destructor, this lint remains only a suggestion and is set to `allow` by default. /// /// Whenever possible, a rewrite into an equivalent `match` expression that /// observe the same order of calls to such destructors is proposed by this lint. /// Authors may take their own discretion whether the rewrite suggestion shall be /// accepted, or rejected to continue the use of the `if let` expression. pub IF_LET_RESCOPE, Allow, "`if let` assigns a shorter lifetime to temporary values being pattern-matched against in Edition 2024 and \ rewriting in `match` is an option to preserve the semantics up to Edition 2021", @future_incompatible = FutureIncompatibleInfo { reason: FutureIncompatibilityReason::EditionSemanticsChange(Edition::Edition2024), reference: "", }; } /// Lint for potential change in program semantics of `if let`s #[derive(Default)] pub(crate) struct IfLetRescope { skip: HirIdSet, } fn expr_parent_is_else(tcx: TyCtxt<'_>, hir_id: hir::HirId) -> bool { let Some((_, hir::Node::Expr(expr))) = tcx.hir_parent_iter(hir_id).next() else { return false; }; let hir::ExprKind::If(_cond, _conseq, Some(alt)) = expr.kind else { return false }; alt.hir_id == hir_id } fn expr_parent_is_stmt(tcx: TyCtxt<'_>, hir_id: hir::HirId) -> bool { let mut parents = tcx.hir_parent_iter(hir_id); let stmt = match parents.next() { Some((_, hir::Node::Stmt(stmt))) => stmt, Some((_, hir::Node::Block(_) | hir::Node::Arm(_))) => return true, _ => return false, }; let (hir::StmtKind::Semi(expr) | hir::StmtKind::Expr(expr)) = stmt.kind else { return false }; expr.hir_id == hir_id } fn match_head_needs_bracket(tcx: TyCtxt<'_>, expr: &hir::Expr<'_>) -> bool { expr_parent_is_else(tcx, expr.hir_id) && matches!(expr.kind, hir::ExprKind::If(..)) } impl IfLetRescope { fn probe_if_cascade<'tcx>(&mut self, cx: &LateContext<'tcx>, mut expr: &'tcx hir::Expr<'tcx>) { if self.skip.contains(&expr.hir_id) { return; } let tcx = cx.tcx; let source_map = tcx.sess.source_map(); let expr_end = match expr.kind { hir::ExprKind::If(_cond, conseq, None) => conseq.span.shrink_to_hi(), hir::ExprKind::If(_cond, _conseq, Some(alt)) => alt.span.shrink_to_hi(), _ => return, }; let mut seen_dyn = false; let mut add_bracket_to_match_head = match_head_needs_bracket(tcx, expr); let mut significant_droppers = vec![]; let mut lifetime_ends = vec![]; let mut closing_brackets = 0; let mut alt_heads = vec![]; let mut match_heads = vec![]; let mut consequent_heads = vec![]; let mut destructors = vec![]; let mut first_if_to_lint = None; let mut first_if_to_rewrite = false; let mut empty_alt = false; while let hir::ExprKind::If(cond, conseq, alt) = expr.kind { self.skip.insert(expr.hir_id); // We are interested in `let` fragment of the condition. // Otherwise, we probe into the `else` fragment. if let hir::ExprKind::Let(&hir::LetExpr { span, pat, init, ty: ty_ascription, recovered: Recovered::No, }) = cond.kind { // Peel off round braces let if_let_pat = source_map .span_take_while(expr.span, |&ch| ch == '(' || ch.is_whitespace()) .between(init.span); // The consequent fragment is always a block. let before_conseq = conseq.span.shrink_to_lo(); let lifetime_end = source_map.end_point(conseq.span); if let ControlFlow::Break((drop_span, drop_tys)) = (FindSignificantDropper { cx }).check_if_let_scrutinee(init) { destructors.extend(drop_tys.into_iter().filter_map(|ty| { if let Some(span) = ty_dtor_span(tcx, ty) { Some(DestructorLabel { span, dtor_kind: "concrete" }) } else if matches!(ty.kind(), ty::Dynamic(..)) { if seen_dyn { None } else { seen_dyn = true; Some(DestructorLabel { span: DUMMY_SP, dtor_kind: "dyn" }) } } else { None } })); first_if_to_lint = first_if_to_lint.or_else(|| Some((span, expr.hir_id))); significant_droppers.push(drop_span); lifetime_ends.push(lifetime_end); if ty_ascription.is_some() || !expr.span.can_be_used_for_suggestions() || !pat.span.can_be_used_for_suggestions() || !if_let_pat.can_be_used_for_suggestions() || !before_conseq.can_be_used_for_suggestions() { // Our `match` rewrites does not support type ascription, // so we just bail. // Alternatively when the span comes from proc macro expansion, // we will also bail. // FIXME(#101728): change this when type ascription syntax is stabilized again } else if let Ok(pat) = source_map.span_to_snippet(pat.span) { let emit_suggestion = |alt_span| { first_if_to_rewrite = true; if add_bracket_to_match_head { closing_brackets += 2; match_heads.push(SingleArmMatchBegin::WithOpenBracket(if_let_pat)); } else { // Sometimes, wrapping `match` into a block is undesirable, // because the scrutinee temporary lifetime is shortened and // the proposed fix will not work. closing_brackets += 1; match_heads .push(SingleArmMatchBegin::WithoutOpenBracket(if_let_pat)); } consequent_heads.push(ConsequentRewrite { span: before_conseq, pat }); if let Some(alt_span) = alt_span { alt_heads.push(AltHead(alt_span)); } }; if let Some(alt) = alt { let alt_head = conseq.span.between(alt.span); if alt_head.can_be_used_for_suggestions() { // We lint only when the `else` span is user code, too. emit_suggestion(Some(alt_head)); } } else { // This is the end of the `if .. else ..` cascade. // We can stop here. emit_suggestion(None); empty_alt = true; break; } } } } // At this point, any `if let` fragment in the cascade is definitely preceded by `else`, // so a opening bracket is mandatory before each `match`. add_bracket_to_match_head = true; if let Some(alt) = alt { expr = alt; } else { break; } } if let Some((span, hir_id)) = first_if_to_lint { tcx.emit_node_span_lint( IF_LET_RESCOPE, hir_id, span, IfLetRescopeLint { destructors, significant_droppers, lifetime_ends, rewrite: first_if_to_rewrite.then_some(IfLetRescopeRewrite { match_heads, consequent_heads, closing_brackets: ClosingBrackets { span: expr_end, count: closing_brackets, empty_alt, }, alt_heads, }), }, ); } } } impl_lint_pass!( IfLetRescope => [IF_LET_RESCOPE] ); impl<'tcx> LateLintPass<'tcx> for IfLetRescope { fn check_expr(&mut self, cx: &LateContext<'tcx>, expr: &'tcx hir::Expr<'tcx>) { if expr.span.edition().at_least_rust_2024() || cx.tcx.lints_that_dont_need_to_run(()).contains(&LintId::of(IF_LET_RESCOPE)) { return; } if let hir::ExprKind::Loop(block, _label, hir::LoopSource::While, _span) = expr.kind && let Some(value) = block.expr && let hir::ExprKind::If(cond, _conseq, _alt) = value.kind && let hir::ExprKind::Let(..) = cond.kind { // Recall that `while let` is lowered into this: // ``` // loop { // if let .. { body } else { break; } // } // ``` // There is no observable change in drop order on the overall `if let` expression // given that the `{ break; }` block is trivial so the edition change // means nothing substantial to this `while` statement. self.skip.insert(value.hir_id); return; } if expr_parent_is_stmt(cx.tcx, expr.hir_id) && matches!(expr.kind, hir::ExprKind::If(_cond, _conseq, None)) { // `if let` statement without an `else` branch has no observable change // so we can skip linting it return; } self.probe_if_cascade(cx, expr); } } #[derive(LintDiagnostic)] #[diag(lint_if_let_rescope)] struct IfLetRescopeLint { #[subdiagnostic] destructors: Vec, #[label] significant_droppers: Vec, #[help] lifetime_ends: Vec, #[subdiagnostic] rewrite: Option, } struct IfLetRescopeRewrite { match_heads: Vec, consequent_heads: Vec, closing_brackets: ClosingBrackets, alt_heads: Vec, } impl Subdiagnostic for IfLetRescopeRewrite { fn add_to_diag(self, diag: &mut Diag<'_, G>) { let mut suggestions = vec![]; for match_head in self.match_heads { match match_head { SingleArmMatchBegin::WithOpenBracket(span) => { suggestions.push((span, "{ match ".into())) } SingleArmMatchBegin::WithoutOpenBracket(span) => { suggestions.push((span, "match ".into())) } } } for ConsequentRewrite { span, pat } in self.consequent_heads { suggestions.push((span, format!("{{ {pat} => "))); } for AltHead(span) in self.alt_heads { suggestions.push((span, " _ => ".into())); } let closing_brackets = self.closing_brackets; suggestions.push(( closing_brackets.span, closing_brackets .empty_alt .then_some(" _ => {}".chars()) .into_iter() .flatten() .chain(repeat('}').take(closing_brackets.count)) .collect(), )); let msg = diag.eagerly_translate(crate::fluent_generated::lint_suggestion); diag.multipart_suggestion_with_style( msg, suggestions, Applicability::MachineApplicable, SuggestionStyle::ShowCode, ); } } #[derive(Subdiagnostic)] #[note(lint_if_let_dtor)] struct DestructorLabel { #[primary_span] span: Span, dtor_kind: &'static str, } struct AltHead(Span); struct ConsequentRewrite { span: Span, pat: String, } struct ClosingBrackets { span: Span, count: usize, empty_alt: bool, } enum SingleArmMatchBegin { WithOpenBracket(Span), WithoutOpenBracket(Span), } struct FindSignificantDropper<'a, 'tcx> { cx: &'a LateContext<'tcx>, } impl<'tcx> FindSignificantDropper<'_, 'tcx> { /// Check the scrutinee of an `if let` to see if it promotes any temporary values /// that would change drop order in edition 2024. Specifically, it checks the value /// of the scrutinee itself, and also recurses into the expression to find any ref /// exprs (or autoref) which would promote temporaries that would be scoped to the /// end of this `if`. fn check_if_let_scrutinee( &mut self, init: &'tcx hir::Expr<'tcx>, ) -> ControlFlow<(Span, SmallVec<[Ty<'tcx>; 4]>)> { self.check_promoted_temp_with_drop(init)?; self.visit_expr(init) } /// Check that an expression is not a promoted temporary with a significant /// drop impl. /// /// An expression is a promoted temporary if it has an addr taken (i.e. `&expr` or autoref) /// or is the scrutinee of the `if let`, *and* the expression is not a place /// expr, and it has a significant drop. fn check_promoted_temp_with_drop( &self, expr: &'tcx hir::Expr<'tcx>, ) -> ControlFlow<(Span, SmallVec<[Ty<'tcx>; 4]>)> { if expr.is_place_expr(|base| { self.cx .typeck_results() .adjustments() .get(base.hir_id) .is_some_and(|x| x.iter().any(|adj| matches!(adj.kind, Adjust::Deref(_)))) }) { return ControlFlow::Continue(()); } let drop_tys = extract_component_with_significant_dtor( self.cx.tcx, self.cx.typing_env(), self.cx.typeck_results().expr_ty(expr), ); if drop_tys.is_empty() { return ControlFlow::Continue(()); } ControlFlow::Break((expr.span, drop_tys)) } } impl<'tcx> Visitor<'tcx> for FindSignificantDropper<'_, 'tcx> { type Result = ControlFlow<(Span, SmallVec<[Ty<'tcx>; 4]>)>; fn visit_block(&mut self, b: &'tcx hir::Block<'tcx>) -> Self::Result { // Blocks introduce temporary terminating scope for all of its // statements, so just visit the tail expr, skipping over any // statements. This prevents false positives like `{ let x = &Drop; }`. if let Some(expr) = b.expr { self.visit_expr(expr) } else { ControlFlow::Continue(()) } } fn visit_expr(&mut self, expr: &'tcx hir::Expr<'tcx>) -> Self::Result { // Check for promoted temporaries from autoref, e.g. // `if let None = TypeWithDrop.as_ref() {} else {}` // where `fn as_ref(&self) -> Option<...>`. for adj in self.cx.typeck_results().expr_adjustments(expr) { match adj.kind { // Skip when we hit the first deref expr. Adjust::Deref(_) => break, Adjust::Borrow(_) => { self.check_promoted_temp_with_drop(expr)?; } _ => {} } } match expr.kind { // Account for cases like `if let None = Some(&Drop) {} else {}`. hir::ExprKind::AddrOf(_, _, expr) => { self.check_promoted_temp_with_drop(expr)?; intravisit::walk_expr(self, expr) } // `(Drop, ()).1` introduces a temporary and then moves out of // part of it, therefore we should check it for temporaries. // FIXME: This may have false positives if we move the part // that actually has drop, but oh well. hir::ExprKind::Index(expr, _, _) | hir::ExprKind::Field(expr, _) => { self.check_promoted_temp_with_drop(expr)?; intravisit::walk_expr(self, expr) } // If always introduces a temporary terminating scope for its cond and arms, // so don't visit them. hir::ExprKind::If(..) => ControlFlow::Continue(()), // Match introduces temporary terminating scopes for arms, so don't visit // them, and only visit the scrutinee to account for cases like: // `if let None = match &Drop { _ => Some(1) } {} else {}`. hir::ExprKind::Match(scrut, _, _) => self.visit_expr(scrut), // Self explanatory. hir::ExprKind::DropTemps(_) => ControlFlow::Continue(()), // Otherwise, walk into the expr's parts. _ => intravisit::walk_expr(self, expr), } } }