Auto merge of #143213 - dianne:lower-cond-tweaks, r=cjgillot

de-duplicate condition scoping logic between AST→HIR lowering and `ScopeTree` construction

There was some overlap between `rustc_ast_lowering::LoweringContext::lower_cond` and `rustc_hir_analysis::check::region::resolve_expr`, so I've removed the former and migrated its logic to the latter, with some simplifications.

Consequences:
- For `while` and `if` expressions' `let`-chains, this changes the `HirId`s for the `&&`s to properly correspond to their AST nodes. This is how guards were handled already.
- This makes match guards share previously-duplicated logic with `if`/`while` expressions. This will also be used by guard pattern[^1] guards.
- Aside from legacy syntax extensions (e.g. some builtin macros) that directly feed AST to the compiler, it's currently impossible to put attributes directly on `&&` operators in `let` chains[^2]. Nonetheless, attributes on `&&` operators in `let` chains in `if`/`while` expression conditions are no longer silently ignored and will be lowered.
- This no longer wraps conditions in `DropTemps`, so the HIR and THIR will be slightly smaller.
- `DesugaringKind::CondTemporary` is now gone. It's no longer applied to any spans, and all uses of it were dead since they were made to account for `if` and `while` being desugared to `match` on a boolean scrutinee.
- Should be a marginal perf improvement beyond that due to leveraging [`ScopeTree` construction](5e749eb66f/compiler/rustc_hir_analysis/src/check/region.rs (L312-L355))'s clever handling of `&&` and `||`:
  - This removes some unnecessary terminating scopes that were placed around top-level `&&` and `||` operators in conditions. When lowered to MIR, logical operator chains don't create intermediate boolean temporaries, so there's no temporary to drop. The linked snippet handles wrapping the operands in terminating scopes as necessary, in case they create temporaries.
  - The linked snippet takes care of letting `let` temporaries live and terminating other operands, so we don't need separate traversals of `&&` chains for that.

