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
143fn lifetimes_use_matched_syntax(input_info: &[Info<'_>], output_info: &[Info<'_>]) -> bool {
144    // Categorize lifetimes into source/syntax buckets.
145    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            // Ignore any other kind of lifetime.
157            (_, Other) => continue,
158
159            // E.g. `&T`.
160            (Implicit, Reference | OutlivesBound | PreciseCapturing) |
161            // E.g. `&'_ T`.
162            (ExplicitAnonymous, Reference | OutlivesBound | PreciseCapturing) |
163            // E.g. `ContainsLifetime<'_>`.
164            (ExplicitAnonymous, Path { .. }) => n_elided += 1,
165
166            // E.g. `ContainsLifetime`.
167            (Implicit, Path { .. }) => n_hidden += 1,
168
169            // E.g. `&'a T`.
170            (ExplicitBound, Reference | OutlivesBound | PreciseCapturing) |
171            // E.g. `ContainsLifetime<'a>`.
172            (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    // There can only ever be zero or one bound lifetime
188    // for a given lifetime resolution.
189    let mut bound_lifetime = None;
190
191    // We offer the following kinds of suggestions (when appropriate
192    // such that the suggestion wouldn't violate the lint):
193    //
194    // 1. Every lifetime becomes named, when there is already a
195    //    user-provided name.
196    //
197    // 2. A "mixed" signature, where references become implicit
198    //    and paths become explicitly anonymous.
199    //
200    // 3. Every lifetime becomes implicit.
201    //
202    // 4. Every lifetime becomes explicitly anonymous.
203    //
204    // Number 2 is arguably the most common pattern and the one we
205    // should push strongest. Number 3 is likely the next most common,
206    // followed by number 1. Coming in at a distant last would be
207    // number 4.
208    //
209    // Beyond these, there are variants of acceptable signatures that
210    // we won't suggest because they are very low-value. For example,
211    // we will never suggest `fn(&T1, &'_ T2) -> &T3` even though that
212    // would pass the lint.
213    //
214    // The following collections are the lifetime instances that we
215    // suggest changing to a given alternate style.
216
217    // 1. Convert all to named.
218    let mut suggest_change_to_explicit_bound = Vec::new();
219
220    // 2. Convert to mixed. We track each kind of change separately.
221    let mut suggest_change_to_mixed_implicit = Vec::new();
222    let mut suggest_change_to_mixed_explicit_anonymous = Vec::new();
223
224    // 3. Convert all to implicit.
225    let mut suggest_change_to_implicit = Vec::new();
226
227    // 4. Convert all to explicit anonymous.
228    let mut suggest_change_to_explicit_anonymous = Vec::new();
229
230    // Some styles prevent using implicit syntax at all.
231    let mut allow_suggesting_implicit = true;
232
233    // It only makes sense to suggest mixed if we have both sources.
234    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            // Ignore any other kind of lifetime.
245            continue;
246        }
247
248        if let (ExplicitBound, _) = syntax_source {
249            bound_lifetime = Some(info);
250        }
251
252        match syntax_source {
253            // E.g. `&T`.
254            (Implicit, Reference) => {
255                suggest_change_to_explicit_anonymous.push(info);
256                suggest_change_to_explicit_bound.push(info);
257            }
258
259            // E.g. `&'_ T`.
260            (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            // E.g. `ContainsLifetime`.
267            (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            // E.g. `ContainsLifetime<'_>`.
274            (ExplicitAnonymous, Path { .. }) => {
275                suggest_change_to_explicit_bound.push(info);
276            }
277
278            // E.g. `&'a T`.
279            (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            // E.g. `ContainsLifetime<'a>`.
286            (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            // E.g. `+ '_`, `+ use<'_>`.
296            (ExplicitAnonymous, OutlivesBound | PreciseCapturing) => {
297                suggest_change_to_explicit_bound.push(info);
298            }
299
300            // E.g. `+ 'a`, `+ use<'a>`.
301            (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        // Do we have a mixed case?
338        (saw_a_reference && saw_a_path) &&
339        // Is there anything to change?
340        (!suggest_change_to_mixed_implicit.is_empty() ||
341         !suggest_change_to_mixed_explicit_anonymous.is_empty()) &&
342        // If we have `'static`, we don't want to remove it.
343        !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        // Is there anything to change?
368        !suggest_change_to_implicit.is_empty() &&
369        // We never want to hide the lifetime in a path (or similar).
370        allow_suggesting_implicit &&
371        // If we have `'static`, we don't want to remove it.
372        !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        // Is there anything to change?
389        !suggest_change_to_explicit_anonymous.is_empty() &&
390        // If we have `'static`, we don't want to remove it.
391        !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    // We can produce a number of suggestions which may overwhelm
405    // the user. Instead, we order the suggestions based on Rust
406    // idioms. The "best" choice is shown to the user and the
407    // remaining choices are shown to tools only.
408    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    /// When reporting a lifetime that is implicit, we expand the span
453    /// to include the type. Otherwise we end up pointing at nothing,
454    /// which is a bit confusing.
455    fn reporting_span(&self) -> Span {
456        if self.lifetime.is_implicit() { self.type_span } else { self.lifetime.ident.span }
457    }
458
459    /// When removing an explicit lifetime from a reference,
460    /// we want to remove the whitespace after the lifetime.
461    ///
462    /// ```rust
463    /// fn x(a: &'_ u8) {}
464    /// ```
465    ///
466    /// Should become:
467    ///
468    /// ```rust
469    /// fn x(a: &u8) {}
470    /// ```
471    // FIXME: Ideally, we'd also remove the lifetime declaration.
472    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}