blob: d6c746901fc7270d2fef7b158655495f60083465 [file] [log] [blame]
use clippy_utils::diagnostics::{span_lint_and_help, span_lint_and_then};
use clippy_utils::{is_from_proc_macro, trait_ref_of_method};
use rustc_data_structures::fx::{FxHashMap, FxHashSet};
use rustc_errors::Applicability;
use rustc_hir::intravisit::{walk_impl_item, walk_item, walk_param_bound, walk_ty, Visitor};
use rustc_hir::{
BodyId, ExprKind, GenericBound, GenericParam, GenericParamKind, Generics, ImplItem, ImplItemKind, Item, ItemKind,
PredicateOrigin, Ty, TyKind, WherePredicate,
};
use rustc_lint::{LateContext, LateLintPass, LintContext};
use rustc_middle::hir::nested_filter;
use rustc_middle::lint::in_external_macro;
use rustc_session::{declare_tool_lint, impl_lint_pass};
use rustc_span::def_id::{DefId, LocalDefId};
use rustc_span::Span;
declare_clippy_lint! {
/// ### What it does
/// Checks for type parameters in generics that are never used anywhere else.
///
/// ### Why is this bad?
/// Functions cannot infer the value of unused type parameters; therefore, calling them
/// requires using a turbofish, which serves no purpose but to satisfy the compiler.
///
/// ### Example
/// ```no_run
/// fn unused_ty<T>(x: u8) {
/// // ..
/// }
/// ```
/// Use instead:
/// ```no_run
/// fn no_unused_ty(x: u8) {
/// // ..
/// }
/// ```
#[clippy::version = "1.69.0"]
pub EXTRA_UNUSED_TYPE_PARAMETERS,
complexity,
"unused type parameters in function definitions"
}
pub struct ExtraUnusedTypeParameters {
avoid_breaking_exported_api: bool,
}
impl ExtraUnusedTypeParameters {
pub fn new(avoid_breaking_exported_api: bool) -> Self {
Self {
avoid_breaking_exported_api,
}
}
/// Don't lint external macros or functions with empty bodies. Also, don't lint exported items
/// if the `avoid_breaking_exported_api` config option is set.
fn is_empty_exported_or_macro(
&self,
cx: &LateContext<'_>,
span: Span,
def_id: LocalDefId,
body_id: BodyId,
) -> bool {
let body = cx.tcx.hir().body(body_id).value;
let fn_empty = matches!(&body.kind, ExprKind::Block(blk, None) if blk.stmts.is_empty() && blk.expr.is_none());
let is_exported = cx.effective_visibilities.is_exported(def_id);
in_external_macro(cx.sess(), span) || fn_empty || (is_exported && self.avoid_breaking_exported_api)
}
}
impl_lint_pass!(ExtraUnusedTypeParameters => [EXTRA_UNUSED_TYPE_PARAMETERS]);
/// A visitor struct that walks a given function and gathers generic type parameters, plus any
/// trait bounds those parameters have.
struct TypeWalker<'cx, 'tcx> {
cx: &'cx LateContext<'tcx>,
/// Collection of the function's type parameters. Once the function has been walked, this will
/// contain only unused type parameters.
ty_params: FxHashMap<DefId, Span>,
/// Collection of any inline trait bounds corresponding to each type parameter.
inline_bounds: FxHashMap<DefId, Span>,
/// Collection of any type parameters with trait bounds that appear in a where clause.
where_bounds: FxHashSet<DefId>,
/// The entire `Generics` object of the function, useful for querying purposes.
generics: &'tcx Generics<'tcx>,
}
impl<'cx, 'tcx> TypeWalker<'cx, 'tcx> {
fn new(cx: &'cx LateContext<'tcx>, generics: &'tcx Generics<'tcx>) -> Self {
let ty_params = generics
.params
.iter()
.filter_map(|param| match param.kind {
GenericParamKind::Type { synthetic, .. } if !synthetic => Some((param.def_id.into(), param.span)),
_ => None,
})
.collect();
Self {
cx,
ty_params,
inline_bounds: FxHashMap::default(),
where_bounds: FxHashSet::default(),
generics,
}
}
fn get_bound_span(&self, param: &'tcx GenericParam<'tcx>) -> Span {
self.inline_bounds
.get(&param.def_id.to_def_id())
.map_or(param.span, |bound_span| param.span.with_hi(bound_span.hi()))
}
fn emit_help(&self, spans: Vec<Span>, msg: &str, help: &'static str) {
span_lint_and_help(self.cx, EXTRA_UNUSED_TYPE_PARAMETERS, spans, msg, None, help);
}
fn emit_sugg(&self, spans: Vec<Span>, msg: &str, help: &'static str) {
let suggestions: Vec<(Span, String)> = spans.iter().copied().zip(std::iter::repeat(String::new())).collect();
span_lint_and_then(self.cx, EXTRA_UNUSED_TYPE_PARAMETERS, spans, msg, |diag| {
diag.multipart_suggestion(help, suggestions, Applicability::MachineApplicable);
});
}
fn emit_lint(&self) {
let explicit_params = self
.generics
.params
.iter()
.filter(|param| !param.is_elided_lifetime() && !param.is_impl_trait())
.collect::<Vec<_>>();
let extra_params = explicit_params
.iter()
.enumerate()
.filter(|(_, param)| self.ty_params.contains_key(&param.def_id.to_def_id()))
.collect::<Vec<_>>();
let (msg, help) = match extra_params.len() {
0 => return,
1 => (
format!(
"type parameter `{}` goes unused in function definition",
extra_params[0].1.name.ident()
),
"consider removing the parameter",
),
_ => (
format!(
"type parameters go unused in function definition: {}",
extra_params
.iter()
.map(|(_, param)| param.name.ident().to_string())
.collect::<Vec<_>>()
.join(", ")
),
"consider removing the parameters",
),
};
// If any parameters are bounded in where clauses, don't try to form a suggestion.
// Otherwise, the leftover where bound would produce code that wouldn't compile.
if extra_params
.iter()
.any(|(_, param)| self.where_bounds.contains(&param.def_id.to_def_id()))
{
let spans = extra_params
.iter()
.map(|(_, param)| self.get_bound_span(param))
.collect::<Vec<_>>();
self.emit_help(spans, &msg, help);
} else {
let spans = if explicit_params.len() == extra_params.len() {
vec![self.generics.span] // Remove the entire list of generics
} else {
let mut end: Option<LocalDefId> = None;
extra_params
.iter()
.rev()
.map(|(idx, param)| {
if let Some(next) = explicit_params.get(idx + 1)
&& end != Some(next.def_id)
{
// Extend the current span forward, up until the next param in the list.
param.span.until(next.span)
} else {
// Extend the current span back to include the comma following the previous
// param. If the span of the next param in the list has already been
// extended, we continue the chain. This is why we're iterating in reverse.
end = Some(param.def_id);
// idx will never be 0, else we'd be removing the entire list of generics
let prev = explicit_params[idx - 1];
let prev_span = self.get_bound_span(prev);
self.get_bound_span(param).with_lo(prev_span.hi())
}
})
.collect()
};
self.emit_sugg(spans, &msg, help);
};
}
}
/// Given a generic bound, if the bound is for a trait that's not a `LangItem`, return the
/// `LocalDefId` for that trait.
fn bound_to_trait_def_id(bound: &GenericBound<'_>) -> Option<LocalDefId> {
bound.trait_ref()?.trait_def_id()?.as_local()
}
impl<'cx, 'tcx> Visitor<'tcx> for TypeWalker<'cx, 'tcx> {
type NestedFilter = nested_filter::OnlyBodies;
fn visit_ty(&mut self, t: &'tcx Ty<'tcx>) {
if let Some((def_id, _)) = t.peel_refs().as_generic_param() {
self.ty_params.remove(&def_id);
} else if let TyKind::OpaqueDef(id, _, _) = t.kind {
// Explicitly walk OpaqueDef. Normally `walk_ty` would do the job, but it calls
// `visit_nested_item`, which checks that `Self::NestedFilter::INTER` is set. We're
// using `OnlyBodies`, so the check ends up failing and the type isn't fully walked.
let item = self.nested_visit_map().item(id);
walk_item(self, item);
} else {
walk_ty(self, t);
}
}
fn visit_where_predicate(&mut self, predicate: &'tcx WherePredicate<'tcx>) {
if let WherePredicate::BoundPredicate(predicate) = predicate {
// Collect spans for any bounds on type parameters.
if let Some((def_id, _)) = predicate.bounded_ty.peel_refs().as_generic_param() {
match predicate.origin {
PredicateOrigin::GenericParam => {
self.inline_bounds.insert(def_id, predicate.span);
},
PredicateOrigin::WhereClause => {
self.where_bounds.insert(def_id);
},
PredicateOrigin::ImplTrait => (),
}
// If the bound contains non-public traits, err on the safe side and don't lint the
// corresponding parameter.
if !predicate
.bounds
.iter()
.filter_map(bound_to_trait_def_id)
.all(|id| self.cx.effective_visibilities.is_exported(id))
{
self.ty_params.remove(&def_id);
}
} else {
// If the bounded type isn't a generic param, but is instead a concrete generic
// type, any params we find nested inside of it are being used as concrete types,
// and can therefore can be considered used. So, we're fine to walk the left-hand
// side of the where bound.
walk_ty(self, predicate.bounded_ty);
}
for bound in predicate.bounds {
walk_param_bound(self, bound);
}
}
}
fn nested_visit_map(&mut self) -> Self::Map {
self.cx.tcx.hir()
}
}
impl<'tcx> LateLintPass<'tcx> for ExtraUnusedTypeParameters {
fn check_item(&mut self, cx: &LateContext<'tcx>, item: &'tcx Item<'tcx>) {
if let ItemKind::Fn(_, generics, body_id) = item.kind
&& !self.is_empty_exported_or_macro(cx, item.span, item.owner_id.def_id, body_id)
&& !is_from_proc_macro(cx, item)
{
let mut walker = TypeWalker::new(cx, generics);
walk_item(&mut walker, item);
walker.emit_lint();
}
}
fn check_impl_item(&mut self, cx: &LateContext<'tcx>, item: &'tcx ImplItem<'tcx>) {
// Only lint on inherent methods, not trait methods.
if let ImplItemKind::Fn(.., body_id) = item.kind
&& trait_ref_of_method(cx, item.owner_id.def_id).is_none()
&& !self.is_empty_exported_or_macro(cx, item.span, item.owner_id.def_id, body_id)
{
let mut walker = TypeWalker::new(cx, item.generics);
walk_impl_item(&mut walker, item);
walker.emit_lint();
}
}
}