1
Fork 0

Rollup merge of #137444 - compiler-errors:drop-lint, r=oli-obk

Improve behavior of `IF_LET_RESCOPE` around temporaries and place expressions

Heavily reworks the `IF_LET_RESCOPE` to be more sensitive around 1. temporaries that get consumed/terminated and therefore should not trigger the lint, and 2. borrows of place expressions, which are not temporary values.

Fixes #137411
This commit is contained in:
León Orell Valerian Liehr 2025-02-25 13:07:27 +01:00 committed by GitHub
commit 4e3edce492
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 152 additions and 84 deletions

View file

@ -1,7 +1,7 @@
use std::iter::repeat; use std::iter::repeat;
use std::ops::ControlFlow; use std::ops::ControlFlow;
use hir::intravisit::Visitor; use hir::intravisit::{self, Visitor};
use rustc_ast::Recovered; use rustc_ast::Recovered;
use rustc_errors::{ use rustc_errors::{
Applicability, Diag, EmissionGuarantee, SubdiagMessageOp, Subdiagnostic, SuggestionStyle, Applicability, Diag, EmissionGuarantee, SubdiagMessageOp, Subdiagnostic, SuggestionStyle,
@ -9,6 +9,7 @@ use rustc_errors::{
use rustc_hir::{self as hir, HirIdSet}; use rustc_hir::{self as hir, HirIdSet};
use rustc_macros::LintDiagnostic; use rustc_macros::LintDiagnostic;
use rustc_middle::ty::TyCtxt; use rustc_middle::ty::TyCtxt;
use rustc_middle::ty::adjustment::Adjust;
use rustc_session::lint::{FutureIncompatibilityReason, LintId}; use rustc_session::lint::{FutureIncompatibilityReason, LintId};
use rustc_session::{declare_lint, impl_lint_pass}; use rustc_session::{declare_lint, impl_lint_pass};
use rustc_span::Span; use rustc_span::Span;
@ -160,7 +161,7 @@ impl IfLetRescope {
let lifetime_end = source_map.end_point(conseq.span); let lifetime_end = source_map.end_point(conseq.span);
if let ControlFlow::Break(significant_dropper) = if let ControlFlow::Break(significant_dropper) =
(FindSignificantDropper { cx }).visit_expr(init) (FindSignificantDropper { cx }).check_if_let_scrutinee(init)
{ {
first_if_to_lint = first_if_to_lint.or_else(|| Some((span, expr.hir_id))); first_if_to_lint = first_if_to_lint.or_else(|| Some((span, expr.hir_id)));
significant_droppers.push(significant_dropper); significant_droppers.push(significant_dropper);
@ -363,96 +364,97 @@ enum SingleArmMatchBegin {
WithoutOpenBracket(Span), WithoutOpenBracket(Span),
} }
struct FindSignificantDropper<'tcx, 'a> { struct FindSignificantDropper<'a, 'tcx> {
cx: &'a LateContext<'tcx>, cx: &'a LateContext<'tcx>,
} }
impl<'tcx, 'a> Visitor<'tcx> for FindSignificantDropper<'tcx, 'a> { impl<'tcx> FindSignificantDropper<'_, 'tcx> {
type Result = ControlFlow<Span>; /// 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> {
self.check_promoted_temp_with_drop(init)?;
self.visit_expr(init)
}
fn visit_expr(&mut self, expr: &'tcx hir::Expr<'tcx>) -> Self::Result { /// Check that an expression is not a promoted temporary with a significant
if self /// 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> {
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(_))))
}) && self
.cx .cx
.typeck_results() .typeck_results()
.expr_ty(expr) .expr_ty(expr)
.has_significant_drop(self.cx.tcx, self.cx.typing_env()) .has_significant_drop(self.cx.tcx, self.cx.typing_env())
{ {
return ControlFlow::Break(expr.span); ControlFlow::Break(expr.span)
} } else {
match expr.kind { ControlFlow::Continue(())
hir::ExprKind::ConstBlock(_) }
| hir::ExprKind::Lit(_) }
| hir::ExprKind::Path(_) }
| hir::ExprKind::Assign(_, _, _)
| hir::ExprKind::AssignOp(_, _, _) impl<'tcx> Visitor<'tcx> for FindSignificantDropper<'_, 'tcx> {
| hir::ExprKind::Break(_, _) type Result = ControlFlow<Span>;
| hir::ExprKind::Continue(_)
| hir::ExprKind::Ret(_) fn visit_block(&mut self, b: &'tcx hir::Block<'tcx>) -> Self::Result {
| hir::ExprKind::Become(_) // Blocks introduce temporary terminating scope for all of its
| hir::ExprKind::InlineAsm(_) // statements, so just visit the tail expr, skipping over any
| hir::ExprKind::OffsetOf(_, _) // statements. This prevents false positives like `{ let x = &Drop; }`.
| hir::ExprKind::Repeat(_, _) if let Some(expr) = b.expr { self.visit_expr(expr) } else { ControlFlow::Continue(()) }
| hir::ExprKind::Err(_) }
| hir::ExprKind::Struct(_, _, _)
| hir::ExprKind::Closure(_) fn visit_expr(&mut self, expr: &'tcx hir::Expr<'tcx>) -> Self::Result {
| hir::ExprKind::Block(_, _) // Check for promoted temporaries from autoref, e.g.
| hir::ExprKind::DropTemps(_) // `if let None = TypeWithDrop.as_ref() {} else {}`
| hir::ExprKind::Loop(_, _, _, _) => ControlFlow::Continue(()), // where `fn as_ref(&self) -> Option<...>`.
for adj in self.cx.typeck_results().expr_adjustments(expr) {
hir::ExprKind::Tup(exprs) | hir::ExprKind::Array(exprs) => { match adj.kind {
for expr in exprs { // Skip when we hit the first deref expr.
self.visit_expr(expr)?; Adjust::Deref(_) => break,
} Adjust::Borrow(_) => {
ControlFlow::Continue(()) self.check_promoted_temp_with_drop(expr)?;
} }
hir::ExprKind::Call(callee, args) => { _ => {}
self.visit_expr(callee)?; }
for expr in args { }
self.visit_expr(expr)?;
} match expr.kind {
ControlFlow::Continue(()) // Account for cases like `if let None = Some(&Drop) {} else {}`.
} hir::ExprKind::AddrOf(_, _, expr) => {
hir::ExprKind::MethodCall(_, receiver, args, _) => { self.check_promoted_temp_with_drop(expr)?;
self.visit_expr(receiver)?; intravisit::walk_expr(self, expr)
for expr in args { }
self.visit_expr(expr)?; // `(Drop, ()).1` introduces a temporary and then moves out of
} // part of it, therefore we should check it for temporaries.
ControlFlow::Continue(()) // FIXME: This may have false positives if we move the part
} // that actually has drop, but oh well.
hir::ExprKind::Index(left, right, _) | hir::ExprKind::Binary(_, left, right) => { hir::ExprKind::Index(expr, _, _) | hir::ExprKind::Field(expr, _) => {
self.visit_expr(left)?; self.check_promoted_temp_with_drop(expr)?;
self.visit_expr(right) intravisit::walk_expr(self, expr)
} }
hir::ExprKind::Unary(_, expr) // If always introduces a temporary terminating scope for its cond and arms,
| hir::ExprKind::Cast(expr, _) // so don't visit them.
| hir::ExprKind::Type(expr, _) hir::ExprKind::If(..) => ControlFlow::Continue(()),
| hir::ExprKind::UnsafeBinderCast(_, expr, _) // Match introduces temporary terminating scopes for arms, so don't visit
| hir::ExprKind::Yield(expr, _) // them, and only visit the scrutinee to account for cases like:
| hir::ExprKind::AddrOf(_, _, expr) // `if let None = match &Drop { _ => Some(1) } {} else {}`.
| hir::ExprKind::Match(expr, _, _) hir::ExprKind::Match(scrut, _, _) => self.visit_expr(scrut),
| hir::ExprKind::Field(expr, _) // Self explanatory.
| hir::ExprKind::Let(&hir::LetExpr { hir::ExprKind::DropTemps(_) => ControlFlow::Continue(()),
init: expr, // Otherwise, walk into the expr's parts.
span: _, _ => intravisit::walk_expr(self, expr),
pat: _,
ty: _,
recovered: Recovered::No,
}) => self.visit_expr(expr),
hir::ExprKind::Let(_) => ControlFlow::Continue(()),
hir::ExprKind::If(cond, _, _) => {
if let hir::ExprKind::Let(hir::LetExpr {
init,
span: _,
pat: _,
ty: _,
recovered: Recovered::No,
}) = cond.kind
{
self.visit_expr(init)?;
}
ControlFlow::Continue(())
}
} }
} }
} }

