|
| 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,244 @@ 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 | + if let Some(alt) = alt { |
| 177 | + let alt_head = conseq.span.between(alt.span); |
| 178 | + if alt_head.can_be_used_for_suggestions() { |
| 179 | + // lint |
| 180 | + first_if_to_rewrite = |
| 181 | + first_if_to_rewrite.or_else(|| Some((expr.span, expr.hir_id))); |
| 182 | + if add_bracket_to_match_head { |
| 183 | + closing_brackets += 2; |
| 184 | + match_heads.push(SingleArmMatchBegin::WithOpenBracket(if_let_pat)); |
| 185 | + } else { |
| 186 | + // It has to be a block |
| 187 | + closing_brackets += 1; |
| 188 | + match_heads |
| 189 | + .push(SingleArmMatchBegin::WithoutOpenBracket(if_let_pat)); |
| 190 | + } |
| 191 | + consequent_heads.push(ConsequentRewrite { span: before_conseq, pat }); |
| 192 | + alt_heads.push(AltHead(alt_head)); |
| 193 | + } |
| 194 | + } else { |
| 195 | + first_if_to_rewrite = |
| 196 | + first_if_to_rewrite.or_else(|| Some((expr.span, expr.hir_id))); |
| 197 | + if add_bracket_to_match_head { |
| 198 | + closing_brackets += 2; |
| 199 | + match_heads.push(SingleArmMatchBegin::WithOpenBracket(if_let_pat)); |
| 200 | + } else { |
| 201 | + // It has to be a block |
| 202 | + closing_brackets += 1; |
| 203 | + match_heads.push(SingleArmMatchBegin::WithoutOpenBracket(if_let_pat)); |
| 204 | + } |
| 205 | + consequent_heads.push(ConsequentRewrite { span: before_conseq, pat }); |
| 206 | + empty_alt = true; |
| 207 | + break; |
145 | 208 | }
|
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 | 209 | }
|
179 | 210 | }
|
| 211 | + if let Some(alt) = alt { |
| 212 | + add_bracket_to_match_head = matches!(alt.kind, hir::ExprKind::If(..)); |
| 213 | + expr = alt; |
| 214 | + } else { |
| 215 | + break; |
| 216 | + } |
| 217 | + } |
| 218 | + if let Some((span, hir_id)) = first_if_to_rewrite { |
| 219 | + tcx.emit_node_span_lint( |
| 220 | + IF_LET_RESCOPE, |
| 221 | + hir_id, |
| 222 | + span, |
| 223 | + IfLetRescopeRewrite { |
| 224 | + match_heads, |
| 225 | + consequent_heads, |
| 226 | + closing_brackets: ClosingBrackets { |
| 227 | + span: expr_end, |
| 228 | + count: closing_brackets, |
| 229 | + empty_alt, |
| 230 | + }, |
| 231 | + alt_heads, |
| 232 | + }, |
| 233 | + ); |
180 | 234 | }
|
181 | 235 | }
|
182 | 236 | }
|
183 | 237 |
|
| 238 | +impl_lint_pass!( |
| 239 | + IfLetRescope => [IF_LET_RESCOPE] |
| 240 | +); |
| 241 | + |
| 242 | +impl<'tcx> LateLintPass<'tcx> for IfLetRescope { |
| 243 | + fn check_expr(&mut self, cx: &LateContext<'tcx>, expr: &'tcx hir::Expr<'tcx>) { |
| 244 | + if expr.span.edition().at_least_rust_2024() || !cx.tcx.features().if_let_rescope { |
| 245 | + return; |
| 246 | + } |
| 247 | + if let (Level::Allow, _) = cx.tcx.lint_level_at_node(IF_LET_RESCOPE, expr.hir_id) { |
| 248 | + return; |
| 249 | + } |
| 250 | + if expr_parent_is_stmt(cx.tcx, expr.hir_id) |
| 251 | + && matches!(expr.kind, hir::ExprKind::If(_cond, _conseq, None)) |
| 252 | + { |
| 253 | + // `if let` statement without an `else` branch has no observable change |
| 254 | + // so we can skip linting it |
| 255 | + return; |
| 256 | + } |
| 257 | + self.probe_if_cascade(cx, expr); |
| 258 | + } |
| 259 | +} |
| 260 | + |
184 | 261 | #[derive(LintDiagnostic)]
|
185 | 262 | #[diag(lint_if_let_rescope)]
|
186 |
| -struct IfLetRescopeRewrite { |
| 263 | +struct IfLetRescopeLint { |
187 | 264 | #[label]
|
188 | 265 | significant_dropper: Span,
|
189 | 266 | #[help]
|
190 | 267 | lifetime_end: Span,
|
| 268 | +} |
| 269 | + |
| 270 | +#[derive(LintDiagnostic)] |
| 271 | +#[diag(lint_if_let_rescope_suggestion)] |
| 272 | +struct IfLetRescopeRewrite { |
| 273 | + #[subdiagnostic] |
| 274 | + match_heads: Vec<SingleArmMatchBegin>, |
| 275 | + #[subdiagnostic] |
| 276 | + consequent_heads: Vec<ConsequentRewrite>, |
191 | 277 | #[subdiagnostic]
|
192 |
| - sugg: Option<IfLetRescopeRewriteSuggestion>, |
| 278 | + closing_brackets: ClosingBrackets, |
| 279 | + #[subdiagnostic] |
| 280 | + alt_heads: Vec<AltHead>, |
| 281 | +} |
| 282 | + |
| 283 | +#[derive(Subdiagnostic)] |
| 284 | +#[multipart_suggestion(lint_suggestion, applicability = "machine-applicable")] |
| 285 | +struct AltHead(#[suggestion_part(code = " _ => ")] Span); |
| 286 | + |
| 287 | +#[derive(Subdiagnostic)] |
| 288 | +#[multipart_suggestion(lint_suggestion, applicability = "machine-applicable")] |
| 289 | +struct ConsequentRewrite { |
| 290 | + #[suggestion_part(code = " {{ {pat} => ")] |
| 291 | + span: Span, |
| 292 | + pat: String, |
| 293 | +} |
| 294 | + |
| 295 | +struct ClosingBrackets { |
| 296 | + span: Span, |
| 297 | + count: usize, |
| 298 | + empty_alt: bool, |
| 299 | +} |
| 300 | + |
| 301 | +impl Subdiagnostic for ClosingBrackets { |
| 302 | + fn add_to_diag_with<G: EmissionGuarantee, F: SubdiagMessageOp<G>>( |
| 303 | + self, |
| 304 | + diag: &mut Diag<'_, G>, |
| 305 | + f: &F, |
| 306 | + ) { |
| 307 | + let code: String = self |
| 308 | + .empty_alt |
| 309 | + .then_some(" _ => {}".chars()) |
| 310 | + .into_iter() |
| 311 | + .flatten() |
| 312 | + .chain(repeat('}').take(self.count)) |
| 313 | + .collect(); |
| 314 | + let msg = f(diag, crate::fluent_generated::lint_suggestion.into()); |
| 315 | + diag.multipart_suggestion_with_style( |
| 316 | + msg, |
| 317 | + vec![(self.span, code)], |
| 318 | + Applicability::MachineApplicable, |
| 319 | + SuggestionStyle::ShowCode, |
| 320 | + ); |
| 321 | + } |
193 | 322 | }
|
194 | 323 |
|
195 | 324 | #[derive(Subdiagnostic)]
|
196 |
| -enum IfLetRescopeRewriteSuggestion { |
| 325 | +enum SingleArmMatchBegin { |
197 | 326 | #[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 |
| - }, |
| 327 | + WithOpenBracket(#[suggestion_part(code = "{{ match ")] Span), |
209 | 328 | #[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 |
| - }, |
| 329 | + WithoutOpenBracket(#[suggestion_part(code = " match ")] Span), |
219 | 330 | }
|
220 | 331 |
|
221 | 332 | struct FindSignificantDropper<'tcx, 'a> {
|
|
0 commit comments