blob: e66c088617cb5f7ba57856c9c8554f040abf6af2 [file] [log] [blame] [edit]
use clippy_utils::diagnostics::{span_lint_and_sugg, span_lint_and_then};
use clippy_utils::res::{MaybeDef, MaybeQPath};
use clippy_utils::ty::implements_trait;
use clippy_utils::{is_from_proc_macro, last_path_segment, std_or_core};
use rustc_errors::Applicability;
use rustc_hir::def_id::DefId;
use rustc_hir::{Block, Body, Expr, ExprKind, ImplItem, ImplItemKind, Item, ItemKind, LangItem, UnOp};
use rustc_lint::{LateContext, LateLintPass, LintContext};
use rustc_middle::ty::{TyCtxt, TypeckResults};
use rustc_session::impl_lint_pass;
use rustc_span::sym;
use rustc_span::symbol::kw;
declare_clippy_lint! {
/// ### What it does
/// Checks for non-canonical implementations of `Clone` when `Copy` is already implemented.
///
/// ### Why is this bad?
/// If both `Clone` and `Copy` are implemented, they must agree. This can done by dereferencing
/// `self` in `Clone`'s implementation, which will avoid any possibility of the implementations
/// becoming out of sync.
///
/// ### Example
/// ```rust,ignore
/// #[derive(Eq, PartialEq)]
/// struct A(u32);
///
/// impl Clone for A {
/// fn clone(&self) -> Self {
/// Self(self.0)
/// }
/// }
///
/// impl Copy for A {}
/// ```
/// Use instead:
/// ```rust,ignore
/// #[derive(Eq, PartialEq)]
/// struct A(u32);
///
/// impl Clone for A {
/// fn clone(&self) -> Self {
/// *self
/// }
/// }
///
/// impl Copy for A {}
/// ```
#[clippy::version = "1.72.0"]
pub NON_CANONICAL_CLONE_IMPL,
suspicious,
"non-canonical implementation of `Clone` on a `Copy` type"
}
declare_clippy_lint! {
/// ### What it does
/// Checks for non-canonical implementations of `PartialOrd` when `Ord` is already implemented.
///
/// ### Why is this bad?
/// If both `PartialOrd` and `Ord` are implemented, they must agree. This is commonly done by
/// wrapping the result of `cmp` in `Some` for `partial_cmp`. Not doing this may silently
/// introduce an error upon refactoring.
///
/// ### Known issues
/// Code that calls the `.into()` method instead will be flagged, despite `.into()` wrapping it
/// in `Some`.
///
/// ### Example
/// ```no_run
/// # use std::cmp::Ordering;
/// #[derive(Eq, PartialEq)]
/// struct A(u32);
///
/// impl Ord for A {
/// fn cmp(&self, other: &Self) -> Ordering {
/// // ...
/// # todo!();
/// }
/// }
///
/// impl PartialOrd for A {
/// fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
/// // ...
/// # todo!();
/// }
/// }
/// ```
/// Use instead:
/// ```no_run
/// # use std::cmp::Ordering;
/// #[derive(Eq, PartialEq)]
/// struct A(u32);
///
/// impl Ord for A {
/// fn cmp(&self, other: &Self) -> Ordering {
/// // ...
/// # todo!();
/// }
/// }
///
/// impl PartialOrd for A {
/// fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
/// Some(self.cmp(other)) // or self.cmp(other).into()
/// }
/// }
/// ```
#[clippy::version = "1.73.0"]
pub NON_CANONICAL_PARTIAL_ORD_IMPL,
suspicious,
"non-canonical implementation of `PartialOrd` on an `Ord` type"
}
impl_lint_pass!(NonCanonicalImpls => [NON_CANONICAL_CLONE_IMPL, NON_CANONICAL_PARTIAL_ORD_IMPL]);
#[expect(
clippy::struct_field_names,
reason = "`_trait` suffix is meaningful on its own, \
and creating an inner `StoredTraits` struct would just add a level of indirection"
)]
pub(crate) struct NonCanonicalImpls {
partial_ord_trait: Option<DefId>,
ord_trait: Option<DefId>,
clone_trait: Option<DefId>,
copy_trait: Option<DefId>,
}
impl NonCanonicalImpls {
pub(crate) fn new(tcx: TyCtxt<'_>) -> Self {
let lang_items = tcx.lang_items();
Self {
partial_ord_trait: lang_items.partial_ord_trait(),
ord_trait: tcx.get_diagnostic_item(sym::Ord),
clone_trait: lang_items.clone_trait(),
copy_trait: lang_items.copy_trait(),
}
}
}
/// The traits that this lint looks at
enum Trait {
Clone,
PartialOrd,
}
impl LateLintPass<'_> for NonCanonicalImpls {
fn check_item(&mut self, cx: &LateContext<'_>, item: &Item<'_>) {
if let ItemKind::Impl(impl_) = item.kind
// Both `PartialOrd` and `Clone` have one required method, and `PartialOrd` can have 5 methods in total
&& (1..=5).contains(&impl_.items.len())
&& let Some(of_trait) = impl_.of_trait
&& let Some(trait_did) = of_trait.trait_ref.trait_def_id()
// Check this early to hopefully bail out as soon as possible
&& let trait_ = if Some(trait_did) == self.clone_trait {
Trait::Clone
} else if Some(trait_did) == self.partial_ord_trait {
Trait::PartialOrd
} else {
return;
}
&& !cx.tcx.is_automatically_derived(item.owner_id.to_def_id())
{
let mut assoc_fns = impl_
.items
.iter()
.map(|id| cx.tcx.hir_impl_item(*id))
.filter_map(|assoc| {
if let ImplItemKind::Fn(_, body_id) = assoc.kind
&& let body = cx.tcx.hir_body(body_id)
&& let ExprKind::Block(block, ..) = body.value.kind
&& !block.span.in_external_macro(cx.sess().source_map())
{
Some((assoc, body, block))
} else {
None
}
});
let trait_impl = cx.tcx.impl_trait_ref(item.owner_id).skip_binder();
match trait_ {
Trait::Clone => {
if let Some(copy_trait) = self.copy_trait
&& implements_trait(cx, trait_impl.self_ty(), copy_trait, &[])
{
for (assoc, _, block) in assoc_fns {
check_clone_on_copy(cx, assoc, block);
}
}
},
Trait::PartialOrd => {
// If `Self` and `Rhs` are not the same type, then a corresponding `Ord` impl is not possible,
// since it doesn't have an `Rhs`
if let [lhs, rhs] = trait_impl.args.as_slice()
&& lhs == rhs
&& let Some(ord_trait) = self.ord_trait
&& implements_trait(cx, trait_impl.self_ty(), ord_trait, &[])
&& let Some((assoc, body, block)) =
assoc_fns.find(|(assoc, _, _)| assoc.ident.name == sym::partial_cmp)
{
check_partial_ord_on_ord(cx, assoc, item, body, block);
}
},
}
}
}
}
fn check_clone_on_copy(cx: &LateContext<'_>, impl_item: &ImplItem<'_>, block: &Block<'_>) {
if impl_item.ident.name == sym::clone {
if block.stmts.is_empty()
&& let Some(expr) = block.expr
&& let ExprKind::Unary(UnOp::Deref, deref) = expr.kind
&& let ExprKind::Path(qpath) = deref.kind
&& last_path_segment(&qpath).ident.name == kw::SelfLower
{
// this is the canonical implementation, `fn clone(&self) -> Self { *self }`
return;
}
if is_from_proc_macro(cx, impl_item) {
return;
}
span_lint_and_sugg(
cx,
NON_CANONICAL_CLONE_IMPL,
block.span,
"non-canonical implementation of `clone` on a `Copy` type",
"change this to",
"{ *self }".to_owned(),
Applicability::MaybeIncorrect,
);
}
if impl_item.ident.name == sym::clone_from && !is_from_proc_macro(cx, impl_item) {
span_lint_and_sugg(
cx,
NON_CANONICAL_CLONE_IMPL,
impl_item.span,
"unnecessary implementation of `clone_from` on a `Copy` type",
"remove it",
String::new(),
Applicability::MaybeIncorrect,
);
}
}
fn check_partial_ord_on_ord<'tcx>(
cx: &LateContext<'tcx>,
impl_item: &ImplItem<'_>,
item: &Item<'_>,
body: &Body<'_>,
block: &Block<'tcx>,
) {
// If the `cmp` call likely needs to be fully qualified in the suggestion
// (like `std::cmp::Ord::cmp`). It's unfortunate we must put this here but we can't
// access `cmp_expr` in the suggestion without major changes, as we lint in `else`.
let mut needs_fully_qualified = false;
if block.stmts.is_empty()
&& let Some(expr) = block.expr
&& expr_is_cmp(cx, expr, impl_item, &mut needs_fully_qualified)
{
return;
}
// Fix #12683, allow [`needless_return`] here
else if block.expr.is_none()
&& let Some(stmt) = block.stmts.first()
&& let rustc_hir::StmtKind::Semi(Expr {
kind: ExprKind::Ret(Some(ret)),
..
}) = stmt.kind
&& expr_is_cmp(cx, ret, impl_item, &mut needs_fully_qualified)
{
return;
} else if is_from_proc_macro(cx, impl_item) {
return;
}
span_lint_and_then(
cx,
NON_CANONICAL_PARTIAL_ORD_IMPL,
item.span,
"non-canonical implementation of `partial_cmp` on an `Ord` type",
|diag| {
let [_, other] = body.params else {
return;
};
let Some(std_or_core) = std_or_core(cx) else {
return;
};
let suggs = match (other.pat.simple_ident(), needs_fully_qualified) {
(Some(other_ident), true) => vec![(
block.span,
format!("{{ Some({std_or_core}::cmp::Ord::cmp(self, {})) }}", other_ident.name),
)],
(Some(other_ident), false) => {
vec![(block.span, format!("{{ Some(self.cmp({})) }}", other_ident.name))]
},
(None, true) => vec![
(
block.span,
format!("{{ Some({std_or_core}::cmp::Ord::cmp(self, other)) }}"),
),
(other.pat.span, "other".to_owned()),
],
(None, false) => vec![
(block.span, "{ Some(self.cmp(other)) }".to_owned()),
(other.pat.span, "other".to_owned()),
],
};
diag.multipart_suggestion("change this to", suggs, Applicability::Unspecified);
},
);
}
/// Return true if `expr_kind` is a `cmp` call.
fn expr_is_cmp<'tcx>(
cx: &LateContext<'tcx>,
expr: &'tcx Expr<'tcx>,
impl_item: &ImplItem<'_>,
needs_fully_qualified: &mut bool,
) -> bool {
let typeck = cx.tcx.typeck(impl_item.owner_id.def_id);
match expr.kind {
ExprKind::Call(
Expr {
kind: ExprKind::Path(some_path),
hir_id: some_hir_id,
..
},
[cmp_expr],
) => {
typeck.qpath_res(some_path, *some_hir_id).ctor_parent(cx).is_lang_item(cx, LangItem::OptionSome)
// Fix #11178, allow `Self::cmp(self, ..)`
&& self_cmp_call(cx, typeck, cmp_expr, needs_fully_qualified)
},
ExprKind::MethodCall(_, recv, [], _) => {
typeck
.type_dependent_def(expr.hir_id)
.assoc_parent(cx)
.is_diag_item(cx, sym::Into)
&& self_cmp_call(cx, typeck, recv, needs_fully_qualified)
},
_ => false,
}
}
/// Returns whether this is any of `self.cmp(..)`, `Self::cmp(self, ..)` or `Ord::cmp(self, ..)`.
fn self_cmp_call<'tcx>(
cx: &LateContext<'tcx>,
typeck: &TypeckResults<'tcx>,
cmp_expr: &'tcx Expr<'tcx>,
needs_fully_qualified: &mut bool,
) -> bool {
match cmp_expr.kind {
ExprKind::Call(path, [_, _]) => path.res(typeck).is_diag_item(cx, sym::ord_cmp_method),
ExprKind::MethodCall(_, recv, [_], ..) => {
let ExprKind::Path(path) = recv.kind else {
return false;
};
if last_path_segment(&path).ident.name != kw::SelfLower {
return false;
}
// We can set this to true here no matter what as if it's a `MethodCall` and goes to the
// `else` branch, it must be a method named `cmp` that isn't `Ord::cmp`
*needs_fully_qualified = true;
typeck
.type_dependent_def_id(cmp_expr.hir_id)
.is_some_and(|def_id| cx.tcx.is_diagnostic_item(sym::ord_cmp_method, def_id))
},
_ => false,
}
}