1use std::iter::repeat;
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::{FutureIncompatibilityReason, LintId};
15use rustc_session::{declare_lint, impl_lint_pass};
16use rustc_span::edition::Edition;
17use rustc_span::{DUMMY_SP, Span};
18use smallvec::SmallVec;
19
20use crate::{LateContext, LateLintPass};
21
22declare_lint! {
23 pub IF_LET_RESCOPE,
85 Allow,
86 "`if let` assigns a shorter lifetime to temporary values being pattern-matched against in Edition 2024 and \
87 rewriting in `match` is an option to preserve the semantics up to Edition 2021",
88 @future_incompatible = FutureIncompatibleInfo {
89 reason: FutureIncompatibilityReason::EditionSemanticsChange(Edition::Edition2024),
90 reference: "<https://doc.rust-lang.org/nightly/edition-guide/rust-2024/temporary-if-let-scope.html>",
91 };
92}
93
94#[derive(Default)]
96pub(crate) struct IfLetRescope {
97 skip: HirIdSet,
98}
99
100fn expr_parent_is_else(tcx: TyCtxt<'_>, hir_id: hir::HirId) -> bool {
101 let Some((_, hir::Node::Expr(expr))) = tcx.hir_parent_iter(hir_id).next() else {
102 return false;
103 };
104 let hir::ExprKind::If(_cond, _conseq, Some(alt)) = expr.kind else { return false };
105 alt.hir_id == hir_id
106}
107
108fn expr_parent_is_stmt(tcx: TyCtxt<'_>, hir_id: hir::HirId) -> bool {
109 let mut parents = tcx.hir_parent_iter(hir_id);
110 let stmt = match parents.next() {
111 Some((_, hir::Node::Stmt(stmt))) => stmt,
112 Some((_, hir::Node::Block(_) | hir::Node::Arm(_))) => return true,
113 _ => return false,
114 };
115 let (hir::StmtKind::Semi(expr) | hir::StmtKind::Expr(expr)) = stmt.kind else { return false };
116 expr.hir_id == hir_id
117}
118
119fn match_head_needs_bracket(tcx: TyCtxt<'_>, expr: &hir::Expr<'_>) -> bool {
120 expr_parent_is_else(tcx, expr.hir_id) && matches!(expr.kind, hir::ExprKind::If(..))
121}
122
123impl IfLetRescope {
124 fn probe_if_cascade<'tcx>(&mut self, cx: &LateContext<'tcx>, mut expr: &'tcx hir::Expr<'tcx>) {
125 if self.skip.contains(&expr.hir_id) {
126 return;
127 }
128 let tcx = cx.tcx;
129 let source_map = tcx.sess.source_map();
130 let expr_end = match expr.kind {
131 hir::ExprKind::If(_cond, conseq, None) => conseq.span.shrink_to_hi(),
132 hir::ExprKind::If(_cond, _conseq, Some(alt)) => alt.span.shrink_to_hi(),
133 _ => return,
134 };
135 let mut seen_dyn = false;
136 let mut add_bracket_to_match_head = match_head_needs_bracket(tcx, expr);
137 let mut significant_droppers = vec![];
138 let mut lifetime_ends = vec![];
139 let mut closing_brackets = 0;
140 let mut alt_heads = vec![];
141 let mut match_heads = vec![];
142 let mut consequent_heads = vec![];
143 let mut destructors = vec![];
144 let mut first_if_to_lint = None;
145 let mut first_if_to_rewrite = false;
146 let mut empty_alt = false;
147 while let hir::ExprKind::If(cond, conseq, alt) = expr.kind {
148 self.skip.insert(expr.hir_id);
149 if let hir::ExprKind::Let(&hir::LetExpr {
152 span,
153 pat,
154 init,
155 ty: ty_ascription,
156 recovered: Recovered::No,
157 }) = cond.kind
158 {
159 let if_let_pat = source_map
161 .span_take_while(expr.span, |&ch| ch == '(' || ch.is_whitespace())
162 .between(init.span);
163 let before_conseq = conseq.span.shrink_to_lo();
165 let lifetime_end = source_map.end_point(conseq.span);
166
167 if let ControlFlow::Break((drop_span, drop_tys)) =
168 (FindSignificantDropper { cx }).check_if_let_scrutinee(init)
169 {
170 destructors.extend(drop_tys.into_iter().filter_map(|ty| {
171 if let Some(span) = ty_dtor_span(tcx, ty) {
172 Some(DestructorLabel { span, dtor_kind: "concrete" })
173 } else if matches!(ty.kind(), ty::Dynamic(..)) {
174 if seen_dyn {
175 None
176 } else {
177 seen_dyn = true;
178 Some(DestructorLabel { span: DUMMY_SP, dtor_kind: "dyn" })
179 }
180 } else {
181 None
182 }
183 }));
184 first_if_to_lint = first_if_to_lint.or_else(|| Some((span, expr.hir_id)));
185 significant_droppers.push(drop_span);
186 lifetime_ends.push(lifetime_end);
187 if ty_ascription.is_some()
188 || !expr.span.can_be_used_for_suggestions()
189 || !pat.span.can_be_used_for_suggestions()
190 || !if_let_pat.can_be_used_for_suggestions()
191 || !before_conseq.can_be_used_for_suggestions()
192 {
193 } else if let Ok(pat) = source_map.span_to_snippet(pat.span) {
199 let emit_suggestion = |alt_span| {
200 first_if_to_rewrite = true;
201 if add_bracket_to_match_head {
202 closing_brackets += 2;
203 match_heads.push(SingleArmMatchBegin::WithOpenBracket(if_let_pat));
204 } else {
205 closing_brackets += 1;
209 match_heads
210 .push(SingleArmMatchBegin::WithoutOpenBracket(if_let_pat));
211 }
212 consequent_heads.push(ConsequentRewrite { span: before_conseq, pat });
213 if let Some(alt_span) = alt_span {
214 alt_heads.push(AltHead(alt_span));
215 }
216 };
217 if let Some(alt) = alt {
218 let alt_head = conseq.span.between(alt.span);
219 if alt_head.can_be_used_for_suggestions() {
220 emit_suggestion(Some(alt_head));
222 }
223 } else {
224 emit_suggestion(None);
227 empty_alt = true;
228 break;
229 }
230 }
231 }
232 }
233 add_bracket_to_match_head = true;
236 if let Some(alt) = alt {
237 expr = alt;
238 } else {
239 break;
240 }
241 }
242 if let Some((span, hir_id)) = first_if_to_lint {
243 tcx.emit_node_span_lint(
244 IF_LET_RESCOPE,
245 hir_id,
246 span,
247 IfLetRescopeLint {
248 destructors,
249 significant_droppers,
250 lifetime_ends,
251 rewrite: first_if_to_rewrite.then_some(IfLetRescopeRewrite {
252 match_heads,
253 consequent_heads,
254 closing_brackets: ClosingBrackets {
255 span: expr_end,
256 count: closing_brackets,
257 empty_alt,
258 },
259 alt_heads,
260 }),
261 },
262 );
263 }
264 }
265}
266
267impl_lint_pass!(
268 IfLetRescope => [IF_LET_RESCOPE]
269);
270
271impl<'tcx> LateLintPass<'tcx> for IfLetRescope {
272 fn check_expr(&mut self, cx: &LateContext<'tcx>, expr: &'tcx hir::Expr<'tcx>) {
273 if expr.span.edition().at_least_rust_2024()
274 || cx.tcx.lints_that_dont_need_to_run(()).contains(&LintId::of(IF_LET_RESCOPE))
275 {
276 return;
277 }
278
279 if let hir::ExprKind::Loop(block, _label, hir::LoopSource::While, _span) = expr.kind
280 && let Some(value) = block.expr
281 && let hir::ExprKind::If(cond, _conseq, _alt) = value.kind
282 && let hir::ExprKind::Let(..) = cond.kind
283 {
284 self.skip.insert(value.hir_id);
294 return;
295 }
296 if expr_parent_is_stmt(cx.tcx, expr.hir_id)
297 && matches!(expr.kind, hir::ExprKind::If(_cond, _conseq, None))
298 {
299 return;
302 }
303 self.probe_if_cascade(cx, expr);
304 }
305}
306
307#[derive(LintDiagnostic)]
308#[diag(lint_if_let_rescope)]
309struct IfLetRescopeLint {
310 #[subdiagnostic]
311 destructors: Vec<DestructorLabel>,
312 #[label]
313 significant_droppers: Vec<Span>,
314 #[help]
315 lifetime_ends: Vec<Span>,
316 #[subdiagnostic]
317 rewrite: Option<IfLetRescopeRewrite>,
318}
319
320struct IfLetRescopeRewrite {
321 match_heads: Vec<SingleArmMatchBegin>,
322 consequent_heads: Vec<ConsequentRewrite>,
323 closing_brackets: ClosingBrackets,
324 alt_heads: Vec<AltHead>,
325}
326
327impl Subdiagnostic for IfLetRescopeRewrite {
328 fn add_to_diag<G: EmissionGuarantee>(self, diag: &mut Diag<'_, G>) {
329 let mut suggestions = vec![];
330 for match_head in self.match_heads {
331 match match_head {
332 SingleArmMatchBegin::WithOpenBracket(span) => {
333 suggestions.push((span, "{ match ".into()))
334 }
335 SingleArmMatchBegin::WithoutOpenBracket(span) => {
336 suggestions.push((span, "match ".into()))
337 }
338 }
339 }
340 for ConsequentRewrite { span, pat } in self.consequent_heads {
341 suggestions.push((span, format!("{{ {pat} => ")));
342 }
343 for AltHead(span) in self.alt_heads {
344 suggestions.push((span, " _ => ".into()));
345 }
346 let closing_brackets = self.closing_brackets;
347 suggestions.push((
348 closing_brackets.span,
349 closing_brackets
350 .empty_alt
351 .then_some(" _ => {}".chars())
352 .into_iter()
353 .flatten()
354 .chain(repeat('}').take(closing_brackets.count))
355 .collect(),
356 ));
357 let msg = diag.eagerly_translate(crate::fluent_generated::lint_suggestion);
358 diag.multipart_suggestion_with_style(
359 msg,
360 suggestions,
361 Applicability::MachineApplicable,
362 SuggestionStyle::ShowCode,
363 );
364 }
365}
366
367#[derive(Subdiagnostic)]
368#[note(lint_if_let_dtor)]
369struct DestructorLabel {
370 #[primary_span]
371 span: Span,
372 dtor_kind: &'static str,
373}
374
375struct AltHead(Span);
376
377struct ConsequentRewrite {
378 span: Span,
379 pat: String,
380}
381
382struct ClosingBrackets {
383 span: Span,
384 count: usize,
385 empty_alt: bool,
386}
387enum SingleArmMatchBegin {
388 WithOpenBracket(Span),
389 WithoutOpenBracket(Span),
390}
391
392struct FindSignificantDropper<'a, 'tcx> {
393 cx: &'a LateContext<'tcx>,
394}
395
396impl<'tcx> FindSignificantDropper<'_, 'tcx> {
397 fn check_if_let_scrutinee(
403 &mut self,
404 init: &'tcx hir::Expr<'tcx>,
405 ) -> ControlFlow<(Span, SmallVec<[Ty<'tcx>; 4]>)> {
406 self.check_promoted_temp_with_drop(init)?;
407 self.visit_expr(init)
408 }
409
410 fn check_promoted_temp_with_drop(
417 &self,
418 expr: &'tcx hir::Expr<'tcx>,
419 ) -> ControlFlow<(Span, SmallVec<[Ty<'tcx>; 4]>)> {
420 if expr.is_place_expr(|base| {
421 self.cx
422 .typeck_results()
423 .adjustments()
424 .get(base.hir_id)
425 .is_some_and(|x| x.iter().any(|adj| matches!(adj.kind, Adjust::Deref(_))))
426 }) {
427 return ControlFlow::Continue(());
428 }
429
430 let drop_tys = extract_component_with_significant_dtor(
431 self.cx.tcx,
432 self.cx.typing_env(),
433 self.cx.typeck_results().expr_ty(expr),
434 );
435 if drop_tys.is_empty() {
436 return ControlFlow::Continue(());
437 }
438
439 ControlFlow::Break((expr.span, drop_tys))
440 }
441}
442
443impl<'tcx> Visitor<'tcx> for FindSignificantDropper<'_, 'tcx> {
444 type Result = ControlFlow<(Span, SmallVec<[Ty<'tcx>; 4]>)>;
445
446 fn visit_block(&mut self, b: &'tcx hir::Block<'tcx>) -> Self::Result {
447 if let Some(expr) = b.expr { self.visit_expr(expr) } else { ControlFlow::Continue(()) }
451 }
452
453 fn visit_expr(&mut self, expr: &'tcx hir::Expr<'tcx>) -> Self::Result {
454 for adj in self.cx.typeck_results().expr_adjustments(expr) {
458 match adj.kind {
459 Adjust::Deref(_) => break,
461 Adjust::Borrow(_) => {
462 self.check_promoted_temp_with_drop(expr)?;
463 }
464 _ => {}
465 }
466 }
467
468 match expr.kind {
469 hir::ExprKind::AddrOf(_, _, expr) => {
471 self.check_promoted_temp_with_drop(expr)?;
472 intravisit::walk_expr(self, expr)
473 }
474 hir::ExprKind::Index(expr, _, _) | hir::ExprKind::Field(expr, _) => {
479 self.check_promoted_temp_with_drop(expr)?;
480 intravisit::walk_expr(self, expr)
481 }
482 hir::ExprKind::If(..) => ControlFlow::Continue(()),
485 hir::ExprKind::Match(scrut, _, _) => self.visit_expr(scrut),
489 hir::ExprKind::DropTemps(_) => ControlFlow::Continue(()),
491 _ => intravisit::walk_expr(self, expr),
493 }
494 }
495}