rustc_lint/
lifetime_syntax.rs

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    /// The `mismatched_lifetime_syntaxes` lint detects when the same
12    /// lifetime is referred to by different syntaxes between function
13    /// arguments and return values.
14    ///
15    /// The three kinds of syntaxes are:
16    ///
17    /// 1. Named lifetimes. These are references (`&'a str`) or paths
18    ///    (`Person<'a>`) that use a lifetime with a name, such as
19    ///    `'static` or `'a`.
20    ///
21    /// 2. Elided lifetimes. These are references with no explicit
22    ///    lifetime (`&str`), references using the anonymous lifetime
23    ///    (`&'_ str`), and paths using the anonymous lifetime
24    ///    (`Person<'_>`).
25    ///
26    /// 3. Hidden lifetimes. These are paths that do not contain any
27    ///    visual indication that it contains a lifetime (`Person`).
28    ///
29    /// ### Example
30    ///
31    /// ```rust,compile_fail
32    /// #![deny(mismatched_lifetime_syntaxes)]
33    ///
34    /// pub fn mixing_named_with_elided(v: &'static u8) -> &u8 {
35    ///     v
36    /// }
37    ///
38    /// struct Person<'a> {
39    ///     name: &'a str,
40    /// }
41    ///
42    /// pub fn mixing_hidden_with_elided(v: Person) -> Person<'_> {
43    ///     v
44    /// }
45    ///
46    /// struct Foo;
47    ///
48    /// impl Foo {
49    ///     // Lifetime elision results in the output lifetime becoming
50    ///     // `'static`, which is not what was intended.
51    ///     pub fn get_mut(&'static self, x: &mut u8) -> &mut u8 {
52    ///         unsafe { &mut *(x as *mut _) }
53    ///     }
54    /// }
55    /// ```
56    ///
57    /// {{produces}}
58    ///
59    /// ### Explanation
60    ///
61    /// Lifetime elision is useful because it frees you from having to
62    /// give each lifetime its own name and show the relation of input
63    /// and output lifetimes for common cases. However, a lifetime
64    /// that uses inconsistent syntax between related arguments and
65    /// return values is more confusing.
66    ///
67    /// In certain `unsafe` code, lifetime elision combined with
68    /// inconsistent lifetime syntax may result in unsound code.
69    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            // E.g. `&T`.
157            (Implicit, Reference) |
158            // E.g. `&'_ T`.
159            (ExplicitAnonymous, Reference) |
160            // E.g. `ContainsLifetime<'_>`.
161            (ExplicitAnonymous, Path { .. }) |
162            // E.g. `+ '_`, `+ use<'_>`.
163            (ExplicitAnonymous, OutlivesBound | PreciseCapturing) => {
164                Some(Self::Elided)
165            }
166
167            // E.g. `ContainsLifetime`.
168            (Implicit, Path { .. }) => {
169                Some(Self::Hidden)
170            }
171
172            // E.g. `&'a T`.
173            (ExplicitBound, Reference) |
174            // E.g. `ContainsLifetime<'a>`.
175            (ExplicitBound, Path { .. }) |
176            // E.g. `+ 'a`, `+ use<'a>`.
177            (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    // There can only ever be zero or one bound lifetime
260    // for a given lifetime resolution.
261    let mut bound_lifetime = None;
262
263    // We offer the following kinds of suggestions (when appropriate
264    // such that the suggestion wouldn't violate the lint):
265    //
266    // 1. Every lifetime becomes named, when there is already a
267    //    user-provided name.
268    //
269    // 2. A "mixed" signature, where references become implicit
270    //    and paths become explicitly anonymous.
271    //
272    // 3. Every lifetime becomes implicit.
273    //
274    // 4. Every lifetime becomes explicitly anonymous.
275    //
276    // Number 2 is arguably the most common pattern and the one we
277    // should push strongest. Number 3 is likely the next most common,
278    // followed by number 1. Coming in at a distant last would be
279    // number 4.
280    //
281    // Beyond these, there are variants of acceptable signatures that
282    // we won't suggest because they are very low-value. For example,
283    // we will never suggest `fn(&T1, &'_ T2) -> &T3` even though that
284    // would pass the lint.
285    //
286    // The following collections are the lifetime instances that we
287    // suggest changing to a given alternate style.
288
289    // 1. Convert all to named.
290    let mut suggest_change_to_explicit_bound = Vec::new();
291
292    // 2. Convert to mixed. We track each kind of change separately.
293    let mut suggest_change_to_mixed_implicit = Vec::new();
294    let mut suggest_change_to_mixed_explicit_anonymous = Vec::new();
295
296    // 3. Convert all to implicit.
297    let mut suggest_change_to_implicit = Vec::new();
298
299    // 4. Convert all to explicit anonymous.
300    let mut suggest_change_to_explicit_anonymous = Vec::new();
301
302    // Some styles prevent using implicit syntax at all.
303    let mut allow_suggesting_implicit = true;
304
305    // It only makes sense to suggest mixed if we have both sources.
306    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            // Ignore any other kind of lifetime.
317            continue;
318        }
319
320        if let (ExplicitBound, _) = syntax_source {
321            bound_lifetime = Some(info);
322        }
323
324        match syntax_source {
325            // E.g. `&T`.
326            (Implicit, Reference) => {
327                suggest_change_to_explicit_anonymous.push(info);
328                suggest_change_to_explicit_bound.push(info);
329            }
330
331            // E.g. `&'_ T`.
332            (ExplicitAnonymous, Reference) => {
333                suggest_change_to_implicit.push(info);
334                suggest_change_to_explicit_bound.push(info);
335            }
336
337            // E.g. `ContainsLifetime`.
338            (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            // E.g. `ContainsLifetime<'_>`.
345            (ExplicitAnonymous, Path { .. }) => {
346                suggest_change_to_explicit_bound.push(info);
347            }
348
349            // E.g. `&'a T`.
350            (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            // E.g. `ContainsLifetime<'a>`.
357            (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            // E.g. `+ '_`, `+ use<'_>`.
367            (ExplicitAnonymous, OutlivesBound | PreciseCapturing) => {
368                suggest_change_to_explicit_bound.push(info);
369            }
370
371            // E.g. `+ 'a`, `+ use<'a>`.
372            (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        // Do we have a mixed case?
419        (saw_a_reference && saw_a_path) &&
420        // Is there anything to change?
421        (!suggest_change_to_mixed_implicit.is_empty() ||
422         !suggest_change_to_mixed_explicit_anonymous.is_empty()) &&
423        // If we have `'static`, we don't want to remove it.
424        !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        // Is there anything to change?
449        !suggest_change_to_implicit.is_empty() &&
450        // We never want to hide the lifetime in a path (or similar).
451        allow_suggesting_implicit &&
452        // If we have `'static`, we don't want to remove it.
453        !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        // Is there anything to change?
473        !suggest_change_to_explicit_anonymous.is_empty() &&
474        // If we have `'static`, we don't want to remove it.
475        !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    // We can produce a number of suggestions which may overwhelm
487    // the user. Instead, we order the suggestions based on Rust
488    // idioms. The "best" choice is shown to the user and the
489    // remaining choices are shown to tools only.
490    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    /// When reporting a lifetime that is implicit, we expand the span
543    /// to include the type. Otherwise we end up pointing at nothing,
544    /// which is a bit confusing.
545    fn reporting_span(&self) -> Span {
546        if self.lifetime.is_implicit() { self.type_span } else { self.lifetime.ident.span }
547    }
548
549    /// When removing an explicit lifetime from a reference,
550    /// we want to remove the whitespace after the lifetime.
551    ///
552    /// ```rust
553    /// fn x(a: &'_ u8) {}
554    /// ```
555    ///
556    /// Should become:
557    ///
558    /// ```rust
559    /// fn x(a: &u8) {}
560    /// ```
561    // FIXME: Ideally, we'd also remove the lifetime declaration.
562    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}