1use rustc_data_structures::fx::FxIndexMap;
2use rustc_hir::intravisit::{self, Visitor};
3use rustc_hir::{self as hir, LifetimeSource};
4use rustc_session::{declare_lint, declare_lint_pass};
5use rustc_span::Span;
6use tracing::instrument;
7
8use crate::{LateContext, LateLintPass, LintContext, lints};
9
10declare_lint! {
11 pub MISMATCHED_LIFETIME_SYNTAXES,
70 Warn,
71 "detects when a lifetime uses different syntax between arguments and return values"
72}
73
74declare_lint_pass!(LifetimeSyntax => [MISMATCHED_LIFETIME_SYNTAXES]);
75
76impl<'tcx> LateLintPass<'tcx> for LifetimeSyntax {
77 #[instrument(skip_all)]
78 fn check_fn(
79 &mut self,
80 cx: &LateContext<'tcx>,
81 _: hir::intravisit::FnKind<'tcx>,
82 fd: &'tcx hir::FnDecl<'tcx>,
83 _: &'tcx hir::Body<'tcx>,
84 _: rustc_span::Span,
85 _: rustc_span::def_id::LocalDefId,
86 ) {
87 let mut input_map = Default::default();
88 let mut output_map = Default::default();
89
90 for input in fd.inputs {
91 LifetimeInfoCollector::collect(input, &mut input_map);
92 }
93
94 if let hir::FnRetTy::Return(output) = fd.output {
95 LifetimeInfoCollector::collect(output, &mut output_map);
96 }
97
98 report_mismatches(cx, &input_map, &output_map);
99 }
100}
101
102#[instrument(skip_all)]
103fn report_mismatches<'tcx>(
104 cx: &LateContext<'tcx>,
105 inputs: &LifetimeInfoMap<'tcx>,
106 outputs: &LifetimeInfoMap<'tcx>,
107) {
108 for (resolved_lifetime, output_info) in outputs {
109 if let Some(input_info) = inputs.get(resolved_lifetime) {
110 if !lifetimes_use_matched_syntax(input_info, output_info) {
111 emit_mismatch_diagnostic(cx, input_info, output_info);
112 }
113 }
114 }
115}
116
117fn lifetimes_use_matched_syntax(input_info: &[Info<'_>], output_info: &[Info<'_>]) -> bool {
118 let mut n_hidden = 0;
120 let mut n_elided = 0;
121 let mut n_named = 0;
122
123 for info in input_info.iter().chain(output_info) {
124 use LifetimeSource::*;
125 use hir::LifetimeSyntax::*;
126
127 let syntax_source = (info.lifetime.syntax, info.lifetime.source);
128
129 match syntax_source {
130 (_, Other) => continue,
132
133 (Implicit, Reference | OutlivesBound | PreciseCapturing) |
135 (ExplicitAnonymous, Reference | OutlivesBound | PreciseCapturing) |
137 (ExplicitAnonymous, Path { .. }) => n_elided += 1,
139
140 (Implicit, Path { .. }) => n_hidden += 1,
142
143 (ExplicitBound, Reference | OutlivesBound | PreciseCapturing) |
145 (ExplicitBound, Path { .. }) => n_named += 1,
147 };
148 }
149
150 let syntax_counts = (n_hidden, n_elided, n_named);
151 tracing::debug!(?syntax_counts);
152
153 matches!(syntax_counts, (_, 0, 0) | (0, _, 0) | (0, 0, _))
154}
155
156fn emit_mismatch_diagnostic<'tcx>(
157 cx: &LateContext<'tcx>,
158 input_info: &[Info<'_>],
159 output_info: &[Info<'_>],
160) {
161 let mut bound_lifetime = None;
164
165 let mut suggest_change_to_explicit_bound = Vec::new();
193
194 let mut suggest_change_to_mixed_implicit = Vec::new();
196 let mut suggest_change_to_mixed_explicit_anonymous = Vec::new();
197
198 let mut suggest_change_to_implicit = Vec::new();
200
201 let mut suggest_change_to_explicit_anonymous = Vec::new();
203
204 let mut allow_suggesting_implicit = true;
206
207 let mut saw_a_reference = false;
209 let mut saw_a_path = false;
210
211 for info in input_info.iter().chain(output_info) {
212 use LifetimeSource::*;
213 use hir::LifetimeSyntax::*;
214
215 let syntax_source = (info.lifetime.syntax, info.lifetime.source);
216
217 if let (_, Other) = syntax_source {
218 continue;
220 }
221
222 if let (ExplicitBound, _) = syntax_source {
223 bound_lifetime = Some(info);
224 }
225
226 match syntax_source {
227 (Implicit, Reference) => {
229 suggest_change_to_explicit_anonymous.push(info);
230 suggest_change_to_explicit_bound.push(info);
231 }
232
233 (ExplicitAnonymous, Reference) => {
235 suggest_change_to_implicit.push(info);
236 suggest_change_to_mixed_implicit.push(info);
237 suggest_change_to_explicit_bound.push(info);
238 }
239
240 (Implicit, Path { .. }) => {
242 suggest_change_to_mixed_explicit_anonymous.push(info);
243 suggest_change_to_explicit_anonymous.push(info);
244 suggest_change_to_explicit_bound.push(info);
245 }
246
247 (ExplicitAnonymous, Path { .. }) => {
249 suggest_change_to_explicit_bound.push(info);
250 }
251
252 (ExplicitBound, Reference) => {
254 suggest_change_to_implicit.push(info);
255 suggest_change_to_mixed_implicit.push(info);
256 suggest_change_to_explicit_anonymous.push(info);
257 }
258
259 (ExplicitBound, Path { .. }) => {
261 suggest_change_to_mixed_explicit_anonymous.push(info);
262 suggest_change_to_explicit_anonymous.push(info);
263 }
264
265 (Implicit, OutlivesBound | PreciseCapturing) => {
266 panic!("This syntax / source combination is not possible");
267 }
268
269 (ExplicitAnonymous, OutlivesBound | PreciseCapturing) => {
271 suggest_change_to_explicit_bound.push(info);
272 }
273
274 (ExplicitBound, OutlivesBound | PreciseCapturing) => {
276 suggest_change_to_mixed_explicit_anonymous.push(info);
277 suggest_change_to_explicit_anonymous.push(info);
278 }
279
280 (_, Other) => {
281 panic!("This syntax / source combination has already been skipped");
282 }
283 }
284
285 if matches!(syntax_source, (_, Path { .. } | OutlivesBound | PreciseCapturing)) {
286 allow_suggesting_implicit = false;
287 }
288
289 match syntax_source {
290 (_, Reference) => saw_a_reference = true,
291 (_, Path { .. }) => saw_a_path = true,
292 _ => {}
293 }
294 }
295
296 let make_implicit_suggestions =
297 |infos: &[&Info<'_>]| infos.iter().map(|i| i.removing_span()).collect::<Vec<_>>();
298
299 let inputs = input_info.iter().map(|info| info.reporting_span()).collect();
300 let outputs = output_info.iter().map(|info| info.reporting_span()).collect();
301
302 let explicit_bound_suggestion = bound_lifetime.map(|info| {
303 build_mismatch_suggestion(info.lifetime_name(), &suggest_change_to_explicit_bound)
304 });
305
306 let is_bound_static = bound_lifetime.is_some_and(|info| info.is_static());
307
308 tracing::debug!(?bound_lifetime, ?explicit_bound_suggestion, ?is_bound_static);
309
310 let should_suggest_mixed =
311 (saw_a_reference && saw_a_path) &&
313 (!suggest_change_to_mixed_implicit.is_empty() ||
315 !suggest_change_to_mixed_explicit_anonymous.is_empty()) &&
316 !is_bound_static;
318
319 let mixed_suggestion = should_suggest_mixed.then(|| {
320 let implicit_suggestions = make_implicit_suggestions(&suggest_change_to_mixed_implicit);
321
322 let explicit_anonymous_suggestions = suggest_change_to_mixed_explicit_anonymous
323 .iter()
324 .map(|info| info.suggestion("'_"))
325 .collect();
326
327 lints::MismatchedLifetimeSyntaxesSuggestion::Mixed {
328 implicit_suggestions,
329 explicit_anonymous_suggestions,
330 tool_only: false,
331 }
332 });
333
334 tracing::debug!(
335 ?suggest_change_to_mixed_implicit,
336 ?suggest_change_to_mixed_explicit_anonymous,
337 ?mixed_suggestion,
338 );
339
340 let should_suggest_implicit =
341 !suggest_change_to_implicit.is_empty() &&
343 allow_suggesting_implicit &&
345 !is_bound_static;
347
348 let implicit_suggestion = should_suggest_implicit.then(|| {
349 let suggestions = make_implicit_suggestions(&suggest_change_to_implicit);
350
351 lints::MismatchedLifetimeSyntaxesSuggestion::Implicit { suggestions, tool_only: false }
352 });
353
354 tracing::debug!(
355 ?should_suggest_implicit,
356 ?suggest_change_to_implicit,
357 allow_suggesting_implicit,
358 ?implicit_suggestion,
359 );
360
361 let should_suggest_explicit_anonymous =
362 !suggest_change_to_explicit_anonymous.is_empty() &&
364 !is_bound_static;
366
367 let explicit_anonymous_suggestion = should_suggest_explicit_anonymous
368 .then(|| build_mismatch_suggestion("'_", &suggest_change_to_explicit_anonymous));
369
370 tracing::debug!(
371 ?should_suggest_explicit_anonymous,
372 ?suggest_change_to_explicit_anonymous,
373 ?explicit_anonymous_suggestion,
374 );
375
376 let lifetime_name = bound_lifetime.map(|info| info.lifetime_name()).unwrap_or("'_").to_owned();
377
378 let mut suggestions = Vec::new();
383 suggestions.extend(explicit_bound_suggestion);
384 suggestions.extend(mixed_suggestion);
385 suggestions.extend(implicit_suggestion);
386 suggestions.extend(explicit_anonymous_suggestion);
387
388 cx.emit_span_lint(
389 MISMATCHED_LIFETIME_SYNTAXES,
390 Vec::clone(&inputs),
391 lints::MismatchedLifetimeSyntaxes { lifetime_name, inputs, outputs, suggestions },
392 );
393}
394
395fn build_mismatch_suggestion(
396 lifetime_name: &str,
397 infos: &[&Info<'_>],
398) -> lints::MismatchedLifetimeSyntaxesSuggestion {
399 let lifetime_name = lifetime_name.to_owned();
400
401 let suggestions = infos.iter().map(|info| info.suggestion(&lifetime_name)).collect();
402
403 lints::MismatchedLifetimeSyntaxesSuggestion::Explicit {
404 lifetime_name,
405 suggestions,
406 tool_only: false,
407 }
408}
409
410#[derive(Debug)]
411struct Info<'tcx> {
412 type_span: Span,
413 referenced_type_span: Option<Span>,
414 lifetime: &'tcx hir::Lifetime,
415}
416
417impl<'tcx> Info<'tcx> {
418 fn lifetime_name(&self) -> &str {
419 self.lifetime.ident.as_str()
420 }
421
422 fn is_static(&self) -> bool {
423 self.lifetime.is_static()
424 }
425
426 fn reporting_span(&self) -> Span {
430 if self.lifetime.is_implicit() { self.type_span } else { self.lifetime.ident.span }
431 }
432
433 fn removing_span(&self) -> Span {
447 let mut span = self.suggestion("'dummy").0;
448
449 if let Some(referenced_type_span) = self.referenced_type_span {
450 span = span.until(referenced_type_span);
451 }
452
453 span
454 }
455
456 fn suggestion(&self, lifetime_name: &str) -> (Span, String) {
457 self.lifetime.suggestion(lifetime_name)
458 }
459}
460
461type LifetimeInfoMap<'tcx> = FxIndexMap<&'tcx hir::LifetimeKind, Vec<Info<'tcx>>>;
462
463struct LifetimeInfoCollector<'a, 'tcx> {
464 type_span: Span,
465 referenced_type_span: Option<Span>,
466 map: &'a mut LifetimeInfoMap<'tcx>,
467}
468
469impl<'a, 'tcx> LifetimeInfoCollector<'a, 'tcx> {
470 fn collect(ty: &'tcx hir::Ty<'tcx>, map: &'a mut LifetimeInfoMap<'tcx>) {
471 let mut this = Self { type_span: ty.span, referenced_type_span: None, map };
472
473 intravisit::walk_unambig_ty(&mut this, ty);
474 }
475}
476
477impl<'a, 'tcx> Visitor<'tcx> for LifetimeInfoCollector<'a, 'tcx> {
478 #[instrument(skip(self))]
479 fn visit_lifetime(&mut self, lifetime: &'tcx hir::Lifetime) {
480 let type_span = self.type_span;
481 let referenced_type_span = self.referenced_type_span;
482
483 let info = Info { type_span, referenced_type_span, lifetime };
484
485 self.map.entry(&lifetime.kind).or_default().push(info);
486 }
487
488 #[instrument(skip(self))]
489 fn visit_ty(&mut self, ty: &'tcx hir::Ty<'tcx, hir::AmbigArg>) -> Self::Result {
490 let old_type_span = self.type_span;
491 let old_referenced_type_span = self.referenced_type_span;
492
493 self.type_span = ty.span;
494 if let hir::TyKind::Ref(_, ty) = ty.kind {
495 self.referenced_type_span = Some(ty.ty.span);
496 }
497
498 intravisit::walk_ty(self, ty);
499
500 self.type_span = old_type_span;
501 self.referenced_type_span = old_referenced_type_span;
502 }
503}