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 rustc_span::def_id::LocalDefId;
7use tracing::instrument;
8
9use crate::{LateContext, LateLintPass, LintContext, lints};
10
11declare_lint! {
12 pub MISMATCHED_LIFETIME_SYNTAXES,
71 Warn,
72 "detects when a lifetime uses different syntax between arguments and return values"
73}
74
75declare_lint_pass!(LifetimeSyntax => [MISMATCHED_LIFETIME_SYNTAXES]);
76
77impl<'tcx> LateLintPass<'tcx> for LifetimeSyntax {
78 #[instrument(skip_all)]
79 fn check_fn(
80 &mut self,
81 cx: &LateContext<'tcx>,
82 _: intravisit::FnKind<'tcx>,
83 fd: &'tcx hir::FnDecl<'tcx>,
84 _: &'tcx hir::Body<'tcx>,
85 _: Span,
86 _: LocalDefId,
87 ) {
88 check_fn_like(cx, fd);
89 }
90
91 #[instrument(skip_all)]
92 fn check_trait_item(&mut self, cx: &LateContext<'tcx>, ti: &'tcx hir::TraitItem<'tcx>) {
93 match ti.kind {
94 hir::TraitItemKind::Const(..) => {}
95 hir::TraitItemKind::Fn(fn_sig, _trait_fn) => check_fn_like(cx, fn_sig.decl),
96 hir::TraitItemKind::Type(..) => {}
97 }
98 }
99
100 #[instrument(skip_all)]
101 fn check_foreign_item(&mut self, cx: &LateContext<'tcx>, fi: &'tcx hir::ForeignItem<'tcx>) {
102 match fi.kind {
103 hir::ForeignItemKind::Fn(fn_sig, _idents, _generics) => check_fn_like(cx, fn_sig.decl),
104 hir::ForeignItemKind::Static(..) => {}
105 hir::ForeignItemKind::Type => {}
106 }
107 }
108}
109
110fn check_fn_like<'tcx>(cx: &LateContext<'tcx>, fd: &'tcx hir::FnDecl<'tcx>) {
111 if fd.inputs.is_empty() {
112 return;
113 }
114 let hir::FnRetTy::Return(output) = fd.output else {
115 return;
116 };
117
118 let mut map: FxIndexMap<hir::LifetimeKind, LifetimeGroup<'_>> = FxIndexMap::default();
119
120 LifetimeInfoCollector::collect(output, |info| {
121 let group = map.entry(info.lifetime.kind).or_default();
122 group.outputs.push(info);
123 });
124 if map.is_empty() {
125 return;
126 }
127
128 for input in fd.inputs {
129 LifetimeInfoCollector::collect(input, |info| {
130 if let Some(group) = map.get_mut(&info.lifetime.kind) {
131 group.inputs.push(info);
132 }
133 });
134 }
135
136 for LifetimeGroup { ref inputs, ref outputs } in map.into_values() {
137 if inputs.is_empty() {
138 continue;
139 }
140 if !lifetimes_use_matched_syntax(inputs, outputs) {
141 emit_mismatch_diagnostic(cx, inputs, outputs);
142 }
143 }
144}
145
146#[derive(Default)]
147struct LifetimeGroup<'tcx> {
148 inputs: Vec<Info<'tcx>>,
149 outputs: Vec<Info<'tcx>>,
150}
151
152#[derive(Debug, Copy, Clone, PartialEq)]
153enum LifetimeSyntaxCategory {
154 Hidden,
155 Elided,
156 Named,
157}
158
159impl LifetimeSyntaxCategory {
160 fn new(lifetime: &hir::Lifetime) -> Option<Self> {
161 use LifetimeSource::*;
162 use hir::LifetimeSyntax::*;
163
164 match (lifetime.syntax, lifetime.source) {
165 (Implicit, Reference) |
167 (ExplicitAnonymous, Reference) |
169 (ExplicitAnonymous, Path { .. }) |
171 (ExplicitAnonymous, OutlivesBound | PreciseCapturing) => {
173 Some(Self::Elided)
174 }
175
176 (Implicit, Path { .. }) => {
178 Some(Self::Hidden)
179 }
180
181 (ExplicitBound, Reference) |
183 (ExplicitBound, Path { .. }) |
185 (ExplicitBound, OutlivesBound | PreciseCapturing) => {
187 Some(Self::Named)
188 }
189
190 (Implicit, OutlivesBound | PreciseCapturing) |
191 (_, Other) => {
192 None
193 }
194 }
195 }
196}
197
198#[derive(Debug, Default)]
199pub struct LifetimeSyntaxCategories<T> {
200 pub hidden: T,
201 pub elided: T,
202 pub named: T,
203}
204
205impl<T> LifetimeSyntaxCategories<T> {
206 fn select(&mut self, category: LifetimeSyntaxCategory) -> &mut T {
207 use LifetimeSyntaxCategory::*;
208
209 match category {
210 Elided => &mut self.elided,
211 Hidden => &mut self.hidden,
212 Named => &mut self.named,
213 }
214 }
215}
216
217impl<T> LifetimeSyntaxCategories<Vec<T>> {
218 pub fn len(&self) -> LifetimeSyntaxCategories<usize> {
219 LifetimeSyntaxCategories {
220 hidden: self.hidden.len(),
221 elided: self.elided.len(),
222 named: self.named.len(),
223 }
224 }
225
226 pub fn iter_unnamed(&self) -> impl Iterator<Item = &T> {
227 let Self { hidden, elided, named: _ } = self;
228 std::iter::chain(hidden, elided)
229 }
230}
231
232impl std::ops::Add for LifetimeSyntaxCategories<usize> {
233 type Output = Self;
234
235 fn add(self, rhs: Self) -> Self::Output {
236 Self {
237 hidden: self.hidden + rhs.hidden,
238 elided: self.elided + rhs.elided,
239 named: self.named + rhs.named,
240 }
241 }
242}
243
244fn lifetimes_use_matched_syntax(input_info: &[Info<'_>], output_info: &[Info<'_>]) -> bool {
245 let (first, inputs) = input_info.split_first().unwrap();
246 std::iter::chain(inputs, output_info).all(|info| info.syntax_category == first.syntax_category)
247}
248
249fn emit_mismatch_diagnostic<'tcx>(
250 cx: &LateContext<'tcx>,
251 input_info: &[Info<'_>],
252 output_info: &[Info<'_>],
253) {
254 let mut bound_lifetime = None;
257
258 let mut suggest_change_to_explicit_bound = Vec::new();
286
287 let mut suggest_change_to_mixed_implicit = Vec::new();
289 let mut suggest_change_to_mixed_explicit_anonymous = Vec::new();
290
291 let mut suggest_change_to_implicit = Vec::new();
293
294 let mut suggest_change_to_explicit_anonymous = Vec::new();
296
297 let mut allow_suggesting_implicit = true;
299
300 let mut saw_a_reference = false;
302 let mut saw_a_path = false;
303
304 for info in input_info.iter().chain(output_info) {
305 use LifetimeSource::*;
306 use hir::LifetimeSyntax::*;
307
308 let lifetime = info.lifetime;
309
310 if lifetime.syntax == ExplicitBound {
311 bound_lifetime = Some(info);
312 }
313
314 match (lifetime.syntax, lifetime.source) {
315 (Implicit, Reference) => {
317 suggest_change_to_explicit_anonymous.push(info);
318 suggest_change_to_explicit_bound.push(info);
319 }
320
321 (ExplicitAnonymous, Reference) => {
323 suggest_change_to_implicit.push(info);
324 suggest_change_to_explicit_bound.push(info);
325 }
326
327 (Implicit, Path { .. }) => {
329 suggest_change_to_mixed_explicit_anonymous.push(info);
330 suggest_change_to_explicit_anonymous.push(info);
331 suggest_change_to_explicit_bound.push(info);
332 }
333
334 (ExplicitAnonymous, Path { .. } | OutlivesBound | PreciseCapturing) => {
336 suggest_change_to_explicit_bound.push(info);
337 }
338
339 (ExplicitBound, Reference) => {
341 suggest_change_to_implicit.push(info);
342 suggest_change_to_mixed_implicit.push(info);
343 suggest_change_to_explicit_anonymous.push(info);
344 }
345
346 (ExplicitBound, Path { .. } | OutlivesBound | PreciseCapturing) => {
348 suggest_change_to_mixed_explicit_anonymous.push(info);
349 suggest_change_to_explicit_anonymous.push(info);
350 }
351
352 (Implicit, OutlivesBound | PreciseCapturing) => {
353 panic!("This syntax / source combination is not possible");
354 }
355
356 (_, Other) => {
357 panic!("This syntax / source combination has already been skipped");
358 }
359 }
360
361 if matches!(lifetime.source, Path { .. } | OutlivesBound | PreciseCapturing) {
362 allow_suggesting_implicit = false;
363 }
364
365 match lifetime.source {
366 Reference => saw_a_reference = true,
367 Path { .. } => saw_a_path = true,
368 _ => {}
369 }
370 }
371
372 let categorize = |infos: &[Info<'_>]| {
373 let mut categories = LifetimeSyntaxCategories::<Vec<_>>::default();
374 for info in infos {
375 categories.select(info.syntax_category).push(info.reporting_span());
376 }
377 categories
378 };
379
380 let inputs = categorize(input_info);
381 let outputs = categorize(output_info);
382
383 let make_implicit_suggestions =
384 |infos: &[&Info<'_>]| infos.iter().map(|i| i.removing_span()).collect::<Vec<_>>();
385
386 let explicit_bound_suggestion = bound_lifetime.map(|info| {
387 build_mismatch_suggestion(info.lifetime.ident.as_str(), &suggest_change_to_explicit_bound)
388 });
389
390 let is_bound_static = bound_lifetime.is_some_and(|info| info.lifetime.is_static());
391
392 tracing::debug!(?bound_lifetime, ?explicit_bound_suggestion, ?is_bound_static);
393
394 let should_suggest_mixed =
395 (saw_a_reference && saw_a_path) &&
397 (!suggest_change_to_mixed_implicit.is_empty() ||
399 !suggest_change_to_mixed_explicit_anonymous.is_empty()) &&
400 !is_bound_static;
402
403 let mixed_suggestion = should_suggest_mixed.then(|| {
404 let implicit_suggestions = make_implicit_suggestions(&suggest_change_to_mixed_implicit);
405
406 let explicit_anonymous_suggestions = suggest_change_to_mixed_explicit_anonymous
407 .iter()
408 .map(|info| info.suggestion("'_"))
409 .collect();
410
411 lints::MismatchedLifetimeSyntaxesSuggestion::Mixed {
412 implicit_suggestions,
413 explicit_anonymous_suggestions,
414 optional_alternative: false,
415 }
416 });
417
418 tracing::debug!(
419 ?suggest_change_to_mixed_implicit,
420 ?suggest_change_to_mixed_explicit_anonymous,
421 ?mixed_suggestion,
422 );
423
424 let should_suggest_implicit =
425 !suggest_change_to_implicit.is_empty() &&
427 allow_suggesting_implicit &&
429 !is_bound_static;
431
432 let implicit_suggestion = should_suggest_implicit.then(|| {
433 let suggestions = make_implicit_suggestions(&suggest_change_to_implicit);
434
435 lints::MismatchedLifetimeSyntaxesSuggestion::Implicit {
436 suggestions,
437 optional_alternative: false,
438 }
439 });
440
441 tracing::debug!(
442 ?should_suggest_implicit,
443 ?suggest_change_to_implicit,
444 allow_suggesting_implicit,
445 ?implicit_suggestion,
446 );
447
448 let should_suggest_explicit_anonymous =
449 !suggest_change_to_explicit_anonymous.is_empty() &&
451 !is_bound_static;
453
454 let explicit_anonymous_suggestion = should_suggest_explicit_anonymous
455 .then(|| build_mismatch_suggestion("'_", &suggest_change_to_explicit_anonymous));
456
457 tracing::debug!(
458 ?should_suggest_explicit_anonymous,
459 ?suggest_change_to_explicit_anonymous,
460 ?explicit_anonymous_suggestion,
461 );
462
463 let mut suggestions = Vec::new();
468 suggestions.extend(explicit_bound_suggestion);
469 suggestions.extend(mixed_suggestion);
470 suggestions.extend(implicit_suggestion);
471 suggestions.extend(explicit_anonymous_suggestion);
472
473 cx.emit_span_lint(
474 MISMATCHED_LIFETIME_SYNTAXES,
475 inputs.iter_unnamed().chain(outputs.iter_unnamed()).copied().collect::<Vec<_>>(),
476 lints::MismatchedLifetimeSyntaxes { inputs, outputs, suggestions },
477 );
478}
479
480fn build_mismatch_suggestion(
481 lifetime_name: &str,
482 infos: &[&Info<'_>],
483) -> lints::MismatchedLifetimeSyntaxesSuggestion {
484 let lifetime_name = lifetime_name.to_owned();
485
486 let suggestions = infos.iter().map(|info| info.suggestion(&lifetime_name)).collect();
487
488 lints::MismatchedLifetimeSyntaxesSuggestion::Explicit {
489 lifetime_name,
490 suggestions,
491 optional_alternative: false,
492 }
493}
494
495#[derive(Debug)]
496struct Info<'tcx> {
497 lifetime: &'tcx hir::Lifetime,
498 syntax_category: LifetimeSyntaxCategory,
499 ty: &'tcx hir::Ty<'tcx>,
500}
501
502impl<'tcx> Info<'tcx> {
503 fn reporting_span(&self) -> Span {
507 if self.lifetime.is_implicit() { self.ty.span } else { self.lifetime.ident.span }
508 }
509
510 fn removing_span(&self) -> Span {
524 let mut span = self.lifetime.ident.span;
525 if let hir::TyKind::Ref(_, mut_ty) = self.ty.kind {
526 span = span.until(mut_ty.ty.span);
527 }
528 span
529 }
530
531 fn suggestion(&self, lifetime_name: &str) -> (Span, String) {
532 self.lifetime.suggestion(lifetime_name)
533 }
534}
535
536struct LifetimeInfoCollector<'tcx, F> {
537 info_func: F,
538 ty: &'tcx hir::Ty<'tcx>,
539}
540
541impl<'tcx, F> LifetimeInfoCollector<'tcx, F>
542where
543 F: FnMut(Info<'tcx>),
544{
545 fn collect(ty: &'tcx hir::Ty<'tcx>, info_func: F) {
546 let mut this = Self { info_func, ty };
547
548 intravisit::walk_unambig_ty(&mut this, ty);
549 }
550}
551
552impl<'tcx, F> Visitor<'tcx> for LifetimeInfoCollector<'tcx, F>
553where
554 F: FnMut(Info<'tcx>),
555{
556 #[instrument(skip(self))]
557 fn visit_lifetime(&mut self, lifetime: &'tcx hir::Lifetime) {
558 if let Some(syntax_category) = LifetimeSyntaxCategory::new(lifetime) {
559 let info = Info { lifetime, syntax_category, ty: self.ty };
560 (self.info_func)(info);
561 }
562 }
563
564 #[instrument(skip(self))]
565 fn visit_ty(&mut self, ty: &'tcx hir::Ty<'tcx, hir::AmbigArg>) -> Self::Result {
566 let old_ty = std::mem::replace(&mut self.ty, ty.as_unambig_ty());
567 intravisit::walk_ty(self, ty);
568 self.ty = old_ty;
569 }
570}