| 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<u8> { |
| /// 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: "<https://doc.rust-lang.org/edition-guide/rust-2024/temporary-if-let-scope.html>", |
| }; |
| } |
| |
| /// 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<DestructorLabel>, |
| #[label] |
| significant_droppers: Vec<Span>, |
| #[help] |
| lifetime_ends: Vec<Span>, |
| #[subdiagnostic] |
| rewrite: Option<IfLetRescopeRewrite>, |
| } |
| |
| struct IfLetRescopeRewrite { |
| match_heads: Vec<SingleArmMatchBegin>, |
| consequent_heads: Vec<ConsequentRewrite>, |
| closing_brackets: ClosingBrackets, |
| alt_heads: Vec<AltHead>, |
| } |
| |
| impl Subdiagnostic for IfLetRescopeRewrite { |
| fn add_to_diag<G: EmissionGuarantee>(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), |
| } |
| } |
| } |