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 flatten(&self) -> impl Iterator<Item = &T> {
218 let Self { hidden, elided, named } = self;
219 [hidden.iter(), elided.iter(), named.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 tool_only: 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 { suggestions, tool_only: false }
459 });
460
461 tracing::debug!(
462 ?should_suggest_implicit,
463 ?suggest_change_to_implicit,
464 allow_suggesting_implicit,
465 ?implicit_suggestion,
466 );
467
468 let should_suggest_explicit_anonymous =
469 !suggest_change_to_explicit_anonymous.is_empty() &&
471 !is_bound_static;
473
474 let explicit_anonymous_suggestion = should_suggest_explicit_anonymous
475 .then(|| build_mismatch_suggestion("'_", &suggest_change_to_explicit_anonymous));
476
477 tracing::debug!(
478 ?should_suggest_explicit_anonymous,
479 ?suggest_change_to_explicit_anonymous,
480 ?explicit_anonymous_suggestion,
481 );
482
483 let mut suggestions = Vec::new();
488 suggestions.extend(explicit_bound_suggestion);
489 suggestions.extend(mixed_suggestion);
490 suggestions.extend(implicit_suggestion);
491 suggestions.extend(explicit_anonymous_suggestion);
492
493 cx.emit_span_lint(
494 MISMATCHED_LIFETIME_SYNTAXES,
495 inputs.flatten().copied().collect::<Vec<_>>(),
496 lints::MismatchedLifetimeSyntaxes { inputs, outputs, suggestions },
497 );
498}
499
500fn build_mismatch_suggestion(
501 lifetime_name: &str,
502 infos: &[&Info<'_>],
503) -> lints::MismatchedLifetimeSyntaxesSuggestion {
504 let lifetime_name = lifetime_name.to_owned();
505
506 let suggestions = infos.iter().map(|info| info.suggestion(&lifetime_name)).collect();
507
508 lints::MismatchedLifetimeSyntaxesSuggestion::Explicit {
509 lifetime_name,
510 suggestions,
511 tool_only: false,
512 }
513}
514
515#[derive(Debug)]
516struct Info<'tcx> {
517 type_span: Span,
518 referenced_type_span: Option<Span>,
519 lifetime: &'tcx hir::Lifetime,
520}
521
522impl<'tcx> Info<'tcx> {
523 fn syntax_source(&self) -> (hir::LifetimeSyntax, LifetimeSource) {
524 (self.lifetime.syntax, self.lifetime.source)
525 }
526
527 fn lifetime_syntax_category(&self) -> Option<LifetimeSyntaxCategory> {
528 LifetimeSyntaxCategory::new(self.syntax_source())
529 }
530
531 fn lifetime_name(&self) -> &str {
532 self.lifetime.ident.as_str()
533 }
534
535 fn is_static(&self) -> bool {
536 self.lifetime.is_static()
537 }
538
539 fn reporting_span(&self) -> Span {
543 if self.lifetime.is_implicit() { self.type_span } else { self.lifetime.ident.span }
544 }
545
546 fn removing_span(&self) -> Span {
560 let mut span = self.suggestion("'dummy").0;
561
562 if let Some(referenced_type_span) = self.referenced_type_span {
563 span = span.until(referenced_type_span);
564 }
565
566 span
567 }
568
569 fn suggestion(&self, lifetime_name: &str) -> (Span, String) {
570 self.lifetime.suggestion(lifetime_name)
571 }
572}
573
574type LifetimeInfoMap<'tcx> = FxIndexMap<&'tcx hir::LifetimeKind, Vec<Info<'tcx>>>;
575
576struct LifetimeInfoCollector<'a, 'tcx> {
577 type_span: Span,
578 referenced_type_span: Option<Span>,
579 map: &'a mut LifetimeInfoMap<'tcx>,
580}
581
582impl<'a, 'tcx> LifetimeInfoCollector<'a, 'tcx> {
583 fn collect(ty: &'tcx hir::Ty<'tcx>, map: &'a mut LifetimeInfoMap<'tcx>) {
584 let mut this = Self { type_span: ty.span, referenced_type_span: None, map };
585
586 intravisit::walk_unambig_ty(&mut this, ty);
587 }
588}
589
590impl<'a, 'tcx> Visitor<'tcx> for LifetimeInfoCollector<'a, 'tcx> {
591 #[instrument(skip(self))]
592 fn visit_lifetime(&mut self, lifetime: &'tcx hir::Lifetime) {
593 let type_span = self.type_span;
594 let referenced_type_span = self.referenced_type_span;
595
596 let info = Info { type_span, referenced_type_span, lifetime };
597
598 self.map.entry(&lifetime.kind).or_default().push(info);
599 }
600
601 #[instrument(skip(self))]
602 fn visit_ty(&mut self, ty: &'tcx hir::Ty<'tcx, hir::AmbigArg>) -> Self::Result {
603 let old_type_span = self.type_span;
604 let old_referenced_type_span = self.referenced_type_span;
605
606 self.type_span = ty.span;
607 if let hir::TyKind::Ref(_, ty) = ty.kind {
608 self.referenced_type_span = Some(ty.ty.span);
609 }
610
611 intravisit::walk_ty(self, ty);
612
613 self.type_span = old_type_span;
614 self.referenced_type_span = old_referenced_type_span;
615 }
616}