Skip to content

Commit e2120a7

Browse files
coalesce lint suggestions that can intersect
1 parent f93df1f commit e2120a7

8 files changed

+568
-157
lines changed

compiler/rustc_lint/messages.ftl

+3-1
Original file line numberDiff line numberDiff line change
@@ -337,7 +337,9 @@ lint_identifier_uncommon_codepoints = identifier contains {$codepoints_len ->
337337
lint_if_let_rescope = `if let` assigns a shorter lifetime since Edition 2024
338338
.label = this value has a significant drop implementation which may observe a major change in drop order and requires your discretion
339339
.help = the value is now dropped here in Edition 2024
340-
.suggestion = rewrite this `if let` into a `match` with a single arm to preserve the drop order up to Edition 2021
340+
341+
lint_if_let_rescope_suggestion = a `match` with a single arm can preserve the drop order up to Edition 2021
342+
.suggestion = rewrite this `if let` into `match`
341343
342344
lint_ignored_unless_crate_specified = {$level}({$name}) is ignored unless specified at crate level
343345

compiler/rustc_lint/src/if_let_rescope.rs

+211-108
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,16 @@
1+
use std::iter::repeat;
12
use std::ops::ControlFlow;
23

34
use hir::intravisit::Visitor;
45
use rustc_ast::Recovered;
5-
use rustc_hir as hir;
6+
use rustc_errors::{
7+
Applicability, Diag, EmissionGuarantee, SubdiagMessageOp, Subdiagnostic, SuggestionStyle,
8+
};
9+
use rustc_hir::{self as hir, HirIdSet};
610
use rustc_macros::{LintDiagnostic, Subdiagnostic};
7-
use rustc_session::lint::FutureIncompatibilityReason;
8-
use rustc_session::{declare_lint, declare_lint_pass};
11+
use rustc_middle::ty::TyCtxt;
12+
use rustc_session::lint::{FutureIncompatibilityReason, Level};
13+
use rustc_session::{declare_lint, impl_lint_pass};
914
use rustc_span::edition::Edition;
1015
use rustc_span::Span;
1116

@@ -84,138 +89,236 @@ declare_lint! {
8489
};
8590
}
8691

87-
declare_lint_pass!(
88-
/// Lint for potential change in program semantics of `if let`s
89-
IfLetRescope => [IF_LET_RESCOPE]
90-
);
92+
/// Lint for potential change in program semantics of `if let`s
93+
#[derive(Default)]
94+
pub(crate) struct IfLetRescope {
95+
skip: HirIdSet,
96+
}
9197

