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 check_fn_like(cx, fd);
88 }
89
90 #[instrument(skip_all)]
91 fn check_trait_item(&mut self, cx: &LateContext<'tcx>, ti: &'tcx hir::TraitItem<'tcx>) {
92 match ti.kind {
93 hir::TraitItemKind::Const(..) => {}
94 hir::TraitItemKind::Fn(fn_sig, _trait_fn) => check_fn_like(cx, fn_sig.decl),
95 hir::TraitItemKind::Type(..) => {}
96 }
97 }
98
99 #[instrument(skip_all)]
100 fn check_foreign_item(
101 &mut self,
102 cx: &LateContext<'tcx>,
103 fi: &'tcx rustc_hir::ForeignItem<'tcx>,
104 ) {
105 match fi.kind {
106 hir::ForeignItemKind::Fn(fn_sig, _idents, _generics) => check_fn_like(cx, fn_sig.decl),
107 hir::ForeignItemKind::Static(..) => {}
108 hir::ForeignItemKind::Type => {}
109 }
110 }
111}
112
113fn check_fn_like<'tcx>(cx: &LateContext<'tcx>, fd: &'tcx hir::FnDecl<'tcx>) {
114 let mut input_map = Default::default();
115 let mut output_map = Default::default();
116
117 for input in fd.inputs {
118 LifetimeInfoCollector::collect(input, &mut input_map);
119 }
120
121 if let hir::FnRetTy::Return(output) = fd.output {
122 LifetimeInfoCollector::collect(output, &mut output_map);
123 }
124
125 report_mismatches(cx, &input_map, &output_map);
126}
127
128#[instrument(skip_all)]
129fn report_mismatches<'tcx>(
130 cx: &LateContext<'tcx>,
131 inputs: &LifetimeInfoMap<'tcx>,
132 outputs: &LifetimeInfoMap<'tcx>,
133) {
134 for (resolved_lifetime, output_info) in outputs {
135 if let Some(input_info) = inputs.get(resolved_lifetime) {
136 if !lifetimes_use_matched_syntax(input_info, output_info) {
137 emit_mismatch_diagnostic(cx, input_info, output_info);
138 }
139 }
140 }
141}
142
143#[derive(Debug, Copy, Clone, PartialEq)]
144enum LifetimeSyntaxCategory {
145 Hidden,
146 Elided,
147 Named,
148}
149
150impl LifetimeSyntaxCategory {
151 fn new(syntax_source: (hir::LifetimeSyntax, LifetimeSource)) -> Option<Self> {
152 use LifetimeSource::*;
153 use hir::LifetimeSyntax::*;
154
155 match syntax_source {
156 (Implicit, Reference) |
158 (ExplicitAnonymous, Reference) |
160 (ExplicitAnonymous, Path { .. }) |
162 (ExplicitAnonymous, OutlivesBound | PreciseCapturing) => {
164 Some(Self::Elided)
165 }
166
167 (Implicit, Path { .. }) => {
169 Some(Self::Hidden)
170 }
171
172 (ExplicitBound, Reference) |
174 (ExplicitBound, Path { .. }) |
176 (ExplicitBound, OutlivesBound | PreciseCapturing) => {
178 Some(Self::Named)
179 }
180
181 (Implicit, OutlivesBound | PreciseCapturing) |
182 (_, Other) => {
183 None
184 }
185 }
186 }
187}
188
189#[derive(Debug, Default)]
190pub struct LifetimeSyntaxCategories<T> {
191 pub hidden: T,
192 pub elided: T,
193 pub named: T,
194}
195
196impl<T> LifetimeSyntaxCategories<T> {
197 fn select(&mut self, category: LifetimeSyntaxCategory) -> &mut T {
198 use LifetimeSyntaxCategory::*;
199
200 match category {
201 Elided => &mut self.elided,
202 Hidden => &mut self.hidden,
203 Named => &mut self.named,
204 }
205 }
206}
207
208impl<T> LifetimeSyntaxCategories<Vec<T>> {
209 pub fn len(&self) -> LifetimeSyntaxCategories<usize> {
210 LifetimeSyntaxCategories {
211 hidden: self.hidden.len(),
212 elided: self.elided.len(),
213 named: self.named.len(),
214 }
215 }
216
217 pub fn iter_unnamed(&self) -> impl Iterator<Item = &T> {
218 let Self { hidden, elided, named: _ } = self;
219 [hidden.iter(), elided.iter()].into_iter().flatten()
220 }
221}
222
223impl std::ops::Add for LifetimeSyntaxCategories<usize> {
224 type Output = Self;
225
226 fn add(self, rhs: Self) -> Self::Output {
227 Self {
228 hidden: self.hidden + rhs.hidden,
229 elided: self.elided + rhs.elided,
230 named: self.named + rhs.named,
231 }
232 }
233}
234
235fn lifetimes_use_matched_syntax(input_info: &[Info<'_>], output_info: &[Info<'_>]) -> bool {
236 let mut syntax_counts = LifetimeSyntaxCategories::<usize>::default();
237
238 for info in input_info.iter().chain(output_info) {
239 if let Some(category) = info.lifetime_syntax_category() {
240 *syntax_counts.select(category) += 1;
241 }
242 }
243
244 tracing::debug!(?syntax_counts);
245
246 matches!(
247 syntax_counts,
248 LifetimeSyntaxCategories { hidden: _, elided: 0, named: 0 }
249 | LifetimeSyntaxCategories { hidden: 0, elided: _, named: 0 }
250 | LifetimeSyntaxCategories { hidden: 0, elided: 0, named: _ }
251 )
252}
253
254fn emit_mismatch_diagnostic<'tcx>(
255 cx: &LateContext<'tcx>,
256 input_info: &[Info<'_>],
257 output_info: &[Info<'_>],
258) {
259 let mut bound_lifetime = None;
262
263 let mut suggest_change_to_explicit_bound = Vec::new();
291
292 let mut suggest_change_to_mixed_implicit = Vec::new();
294 let mut suggest_change_to_mixed_explicit_anonymous = Vec::new();
295
296 let mut suggest_change_to_implicit = Vec::new();
298
299 let mut suggest_change_to_explicit_anonymous = Vec::new();
301
302 let mut allow_suggesting_implicit = true;
304
305 let mut saw_a_reference = false;
307 let mut saw_a_path = false;
308
309 for info in input_info.iter().chain(output_info) {
310 use LifetimeSource::*;
311 use hir::LifetimeSyntax::*;
312
313 let syntax_source = info.syntax_source();
314
315 if let (_, Other) = syntax_source {
316 continue;
318 }
319
320 if let (ExplicitBound, _) = syntax_source {
321 bound_lifetime = Some(info);
322 }
323
324 match syntax_source {
325 (Implicit, Reference) => {
327 suggest_change_to_explicit_anonymous.push(info);
328 suggest_change_to_explicit_bound.push(info);
329 }
330
331 (ExplicitAnonymous, Reference) => {
333 suggest_change_to_implicit.push(info);
334 suggest_change_to_explicit_bound.push(info);
335 }
336
337 (Implicit, Path { .. }) => {
339 suggest_change_to_mixed_explicit_anonymous.push(info);
340 suggest_change_to_explicit_anonymous.push(info);
341 suggest_change_to_explicit_bound.push(info);
342 }
343
344 (ExplicitAnonymous, Path { .. }) => {
346 suggest_change_to_explicit_bound.push(info);
347 }
348
349 (ExplicitBound, Reference) => {
351 suggest_change_to_implicit.push(info);
352 suggest_change_to_mixed_implicit.push(info);
353 suggest_change_to_explicit_anonymous.push(info);
354 }
355
356 (ExplicitBound, Path { .. }) => {
358 suggest_change_to_mixed_explicit_anonymous.push(info);
359 suggest_change_to_explicit_anonymous.push(info);
360 }
361
362 (Implicit, OutlivesBound | PreciseCapturing) => {
363 panic!("This syntax / source combination is not possible");
364 }
365
366 (ExplicitAnonymous, OutlivesBound | PreciseCapturing) => {
368 suggest_change_to_explicit_bound.push(info);
369 }
370
371 (ExplicitBound, OutlivesBound | PreciseCapturing) => {
373 suggest_change_to_mixed_explicit_anonymous.push(info);
374 suggest_change_to_explicit_anonymous.push(info);
375 }
376
377 (_, Other) => {
378 panic!("This syntax / source combination has already been skipped");
379 }
380 }
381
382 if matches!(syntax_source, (_, Path { .. } | OutlivesBound | PreciseCapturing)) {
383 allow_suggesting_implicit = false;
384 }
385
386 match syntax_source {
387 (_, Reference) => saw_a_reference = true,
388 (_, Path { .. }) => saw_a_path = true,
389 _ => {}
390 }
391 }
392
393 let categorize = |infos: &[Info<'_>]| {
394 let mut categories = LifetimeSyntaxCategories::<Vec<_>>::default();
395 for info in infos {
396 if let Some(category) = info.lifetime_syntax_category() {
397 categories.select(category).push(info.reporting_span());
398 }
399 }
400 categories
401 };
402
403 let inputs = categorize(input_info);
404 let outputs = categorize(output_info);
405
406 let make_implicit_suggestions =
407 |infos: &[&Info<'_>]| infos.iter().map(|i| i.removing_span()).collect::<Vec<_>>();
408
409 let explicit_bound_suggestion = bound_lifetime.map(|info| {
410 build_mismatch_suggestion(info.lifetime_name(), &suggest_change_to_explicit_bound)
411 });
412
413 let is_bound_static = bound_lifetime.is_some_and(|info| info.is_static());
414
415 tracing::debug!(?bound_lifetime, ?explicit_bound_suggestion, ?is_bound_static);
416
417 let should_suggest_mixed =
418 (saw_a_reference && saw_a_path) &&
420 (!suggest_change_to_mixed_implicit.is_empty() ||
422 !suggest_change_to_mixed_explicit_anonymous.is_empty()) &&
423 !is_bound_static;
425
426 let mixed_suggestion = should_suggest_mixed.then(|| {
427 let implicit_suggestions = make_implicit_suggestions(&suggest_change_to_mixed_implicit);
428
429 let explicit_anonymous_suggestions = suggest_change_to_mixed_explicit_anonymous
430 .iter()
431 .map(|info| info.suggestion("'_"))
432 .collect();
433
434 lints::MismatchedLifetimeSyntaxesSuggestion::Mixed {
435 implicit_suggestions,
436 explicit_anonymous_suggestions,
437 optional_alternative: false,
438 }
439 });
440
441 tracing::debug!(
442 ?suggest_change_to_mixed_implicit,
443 ?suggest_change_to_mixed_explicit_anonymous,
444 ?mixed_suggestion,
445 );
446
447 let should_suggest_implicit =
448 !suggest_change_to_implicit.is_empty() &&
450 allow_suggesting_implicit &&
452 !is_bound_static;
454
455 let implicit_suggestion = should_suggest_implicit.then(|| {
456 let suggestions = make_implicit_suggestions(&suggest_change_to_implicit);
457
458 lints::MismatchedLifetimeSyntaxesSuggestion::Implicit {
459 suggestions,
460 optional_alternative: false,
461 }
462 });
463
464 tracing::debug!(
465 ?should_suggest_implicit,
466 ?suggest_change_to_implicit,
467 allow_suggesting_implicit,
468 ?implicit_suggestion,
469 );
470
471 let should_suggest_explicit_anonymous =
472 !suggest_change_to_explicit_anonymous.is_empty() &&
474 !is_bound_static;
476
477 let explicit_anonymous_suggestion = should_suggest_explicit_anonymous
478 .then(|| build_mismatch_suggestion("'_", &suggest_change_to_explicit_anonymous));
479
480 tracing::debug!(
481 ?should_suggest_explicit_anonymous,
482 ?suggest_change_to_explicit_anonymous,
483 ?explicit_anonymous_suggestion,
484 );
485
486 let mut suggestions = Vec::new();
491 suggestions.extend(explicit_bound_suggestion);
492 suggestions.extend(mixed_suggestion);
493 suggestions.extend(implicit_suggestion);
494 suggestions.extend(explicit_anonymous_suggestion);
495
496 cx.emit_span_lint(
497 MISMATCHED_LIFETIME_SYNTAXES,
498 inputs.iter_unnamed().chain(outputs.iter_unnamed()).copied().collect::<Vec<_>>(),
499 lints::MismatchedLifetimeSyntaxes { inputs, outputs, suggestions },
500 );
501}
502
503fn build_mismatch_suggestion(
504 lifetime_name: &str,
505 infos: &[&Info<'_>],
506) -> lints::MismatchedLifetimeSyntaxesSuggestion {
507 let lifetime_name = lifetime_name.to_owned();
508
509 let suggestions = infos.iter().map(|info| info.suggestion(&lifetime_name)).collect();
510
511 lints::MismatchedLifetimeSyntaxesSuggestion::Explicit {
512 lifetime_name,
513 suggestions,
514 optional_alternative: false,
515 }
516}
517
518#[derive(Debug)]
519struct Info<'tcx> {
520 type_span: Span,
521 referenced_type_span: Option<Span>,
522 lifetime: &'tcx hir::Lifetime,
523}
524
525impl<'tcx> Info<'tcx> {
526 fn syntax_source(&self) -> (hir::LifetimeSyntax, LifetimeSource) {
527 (self.lifetime.syntax, self.lifetime.source)
528 }
529
530 fn lifetime_syntax_category(&self) -> Option<LifetimeSyntaxCategory> {
531 LifetimeSyntaxCategory::new(self.syntax_source())
532 }
533
534 fn lifetime_name(&self) -> &str {
535 self.lifetime.ident.as_str()
536 }
537
538 fn is_static(&self) -> bool {
539 self.lifetime.is_static()
540 }
541
542 fn reporting_span(&self) -> Span {
546 if self.lifetime.is_implicit() { self.type_span } else { self.lifetime.ident.span }
547 }
548
549 fn removing_span(&self) -> Span {
563 let mut span = self.suggestion("'dummy").0;
564
565 if let Some(referenced_type_span) = self.referenced_type_span {
566 span = span.until(referenced_type_span);
567 }
568
569 span
570 }
571
572 fn suggestion(&self, lifetime_name: &str) -> (Span, String) {
573 self.lifetime.suggestion(lifetime_name)
574 }
575}
576
577type LifetimeInfoMap<'tcx> = FxIndexMap<&'tcx hir::LifetimeKind, Vec<Info<'tcx>>>;
578
579struct LifetimeInfoCollector<'a, 'tcx> {
580 type_span: Span,
581 referenced_type_span: Option<Span>,
582 map: &'a mut LifetimeInfoMap<'tcx>,
583}
584
585impl<'a, 'tcx> LifetimeInfoCollector<'a, 'tcx> {
586 fn collect(ty: &'tcx hir::Ty<'tcx>, map: &'a mut LifetimeInfoMap<'tcx>) {
587 let mut this = Self { type_span: ty.span, referenced_type_span: None, map };
588
589 intravisit::walk_unambig_ty(&mut this, ty);
590 }
591}
592
593impl<'a, 'tcx> Visitor<'tcx> for LifetimeInfoCollector<'a, 'tcx> {
594 #[instrument(skip(self))]
595 fn visit_lifetime(&mut self, lifetime: &'tcx hir::Lifetime) {
596 let type_span = self.type_span;
597 let referenced_type_span = self.referenced_type_span;
598
599 let info = Info { type_span, referenced_type_span, lifetime };
600
601 self.map.entry(&lifetime.kind).or_default().push(info);
602 }
603
604 #[instrument(skip(self))]
605 fn visit_ty(&mut self, ty: &'tcx hir::Ty<'tcx, hir::AmbigArg>) -> Self::Result {
606 let old_type_span = self.type_span;
607 let old_referenced_type_span = self.referenced_type_span;
608
609 self.type_span = ty.span;
610 if let hir::TyKind::Ref(_, ty) = ty.kind {
611 self.referenced_type_span = Some(ty.ty.span);
612 }
613
614 intravisit::walk_ty(self, ty);
615
616 self.type_span = old_type_span;
617 self.referenced_type_span = old_referenced_type_span;
618 }
619}