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
143fn lifetimes_use_matched_syntax(input_info: &[Info<'_>], output_info: &[Info<'_>]) -> bool {
144 let mut n_hidden = 0;
146 let mut n_elided = 0;
147 let mut n_named = 0;
148
149 for info in input_info.iter().chain(output_info) {
150 use LifetimeSource::*;
151 use hir::LifetimeSyntax::*;
152
153 let syntax_source = (info.lifetime.syntax, info.lifetime.source);
154
155 match syntax_source {
156 (_, Other) => continue,
158
159 (Implicit, Reference | OutlivesBound | PreciseCapturing) |
161 (ExplicitAnonymous, Reference | OutlivesBound | PreciseCapturing) |
163 (ExplicitAnonymous, Path { .. }) => n_elided += 1,
165
166 (Implicit, Path { .. }) => n_hidden += 1,
168
169 (ExplicitBound, Reference | OutlivesBound | PreciseCapturing) |
171 (ExplicitBound, Path { .. }) => n_named += 1,
173 };
174 }
175
176 let syntax_counts = (n_hidden, n_elided, n_named);
177 tracing::debug!(?syntax_counts);
178
179 matches!(syntax_counts, (_, 0, 0) | (0, _, 0) | (0, 0, _))
180}
181
182fn emit_mismatch_diagnostic<'tcx>(
183 cx: &LateContext<'tcx>,
184 input_info: &[Info<'_>],
185 output_info: &[Info<'_>],
186) {
187 let mut bound_lifetime = None;
190
191 let mut suggest_change_to_explicit_bound = Vec::new();
219
220 let mut suggest_change_to_mixed_implicit = Vec::new();
222 let mut suggest_change_to_mixed_explicit_anonymous = Vec::new();
223
224 let mut suggest_change_to_implicit = Vec::new();
226
227 let mut suggest_change_to_explicit_anonymous = Vec::new();
229
230 let mut allow_suggesting_implicit = true;
232
233 let mut saw_a_reference = false;
235 let mut saw_a_path = false;
236
237 for info in input_info.iter().chain(output_info) {
238 use LifetimeSource::*;
239 use hir::LifetimeSyntax::*;
240
241 let syntax_source = (info.lifetime.syntax, info.lifetime.source);
242
243 if let (_, Other) = syntax_source {
244 continue;
246 }
247
248 if let (ExplicitBound, _) = syntax_source {
249 bound_lifetime = Some(info);
250 }
251
252 match syntax_source {
253 (Implicit, Reference) => {
255 suggest_change_to_explicit_anonymous.push(info);
256 suggest_change_to_explicit_bound.push(info);
257 }
258
259 (ExplicitAnonymous, Reference) => {
261 suggest_change_to_implicit.push(info);
262 suggest_change_to_mixed_implicit.push(info);
263 suggest_change_to_explicit_bound.push(info);
264 }
265
266 (Implicit, Path { .. }) => {
268 suggest_change_to_mixed_explicit_anonymous.push(info);
269 suggest_change_to_explicit_anonymous.push(info);
270 suggest_change_to_explicit_bound.push(info);
271 }
272
273 (ExplicitAnonymous, Path { .. }) => {
275 suggest_change_to_explicit_bound.push(info);
276 }
277
278 (ExplicitBound, Reference) => {
280 suggest_change_to_implicit.push(info);
281 suggest_change_to_mixed_implicit.push(info);
282 suggest_change_to_explicit_anonymous.push(info);
283 }
284
285 (ExplicitBound, Path { .. }) => {
287 suggest_change_to_mixed_explicit_anonymous.push(info);
288 suggest_change_to_explicit_anonymous.push(info);
289 }
290
291 (Implicit, OutlivesBound | PreciseCapturing) => {
292 panic!("This syntax / source combination is not possible");
293 }
294
295 (ExplicitAnonymous, OutlivesBound | PreciseCapturing) => {
297 suggest_change_to_explicit_bound.push(info);
298 }
299
300 (ExplicitBound, OutlivesBound | PreciseCapturing) => {
302 suggest_change_to_mixed_explicit_anonymous.push(info);
303 suggest_change_to_explicit_anonymous.push(info);
304 }
305
306 (_, Other) => {
307 panic!("This syntax / source combination has already been skipped");
308 }
309 }
310
311 if matches!(syntax_source, (_, Path { .. } | OutlivesBound | PreciseCapturing)) {
312 allow_suggesting_implicit = false;
313 }
314
315 match syntax_source {
316 (_, Reference) => saw_a_reference = true,
317 (_, Path { .. }) => saw_a_path = true,
318 _ => {}
319 }
320 }
321
322 let make_implicit_suggestions =
323 |infos: &[&Info<'_>]| infos.iter().map(|i| i.removing_span()).collect::<Vec<_>>();
324
325 let inputs = input_info.iter().map(|info| info.reporting_span()).collect();
326 let outputs = output_info.iter().map(|info| info.reporting_span()).collect();
327
328 let explicit_bound_suggestion = bound_lifetime.map(|info| {
329 build_mismatch_suggestion(info.lifetime_name(), &suggest_change_to_explicit_bound)
330 });
331
332 let is_bound_static = bound_lifetime.is_some_and(|info| info.is_static());
333
334 tracing::debug!(?bound_lifetime, ?explicit_bound_suggestion, ?is_bound_static);
335
336 let should_suggest_mixed =
337 (saw_a_reference && saw_a_path) &&
339 (!suggest_change_to_mixed_implicit.is_empty() ||
341 !suggest_change_to_mixed_explicit_anonymous.is_empty()) &&
342 !is_bound_static;
344
345 let mixed_suggestion = should_suggest_mixed.then(|| {
346 let implicit_suggestions = make_implicit_suggestions(&suggest_change_to_mixed_implicit);
347
348 let explicit_anonymous_suggestions = suggest_change_to_mixed_explicit_anonymous
349 .iter()
350 .map(|info| info.suggestion("'_"))
351 .collect();
352
353 lints::MismatchedLifetimeSyntaxesSuggestion::Mixed {
354 implicit_suggestions,
355 explicit_anonymous_suggestions,
356 tool_only: false,
357 }
358 });
359
360 tracing::debug!(
361 ?suggest_change_to_mixed_implicit,
362 ?suggest_change_to_mixed_explicit_anonymous,
363 ?mixed_suggestion,
364 );
365
366 let should_suggest_implicit =
367 !suggest_change_to_implicit.is_empty() &&
369 allow_suggesting_implicit &&
371 !is_bound_static;
373
374 let implicit_suggestion = should_suggest_implicit.then(|| {
375 let suggestions = make_implicit_suggestions(&suggest_change_to_implicit);
376
377 lints::MismatchedLifetimeSyntaxesSuggestion::Implicit { suggestions, tool_only: false }
378 });
379
380 tracing::debug!(
381 ?should_suggest_implicit,
382 ?suggest_change_to_implicit,
383 allow_suggesting_implicit,
384 ?implicit_suggestion,
385 );
386
387 let should_suggest_explicit_anonymous =
388 !suggest_change_to_explicit_anonymous.is_empty() &&
390 !is_bound_static;
392
393 let explicit_anonymous_suggestion = should_suggest_explicit_anonymous
394 .then(|| build_mismatch_suggestion("'_", &suggest_change_to_explicit_anonymous));
395
396 tracing::debug!(
397 ?should_suggest_explicit_anonymous,
398 ?suggest_change_to_explicit_anonymous,
399 ?explicit_anonymous_suggestion,
400 );
401
402 let lifetime_name = bound_lifetime.map(|info| info.lifetime_name()).unwrap_or("'_").to_owned();
403
404 let mut suggestions = Vec::new();
409 suggestions.extend(explicit_bound_suggestion);
410 suggestions.extend(mixed_suggestion);
411 suggestions.extend(implicit_suggestion);
412 suggestions.extend(explicit_anonymous_suggestion);
413
414 cx.emit_span_lint(
415 MISMATCHED_LIFETIME_SYNTAXES,
416 Vec::clone(&inputs),
417 lints::MismatchedLifetimeSyntaxes { lifetime_name, inputs, outputs, suggestions },
418 );
419}
420
421fn build_mismatch_suggestion(
422 lifetime_name: &str,
423 infos: &[&Info<'_>],
424) -> lints::MismatchedLifetimeSyntaxesSuggestion {
425 let lifetime_name_sugg = lifetime_name.to_owned();
426
427 let suggestions = infos.iter().map(|info| info.suggestion(&lifetime_name)).collect();
428
429 lints::MismatchedLifetimeSyntaxesSuggestion::Explicit {
430 lifetime_name_sugg,
431 suggestions,
432 tool_only: false,
433 }
434}
435
436#[derive(Debug)]
437struct Info<'tcx> {
438 type_span: Span,
439 referenced_type_span: Option<Span>,
440 lifetime: &'tcx hir::Lifetime,
441}
442
443impl<'tcx> Info<'tcx> {
444 fn lifetime_name(&self) -> &str {
445 self.lifetime.ident.as_str()
446 }
447
448 fn is_static(&self) -> bool {
449 self.lifetime.is_static()
450 }
451
452 fn reporting_span(&self) -> Span {
456 if self.lifetime.is_implicit() { self.type_span } else { self.lifetime.ident.span }
457 }
458
459 fn removing_span(&self) -> Span {
473 let mut span = self.suggestion("'dummy").0;
474
475 if let Some(referenced_type_span) = self.referenced_type_span {
476 span = span.until(referenced_type_span);
477 }
478
479 span
480 }
481
482 fn suggestion(&self, lifetime_name: &str) -> (Span, String) {
483 self.lifetime.suggestion(lifetime_name)
484 }
485}
486
487type LifetimeInfoMap<'tcx> = FxIndexMap<&'tcx hir::LifetimeKind, Vec<Info<'tcx>>>;
488
489struct LifetimeInfoCollector<'a, 'tcx> {
490 type_span: Span,
491 referenced_type_span: Option<Span>,
492 map: &'a mut LifetimeInfoMap<'tcx>,
493}
494
495impl<'a, 'tcx> LifetimeInfoCollector<'a, 'tcx> {
496 fn collect(ty: &'tcx hir::Ty<'tcx>, map: &'a mut LifetimeInfoMap<'tcx>) {
497 let mut this = Self { type_span: ty.span, referenced_type_span: None, map };
498
499 intravisit::walk_unambig_ty(&mut this, ty);
500 }
501}
502
503impl<'a, 'tcx> Visitor<'tcx> for LifetimeInfoCollector<'a, 'tcx> {
504 #[instrument(skip(self))]
505 fn visit_lifetime(&mut self, lifetime: &'tcx hir::Lifetime) {
506 let type_span = self.type_span;
507 let referenced_type_span = self.referenced_type_span;
508
509 let info = Info { type_span, referenced_type_span, lifetime };
510
511 self.map.entry(&lifetime.kind).or_default().push(info);
512 }
513
514 #[instrument(skip(self))]
515 fn visit_ty(&mut self, ty: &'tcx hir::Ty<'tcx, hir::AmbigArg>) -> Self::Result {
516 let old_type_span = self.type_span;
517 let old_referenced_type_span = self.referenced_type_span;
518
519 self.type_span = ty.span;
520 if let hir::TyKind::Ref(_, ty) = ty.kind {
521 self.referenced_type_span = Some(ty.ty.span);
522 }
523
524 intravisit::walk_ty(self, ty);
525
526 self.type_span = old_type_span;
527 self.referenced_type_span = old_referenced_type_span;
528 }
529}