1use std::iter::repeat_n;
2use std::ops::ControlFlow;
3
4use hir::intravisit::{self, Visitor};
5use rustc_ast::Recovered;
6use rustc_errors::{Applicability, Diag, EmissionGuarantee, Subdiagnostic, SuggestionStyle};
7use rustc_hir::{self as hir, HirIdSet};
8use rustc_macros::{LintDiagnostic, Subdiagnostic};
9use rustc_middle::ty::adjustment::Adjust;
10use rustc_middle::ty::significant_drop_order::{
11 extract_component_with_significant_dtor, ty_dtor_span,
12};
13use rustc_middle::ty::{self, Ty, TyCtxt};
14use rustc_session::lint::{LintId, fcw};
15use rustc_session::{declare_lint, impl_lint_pass};
16use rustc_span::{DUMMY_SP, Span};
17use smallvec::SmallVec;
18
19use crate::{LateContext, LateLintPass};
20
21declare_lint! {
22 pub IF_LET_RESCOPE,
84 Allow,
85 "`if let` assigns a shorter lifetime to temporary values being pattern-matched against in Edition 2024 and \
86 rewriting in `match` is an option to preserve the semantics up to Edition 2021",
87 @future_incompatible = FutureIncompatibleInfo {
88 reason: fcw!(EditionSemanticsChange 2024 "temporary-if-let-scope"),
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 seen_dyn = false;
134 let mut add_bracket_to_match_head = match_head_needs_bracket(tcx, expr);
135 let mut significant_droppers = vec![];
136 let mut lifetime_ends = vec![];
137 let mut closing_brackets = 0;
138 let mut alt_heads = vec![];
139 let mut match_heads = vec![];
140 let mut consequent_heads = vec![];
141 let mut destructors = vec![];
142 let mut first_if_to_lint = None;
143 let mut first_if_to_rewrite = false;
144 let mut empty_alt = false;
145 while let hir::ExprKind::If(cond, conseq, alt) = expr.kind {
146 self.skip.insert(expr.hir_id);
147 if let hir::ExprKind::Let(&hir::LetExpr {
150 span,
151 pat,
152 init,
153 ty: ty_ascription,
154 recovered: Recovered::No,
155 }) = cond.kind
156 {
157 let if_let_pat = source_map
159 .span_take_while(expr.span, |&ch| ch == '(' || ch.is_whitespace())
160 .between(init.span);
161 let before_conseq = conseq.span.shrink_to_lo();
163 let lifetime_end = source_map.end_point(conseq.span);
164
165 if let ControlFlow::Break((drop_span, drop_tys)) =
166 (FindSignificantDropper { cx }).check_if_let_scrutinee(init)
167 {
168 destructors.extend(drop_tys.into_iter().filter_map(|ty| {
169 if let Some(span) = ty_dtor_span(tcx, ty) {
170 Some(DestructorLabel { span, dtor_kind: "concrete" })
171 } else if matches!(ty.kind(), ty::Dynamic(..)) {
172 if seen_dyn {
173 None
174 } else {
175 seen_dyn = true;
176 Some(DestructorLabel { span: DUMMY_SP, dtor_kind: "dyn" })
177 }
178 } else {
179 None
180 }
181 }));
182 first_if_to_lint = first_if_to_lint.or_else(|| Some((span, expr.hir_id)));
183 significant_droppers.push(drop_span);
184 lifetime_ends.push(lifetime_end);
185 if ty_ascription.is_some()
186 || !expr.span.can_be_used_for_suggestions()
187 || !pat.span.can_be_used_for_suggestions()
188 || !if_let_pat.can_be_used_for_suggestions()
189 || !before_conseq.can_be_used_for_suggestions()
190 {
191 } else if let Ok(pat) = source_map.span_to_snippet(pat.span) {
197 let emit_suggestion = |alt_span| {
198 first_if_to_rewrite = true;
199 if add_bracket_to_match_head {
200 closing_brackets += 2;
201 match_heads.push(SingleArmMatchBegin::WithOpenBracket(if_let_pat));
202 } else {
203 closing_brackets += 1;
207 match_heads
208 .push(SingleArmMatchBegin::WithoutOpenBracket(if_let_pat));
209 }
210 consequent_heads.push(ConsequentRewrite { span: before_conseq, pat });
211 if let Some(alt_span) = alt_span {
212 alt_heads.push(AltHead(alt_span));
213 }
214 };
215 if let Some(alt) = alt {
216 let alt_head = conseq.span.between(alt.span);
217 if alt_head.can_be_used_for_suggestions() {
218 emit_suggestion(Some(alt_head));
220 }
221 } else {
222 emit_suggestion(None);
225 empty_alt = true;
226 break;
227 }
228 }
229 }
230 }
231 add_bracket_to_match_head = true;
234 if let Some(alt) = alt {
235 expr = alt;
236 } else {
237 break;
238 }
239 }
240 if let Some((span, hir_id)) = first_if_to_lint {
241 tcx.emit_node_span_lint(
242 IF_LET_RESCOPE,
243 hir_id,
244 span,
245 IfLetRescopeLint {
246 destructors,
247 significant_droppers,
248 lifetime_ends,
249 rewrite: first_if_to_rewrite.then_some(IfLetRescopeRewrite {
250 match_heads,
251 consequent_heads,
252 closing_brackets: ClosingBrackets {
253 span: expr_end,
254 count: closing_brackets,
255 empty_alt,
256 },
257 alt_heads,
258 }),
259 },
260 );
261 }
262 }
263}
264
265impl_lint_pass!(
266 IfLetRescope => [IF_LET_RESCOPE]
267);
268
269impl<'tcx> LateLintPass<'tcx> for IfLetRescope {
270 fn check_expr(&mut self, cx: &LateContext<'tcx>, expr: &'tcx hir::Expr<'tcx>) {
271 if expr.span.edition().at_least_rust_2024()
272 || cx.tcx.lints_that_dont_need_to_run(()).contains(&LintId::of(IF_LET_RESCOPE))
273 {
274 return;
275 }
276
277 if let hir::ExprKind::Loop(block, _label, hir::LoopSource::While, _span) = expr.kind
278 && let Some(value) = block.expr
279 && let hir::ExprKind::If(cond, _conseq, _alt) = value.kind
280 && let hir::ExprKind::Let(..) = cond.kind
281 {
282 self.skip.insert(value.hir_id);
292 return;
293 }
294 if expr_parent_is_stmt(cx.tcx, expr.hir_id)
295 && matches!(expr.kind, hir::ExprKind::If(_cond, _conseq, None))
296 {
297 return;
300 }
301 self.probe_if_cascade(cx, expr);
302 }
303}
304
305#[derive(LintDiagnostic)]
306#[diag(lint_if_let_rescope)]
307struct IfLetRescopeLint {
308 #[subdiagnostic]
309 destructors: Vec<DestructorLabel>,
310 #[label]
311 significant_droppers: Vec<Span>,
312 #[help]
313 lifetime_ends: Vec<Span>,
314 #[subdiagnostic]
315 rewrite: Option<IfLetRescopeRewrite>,
316}
317
318struct IfLetRescopeRewrite {
319 match_heads: Vec<SingleArmMatchBegin>,
320 consequent_heads: Vec<ConsequentRewrite>,
321 closing_brackets: ClosingBrackets,
322 alt_heads: Vec<AltHead>,
323}
324
325impl Subdiagnostic for IfLetRescopeRewrite {
326 fn add_to_diag<G: EmissionGuarantee>(self, diag: &mut Diag<'_, G>) {
327 let mut suggestions = vec![];
328 for match_head in self.match_heads {
329 match match_head {
330 SingleArmMatchBegin::WithOpenBracket(span) => {
331 suggestions.push((span, "{ match ".into()))
332 }
333 SingleArmMatchBegin::WithoutOpenBracket(span) => {
334 suggestions.push((span, "match ".into()))
335 }
336 }
337 }
338 for ConsequentRewrite { span, pat } in self.consequent_heads {
339 suggestions.push((span, format!("{{ {pat} => ")));
340 }
341 for AltHead(span) in self.alt_heads {
342 suggestions.push((span, " _ => ".into()));
343 }
344 let closing_brackets = self.closing_brackets;
345 suggestions.push((
346 closing_brackets.span,
347 closing_brackets
348 .empty_alt
349 .then_some(" _ => {}".chars())
350 .into_iter()
351 .flatten()
352 .chain(repeat_n('}', closing_brackets.count))
353 .collect(),
354 ));
355 let msg = diag.eagerly_translate(crate::fluent_generated::lint_suggestion);
356 diag.multipart_suggestion_with_style(
357 msg,
358 suggestions,
359 Applicability::MachineApplicable,
360 SuggestionStyle::ShowCode,
361 );
362 }
363}
364
365#[derive(Subdiagnostic)]
366#[note(lint_if_let_dtor)]
367struct DestructorLabel {
368 #[primary_span]
369 span: Span,
370 dtor_kind: &'static str,
371}
372
373struct AltHead(Span);
374
375struct ConsequentRewrite {
376 span: Span,
377 pat: String,
378}
379
380struct ClosingBrackets {
381 span: Span,
382 count: usize,
383 empty_alt: bool,
384}
385enum SingleArmMatchBegin {
386 WithOpenBracket(Span),
387 WithoutOpenBracket(Span),
388}
389
390struct FindSignificantDropper<'a, 'tcx> {
391 cx: &'a LateContext<'tcx>,
392}
393
394impl<'tcx> FindSignificantDropper<'_, 'tcx> {
395 fn check_if_let_scrutinee(
401 &mut self,
402 init: &'tcx hir::Expr<'tcx>,
403 ) -> ControlFlow<(Span, SmallVec<[Ty<'tcx>; 4]>)> {
404 self.check_promoted_temp_with_drop(init)?;
405 self.visit_expr(init)
406 }
407
408 fn check_promoted_temp_with_drop(
415 &self,
416 expr: &'tcx hir::Expr<'tcx>,
417 ) -> ControlFlow<(Span, SmallVec<[Ty<'tcx>; 4]>)> {
418 if expr.is_place_expr(|base| {
419 self.cx
420 .typeck_results()
421 .adjustments()
422 .get(base.hir_id)
423 .is_some_and(|x| x.iter().any(|adj| matches!(adj.kind, Adjust::Deref(_))))
424 }) {
425 return ControlFlow::Continue(());
426 }
427
428 let drop_tys = extract_component_with_significant_dtor(
429 self.cx.tcx,
430 self.cx.typing_env(),
431 self.cx.typeck_results().expr_ty(expr),
432 );
433 if drop_tys.is_empty() {
434 return ControlFlow::Continue(());
435 }
436
437 ControlFlow::Break((expr.span, drop_tys))
438 }
439}
440
441impl<'tcx> Visitor<'tcx> for FindSignificantDropper<'_, 'tcx> {
442 type Result = ControlFlow<(Span, SmallVec<[Ty<'tcx>; 4]>)>;
443
444 fn visit_block(&mut self, b: &'tcx hir::Block<'tcx>) -> Self::Result {
445 if let Some(expr) = b.expr { self.visit_expr(expr) } else { ControlFlow::Continue(()) }
449 }
450
451 fn visit_expr(&mut self, expr: &'tcx hir::Expr<'tcx>) -> Self::Result {
452 for adj in self.cx.typeck_results().expr_adjustments(expr) {
456 match adj.kind {
457 Adjust::Deref(_) => break,
459 Adjust::Borrow(_) => {
460 self.check_promoted_temp_with_drop(expr)?;
461 }
462 _ => {}
463 }
464 }
465
466 match expr.kind {
467 hir::ExprKind::AddrOf(_, _, expr) => {
469 self.check_promoted_temp_with_drop(expr)?;
470 intravisit::walk_expr(self, expr)
471 }
472 hir::ExprKind::Index(expr, _, _) | hir::ExprKind::Field(expr, _) => {
477 self.check_promoted_temp_with_drop(expr)?;
478 intravisit::walk_expr(self, expr)
479 }
480 hir::ExprKind::If(..) => ControlFlow::Continue(()),
483 hir::ExprKind::Match(scrut, _, _) => self.visit_expr(scrut),
487 hir::ExprKind::DropTemps(_) => ControlFlow::Continue(()),
489 _ => intravisit::walk_expr(self, expr),
491 }
492 }
493}