View file

@ -0,0 +1,33 @@
//@ edition: 2021
//@ check-pass
#![deny(if_let_rescope)]
struct Drop;
impl std::ops::Drop for Drop {
fn drop(&mut self) {
println!("drop")
}
}
impl Drop {
fn as_ref(&self) -> Option<i32> {
Some(1)
}
}
fn consume(_: impl Sized) -> Option<i32> { Some(1) }
fn main() {
let drop = Drop;
// Make sure we don't drop if we don't actually make a temporary.
if let None = drop.as_ref() {} else {}
// Make sure we don't lint if we consume the droppy value.
if let None = consume(Drop) {} else {}
// Make sure we don't lint on field exprs of place exprs.
let tup_place = (Drop, ());
if let None = consume(tup_place.1) {} else {}
}

View file

@ -94,6 +94,12 @@ fn main() {
//~| HELP: a `match` with a single arm can preserve the drop order up to Edition 2021 //~| HELP: a `match` with a single arm can preserve the drop order up to Edition 2021
} }
match Some((droppy(), ()).1) { Some(_value) => {} _ => {}}
//~^ ERROR: `if let` assigns a shorter lifetime since Edition 2024
//~| WARN: this changes meaning in Rust 2024
//~| HELP: the value is now dropped here in Edition 2024
//~| HELP: a `match` with a single arm can preserve the drop order up to Edition 2021
// We want to keep the `if let`s below as direct descendents of match arms, // We want to keep the `if let`s below as direct descendents of match arms,
// so the formatting is suppressed. // so the formatting is suppressed.
#[rustfmt::skip] #[rustfmt::skip]

