Skip to content

Commit e9216d8

Browse files
committed
Improve needless_lifetimes
1 parent 4326814 commit e9216d8

File tree

3 files changed

+168
-60
lines changed

3 files changed

+168
-60
lines changed

clippy_lints/src/lifetimes.rs

Lines changed: 92 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
use clippy_utils::diagnostics::span_lint;
1+
use clippy_utils::diagnostics::{span_lint, span_lint_and_then};
22
use clippy_utils::trait_ref_of_method;
33
use rustc_data_structures::fx::{FxHashMap, FxHashSet};
44
use rustc_hir::intravisit::nested_filter::{self as hir_nested_filter, NestedFilter};
@@ -151,6 +151,7 @@ fn check_fn_inner<'tcx>(
151151
.params
152152
.iter()
153153
.filter(|param| matches!(param.kind, GenericParamKind::Type { .. }));
154+
154155
for typ in types {
155156
for pred in generics.bounds_for_param(cx.tcx.hir().local_def_id(typ.hir_id)) {
156157
if pred.origin == PredicateOrigin::WhereClause {
@@ -187,15 +188,30 @@ fn check_fn_inner<'tcx>(
187188
}
188189
}
189190
}
190-
if could_use_elision(cx, decl, body, trait_sig, generics.params) {
191-
span_lint(
191+
192+
if let Some(elidable_lts) = could_use_elision(cx, decl, body, trait_sig, generics.params) {
193+
let lts = elidable_lts
194+
.iter()
195+
// In principle, the result of the call to `Node::ident` could be `unwrap`ped, as `DefId` should refer to a
196+
// `Node::GenericParam`.
197+
.filter_map(|&(def_id, _)| cx.tcx.hir().get_by_def_id(def_id).ident())
198+
.map(|ident| ident.to_string())
199+
.collect::<Vec<_>>()
200+
.join(", ");
201+
202+
span_lint_and_then(
192203
cx,
193204
NEEDLESS_LIFETIMES,
194205
span.with_hi(decl.output.span().hi()),
195-
"explicit lifetimes given in parameter types where they could be elided \
196-
(or replaced with `'_` if needed by type declaration)",
206+
&format!("the following explicit lifetimes could be elided: {lts}"),
207+
|diag| {
208+
if let Some(span) = elidable_lts.iter().find_map(|&(_, span)| span) {
209+
diag.span_help(span, "replace with `'_` in generic arguments such as here");
210+
}
211+
},
197212
);
198213
}
214+
199215
if report_extra_lifetimes {
200216
self::report_extra_lifetimes(cx, decl, generics);
201217
}
@@ -220,13 +236,14 @@ fn explicit_self_type<'tcx>(cx: &LateContext<'tcx>, func: &FnDecl<'tcx>, ident:
220236
}
221237
}
222238

239+
#[expect(clippy::too_many_lines)]
223240
fn could_use_elision<'tcx>(
224241
cx: &LateContext<'tcx>,
225242
func: &'tcx FnDecl<'_>,
226243
body: Option<BodyId>,
227244
trait_sig: Option<&[Ident]>,
228245
named_generics: &'tcx [GenericParam<'_>],
229-
) -> bool {
246+
) -> Option<Vec<(LocalDefId, Option<Span>)>> {
230247
// There are two scenarios where elision works:
231248
// * no output references, all input references have different LT
232249
// * output references, exactly one input reference with same LT
@@ -253,15 +270,15 @@ fn could_use_elision<'tcx>(
253270
}
254271

255272
if input_visitor.abort() || output_visitor.abort() {
256-
return false;
273+
return None;
257274
}
258275

259276
let input_lts = input_visitor.lts;
260277
let output_lts = output_visitor.lts;
261278

262279
if let Some(trait_sig) = trait_sig {
263280
if explicit_self_type(cx, func, trait_sig.first().copied()) {
264-
return false;
281+
return None;
265282
}
266283
}
267284

