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        let mut input_map = Default::default();
88        let mut output_map = Default::default();
89
90        for input in fd.inputs {
91            LifetimeInfoCollector::collect(input, &mut input_map);
92        }
93
94        if let hir::FnRetTy::Return(output) = fd.output {
95            LifetimeInfoCollector::collect(output, &mut output_map);
96        }
97
98        report_mismatches(cx, &input_map, &output_map);
99    }
100}
101
102#[instrument(skip_all)]
103fn report_mismatches<'tcx>(
104    cx: &LateContext<'tcx>,
105    inputs: &LifetimeInfoMap<'tcx>,
106    outputs: &LifetimeInfoMap<'tcx>,
107) {
108    for (resolved_lifetime, output_info) in outputs {
109        if let Some(input_info) = inputs.get(resolved_lifetime) {
110            if !lifetimes_use_matched_syntax(input_info, output_info) {
111                emit_mismatch_diagnostic(cx, input_info, output_info);
112            }
113        }
114    }
115}
116
117fn lifetimes_use_matched_syntax(input_info: &[Info<'_>], output_info: &[Info<'_>]) -> bool {
118    // Categorize lifetimes into source/syntax buckets.
119    let mut n_hidden = 0;
120    let mut n_elided = 0;
121    let mut n_named = 0;
122
123    for info in input_info.iter().chain(output_info) {
124        use LifetimeSource::*;
125        use hir::LifetimeSyntax::*;
126
127        let syntax_source = (info.lifetime.syntax, info.lifetime.source);
128
129        match syntax_source {
130            // Ignore any other kind of lifetime.
131            (_, Other) => continue,
132
133            // E.g. `&T`.
134            (Implicit, Reference | OutlivesBound | PreciseCapturing) |
135            // E.g. `&'_ T`.
136            (ExplicitAnonymous, Reference | OutlivesBound | PreciseCapturing) |
137            // E.g. `ContainsLifetime<'_>`.
138            (ExplicitAnonymous, Path { .. }) => n_elided += 1,
139
140            // E.g. `ContainsLifetime`.
141            (Implicit, Path { .. }) => n_hidden += 1,
142
143            // E.g. `&'a T`.
144            (ExplicitBound, Reference | OutlivesBound | PreciseCapturing) |
145            // E.g. `ContainsLifetime<'a>`.
146            (ExplicitBound, Path { .. }) => n_named += 1,
147        };
148    }
149
150    let syntax_counts = (n_hidden, n_elided, n_named);
151    tracing::debug!(?syntax_counts);
152
153    matches!(syntax_counts, (_, 0, 0) | (0, _, 0) | (0, 0, _))
154}
155
156fn emit_mismatch_diagnostic<'tcx>(
157    cx: &LateContext<'tcx>,
158    input_info: &[Info<'_>],
159    output_info: &[Info<'_>],
160) {
161    // There can only ever be zero or one bound lifetime
162    // for a given lifetime resolution.
163    let mut bound_lifetime = None;
164
165    // We offer the following kinds of suggestions (when appropriate
166    // such that the suggestion wouldn't violate the lint):
167    //
168    // 1. Every lifetime becomes named, when there is already a
169    //    user-provided name.
170    //
171    // 2. A "mixed" signature, where references become implicit
172    //    and paths become explicitly anonymous.
173    //
174    // 3. Every lifetime becomes implicit.
175    //
176    // 4. Every lifetime becomes explicitly anonymous.
177    //
178    // Number 2 is arguably the most common pattern and the one we
179    // should push strongest. Number 3 is likely the next most common,
180    // followed by number 1. Coming in at a distant last would be
181    // number 4.
182    //
183    // Beyond these, there are variants of acceptable signatures that
184    // we won't suggest because they are very low-value. For example,
185    // we will never suggest `fn(&T1, &'_ T2) -> &T3` even though that
186    // would pass the lint.
187    //
188    // The following collections are the lifetime instances that we
189    // suggest changing to a given alternate style.
190
191    // 1. Convert all to named.
192    let mut suggest_change_to_explicit_bound = Vec::new();
193
194    // 2. Convert to mixed. We track each kind of change separately.
195    let mut suggest_change_to_mixed_implicit = Vec::new();
196    let mut suggest_change_to_mixed_explicit_anonymous = Vec::new();
197
198    // 3. Convert all to implicit.
199    let mut suggest_change_to_implicit = Vec::new();
200
201    // 4. Convert all to explicit anonymous.
202    let mut suggest_change_to_explicit_anonymous = Vec::new();
203
204    // Some styles prevent using implicit syntax at all.
205    let mut allow_suggesting_implicit = true;
206
207    // It only makes sense to suggest mixed if we have both sources.
208    let mut saw_a_reference = false;
209    let mut saw_a_path = false;
210
211    for info in input_info.iter().chain(output_info) {
212        use LifetimeSource::*;
213        use hir::LifetimeSyntax::*;
214
215        let syntax_source = (info.lifetime.syntax, info.lifetime.source);
216
217        if let (_, Other) = syntax_source {
218            // Ignore any other kind of lifetime.
219            continue;
220        }
221
222        if let (ExplicitBound, _) = syntax_source {
223            bound_lifetime = Some(info);
224        }
225
226        match syntax_source {
227            // E.g. `&T`.
228            (Implicit, Reference) => {
229                suggest_change_to_explicit_anonymous.push(info);
230                suggest_change_to_explicit_bound.push(info);
231            }
232
233            // E.g. `&'_ T`.
234            (ExplicitAnonymous, Reference) => {
235                suggest_change_to_implicit.push(info);
236                suggest_change_to_mixed_implicit.push(info);
237                suggest_change_to_explicit_bound.push(info);
238            }
239
240            // E.g. `ContainsLifetime`.
241            (Implicit, Path { .. }) => {
242                suggest_change_to_mixed_explicit_anonymous.push(info);
243                suggest_change_to_explicit_anonymous.push(info);
244                suggest_change_to_explicit_bound.push(info);
245            }
246
247            // E.g. `ContainsLifetime<'_>`.
248            (ExplicitAnonymous, Path { .. }) => {
249                suggest_change_to_explicit_bound.push(info);
250            }
251
252            // E.g. `&'a T`.
253            (ExplicitBound, Reference) => {
254                suggest_change_to_implicit.push(info);
255                suggest_change_to_mixed_implicit.push(info);
256                suggest_change_to_explicit_anonymous.push(info);
257            }
258
259            // E.g. `ContainsLifetime<'a>`.
260            (ExplicitBound, Path { .. }) => {
261                suggest_change_to_mixed_explicit_anonymous.push(info);
262                suggest_change_to_explicit_anonymous.push(info);
263            }
264
265            (Implicit, OutlivesBound | PreciseCapturing) => {
266                panic!("This syntax / source combination is not possible");
267            }
268
269            // E.g. `+ '_`, `+ use<'_>`.
270            (ExplicitAnonymous, OutlivesBound | PreciseCapturing) => {
271                suggest_change_to_explicit_bound.push(info);
272            }
273
274            // E.g. `+ 'a`, `+ use<'a>`.
275            (ExplicitBound, OutlivesBound | PreciseCapturing) => {
276                suggest_change_to_mixed_explicit_anonymous.push(info);
277                suggest_change_to_explicit_anonymous.push(info);
278            }
279
280            (_, Other) => {
281                panic!("This syntax / source combination has already been skipped");
282            }
283        }
284
285        if matches!(syntax_source, (_, Path { .. } | OutlivesBound | PreciseCapturing)) {
286            allow_suggesting_implicit = false;
287        }
288
289        match syntax_source {
290            (_, Reference) => saw_a_reference = true,
291            (_, Path { .. }) => saw_a_path = true,
292            _ => {}
293        }
294    }
295
296    let make_implicit_suggestions =
297        |infos: &[&Info<'_>]| infos.iter().map(|i| i.removing_span()).collect::<Vec<_>>();
298
299    let inputs = input_info.iter().map(|info| info.reporting_span()).collect();
300    let outputs = output_info.iter().map(|info| info.reporting_span()).collect();
301
302    let explicit_bound_suggestion = bound_lifetime.map(|info| {
303        build_mismatch_suggestion(info.lifetime_name(), &suggest_change_to_explicit_bound)
304    });
305
306    let is_bound_static = bound_lifetime.is_some_and(|info| info.is_static());
307
308    tracing::debug!(?bound_lifetime, ?explicit_bound_suggestion, ?is_bound_static);
309
310    let should_suggest_mixed =
311        // Do we have a mixed case?
312        (saw_a_reference && saw_a_path) &&
313        // Is there anything to change?
314        (!suggest_change_to_mixed_implicit.is_empty() ||
315         !suggest_change_to_mixed_explicit_anonymous.is_empty()) &&
316        // If we have `'static`, we don't want to remove it.
317        !is_bound_static;
318
319    let mixed_suggestion = should_suggest_mixed.then(|| {
320        let implicit_suggestions = make_implicit_suggestions(&suggest_change_to_mixed_implicit);
321
322        let explicit_anonymous_suggestions = suggest_change_to_mixed_explicit_anonymous
323            .iter()
324            .map(|info| info.suggestion("'_"))
325            .collect();
326
327        lints::MismatchedLifetimeSyntaxesSuggestion::Mixed {
328            implicit_suggestions,
329            explicit_anonymous_suggestions,
330            tool_only: false,
331        }
332    });
333
334    tracing::debug!(
335        ?suggest_change_to_mixed_implicit,
336        ?suggest_change_to_mixed_explicit_anonymous,
337        ?mixed_suggestion,
338    );
339
340    let should_suggest_implicit =
341        // Is there anything to change?
342        !suggest_change_to_implicit.is_empty() &&
343        // We never want to hide the lifetime in a path (or similar).
344        allow_suggesting_implicit &&
345        // If we have `'static`, we don't want to remove it.
346        !is_bound_static;
347
348    let implicit_suggestion = should_suggest_implicit.then(|| {
349        let suggestions = make_implicit_suggestions(&suggest_change_to_implicit);
350
351        lints::MismatchedLifetimeSyntaxesSuggestion::Implicit { suggestions, tool_only: false }
352    });
353
354    tracing::debug!(
355        ?should_suggest_implicit,
356        ?suggest_change_to_implicit,
357        allow_suggesting_implicit,
358        ?implicit_suggestion,
359    );
360
361    let should_suggest_explicit_anonymous =
362        // Is there anything to change?
363        !suggest_change_to_explicit_anonymous.is_empty() &&
364        // If we have `'static`, we don't want to remove it.
365        !is_bound_static;
366
367    let explicit_anonymous_suggestion = should_suggest_explicit_anonymous
368        .then(|| build_mismatch_suggestion("'_", &suggest_change_to_explicit_anonymous));
369
370    tracing::debug!(
371        ?should_suggest_explicit_anonymous,
372        ?suggest_change_to_explicit_anonymous,
373        ?explicit_anonymous_suggestion,
374    );
375
376    let lifetime_name = bound_lifetime.map(|info| info.lifetime_name()).unwrap_or("'_").to_owned();
377
378    // We can produce a number of suggestions which may overwhelm
379    // the user. Instead, we order the suggestions based on Rust
380    // idioms. The "best" choice is shown to the user and the
381    // remaining choices are shown to tools only.
382    let mut suggestions = Vec::new();
383    suggestions.extend(explicit_bound_suggestion);
384    suggestions.extend(mixed_suggestion);
385    suggestions.extend(implicit_suggestion);
386    suggestions.extend(explicit_anonymous_suggestion);
387
388    cx.emit_span_lint(
389        MISMATCHED_LIFETIME_SYNTAXES,
390        Vec::clone(&inputs),
391        lints::MismatchedLifetimeSyntaxes { lifetime_name, inputs, outputs, suggestions },
392    );
393}
394
395fn build_mismatch_suggestion(
396    lifetime_name: &str,
397    infos: &[&Info<'_>],
398) -> lints::MismatchedLifetimeSyntaxesSuggestion {
399    let lifetime_name = lifetime_name.to_owned();
400
401    let suggestions = infos.iter().map(|info| info.suggestion(&lifetime_name)).collect();
402
403    lints::MismatchedLifetimeSyntaxesSuggestion::Explicit {
404        lifetime_name,
405        suggestions,
406        tool_only: false,
407    }
408}
409
410#[derive(Debug)]
411struct Info<'tcx> {
412    type_span: Span,
413    referenced_type_span: Option<Span>,
414    lifetime: &'tcx hir::Lifetime,
415}
416
417impl<'tcx> Info<'tcx> {
418    fn lifetime_name(&self) -> &str {
419        self.lifetime.ident.as_str()
420    }
421
422    fn is_static(&self) -> bool {
423        self.lifetime.is_static()
424    }
425
426    /// When reporting a lifetime that is implicit, we expand the span
427    /// to include the type. Otherwise we end up pointing at nothing,
428    /// which is a bit confusing.
429    fn reporting_span(&self) -> Span {
430        if self.lifetime.is_implicit() { self.type_span } else { self.lifetime.ident.span }
431    }
432
433    /// When removing an explicit lifetime from a reference,
434    /// we want to remove the whitespace after the lifetime.
435    ///
436    /// ```rust
437    /// fn x(a: &'_ u8) {}
438    /// ```
439    ///
440    /// Should become:
441    ///
442    /// ```rust
443    /// fn x(a: &u8) {}
444    /// ```
445    // FIXME: Ideally, we'd also remove the lifetime declaration.
446    fn removing_span(&self) -> Span {
447        let mut span = self.suggestion("'dummy").0;
448
449        if let Some(referenced_type_span) = self.referenced_type_span {
450            span = span.until(referenced_type_span);
451        }
452
453        span
454    }
455
456    fn suggestion(&self, lifetime_name: &str) -> (Span, String) {
457        self.lifetime.suggestion(lifetime_name)
458    }
459}
460
461type LifetimeInfoMap<'tcx> = FxIndexMap<&'tcx hir::LifetimeKind, Vec<Info<'tcx>>>;
462
463struct LifetimeInfoCollector<'a, 'tcx> {
464    type_span: Span,
465    referenced_type_span: Option<Span>,
466    map: &'a mut LifetimeInfoMap<'tcx>,
467}
468
469impl<'a, 'tcx> LifetimeInfoCollector<'a, 'tcx> {
470    fn collect(ty: &'tcx hir::Ty<'tcx>, map: &'a mut LifetimeInfoMap<'tcx>) {
471        let mut this = Self { type_span: ty.span, referenced_type_span: None, map };
472
473        intravisit::walk_unambig_ty(&mut this, ty);
474    }
475}
476
477impl<'a, 'tcx> Visitor<'tcx> for LifetimeInfoCollector<'a, 'tcx> {
478    #[instrument(skip(self))]
479    fn visit_lifetime(&mut self, lifetime: &'tcx hir::Lifetime) {
480        let type_span = self.type_span;
481        let referenced_type_span = self.referenced_type_span;
482
483        let info = Info { type_span, referenced_type_span, lifetime };
484
485        self.map.entry(&lifetime.kind).or_default().push(info);
486    }
487
488    #[instrument(skip(self))]
489    fn visit_ty(&mut self, ty: &'tcx hir::Ty<'tcx, hir::AmbigArg>) -> Self::Result {
490        let old_type_span = self.type_span;
491        let old_referenced_type_span = self.referenced_type_span;
492
493        self.type_span = ty.span;
494        if let hir::TyKind::Ref(_, ty) = ty.kind {
495            self.referenced_type_span = Some(ty.ty.span);
496        }
497
498        intravisit::walk_ty(self, ty);
499
500        self.type_span = old_type_span;
501        self.referenced_type_span = old_referenced_type_span;
502    }
503}