Skip to content

Commit 4e3edce

Browse files
authoredFeb 25, 2025
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
2 parents e02de83 + bad8e98 commit 4e3edce

5 files changed

+146
-78
lines changed
 

‎compiler/rustc_lint/src/if_let_rescope.rs

+79-77
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,15 @@
11
use std::iter::repeat;
22
use std::ops::ControlFlow;
33

4-
use hir::intravisit::Visitor;
4+
use hir::intravisit::{self, Visitor};
55
use rustc_ast::Recovered;
66
use rustc_errors::{
77
Applicability, Diag, EmissionGuarantee, SubdiagMessageOp, Subdiagnostic, SuggestionStyle,
88
};
99
use rustc_hir::{self as hir, HirIdSet};
1010
use rustc_macros::LintDiagnostic;
1111
use rustc_middle::ty::TyCtxt;
12+
use rustc_middle::ty::adjustment::Adjust;
1213
use rustc_session::lint::{FutureIncompatibilityReason, LintId};
1314
use rustc_session::{declare_lint, impl_lint_pass};
1415
use rustc_span::Span;
@@ -160,7 +161,7 @@ impl IfLetRescope {
160161
let lifetime_end = source_map.end_point(conseq.span);
161162

162163
if let ControlFlow::Break(significant_dropper) =
163-
(FindSignificantDropper { cx }).visit_expr(init)
164+
(FindSignificantDropper { cx }).check_if_let_scrutinee(init)
164165
{
165166
first_if_to_lint = first_if_to_lint.or_else(|| Some((span, expr.hir_id)));
166167
significant_droppers.push(significant_dropper);
@@ -363,96 +364,97 @@ enum SingleArmMatchBegin {
363364
WithoutOpenBracket(Span),
364365
}
365366

366-
struct FindSignificantDropper<'tcx, 'a> {
367+
struct FindSignificantDropper<'a, 'tcx> {
367368
cx: &'a LateContext<'tcx>,
368369
}
369370

370-
impl<'tcx, 'a> Visitor<'tcx> for FindSignificantDropper<'tcx, 'a> {
371-
type Result = ControlFlow<Span>;
371+
impl<'tcx> FindSignificantDropper<'_, 'tcx> {
372+
/// Check the scrutinee of an `if let` to see if it promotes any temporary values
373+
/// that would change drop order in edition 2024. Specifically, it checks the value
374+
/// of the scrutinee itself, and also recurses into the expression to find any ref
375+
/// exprs (or autoref) which would promote temporaries that would be scoped to the
376+
/// end of this `if`.
377+
fn check_if_let_scrutinee(&mut self, init: &'tcx hir::Expr<'tcx>) -> ControlFlow<Span> {
378+
self.check_promoted_temp_with_drop(init)?;
379+
self.visit_expr(init)
380+
}
372381

373-
fn visit_expr(&mut self, expr: &'tcx hir::Expr<'tcx>) -> Self::Result {
374-
if self
382+
/// Check that an expression is not a promoted temporary with a significant
383+
/// drop impl.
384+
///
385+
/// An expression is a promoted temporary if it has an addr taken (i.e. `&expr` or autoref)
386+
/// or is the scrutinee of the `if let`, *and* the expression is not a place
387+
/// expr, and it has a significant drop.
388+
fn check_promoted_temp_with_drop(&self, expr: &'tcx hir::Expr<'tcx>) -> ControlFlow<Span> {
389+
if !expr.is_place_expr(|base| {
390+
self.cx
391+
.typeck_results()
392+
.adjustments()
393+
.get(base.hir_id)
394+
.is_some_and(|x| x.iter().any(|adj| matches!(adj.kind, Adjust::Deref(_))))
395+
}) && self
375396
.cx
376397
.typeck_results()
377398
.expr_ty(expr)
378399
.has_significant_drop(self.cx.tcx, self.cx.typing_env())
379400
{
380-
return ControlFlow::Break(expr.span);
401+
ControlFlow::Break(expr.span)
402+
} else {
403+
ControlFlow::Continue(())
381404
}
382-
match expr.kind {
383-
hir::ExprKind::ConstBlock(_)
384-
| hir::ExprKind::Lit(_)
385-
| hir::ExprKind::Path(_)
386-
| hir::ExprKind::Assign(_, _, _)
387-
| hir::ExprKind::AssignOp(_, _, _)
388-
| hir::ExprKind::Break(_, _)
389-
| hir::ExprKind::Continue(_)
390-
| hir::ExprKind::Ret(_)
391-
| hir::ExprKind::Become(_)
392-
| hir::ExprKind::InlineAsm(_)
393-
| hir::ExprKind::OffsetOf(_, _)
394-
| hir::ExprKind::Repeat(_, _)
395-
| hir::ExprKind::Err(_)
396-
| hir::ExprKind::Struct(_, _, _)
397-
| hir::ExprKind::Closure(_)
398-
| hir::ExprKind::Block(_, _)
399-
| hir::ExprKind::DropTemps(_)
400-
| hir::ExprKind::Loop(_, _, _, _) => ControlFlow::Continue(()),
405+
}
406+
}
401407

402-
hir::ExprKind::Tup(exprs) | hir::ExprKind::Array(exprs) => {
403-
for expr in exprs {
404-
self.visit_expr(expr)?;
405-
}
406-
ControlFlow::Continue(())
407-
}
408-
hir::ExprKind::Call(callee, args) => {
409-
self.visit_expr(callee)?;
410-
for expr in args {
411-
self.visit_expr(expr)?;
412-
}
413-
ControlFlow::Continue(())
414-
}
415-
hir::ExprKind::MethodCall(_, receiver, args, _) => {
416-
self.visit_expr(receiver)?;
417-
for expr in args {
418-
self.visit_expr(expr)?;
408+
impl<'tcx> Visitor<'tcx> for FindSignificantDropper<'_, 'tcx> {
409+
type Result = ControlFlow<Span>;
410+
411+
fn visit_block(&mut self, b: &'tcx hir::Block<'tcx>) -> Self::Result {
412+
// Blocks introduce temporary terminating scope for all of its
413+
// statements, so just visit the tail expr, skipping over any
414+
// statements. This prevents false positives like `{ let x = &Drop; }`.
415+
if let Some(expr) = b.expr { self.visit_expr(expr) } else { ControlFlow::Continue(()) }
416+
}
417+
418+
fn visit_expr(&mut self, expr: &'tcx hir::Expr<'tcx>) -> Self::Result {
419+
// Check for promoted temporaries from autoref, e.g.
420+
// `if let None = TypeWithDrop.as_ref() {} else {}`
421+
// where `fn as_ref(&self) -> Option<...>`.
422+
for adj in self.cx.typeck_results().expr_adjustments(expr) {
423+
match adj.kind {
424+
// Skip when we hit the first deref expr.
425+
Adjust::Deref(_) => break,
426+
Adjust::Borrow(_) => {
427+
self.check_promoted_temp_with_drop(expr)?;
419428
}
420-
ControlFlow::Continue(())
421-
}
422-
hir::ExprKind::Index(left, right, _) | hir::ExprKind::Binary(_, left, right) => {
423-
self.visit_expr(left)?;
424-
self.visit_expr(right)
429+
_ => {}
425430
}
426-
hir::ExprKind::Unary(_, expr)
427-
| hir::ExprKind::Cast(expr, _)
428-
| hir::ExprKind::Type(expr, _)
429-
| hir::ExprKind::UnsafeBinderCast(_, expr, _)
430-
| hir::ExprKind::Yield(expr, _)
431-
| hir::ExprKind::AddrOf(_, _, expr)
432-
| hir::ExprKind::Match(expr, _, _)
433-
| hir::ExprKind::Field(expr, _)
434-
| hir::ExprKind::Let(&hir::LetExpr {
435-
init: expr,
436-
span: _,
437-
pat: _,
438-
ty: _,
439-
recovered: Recovered::No,
440-
}) => self.visit_expr(expr),
441-
hir::ExprKind::Let(_) => ControlFlow::Continue(()),
431+
}
442432

443-
hir::ExprKind::If(cond, _, _) => {
444-
if let hir::ExprKind::Let(hir::LetExpr {
445-
init,
446-
span: _,
447-
pat: _,
448-
ty: _,
449-
recovered: Recovered::No,
450-
}) = cond.kind
451-
{
452-
self.visit_expr(init)?;
453-
}
454-
ControlFlow::Continue(())
433+
match expr.kind {
434+
// Account for cases like `if let None = Some(&Drop) {} else {}`.
435+
hir::ExprKind::AddrOf(_, _, expr) => {
436+
self.check_promoted_temp_with_drop(expr)?;
437+
intravisit::walk_expr(self, expr)
438+
}
439+
// `(Drop, ()).1` introduces a temporary and then moves out of
440+
// part of it, therefore we should check it for temporaries.
441+
// FIXME: This may have false positives if we move the part
442+
// that actually has drop, but oh well.
443+
hir::ExprKind::Index(expr, _, _) | hir::ExprKind::Field(expr, _) => {
444+
self.check_promoted_temp_with_drop(expr)?;
445+
intravisit::walk_expr(self, expr)
455446
}
447+
// If always introduces a temporary terminating scope for its cond and arms,
448+
// so don't visit them.
449+
hir::ExprKind::If(..) => ControlFlow::Continue(()),
450+
// Match introduces temporary terminating scopes for arms, so don't visit
451+
// them, and only visit the scrutinee to account for cases like:
452+
// `if let None = match &Drop { _ => Some(1) } {} else {}`.
453+
hir::ExprKind::Match(scrut, _, _) => self.visit_expr(scrut),
454+
// Self explanatory.
455+
hir::ExprKind::DropTemps(_) => ControlFlow::Continue(()),
456+
// Otherwise, walk into the expr's parts.
457+
_ => intravisit::walk_expr(self, expr),
456458
}
457459
}
458460
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
//@ edition: 2021
2+
//@ check-pass
3+
4+
#![deny(if_let_rescope)]
5+
6+
struct Drop;
7+
impl std::ops::Drop for Drop {
8+
fn drop(&mut self) {
9+
println!("drop")
10+
}
11+
}
12+
13+
impl Drop {
14+
fn as_ref(&self) -> Option<i32> {
15+
Some(1)
16+
}
17+
}
18+
19+
fn consume(_: impl Sized) -> Option<i32> { Some(1) }
20+
21+
fn main() {
22+
let drop = Drop;
23+
24+
// Make sure we don't drop if we don't actually make a temporary.
25+
if let None = drop.as_ref() {} else {}
26+
27+
// Make sure we don't lint if we consume the droppy value.
28+
if let None = consume(Drop) {} else {}
29+
30+
// Make sure we don't lint on field exprs of place exprs.
31+
let tup_place = (Drop, ());
32+
if let None = consume(tup_place.1) {} else {}
33+
}

‎tests/ui/drop/lint-if-let-rescope.fixed

+6
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,12 @@ fn main() {
9494
//~| HELP: a `match` with a single arm can preserve the drop order up to Edition 2021
9595
}
9696

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

‎tests/ui/drop/lint-if-let-rescope.rs

+6
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,12 @@ fn main() {
9494
//~| HELP: a `match` with a single arm can preserve the drop order up to Edition 2021
9595
}
9696

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

‎tests/ui/drop/lint-if-let-rescope.stderr

+22-1
Original file line numberDiff line numberDiff line change
@@ -175,5 +175,26 @@ LL - while (if let Some(_value) = droppy().get() { false } else { true }) {
175175
LL + while (match droppy().get() { Some(_value) => { false } _ => { true }}) {
176176
|
177177

178-
error: aborting due to 7 previous errors
178+
error: `if let` assigns a shorter lifetime since Edition 2024
179+
--> $DIR/lint-if-let-rescope.rs:97:8
180+
|
181+
LL | if let Some(_value) = Some((droppy(), ()).1) {} else {}
182+
| ^^^^^^^^^^^^^^^^^^^^^^^^--------------^^^
183+
| |
184+
| this value has a significant drop implementation which may observe a major change in drop order and requires your discretion
185+
|
186+
= warning: this changes meaning in Rust 2024
187+
= note: for more information, see <https://doc.rust-lang.org/nightly/edition-guide/rust-2024/temporary-if-let-scope.html>
188+
help: the value is now dropped here in Edition 2024
189+
--> $DIR/lint-if-let-rescope.rs:97:51
190+
|
191+
LL | if let Some(_value) = Some((droppy(), ()).1) {} else {}
192+
| ^
193+
help: a `match` with a single arm can preserve the drop order up to Edition 2021
194+
|
195+
LL - if let Some(_value) = Some((droppy(), ()).1) {} else {}
196+
LL + match Some((droppy(), ()).1) { Some(_value) => {} _ => {}}
197+
|
198+
199+
error: aborting due to 8 previous errors
179200

0 commit comments

Comments
 (0)