|
| 1 | +use std::iter::repeat; |
1 | 2 | use std::ops::ControlFlow; |
2 | 3 |
|
3 | 4 | use hir::intravisit::Visitor; |
4 | 5 | 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}; |
6 | 10 | 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}; |
9 | 14 | use rustc_span::edition::Edition; |
10 | 15 | use rustc_span::Span; |
11 | 16 |
|
@@ -84,138 +89,236 @@ declare_lint! { |
84 | 89 | }; |
85 | 90 | } |
86 | 91 |
|
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 | +} |
91 | 97 |
|
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) { |
95 | 121 | return; |
96 | 122 | } |
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(); |
109 | 125 | 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); |
113 | 156 |
|
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( |
119 | 161 | IF_LET_RESCOPE, |
120 | 162 | expr.hir_id, |
121 | 163 | 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; |
145 | 200 | } |
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 | | - ); |
178 | 201 | } |
179 | 202 | } |
| 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 | + ); |
180 | 226 | } |
181 | 227 | } |
182 | 228 | } |
183 | 229 |
|
| 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 | + |
184 | 253 | #[derive(LintDiagnostic)] |
185 | 254 | #[diag(lint_if_let_rescope)] |
186 | | -struct IfLetRescopeRewrite { |
| 255 | +struct IfLetRescopeLint { |
187 | 256 | #[label] |
188 | 257 | significant_dropper: Span, |
189 | 258 | #[help] |
190 | 259 | 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>, |
191 | 269 | #[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 | + } |
193 | 314 | } |
194 | 315 |
|
195 | 316 | #[derive(Subdiagnostic)] |
196 | | -enum IfLetRescopeRewriteSuggestion { |
| 317 | +enum SingleArmMatchBegin { |
197 | 318 | #[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), |
209 | 320 | #[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), |
219 | 322 | } |
220 | 323 |
|
221 | 324 | struct FindSignificantDropper<'tcx, 'a> { |
|
0 commit comments