View file

@ -94,6 +94,12 @@ fn main() {
//~| HELP: a `match` with a single arm can preserve the drop order up to Edition 2021 //~| HELP: a `match` with a single arm can preserve the drop order up to Edition 2021
} }
if let Some(_value) = Some((droppy(), ()).1) {} else {}
//~^ ERROR: `if let` assigns a shorter lifetime since Edition 2024
//~| WARN: this changes meaning in Rust 2024
//~| HELP: the value is now dropped here in Edition 2024
//~| HELP: a `match` with a single arm can preserve the drop order up to Edition 2021
// We want to keep the `if let`s below as direct descendents of match arms, // We want to keep the `if let`s below as direct descendents of match arms,
// so the formatting is suppressed. // so the formatting is suppressed.
#[rustfmt::skip] #[rustfmt::skip]

View file

@ -175,5 +175,26 @@ LL - while (if let Some(_value) = droppy().get() { false } else { true }) {
LL + while (match droppy().get() { Some(_value) => { false } _ => { true }}) { LL + while (match droppy().get() { Some(_value) => { false } _ => { true }}) {
| |
error: aborting due to 7 previous errors error: `if let` assigns a shorter lifetime since Edition 2024
--> $DIR/lint-if-let-rescope.rs:97:8
|
LL | if let Some(_value) = Some((droppy(), ()).1) {} else {}
| ^^^^^^^^^^^^^^^^^^^^^^^^--------------^^^
| |
| this value has a significant drop implementation which may observe a major change in drop order and requires your discretion
|
= warning: this changes meaning in Rust 2024
= note: for more information, see <https://doc.rust-lang.org/nightly/edition-guide/rust-2024/temporary-if-let-scope.html>
help: the value is now dropped here in Edition 2024
--> $DIR/lint-if-let-rescope.rs:97:51
|
LL | if let Some(_value) = Some((droppy(), ()).1) {} else {}
| ^
help: a `match` with a single arm can preserve the drop order up to Edition 2021
|
LL - if let Some(_value) = Some((droppy(), ()).1) {} else {}
LL + match Some((droppy(), ()).1) { Some(_value) => {} _ => {}}
|
error: aborting due to 8 previous errors