1use std::iter::repeat;
2use std::ops::ControlFlow;
3
4use hir::intravisit::{self, Visitor};
5use rustc_ast::Recovered;
6use rustc_errors::{
7 Applicability, Diag, EmissionGuarantee, SubdiagMessageOp, Subdiagnostic, SuggestionStyle,
8};
9use rustc_hir::{self as hir, HirIdSet};
10use rustc_macros::LintDiagnostic;
11use rustc_middle::ty::TyCtxt;
12use rustc_middle::ty::adjustment::Adjust;
13use rustc_session::lint::{FutureIncompatibilityReason, LintId};
14use rustc_session::{declare_lint, impl_lint_pass};
15use rustc_span::Span;
16use rustc_span::edition::Edition;
17
18use crate::{LateContext, LateLintPass};
19
20declare_lint! {
21 pub IF_LET_RESCOPE,
83 Allow,
84 "`if let` assigns a shorter lifetime to temporary values being pattern-matched against in Edition 2024 and \
85 rewriting in `match` is an option to preserve the semantics up to Edition 2021",
86 @future_incompatible = FutureIncompatibleInfo {
87 reason: FutureIncompatibilityReason::EditionSemanticsChange(Edition::Edition2024),
88 reference: "<https://doc.rust-lang.org/nightly/edition-guide/rust-2024/temporary-if-let-scope.html>",
89 };
90}
91
92#[derive(Default)]
94pub(crate) struct IfLetRescope {
95 skip: HirIdSet,
96}
97
98fn expr_parent_is_else(tcx: TyCtxt<'_>, hir_id: hir::HirId) -> bool {
99 let Some((_, hir::Node::Expr(expr))) = tcx.hir().parent_iter(hir_id).next() else {
100 return false;
101 };
102 let hir::ExprKind::If(_cond, _conseq, Some(alt)) = expr.kind else { return false };
103 alt.hir_id == hir_id
104}
105
106fn expr_parent_is_stmt(tcx: TyCtxt<'_>, hir_id: hir::HirId) -> bool {
107 let mut parents = tcx.hir().parent_iter(hir_id);
108 let stmt = match parents.next() {
109 Some((_, hir::Node::Stmt(stmt))) => stmt,
110 Some((_, hir::Node::Block(_) | hir::Node::Arm(_))) => return true,
111 _ => return false,
112 };
113 let (hir::StmtKind::Semi(expr) | hir::StmtKind::Expr(expr)) = stmt.kind else { return false };
114 expr.hir_id == hir_id
115}
116
117fn match_head_needs_bracket(tcx: TyCtxt<'_>, expr: &hir::Expr<'_>) -> bool {
118 expr_parent_is_else(tcx, expr.hir_id) && matches!(expr.kind, hir::ExprKind::If(..))
119}
120
121impl IfLetRescope {
122 fn probe_if_cascade<'tcx>(&mut self, cx: &LateContext<'tcx>, mut expr: &'tcx hir::Expr<'tcx>) {
123 if self.skip.contains(&expr.hir_id) {
124 return;
125 }
126 let tcx = cx.tcx;
127 let source_map = tcx.sess.source_map();
128 let expr_end = match expr.kind {
129 hir::ExprKind::If(_cond, conseq, None) => conseq.span.shrink_to_hi(),
130 hir::ExprKind::If(_cond, _conseq, Some(alt)) => alt.span.shrink_to_hi(),
131 _ => return,
132 };
133 let mut add_bracket_to_match_head = match_head_needs_bracket(tcx, expr);
134 let mut significant_droppers = vec![];
135 let mut lifetime_ends = vec![];
136 let mut closing_brackets = 0;
137 let mut alt_heads = vec![];
138 let mut match_heads = vec![];
139 let mut consequent_heads = vec![];
140 let mut first_if_to_lint = None;
141 let mut first_if_to_rewrite = false;
142 let mut empty_alt = false;
143 while let hir::ExprKind::If(cond, conseq, alt) = expr.kind {
144 self.skip.insert(expr.hir_id);
145 if let hir::ExprKind::Let(&hir::LetExpr {
148 span,
149 pat,
150 init,
151 ty: ty_ascription,
152 recovered: Recovered::No,
153 }) = cond.kind
154 {
155 let if_let_pat = source_map
157 .span_take_while(expr.span, |&ch| ch == '(' || ch.is_whitespace())
158 .between(init.span);
159 let before_conseq = conseq.span.shrink_to_lo();
161 let lifetime_end = source_map.end_point(conseq.span);
162
163 if let ControlFlow::Break(significant_dropper) =
164 (FindSignificantDropper { cx }).check_if_let_scrutinee(init)
165 {
166 first_if_to_lint = first_if_to_lint.or_else(|| Some((span, expr.hir_id)));
167 significant_droppers.push(significant_dropper);
168 lifetime_ends.push(lifetime_end);
169 if ty_ascription.is_some()
170 || !expr.span.can_be_used_for_suggestions()
171 || !pat.span.can_be_used_for_suggestions()
172 || !if_let_pat.can_be_used_for_suggestions()
173 || !before_conseq.can_be_used_for_suggestions()
174 {
175 } else if let Ok(pat) = source_map.span_to_snippet(pat.span) {
181 let emit_suggestion = |alt_span| {
182 first_if_to_rewrite = true;
183 if add_bracket_to_match_head {
184 closing_brackets += 2;
185 match_heads.push(SingleArmMatchBegin::WithOpenBracket(if_let_pat));
186 } else {
187 closing_brackets += 1;
191 match_heads
192 .push(SingleArmMatchBegin::WithoutOpenBracket(if_let_pat));
193 }
194 consequent_heads.push(ConsequentRewrite { span: before_conseq, pat });
195 if let Some(alt_span) = alt_span {
196 alt_heads.push(AltHead(alt_span));
197 }
198 };
199 if let Some(alt) = alt {
200 let alt_head = conseq.span.between(alt.span);
201 if alt_head.can_be_used_for_suggestions() {
202 emit_suggestion(Some(alt_head));
204 }
205 } else {
206 emit_suggestion(None);
209 empty_alt = true;
210 break;
211 }
212 }
213 }
214 }
215 add_bracket_to_match_head = true;
218 if let Some(alt) = alt {
219 expr = alt;
220 } else {
221 break;
222 }
223 }
224 if let Some((span, hir_id)) = first_if_to_lint {
225 tcx.emit_node_span_lint(
226 IF_LET_RESCOPE,
227 hir_id,
228 span,
229 IfLetRescopeLint {
230 significant_droppers,
231 lifetime_ends,
232 rewrite: first_if_to_rewrite.then_some(IfLetRescopeRewrite {
233 match_heads,
234 consequent_heads,
235 closing_brackets: ClosingBrackets {
236 span: expr_end,
237 count: closing_brackets,
238 empty_alt,
239 },
240 alt_heads,
241 }),
242 },
243 );
244 }
245 }
246}
247
248impl_lint_pass!(
249 IfLetRescope => [IF_LET_RESCOPE]
250);
251
252impl<'tcx> LateLintPass<'tcx> for IfLetRescope {
253 fn check_expr(&mut self, cx: &LateContext<'tcx>, expr: &'tcx hir::Expr<'tcx>) {
254 if expr.span.edition().at_least_rust_2024()
255 || cx.tcx.lints_that_dont_need_to_run(()).contains(&LintId::of(IF_LET_RESCOPE))
256 {
257 return;
258 }
259
260 if let hir::ExprKind::Loop(block, _label, hir::LoopSource::While, _span) = expr.kind
261 && let Some(value) = block.expr
262 && let hir::ExprKind::If(cond, _conseq, _alt) = value.kind
263 && let hir::ExprKind::Let(..) = cond.kind
264 {
265 self.skip.insert(value.hir_id);
275 return;
276 }
277 if expr_parent_is_stmt(cx.tcx, expr.hir_id)
278 && matches!(expr.kind, hir::ExprKind::If(_cond, _conseq, None))
279 {
280 return;
283 }
284 self.probe_if_cascade(cx, expr);
285 }
286}
287
288#[derive(LintDiagnostic)]
289#[diag(lint_if_let_rescope)]
290struct IfLetRescopeLint {
291 #[label]
292 significant_droppers: Vec<Span>,
293 #[help]
294 lifetime_ends: Vec<Span>,
295 #[subdiagnostic]
296 rewrite: Option<IfLetRescopeRewrite>,
297}
298
299struct IfLetRescopeRewrite {
300 match_heads: Vec<SingleArmMatchBegin>,
301 consequent_heads: Vec<ConsequentRewrite>,
302 closing_brackets: ClosingBrackets,
303 alt_heads: Vec<AltHead>,
304}
305
306impl Subdiagnostic for IfLetRescopeRewrite {
307 fn add_to_diag_with<G: EmissionGuarantee, F: SubdiagMessageOp<G>>(
308 self,
309 diag: &mut Diag<'_, G>,
310 f: &F,
311 ) {
312 let mut suggestions = vec![];
313 for match_head in self.match_heads {
314 match match_head {
315 SingleArmMatchBegin::WithOpenBracket(span) => {
316 suggestions.push((span, "{ match ".into()))
317 }
318 SingleArmMatchBegin::WithoutOpenBracket(span) => {
319 suggestions.push((span, "match ".into()))
320 }
321 }
322 }
323 for ConsequentRewrite { span, pat } in self.consequent_heads {
324 suggestions.push((span, format!("{{ {pat} => ")));
325 }
326 for AltHead(span) in self.alt_heads {
327 suggestions.push((span, " _ => ".into()));
328 }
329 let closing_brackets = self.closing_brackets;
330 suggestions.push((
331 closing_brackets.span,
332 closing_brackets
333 .empty_alt
334 .then_some(" _ => {}".chars())
335 .into_iter()
336 .flatten()
337 .chain(repeat('}').take(closing_brackets.count))
338 .collect(),
339 ));
340 let msg = f(diag, crate::fluent_generated::lint_suggestion);
341 diag.multipart_suggestion_with_style(
342 msg,
343 suggestions,
344 Applicability::MachineApplicable,
345 SuggestionStyle::ShowCode,
346 );
347 }
348}
349
350struct AltHead(Span);
351
352struct ConsequentRewrite {
353 span: Span,
354 pat: String,
355}
356
357struct ClosingBrackets {
358 span: Span,
359 count: usize,
360 empty_alt: bool,
361}
362enum SingleArmMatchBegin {
363 WithOpenBracket(Span),
364 WithoutOpenBracket(Span),
365}
366
367struct FindSignificantDropper<'a, 'tcx> {
368 cx: &'a LateContext<'tcx>,
369}
370
371impl<'tcx> FindSignificantDropper<'_, 'tcx> {
372 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 }
381
382 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
396 .cx
397 .typeck_results()
398 .expr_ty(expr)
399 .has_significant_drop(self.cx.tcx, self.cx.typing_env())
400 {
401 ControlFlow::Break(expr.span)
402 } else {
403 ControlFlow::Continue(())
404 }
405 }
406}
407
408impl<'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 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 for adj in self.cx.typeck_results().expr_adjustments(expr) {
423 match adj.kind {
424 Adjust::Deref(_) => break,
426 Adjust::Borrow(_) => {
427 self.check_promoted_temp_with_drop(expr)?;
428 }
429 _ => {}
430 }
431 }
432
433 match expr.kind {
434 hir::ExprKind::AddrOf(_, _, expr) => {
436 self.check_promoted_temp_with_drop(expr)?;
437 intravisit::walk_expr(self, expr)
438 }
439 hir::ExprKind::Index(expr, _, _) | hir::ExprKind::Field(expr, _) => {
444 self.check_promoted_temp_with_drop(expr)?;
445 intravisit::walk_expr(self, expr)
446 }
447 hir::ExprKind::If(..) => ControlFlow::Continue(()),
450 hir::ExprKind::Match(scrut, _, _) => self.visit_expr(scrut),
454 hir::ExprKind::DropTemps(_) => ControlFlow::Continue(()),
456 _ => intravisit::walk_expr(self, expr),
458 }
459 }
460}