@@ -270,22 +287,22 @@ fn could_use_elision<'tcx>(
270287

271288
let first_ident = body.params.first().and_then(|param| param.pat.simple_ident());
272289
if explicit_self_type(cx, func, first_ident) {
273-
return false;
290+
return None;
274291
}
275292

276293
let mut checker = BodyLifetimeChecker {
277294
lifetimes_used_in_body: false,
278295
};
279296
checker.visit_expr(body.value);
280297
if checker.lifetimes_used_in_body {
281-
return false;
298+
return None;
282299
}
283300
}
284301

285302
// check for lifetimes from higher scopes
286303
for lt in input_lts.iter().chain(output_lts.iter()) {
287304
if !allowed_lts.contains(lt) {
288-
return false;
305+
return None;
289306
}
290307
}
291308

@@ -301,47 +318,62 @@ fn could_use_elision<'tcx>(
301318
for lt in input_visitor.nested_elision_site_lts {
302319
if let RefLt::Named(def_id) = lt {
303320
if allowed_lts.contains(&cx.tcx.item_name(def_id.to_def_id())) {
304-
return false;
321+
return None;
305322
}
306323
}
307324
}
308325
for lt in output_visitor.nested_elision_site_lts {
309326
if let RefLt::Named(def_id) = lt {
310327
if allowed_lts.contains(&cx.tcx.item_name(def_id.to_def_id())) {
311-
return false;
328+
return None;
312329
}
313330
}
314331
}
315332
}
316333

317334
// no input lifetimes? easy case!
318335
if input_lts.is_empty() {
319-
false
336+
None
320337
} else if output_lts.is_empty() {
321338
// no output lifetimes, check distinctness of input lifetimes
322339

323340
// only unnamed and static, ok
324341
let unnamed_and_static = input_lts.iter().all(|lt| *lt == RefLt::Unnamed || *lt == RefLt::Static);
325342
if unnamed_and_static {
326-
return false;
343+
return None;
344+
}
345+
// we have no output reference, so we can elide explicit lifetimes that occur at most once
346+
let elidable_lts = named_lifetime_occurrences(&input_lts)
347+
.into_iter()
348+
.filter_map(|(def_id, occurrences)| {
349+
if occurrences <= 1 {
350+
Some((def_id, input_visitor.sample_generic_arg_span.get(&def_id).copied()))
351+
} else {
352+
None
353+
}
354+
})
355+
.collect::<Vec<_>>();
356+
if elidable_lts.is_empty() {
357+
None
358+
} else {
359+
Some(elidable_lts)
327360
}
328-
// we have no output reference, so we only need all distinct lifetimes
329-
input_lts.len() == unique_lifetimes(&input_lts)
330361
} else {
331362
// we have output references, so we need one input reference,
332363
// and all output lifetimes must be the same
333-
if unique_lifetimes(&output_lts) > 1 {
334-
return false;
335-
}
336364
if input_lts.len() == 1 {
337365
match (&input_lts[0], &output_lts[0]) {
338-
(&RefLt::Named(n1), &RefLt::Named(n2)) if n1 == n2 => true,
339-
(&RefLt::Named(_), &RefLt::Unnamed) => true,
340-
_ => false, /* already elided, different named lifetimes
341-
* or something static going on */
366+
(&RefLt::Named(n1), &RefLt::Named(n2)) if n1 == n2 => {
367+
Some(vec![(n1, input_visitor.sample_generic_arg_span.get(&n1).copied())])
368+
},
369+
(&RefLt::Named(n), &RefLt::Unnamed) => {
370+
Some(vec![(n, input_visitor.sample_generic_arg_span.get(&n).copied())])
371+
},
372+
_ => None, /* already elided, different named lifetimes
373+
* or something static going on */
342374
}
343375
} else {
344-
false
376+
None
345377
}
346378
}
347379
}
@@ -358,10 +390,24 @@ fn allowed_lts_from(tcx: TyCtxt<'_>, named_generics: &[GenericParam<'_>]) -> FxH
358390
allowed_lts
359391
}
360392

