Skip to content

Commit 18a161d

Browse files
committed
Introduce the mismatched_lifetime_syntaxes lint
1 parent 30c4ab1 commit 18a161d

File tree

7 files changed

+1119
-1
lines changed

7 files changed

+1119
-1
lines changed

compiler/rustc_hir/src/hir.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -182,7 +182,7 @@ impl ParamName {
182182
}
183183
}
184184

185-
#[derive(Debug, Copy, Clone, PartialEq, Eq, HashStable_Generic)]
185+
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, HashStable_Generic)]
186186
pub enum LifetimeName {
187187
/// User-given names or fresh (synthetic) names.
188188
Param(LocalDefId),

compiler/rustc_lint/messages.ftl

+19
Original file line numberDiff line numberDiff line change
@@ -504,6 +504,25 @@ lint_metavariable_still_repeating = variable `{$name}` is still repeating at thi
504504
505505
lint_metavariable_wrong_operator = meta-variable repeats with different Kleene operator
506506
507+
lint_mismatched_lifetime_syntaxes =
508+
lifetime flowing from input to output with different syntax
509+
.label_mismatched_lifetime_syntaxes_inputs =
510+
{$n_inputs ->
511+
[one] this lifetime flows
512+
*[other] these lifetimes flow
513+
} to the output
514+
.label_mismatched_lifetime_syntaxes_outputs =
515+
the elided {$n_outputs ->
516+
[one] lifetime gets
517+
*[other] lifetimes get
518+
} resolved as `{$lifetime_name}`
519+
520+
lint_mismatched_lifetime_syntaxes_suggestion_hidden =
521+
one option is to consistently hide the lifetime
522+
523+
lint_mismatched_lifetime_syntaxes_suggestion_named =
524+
one option is to consistently use `{$lifetime_name}`
525+
507526
lint_missing_fragment_specifier = missing fragment specifier
508527
509528
lint_missing_unsafe_on_extern = extern blocks should be unsafe

compiler/rustc_lint/src/lib.rs

+3
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ mod invalid_from_utf8;
5757
mod late;
5858
mod let_underscore;
5959
mod levels;
60+
mod lifetime_style;
6061
mod lints;
6162
mod macro_expr_fragment_specifier_2024_migration;
6263
mod map_unit_fn;
@@ -96,6 +97,7 @@ use impl_trait_overcaptures::ImplTraitOvercaptures;
9697
use internal::*;
9798
use invalid_from_utf8::*;
9899
use let_underscore::*;
100+
use lifetime_style::*;
99101
use macro_expr_fragment_specifier_2024_migration::*;
100102
use map_unit_fn::*;
101103
use multiple_supertrait_upcastable::*;
@@ -244,6 +246,7 @@ late_lint_methods!(
244246
IfLetRescope: IfLetRescope::default(),
245247
StaticMutRefs: StaticMutRefs,
246248
UnqualifiedLocalImports: UnqualifiedLocalImports,
249+
LifetimeStyle: LifetimeStyle,
247250
]
248251
]
249252
);
+301
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,301 @@
1+
use rustc_data_structures::fx::FxIndexMap;
2+
use rustc_hir::intravisit::{self, Visitor};
3+
use rustc_hir::{self as hir, LifetimeSource, LifetimeSyntax};
4+
use rustc_session::{declare_lint, declare_lint_pass};
5+
use rustc_span::Span;
6+
use tracing::instrument;
7+
8+
use crate::{LateContext, LateLintPass, LintContext, lints};
9+
10+
declare_lint! {
11+
/// The `mismatched_lifetime_syntaxes` lint detects when an
12+
/// elided lifetime uses different syntax between function
13+
/// arguments and return values.
14+
///
15+
/// The three kinds of syntax are:
16+
/// 1. Named lifetimes, such as `'static` or `'a`.
17+
/// 2. The anonymous lifetime `'_`.
18+
/// 3. Hidden lifetimes, such as `&u8` or `ThisHasALifetimeGeneric`.
19+
///
20+
/// As an exception, this lint allows references with hidden or
21+
/// anonymous lifetimes to be paired with paths using anonymous
22+
/// lifetimes.
23+
///
24+
/// ### Example
25+
///
26+
/// ```rust,compile_fail
27+
/// #![deny(mismatched_lifetime_syntaxes)]
28+
///
29+
/// pub fn mixing_named_with_hidden(v: &'static u8) -> &u8 {
30+
/// v
31+
/// }
32+
///
33+
/// struct Person<'a> {
34+
/// name: &'a str,
35+
/// }
36+
///
37+
/// pub fn mixing_hidden_with_anonymous(v: Person) -> Person<'_> {
38+
/// v
39+
/// }
40+
///
41+
/// struct Foo;
42+
///
43+
/// impl Foo {
44+
/// // Lifetime elision results in the output lifetime becoming
45+
/// // `'static`, which is not what was intended.
46+
/// pub fn get_mut(&'static self, x: &mut u8) -> &mut u8 {
47+
/// unsafe { &mut *(x as *mut _) }
48+
/// }
49+
/// }
50+
/// ```
51+
///
52+
/// {{produces}}
53+
///
54+
/// ### Explanation
55+
///
56+
/// Lifetime elision is useful because it frees you from having to
57+
/// give each lifetime its own name and explicitly show the
58+
/// relation of input and output lifetimes for common
59+
/// cases. However, a lifetime that uses inconsistent syntax
60+
/// between related arguments and return values is more confusing.
61+
///
62+
/// In certain `unsafe` code, lifetime elision combined with
63+
/// inconsistent lifetime syntax may result in unsound code.
64+
pub MISMATCHED_LIFETIME_SYNTAXES,
65+
Allow,
66+
"detects when an elided lifetime uses different syntax between arguments and return values"
67+
}
68+
69+
declare_lint_pass!(LifetimeStyle => [MISMATCHED_LIFETIME_SYNTAXES]);
70+
71+
impl<'tcx> LateLintPass<'tcx> for LifetimeStyle {
72+
#[instrument(skip_all)]
73+
fn check_fn(
74+
&mut self,
75+
cx: &LateContext<'tcx>,
76+
_: hir::intravisit::FnKind<'tcx>,
77+
fd: &'tcx hir::FnDecl<'tcx>,
78+
_: &'tcx hir::Body<'tcx>,
79+
_: rustc_span::Span,
80+
_: rustc_span::def_id::LocalDefId,
81+
) {
82+
let mut input_map = Default::default();
83+
let mut output_map = Default::default();
84+
85+
for input in fd.inputs {
86+
LifetimeInfoCollector::collect(input, &mut input_map);
87+
}
88+
89+
if let hir::FnRetTy::Return(output) = fd.output {
90+
LifetimeInfoCollector::collect(output, &mut output_map);
91+
}
92+
93+
report_mismatches(cx, &input_map, &output_map);
94+
}
95+
}
96+
97+
#[instrument(skip_all)]
98+
fn report_mismatches<'tcx>(
99+
cx: &LateContext<'tcx>,
100+
inputs: &LifetimeInfoMap<'tcx>,
101+
outputs: &LifetimeInfoMap<'tcx>,
102+
) {
103+
for (resolved_lifetime, output_info) in outputs {
104+
if let Some(input_info) = inputs.get(resolved_lifetime) {
105+
let relevant_lifetimes = input_info.iter().chain(output_info);
106+
107+
// Categorize lifetimes into source/syntax buckets
108+
let mut hidden = Bucket::default();
109+
let mut anonymous = Bucket::default();
110+
let mut named = Bucket::default();
111+
112+
for info in relevant_lifetimes {
113+
use LifetimeSource::*;
114+
use LifetimeSyntax::*;
115+
116+
let bucket = match info.lifetime.syntax {
117+
Hidden => &mut hidden,
118+
Anonymous => &mut anonymous,
119+
Named => &mut named,
120+
};
121+
122+
match info.lifetime.source {
123+
Reference | OutlivesBound | PreciseCapturing => bucket.n_ref += 1,
124+
Path { .. } => bucket.n_path += 1,
125+
Other => {}
126+
}
127+
128+
bucket.members.push(info);
129+
}
130+
131+
// Check if syntaxes are consistent
132+
133+
let syntax_counts = (
134+
hidden.n_ref,
135+
anonymous.n_ref,
136+
named.n_ref,
137+
hidden.n_path,
138+
anonymous.n_path,
139+
named.n_path,
140+
);
141+
142+
match syntax_counts {
143+
// The lifetimes are all one syntax
144+
(_, 0, 0, _, 0, 0) | (0, _, 0, 0, _, 0) | (0, 0, _, 0, 0, _) => continue,
145+
146+
// Hidden references, anonymous references, and anonymous paths can call be mixed.
147+
(_, _, 0, 0, _, 0) => continue,
148+
149+
_ => (),
150+
}
151+
152+
let inputs = input_info.iter().map(|info| info.reporting_span()).collect();
153+
let outputs = output_info.iter().map(|info| info.reporting_span()).collect();
154+
155+
// There can only ever be zero or one named lifetime
156+
// for a given lifetime resolution.
157+
let named_lifetime = named.members.first();
158+
159+
let named_suggestion = named_lifetime.map(|info| {
160+
build_mismatch_suggestion(info.lifetime_name(), &[&hidden, &anonymous])
161+
});
162+
163+
let is_named_static = named_lifetime.is_some_and(|info| info.is_static());
164+
165+
let should_suggest_hidden = !hidden.members.is_empty() && !is_named_static;
166+
167+
// FIXME: remove comma and space from paths with multiple generics
168+
// FIXME: remove angle brackets from paths when no more generics
169+
// FIXME: remove space after lifetime from references
170+
// FIXME: remove lifetime from function declaration
171+
let hidden_suggestion = should_suggest_hidden.then(|| {
172+
let suggestions = [&anonymous, &named]
173+
.into_iter()
174+
.flat_map(|b| &b.members)
175+
.map(|i| i.suggestion("'dummy").0)
176+
.collect();
177+
178+
lints::MismatchedLifetimeSyntaxesSuggestion::Hidden {
179+
suggestions,
180+
tool_only: false,
181+
}
182+
});
183+
184+
let should_suggest_anonymous = !anonymous.members.is_empty() && !is_named_static;
185+
186+
let anonymous_suggestion = should_suggest_anonymous
187+
.then(|| build_mismatch_suggestion("'_", &[&hidden, &named]));
188+
189+
let lifetime_name =
190+
named_lifetime.map(|info| info.lifetime_name()).unwrap_or("'_").to_owned();
191+
192+
// We can produce a number of suggestions which may overwhelm
193+
// the user. Instead, we order the suggestions based on Rust
194+
// idioms. The "best" choice is shown to the user and the
195+
// remaining choices are shown to tools only.
196+
let mut suggestions = Vec::new();
197+
suggestions.extend(named_suggestion);
198+
suggestions.extend(anonymous_suggestion);
199+
suggestions.extend(hidden_suggestion);
200+
201+
cx.emit_span_lint(
202+
MISMATCHED_LIFETIME_SYNTAXES,
203+
Vec::clone(&inputs),
204+
lints::MismatchedLifetimeSyntaxes { lifetime_name, inputs, outputs, suggestions },
205+
);
206+
}
207+
}
208+
}
209+
210+
#[derive(Default)]
211+
struct Bucket<'a, 'tcx> {
212+
members: Vec<&'a Info<'tcx>>,
213+
n_ref: usize,
214+
n_path: usize,
215+
}
216+
217+
fn build_mismatch_suggestion(
218+
lifetime_name: &str,
219+
buckets: &[&Bucket<'_, '_>],
220+
) -> lints::MismatchedLifetimeSyntaxesSuggestion {
221+
let lifetime_name = lifetime_name.to_owned();
222+
223+
let suggestions = buckets
224+
.iter()
225+
.flat_map(|b| &b.members)
226+
.map(|info| info.suggestion(&lifetime_name))
227+
.collect();
228+
229+
lints::MismatchedLifetimeSyntaxesSuggestion::Named {
230+
lifetime_name,
231+
suggestions,
232+
tool_only: false,
233+
}
234+
}
235+
236+
struct Info<'tcx> {
237+
type_span: Span,
238+
lifetime: &'tcx hir::Lifetime,
239+
}
240+
241+
impl<'tcx> Info<'tcx> {
242+
fn lifetime_name(&self) -> &str {
243+
self.lifetime.ident.as_str()
244+
}
245+
246+
fn is_static(&self) -> bool {
247+
self.lifetime.is_static()
248+
}
249+
250+
/// When reporting a lifetime that is hidden, we expand the span
251+
/// to include the type. Otherwise we end up pointing at nothing,
252+
/// which is a bit confusing.
253+
fn reporting_span(&self) -> Span {
254+
if self.lifetime.is_syntactically_hidden() {
255+
self.type_span
256+
} else {
257+
self.lifetime.ident.span
258+
}
259+
}
260+
261+
fn suggestion(&self, lifetime_name: &str) -> (Span, String) {
262+
self.lifetime.suggestion(lifetime_name)
263+
}
264+
}
265+
266+
type LifetimeInfoMap<'tcx> = FxIndexMap<&'tcx hir::LifetimeName, Vec<Info<'tcx>>>;
267+
268+
struct LifetimeInfoCollector<'a, 'tcx> {
269+
type_span: Span,
270+
map: &'a mut LifetimeInfoMap<'tcx>,
271+
}
272+
273+
impl<'a, 'tcx> LifetimeInfoCollector<'a, 'tcx> {
274+
fn collect(ty: &'tcx hir::Ty<'tcx>, map: &'a mut LifetimeInfoMap<'tcx>) {
275+
let mut this = Self { type_span: ty.span, map };
276+
277+
intravisit::walk_unambig_ty(&mut this, ty);
278+
}
279+
}
280+
281+
impl<'a, 'tcx> Visitor<'tcx> for LifetimeInfoCollector<'a, 'tcx> {
282+
#[instrument(skip(self))]
283+
fn visit_lifetime(&mut self, lifetime: &'tcx hir::Lifetime) {
284+
let type_span = self.type_span;
285+
286+
let info = Info { type_span, lifetime };
287+
288+
self.map.entry(&lifetime.res).or_default().push(info);
289+
}
290+
291+
#[instrument(skip(self))]
292+
fn visit_ty(&mut self, ty: &'tcx hir::Ty<'tcx, hir::AmbigArg>) -> Self::Result {
293+
let old_type_span = self.type_span;
294+
295+
self.type_span = ty.span;
296+
297+
intravisit::walk_ty(self, ty);
298+
299+
self.type_span = old_type_span;
300+
}
301+
}

0 commit comments

Comments
 (0)