[^1]: rust-lang/rust#129967
[^2]: Case-by-case, here's my justification: `#[attr] e1 && e2` applies the attribute to `e1`. In `#[attr] (e1 && e2)` , the attribute is on the parentheses in the AST, plus it'd fail to parse if `e1` or `e2` contains a `let`. In `#[attr] expands_to_let_chain!()`, the attribute would already be ignored (rust-lang/rust#63221) and it'd fail to parse anyway; even if the expansion site is a condition, the expansion wouldn't be parsed with `Restrictions::ALLOW_LET`. If it *was* allowed, the notion of a "reparse context" from https://github.com/rust-lang/rust/issues/61733#issuecomment-509626449 would be necessary in order to make `let`-chains left-associative; multiple places in the compiler assume they are.
This commit is contained in:
bors 2025-07-13 04:20:07 +00:00
commit d2baa49a10
13 changed files with 63 additions and 128 deletions

View file

@ -1,5 +1,6 @@
use clippy_config::Conf;
use clippy_utils::diagnostics::{span_lint_and_sugg, span_lint_hir_and_then};
use clippy_utils::higher::has_let_expr;
use clippy_utils::msrvs::{self, Msrv};
use clippy_utils::source::SpanRangeExt;
use clippy_utils::sugg::Sugg;
@ -646,7 +647,9 @@ impl<'tcx> Visitor<'tcx> for NonminimalBoolVisitor<'_, 'tcx> {
fn visit_expr(&mut self, e: &'tcx Expr<'_>) {
if !e.span.from_expansion() {
match &e.kind {
ExprKind::Binary(binop, _, _) if binop.node == BinOpKind::Or || binop.node == BinOpKind::And => {
ExprKind::Binary(binop, _, _)
if binop.node == BinOpKind::Or || binop.node == BinOpKind::And && !has_let_expr(e) =>
{
self.bool_expr(e);
},
ExprKind::Unary(UnOp::Not, inner) => {

View file

@ -51,7 +51,6 @@ declare_lint_pass!(IfNotElse => [IF_NOT_ELSE]);
impl LateLintPass<'_> for IfNotElse {
fn check_expr(&mut self, cx: &LateContext<'_>, e: &Expr<'_>) {
if let ExprKind::If(cond, cond_inner, Some(els)) = e.kind
&& let ExprKind::DropTemps(cond) = cond.kind
&& let ExprKind::Block(..) = els.kind
{
let (msg, help) = match cond.kind {

View file

@ -41,8 +41,7 @@ declare_lint_pass!(ImplicitSaturatingAdd => [IMPLICIT_SATURATING_ADD]);
impl<'tcx> LateLintPass<'tcx> for ImplicitSaturatingAdd {
fn check_expr(&mut self, cx: &LateContext<'tcx>, expr: &'tcx Expr<'tcx>) {
if let ExprKind::If(cond, then, None) = expr.kind
&& let ExprKind::DropTemps(expr1) = cond.kind
&& let Some((c, op_node, l)) = get_const(cx, expr1)
&& let Some((c, op_node, l)) = get_const(cx, cond)
&& let BinOpKind::Ne | BinOpKind::Lt = op_node
&& let ExprKind::Block(block, None) = then.kind
&& let Block {
@ -66,7 +65,7 @@ impl<'tcx> LateLintPass<'tcx> for ImplicitSaturatingAdd {
&& Some(c) == get_int_max(ty)
&& let ctxt = expr.span.ctxt()
&& ex.span.ctxt() == ctxt
&& expr1.span.ctxt() == ctxt
&& cond.span.ctxt() == ctxt
&& clippy_utils::SpanlessEq::new(cx).eq_expr(l, target)
&& AssignOpKind::AddAssign == op1.node
&& let ExprKind::Lit(lit) = value.kind

View file

@ -62,15 +62,8 @@ impl<'tcx> LateLintPass<'tcx> for LetIfSeq {
if let hir::StmtKind::Let(local) = stmt.kind
&& let hir::PatKind::Binding(mode, canonical_id, ident, None) = local.pat.kind
&& let hir::StmtKind::Expr(if_) = next.kind
&& let hir::ExprKind::If(
hir::Expr {
kind: hir::ExprKind::DropTemps(cond),
..
},
then,
else_,
) = if_.kind
&& !is_local_used(cx, *cond, canonical_id)
&& let hir::ExprKind::If(cond, then, else_) = if_.kind
&& !is_local_used(cx, cond, canonical_id)
&& let hir::ExprKind::Block(then, _) = then.kind
&& let Some(value) = check_assign(cx, canonical_id, then)
&& !is_local_used(cx, value, canonical_id)

View file

@ -54,7 +54,7 @@ impl<'tcx> ForLoop<'tcx> {
}
}
/// An `if` expression without `DropTemps`
/// An `if` expression without `let`
pub struct If<'hir> {
/// `if` condition
pub cond: &'hir Expr<'hir>,
@ -66,16 +66,10 @@ pub struct If<'hir> {
impl<'hir> If<'hir> {
#[inline]
/// Parses an `if` expression
/// Parses an `if` expression without `let`
pub const fn hir(expr: &Expr<'hir>) -> Option<Self> {
if let ExprKind::If(
Expr {
kind: ExprKind::DropTemps(cond),
..
},
then,
r#else,
) = expr.kind
if let ExprKind::If(cond, then, r#else) = expr.kind
&& !has_let_expr(cond)
{
Some(Self { cond, then, r#else })
} else {
@ -198,18 +192,10 @@ impl<'hir> IfOrIfLet<'hir> {
/// Parses an `if` or `if let` expression
pub const fn hir(expr: &Expr<'hir>) -> Option<Self> {
if let ExprKind::If(cond, then, r#else) = expr.kind {
if let ExprKind::DropTemps(new_cond) = cond.kind {
return Some(Self {
cond: new_cond,
then,
r#else,
});
}
if let ExprKind::Let(..) = cond.kind {
return Some(Self { cond, then, r#else });
}
Some(Self { cond, then, r#else })
} else {
None
}
None
}
}
@ -343,15 +329,7 @@ impl<'hir> While<'hir> {
Block {
expr:
Some(Expr {
kind:
ExprKind::If(
Expr {
kind: ExprKind::DropTemps(condition),
..
},
body,
_,
),
kind: ExprKind::If(condition, body, _),
..
}),
..
@ -360,6 +338,7 @@ impl<'hir> While<'hir> {
LoopSource::While,
span,
) = expr.kind
&& !has_let_expr(condition)
{
return Some(Self { condition, body, span });
}
@ -493,3 +472,13 @@ pub fn get_vec_init_kind<'tcx>(cx: &LateContext<'tcx>, expr: &'tcx Expr<'tcx>) -
}
None
}
/// Checks that a condition doesn't have a `let` expression, to keep `If` and `While` from accepting
/// `if let` and `while let`.
pub const fn has_let_expr<'tcx>(cond: &'tcx Expr<'tcx>) -> bool {
match &cond.kind {
ExprKind::Let(_) => true,
ExprKind::Binary(_, lhs, rhs) => has_let_expr(lhs) || has_let_expr(rhs),
_ => false,
}
}

View file

@ -1,8 +1,7 @@
if let StmtKind::Let(local) = stmt.kind
&& let Some(init) = local.init
&& let ExprKind::If(cond, then, Some(else_expr)) = init.kind
&& let ExprKind::DropTemps(expr) = cond.kind
&& let ExprKind::Lit(ref lit) = expr.kind
&& let ExprKind::Lit(ref lit) = cond.kind
&& let LitKind::Bool(true) = lit.node
&& let ExprKind::Block(block, None) = then.kind
&& block.stmts.len() == 1

View file

@ -2,8 +2,7 @@ if let ExprKind::Struct(qpath, fields, None) = expr.kind
&& fields.len() == 1
&& fields[0].ident.as_str() == "field"
&& let ExprKind::If(cond, then, Some(else_expr)) = fields[0].expr.kind
&& let ExprKind::DropTemps(expr1) = cond.kind
&& let ExprKind::Lit(ref lit) = expr1.kind
&& let ExprKind::Lit(ref lit) = cond.kind
&& let LitKind::Bool(true) = lit.node
&& let ExprKind::Block(block, None) = then.kind
&& block.stmts.is_empty()