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 rustc_span::def_id::LocalDefId;
7use tracing::instrument;
8
9use crate::{LateContext, LateLintPass, LintContext, lints};
10
11declare_lint! {
12    /// The `mismatched_lifetime_syntaxes` lint detects when the same
13    /// lifetime is referred to by different syntaxes between function
14    /// arguments and return values.
15    ///
16    /// The three kinds of syntaxes are:
17    ///
18    /// 1. Named lifetimes. These are references (`&'a str`) or paths
19    ///    (`Person<'a>`) that use a lifetime with a name, such as
20    ///    `'static` or `'a`.
21    ///
22    /// 2. Elided lifetimes. These are references with no explicit
23    ///    lifetime (`&str`), references using the anonymous lifetime
24    ///    (`&'_ str`), and paths using the anonymous lifetime
25    ///    (`Person<'_>`).
26    ///
27    /// 3. Hidden lifetimes. These are paths that do not contain any
28    ///    visual indication that it contains a lifetime (`Person`).
29    ///
30    /// ### Example
31    ///
32    /// ```rust,compile_fail
33    /// #![deny(mismatched_lifetime_syntaxes)]
34    ///
35    /// pub fn mixing_named_with_elided(v: &'static u8) -> &u8 {
36    ///     v
37    /// }
38    ///
39    /// struct Person<'a> {
40    ///     name: &'a str,
41    /// }
42    ///
43    /// pub fn mixing_hidden_with_elided(v: Person) -> Person<'_> {
44    ///     v
45    /// }
46    ///
47    /// struct Foo;
48    ///
49    /// impl Foo {
50    ///     // Lifetime elision results in the output lifetime becoming
51    ///     // `'static`, which is not what was intended.
52    ///     pub fn get_mut(&'static self, x: &mut u8) -> &mut u8 {
53    ///         unsafe { &mut *(x as *mut _) }
54    ///     }
55    /// }
56    /// ```
57    ///
58    /// {{produces}}
59    ///
60    /// ### Explanation
61    ///
62    /// Lifetime elision is useful because it frees you from having to
63    /// give each lifetime its own name and show the relation of input
64    /// and output lifetimes for common cases. However, a lifetime
65    /// that uses inconsistent syntax between related arguments and
66    /// return values is more confusing.
67    ///
68    /// In certain `unsafe` code, lifetime elision combined with
69    /// inconsistent lifetime syntax may result in unsound code.
70    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            // E.g. `&T`.
166            (Implicit, Reference) |
167            // E.g. `&'_ T`.
168            (ExplicitAnonymous, Reference) |
169            // E.g. `ContainsLifetime<'_>`.
170            (ExplicitAnonymous, Path { .. }) |
171            // E.g. `+ '_`, `+ use<'_>`.
172            (ExplicitAnonymous, OutlivesBound | PreciseCapturing) => {
173                Some(Self::Elided)
174            }
175
176            // E.g. `ContainsLifetime`.
177            (Implicit, Path { .. }) => {
178                Some(Self::Hidden)
179            }
180
181            // E.g. `&'a T`.
182            (ExplicitBound, Reference) |
183            // E.g. `ContainsLifetime<'a>`.
184            (ExplicitBound, Path { .. }) |
185            // E.g. `+ 'a`, `+ use<'a>`.
186            (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    // There can only ever be zero or one bound lifetime
255    // for a given lifetime resolution.
256    let mut bound_lifetime = None;
257
258    // We offer the following kinds of suggestions (when appropriate
259    // such that the suggestion wouldn't violate the lint):
260    //
261    // 1. Every lifetime becomes named, when there is already a
262    //    user-provided name.
263    //
264    // 2. A "mixed" signature, where references become implicit
265    //    and paths become explicitly anonymous.
266    //
267    // 3. Every lifetime becomes implicit.
268    //
269    // 4. Every lifetime becomes explicitly anonymous.
270    //
271    // Number 2 is arguably the most common pattern and the one we
272    // should push strongest. Number 3 is likely the next most common,
273    // followed by number 1. Coming in at a distant last would be
274    // number 4.
275    //
276    // Beyond these, there are variants of acceptable signatures that
277    // we won't suggest because they are very low-value. For example,
278    // we will never suggest `fn(&T1, &'_ T2) -> &T3` even though that
279    // would pass the lint.
280    //
281    // The following collections are the lifetime instances that we
282    // suggest changing to a given alternate style.
283
284    // 1. Convert all to named.
285    let mut suggest_change_to_explicit_bound = Vec::new();
286
287    // 2. Convert to mixed. We track each kind of change separately.
288    let mut suggest_change_to_mixed_implicit = Vec::new();
289    let mut suggest_change_to_mixed_explicit_anonymous = Vec::new();
290
291    // 3. Convert all to implicit.
292    let mut suggest_change_to_implicit = Vec::new();
293
294    // 4. Convert all to explicit anonymous.
295    let mut suggest_change_to_explicit_anonymous = Vec::new();
296
297    // Some styles prevent using implicit syntax at all.
298    let mut allow_suggesting_implicit = true;
299
300    // It only makes sense to suggest mixed if we have both sources.
301    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            // E.g. `&T`.
316            (Implicit, Reference) => {
317                suggest_change_to_explicit_anonymous.push(info);
318                suggest_change_to_explicit_bound.push(info);
319            }
320
321            // E.g. `&'_ T`.
322            (ExplicitAnonymous, Reference) => {
323                suggest_change_to_implicit.push(info);
324                suggest_change_to_explicit_bound.push(info);
325            }
326
327            // E.g. `ContainsLifetime`.
328            (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            // E.g. `ContainsLifetime<'_>`, `+ '_`, `+ use<'_>`.
335            (ExplicitAnonymous, Path { .. } | OutlivesBound | PreciseCapturing) => {
336                suggest_change_to_explicit_bound.push(info);
337            }
338
339            // E.g. `&'a T`.
340            (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            // E.g. `ContainsLifetime<'a>`, `+ 'a`, `+ use<'a>`.
347            (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        // Do we have a mixed case?
396        (saw_a_reference && saw_a_path) &&
397        // Is there anything to change?
398        (!suggest_change_to_mixed_implicit.is_empty() ||
399         !suggest_change_to_mixed_explicit_anonymous.is_empty()) &&
400        // If we have `'static`, we don't want to remove it.
401        !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        // Is there anything to change?
426        !suggest_change_to_implicit.is_empty() &&
427        // We never want to hide the lifetime in a path (or similar).
428        allow_suggesting_implicit &&
429        // If we have `'static`, we don't want to remove it.
430        !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        // Is there anything to change?
450        !suggest_change_to_explicit_anonymous.is_empty() &&
451        // If we have `'static`, we don't want to remove it.
452        !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    // We can produce a number of suggestions which may overwhelm
464    // the user. Instead, we order the suggestions based on Rust
465    // idioms. The "best" choice is shown to the user and the
466    // remaining choices are shown to tools only.
467    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    /// When reporting a lifetime that is implicit, we expand the span
504    /// to include the type. Otherwise we end up pointing at nothing,
505    /// which is a bit confusing.
506    fn reporting_span(&self) -> Span {
507        if self.lifetime.is_implicit() { self.ty.span } else { self.lifetime.ident.span }
508    }
509
510    /// When removing an explicit lifetime from a reference,
511    /// we want to remove the whitespace after the lifetime.
512    ///
513    /// ```rust
514    /// fn x(a: &'_ u8) {}
515    /// ```
516    ///
517    /// Should become:
518    ///
519    /// ```rust
520    /// fn x(a: &u8) {}
521    /// ```
522    // FIXME: Ideally, we'd also remove the lifetime declaration.
523    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}