361-
/// Number of unique lifetimes in the given vector.
393+
/// Number of times each named lifetime occurs in the given slice. Returns a vector to preserve
394+
/// relative order.
362395
#[must_use]
363-
fn unique_lifetimes(lts: &[RefLt]) -> usize {
364-
lts.iter().collect::<FxHashSet<_>>().len()
396+
fn named_lifetime_occurrences(lts: &[RefLt]) -> Vec<(LocalDefId, usize)> {
397+
let mut occurrences = Vec::new();
398+
for lt in lts {
399+
if let &RefLt::Named(curr_def_id) = lt {
400+
if let Some(i) = occurrences
401+
.iter()
402+
.position(|&(prev_def_id, _)| prev_def_id == curr_def_id)
403+
{
404+
occurrences[i].1 += 1;
405+
} else {
406+
occurrences.push((curr_def_id, 1));
407+
}
408+
}
409+
}
410+
occurrences
365411
}
366412

367413
const CLOSURE_TRAIT_BOUNDS: [LangItem; 3] = [LangItem::Fn, LangItem::FnMut, LangItem::FnOnce];
@@ -370,6 +416,7 @@ const CLOSURE_TRAIT_BOUNDS: [LangItem; 3] = [LangItem::Fn, LangItem::FnMut, Lang
370416
struct RefVisitor<'a, 'tcx> {
371417
cx: &'a LateContext<'tcx>,
372418
lts: Vec<RefLt>,
419+
sample_generic_arg_span: FxHashMap<LocalDefId, Span>,
373420
nested_elision_site_lts: Vec<RefLt>,
374421
unelided_trait_object_lifetime: bool,
375422
}
@@ -379,6 +426,7 @@ impl<'a, 'tcx> RefVisitor<'a, 'tcx> {
379426
Self {
380427
cx,
381428
lts: Vec::new(),
429+
sample_generic_arg_span: FxHashMap::default(),
382430
nested_elision_site_lts: Vec::new(),
383431
unelided_trait_object_lifetime: false,
384432
}
@@ -472,6 +520,22 @@ impl<'a, 'tcx> Visitor<'tcx> for RefVisitor<'a, 'tcx> {
472520
_ => walk_ty(self, ty),
473521
}
474522
}
523+
524+
fn visit_generic_arg(&mut self, generic_arg: &'tcx GenericArg<'tcx>) {
525+
if let GenericArg::Lifetime(l) = generic_arg
526+
&& let LifetimeName::Param(def_id, _) = l.name
527+
{
528+
self.sample_generic_arg_span.entry(def_id).or_insert(l.span);
529+
}
530+
// Replace with `walk_generic_arg` if/when https://github.com/rust-lang/rust/pull/103692 lands.
531+
// walk_generic_arg(self, generic_arg);
532+
match generic_arg {
533+
GenericArg::Lifetime(lt) => self.visit_lifetime(lt),
534+
GenericArg::Type(ty) => self.visit_ty(ty),
535+
GenericArg::Const(ct) => self.visit_anon_const(&ct.value),
536+
GenericArg::Infer(inf) => self.visit_infer(inf),
537+
}
538+
}
475539
}
476540

477541
/// Are any lifetimes mentioned in the `where` clause? If so, we don't try to

tests/ui/needless_lifetimes.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -419,4 +419,12 @@ mod issue7296 {
419419
}
420420
}
421421

422+
mod false_negative {
423+
#![allow(unused)]
424+
425+
fn foo<'a>(x: &'a u8, y: &'_ u8) {}
426+
427+
fn bar<'a>(x: &'a u8, y: &'_ u8, z: &'_ u8) {}
428+
}
429+
422430
fn main() {}

0 commit comments

Comments
 (0)