92-
impl<'tcx> LateLintPass<'tcx> for IfLetRescope {
93-
fn check_expr(&mut self, cx: &LateContext<'tcx>, expr: &'tcx hir::Expr<'tcx>) {
94-
if !expr.span.edition().at_least_rust_2021() || !cx.tcx.features().if_let_rescope {
98+
fn 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+
106+
fn expr_parent_is_stmt(tcx: TyCtxt<'_>, hir_id: hir::HirId) -> bool {
107+
let Some((_, hir::Node::Stmt(stmt))) = tcx.hir().parent_iter(hir_id).next() else {
108+
return false;
109+
};
110+
let (hir::StmtKind::Semi(expr) | hir::StmtKind::Expr(expr)) = stmt.kind else { return false };
111+
expr.hir_id == hir_id
112+
}
113+
114+
fn match_head_needs_bracket(tcx: TyCtxt<'_>, expr: &hir::Expr<'_>) -> bool {
115+
expr_parent_is_else(tcx, expr.hir_id) && matches!(expr.kind, hir::ExprKind::If(..))
116+
}
117+
118+
impl IfLetRescope {
119+
fn probe_if_cascade<'tcx>(&mut self, cx: &LateContext<'tcx>, mut expr: &'tcx hir::Expr<'tcx>) {
120+
if self.skip.contains(&expr.hir_id) {
95121
return;
96122
}
97-
let hir::ExprKind::If(cond, conseq, alt) = expr.kind else { return };
98-
let hir::ExprKind::Let(&hir::LetExpr {
99-
span,
100-
pat,
101-
init,
102-
ty: ty_ascription,
103-
recovered: Recovered::No,
104-
}) = cond.kind
105-
else {
106-
return;
107-
};
108-
let source_map = cx.tcx.sess.source_map();
123+
let tcx = cx.tcx;
124+
let source_map = tcx.sess.source_map();
109125
let expr_end = expr.span.shrink_to_hi();
110-
let if_let_pat = expr.span.shrink_to_lo().between(init.span);
111-
let before_conseq = conseq.span.shrink_to_lo();
112-
let lifetime_end = source_map.end_point(conseq.span);
126+
let mut add_bracket_to_match_head = match_head_needs_bracket(tcx, expr);
127+
let mut closing_brackets = 0;
128+
let mut alt_heads = vec![];
129+
let mut match_heads = vec![];
130+
let mut consequent_heads = vec![];
131+
let mut first_if_to_rewrite = None;
132+
let mut empty_alt = false;
133+
while let hir::ExprKind::If(cond, conseq, alt) = expr.kind {
134+
self.skip.insert(expr.hir_id);
135+
let hir::ExprKind::Let(&hir::LetExpr {
136+
span,
137+
pat,
138+
init,
139+
ty: ty_ascription,
140+
recovered: Recovered::No,
141+
}) = cond.kind
142+
else {
143+
if let Some(alt) = alt {
144+
add_bracket_to_match_head = matches!(alt.kind, hir::ExprKind::If(..));
145+
expr = alt;
146+
continue;
147+
} else {
148+
// finalize and emit span
149+
break;
150+
}
151+
};
152+
let if_let_pat = expr.span.shrink_to_lo().between(init.span);
153+
// the consequent fragment is always a block
154+
let before_conseq = conseq.span.shrink_to_lo();
155+
let lifetime_end = source_map.end_point(conseq.span);
113156

114-
if let ControlFlow::Break(significant_dropper) =
115-
(FindSignificantDropper { cx }).visit_expr(init)
116-
{
117-
let lint_without_suggestion = || {
118-
cx.tcx.emit_node_span_lint(
157+
if let ControlFlow::Break(significant_dropper) =
158+
(FindSignificantDropper { cx }).visit_expr(init)
159+
{
160+
tcx.emit_node_span_lint(
119161
IF_LET_RESCOPE,
120162
expr.hir_id,
121163
span,
122-
IfLetRescopeRewrite { significant_dropper, lifetime_end, sugg: None },
123-
)
124-
};
125-
if ty_ascription.is_some()
126-
|| !expr.span.can_be_used_for_suggestions()
127-
|| !pat.span.can_be_used_for_suggestions()
128-
{
129-
// Our `match` rewrites does not support type ascription,
130-
// so we just bail.
131-
// Alternatively when the span comes from proc macro expansion,
132-
// we will also bail.
133-
// FIXME(#101728): change this when type ascription syntax is stabilized again
134-
lint_without_suggestion();
135-
} else {
136-
let Ok(pat) = source_map.span_to_snippet(pat.span) else {
137-
lint_without_suggestion();
138-
return;
139-
};
140-
if let Some(alt) = alt {
141-
let alt_start = conseq.span.between(alt.span);
142-
if !alt_start.can_be_used_for_suggestions() {
143-
lint_without_suggestion();
144-
return;
164+
IfLetRescopeLint { significant_dropper, lifetime_end },
165+
);
166+
if ty_ascription.is_some()
167+
|| !expr.span.can_be_used_for_suggestions()
168+
|| !pat.span.can_be_used_for_suggestions()
169+
{
170+
// Our `match` rewrites does not support type ascription,
171+
// so we just bail.
172+
// Alternatively when the span comes from proc macro expansion,
173+
// we will also bail.
174+
// FIXME(#101728): change this when type ascription syntax is stabilized again
175+
} else if let Ok(pat) = source_map.span_to_snippet(pat.span) {
176+
let emit_suggestion = || {
177+
first_if_to_rewrite =
178+
first_if_to_rewrite.or_else(|| Some((expr.span, expr.hir_id)));
179+
if add_bracket_to_match_head {
180+
closing_brackets += 2;
181+
match_heads.push(SingleArmMatchBegin::WithOpenBracket(if_let_pat));
182+
} else {
183+
// It has to be a block
184+
closing_brackets += 1;
185+
match_heads.push(SingleArmMatchBegin::WithoutOpenBracket(if_let_pat));
186+
}
187+
consequent_heads.push(ConsequentRewrite { span: before_conseq, pat });
188+
};
189+
if let Some(alt) = alt {
190+
let alt_head = conseq.span.between(alt.span);
191+
if alt_head.can_be_used_for_suggestions() {
192+
// lint
193+
emit_suggestion();
194+
alt_heads.push(AltHead(alt_head));
195+
}
196+
} else {
197+
emit_suggestion();
198+
empty_alt = true;
199+
break;
145200
}
146-
cx.tcx.emit_node_span_lint(
147-
IF_LET_RESCOPE,
148-
expr.hir_id,
149-
span,
150-
IfLetRescopeRewrite {
151-
significant_dropper,
152-
lifetime_end,
153-
sugg: Some(IfLetRescopeRewriteSuggestion::WithElse {
154-
if_let_pat,
155-
before_conseq,
156-
pat,
157-
expr_end,
158-
alt_start,
159-
}),
160-
},
161-
);
162-
} else {
163-
cx.tcx.emit_node_span_lint(
164-
IF_LET_RESCOPE,
165-
expr.hir_id,
166-
span,
167-
IfLetRescopeRewrite {
168-
significant_dropper,
169-
lifetime_end,
170-
sugg: Some(IfLetRescopeRewriteSuggestion::WithoutElse {
171-
if_let_pat,
172-
before_conseq,
173-
pat,
174-
expr_end,
175-
}),
176-
},
177-
);
178201
}
179202
}
203+
if let Some(alt) = alt {
204+
add_bracket_to_match_head = matches!(alt.kind, hir::ExprKind::If(..));
205+
expr = alt;
206+
} else {
207+
break;
208+
}
209+
}
210+
if let Some((span, hir_id)) = first_if_to_rewrite {
211+
tcx.emit_node_span_lint(
212+
IF_LET_RESCOPE,
213+
hir_id,
214+
span,
215+
IfLetRescopeRewrite {
216+
match_heads,
217+
consequent_heads,
218+
closing_brackets: ClosingBrackets {
219+
span: expr_end,
220+
count: closing_brackets,
221+
empty_alt,
222+
},
223+
alt_heads,
224+
},
225+
);
180226
}
181227
}
182228
}
183229

230+
impl_lint_pass!(
231+
IfLetRescope => [IF_LET_RESCOPE]
232+
);
233+
234+
impl<'tcx> LateLintPass<'tcx> for IfLetRescope {
235+
fn check_expr(&mut self, cx: &LateContext<'tcx>, expr: &'tcx hir::Expr<'tcx>) {
236+
if expr.span.edition().at_least_rust_2024() || !cx.tcx.features().if_let_rescope {
237+
return;
238+
}
239+
if let (Level::Allow, _) = cx.tcx.lint_level_at_node(IF_LET_RESCOPE, expr.hir_id) {
240+
return;
241+
}
242+
if expr_parent_is_stmt(cx.tcx, expr.hir_id)
243+
&& matches!(expr.kind, hir::ExprKind::If(_cond, _conseq, None))
244+
{
245+
// `if let` statement without an `else` branch has no observable change
246+
// so we can skip linting it
247+
return;
248+
}
249+
self.probe_if_cascade(cx, expr);
250+
}
251+
}
252+
184253
#[derive(LintDiagnostic)]
185254
#[diag(lint_if_let_rescope)]
186-
struct IfLetRescopeRewrite {
255+
struct IfLetRescopeLint {
187256
#[label]
188257
significant_dropper: Span,
189258
#[help]
190259
lifetime_end: Span,
260+
}
261+
262+
#[derive(LintDiagnostic)]
263+
#[diag(lint_if_let_rescope_suggestion)]
264+
struct IfLetRescopeRewrite {
265+
#[subdiagnostic]
266+
match_heads: Vec<SingleArmMatchBegin>,
267+
#[subdiagnostic]
268+
consequent_heads: Vec<ConsequentRewrite>,
191269
#[subdiagnostic]
192-
sugg: Option<IfLetRescopeRewriteSuggestion>,
270+
closing_brackets: ClosingBrackets,
271+
#[subdiagnostic]
272+
alt_heads: Vec<AltHead>,
273+
}
274+
275+
#[derive(Subdiagnostic)]
276+
#[multipart_suggestion(lint_suggestion, applicability = "machine-applicable")]
277+
struct AltHead(#[suggestion_part(code = " _ => ")] Span);
278+
279+
#[derive(Subdiagnostic)]
280+
#[multipart_suggestion(lint_suggestion, applicability = "machine-applicable")]
281+
struct ConsequentRewrite {
282+
#[suggestion_part(code = "{{ {pat} => ")]
283+
span: Span,
284+
pat: String,
285+
}
286+
287+
struct ClosingBrackets {
288+
span: Span,
289+
count: usize,
290+
empty_alt: bool,
291+
}
292+
293+
impl Subdiagnostic for ClosingBrackets {
294+
fn add_to_diag_with<G: EmissionGuarantee, F: SubdiagMessageOp<G>>(
295+
self,
296+
diag: &mut Diag<'_, G>,
297+
f: &F,
298+
) {
299+
let code: String = self
300+
.empty_alt
301+
.then_some(" _ => {}".chars())
302+
.into_iter()
303+
.flatten()
304+
.chain(repeat('}').take(self.count))
305+
.collect();
306+
let msg = f(diag, crate::fluent_generated::lint_suggestion.into());
307+
diag.multipart_suggestion_with_style(
308+
msg,
309+
vec![(self.span, code)],
310+
Applicability::MachineApplicable,
311+
SuggestionStyle::ShowCode,
312+
);
313+
}
193314
}
194315

195316
#[derive(Subdiagnostic)]
196-
enum IfLetRescopeRewriteSuggestion {
317+
enum SingleArmMatchBegin {
197318
#[multipart_suggestion(lint_suggestion, applicability = "machine-applicable")]
198-
WithElse {
199-
#[suggestion_part(code = "match ")]
200-
if_let_pat: Span,
201-
#[suggestion_part(code = " {{ {pat} => ")]
202-
before_conseq: Span,
203-
pat: String,
204-
#[suggestion_part(code = "}}")]
205-
expr_end: Span,
206-
#[suggestion_part(code = " _ => ")]
207-
alt_start: Span,
208-
},
319+
WithOpenBracket(#[suggestion_part(code = "{{ match ")] Span),
209320
#[multipart_suggestion(lint_suggestion, applicability = "machine-applicable")]
210-
WithoutElse {
211-
#[suggestion_part(code = "match ")]
212-
if_let_pat: Span,
213-
#[suggestion_part(code = " {{ {pat} => ")]
214-
before_conseq: Span,
215-
pat: String,
216-
#[suggestion_part(code = " _ => {{}} }}")]
217-
expr_end: Span,
218-
},
321+
WithoutOpenBracket(#[suggestion_part(code = "match ")] Span),
219322
}
220323

221324
struct FindSignificantDropper<'tcx, 'a> {

compiler/rustc_lint/src/lib.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -245,7 +245,7 @@ late_lint_methods!(
245245
NonLocalDefinitions: NonLocalDefinitions::default(),
246246
ImplTraitOvercaptures: ImplTraitOvercaptures,
247247
TailExprDropOrder: TailExprDropOrder,
248-
IfLetRescope: IfLetRescope,
248+
IfLetRescope: IfLetRescope::default(),
249249
]
250250
]
251251
);

tests/ui/drop/lint-if-let-rescope-gated.rs

+4-1
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,9 @@ impl Droppy {
2626
fn main() {
2727
if let Some(_value) = Droppy.get() {
2828
//[with_feature_gate]~^ ERROR: `if let` assigns a shorter lifetime since Edition 2024
29+
//[with_feature_gate]~| ERROR: a `match` with a single arm can preserve the drop order up to Edition 2021
2930
//[with_feature_gate]~| WARN: this changes meaning in Rust 2024
30-
};
31+
//[with_feature_gate]~| WARN: this changes meaning in Rust 2024
32+
} else {
33+
}
3134
}

0 commit comments

Comments
 (0)