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