Skip to content

Commit

Permalink
Auto merge of #7978 - smoelius:master, r=llogiq
Browse files Browse the repository at this point in the history
Add `unnecessary_to_owned` lint

This PR adds a lint to check for unnecessary calls to `ToOwned::to_owned` and other similar functions (e.g., `Cow::into_owned`, `ToString::to_string`, etc.).

The lint checks for expressions of the form `&receiver.to_owned_like()` used in a position requiring type `&T` where one of the following is true:
* `receiver`'s type is `T` exactly
* `receiver`'s type implements `Deref<Target = T>`
* `receiver`'s type implements `AsRef<T>`

The lint additionally checks for expressions of the form `receiver.to_owned_like()` used as arguments of type `impl AsRef<T>`.

It would be nice if the lint could also check for expressions used as arguments to functions like the following:
```
fn foo<T: AsRef<str>>(x: T) { ... }
```
However, I couldn't figure out how to determine whether a function input type was instantiated from a parameter with a trait bound.

If someone could offer me some guidance, I would be happy to add such functionality.

Closes #7933

changelog: Add [`unnecessary_to_owned`] lint
  • Loading branch information
bors committed Dec 15, 2021
2 parents aa3648a + b891389 commit 40fd785
Show file tree
Hide file tree
Showing 17 changed files with 1,887 additions and 20 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3210,6 +3210,7 @@ Released 2018-09-13
[`unnecessary_operation`]: https://rust-lang.github.io/rust-clippy/master/index.html#unnecessary_operation
[`unnecessary_self_imports`]: https://rust-lang.github.io/rust-clippy/master/index.html#unnecessary_self_imports
[`unnecessary_sort_by`]: https://rust-lang.github.io/rust-clippy/master/index.html#unnecessary_sort_by
[`unnecessary_to_owned`]: https://rust-lang.github.io/rust-clippy/master/index.html#unnecessary_to_owned
[`unnecessary_unwrap`]: https://rust-lang.github.io/rust-clippy/master/index.html#unnecessary_unwrap
[`unnecessary_wraps`]: https://rust-lang.github.io/rust-clippy/master/index.html#unnecessary_wraps
[`unneeded_field_pattern`]: https://rust-lang.github.io/rust-clippy/master/index.html#unneeded_field_pattern
Expand Down
1 change: 1 addition & 0 deletions clippy_lints/src/lib.register_all.rs
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,7 @@ store.register_group(true, "clippy::all", Some("clippy_all"), vec![
LintId::of(methods::UNNECESSARY_FILTER_MAP),
LintId::of(methods::UNNECESSARY_FOLD),
LintId::of(methods::UNNECESSARY_LAZY_EVALUATIONS),
LintId::of(methods::UNNECESSARY_TO_OWNED),
LintId::of(methods::UNWRAP_OR_ELSE_DEFAULT),
LintId::of(methods::USELESS_ASREF),
LintId::of(methods::WRONG_SELF_CONVENTION),
Expand Down
1 change: 1 addition & 0 deletions clippy_lints/src/lib.register_lints.rs
Original file line number Diff line number Diff line change
Expand Up @@ -315,6 +315,7 @@ store.register_lints(&[
methods::UNNECESSARY_FILTER_MAP,
methods::UNNECESSARY_FOLD,
methods::UNNECESSARY_LAZY_EVALUATIONS,
methods::UNNECESSARY_TO_OWNED,
methods::UNWRAP_OR_ELSE_DEFAULT,
methods::UNWRAP_USED,
methods::USELESS_ASREF,
Expand Down
1 change: 1 addition & 0 deletions clippy_lints/src/lib.register_perf.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ store.register_group(true, "clippy::perf", Some("clippy_perf"), vec![
LintId::of(methods::MANUAL_STR_REPEAT),
LintId::of(methods::OR_FUN_CALL),
LintId::of(methods::SINGLE_CHAR_PATTERN),
LintId::of(methods::UNNECESSARY_TO_OWNED),
LintId::of(misc::CMP_OWNED),
LintId::of(mutex_atomic::MUTEX_ATOMIC),
LintId::of(redundant_clone::REDUNDANT_CLONE),
Expand Down
29 changes: 20 additions & 9 deletions clippy_lints/src/methods/implicit_clone.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,15 +12,7 @@ use super::IMPLICIT_CLONE;
pub fn check(cx: &LateContext<'_>, method_name: &str, expr: &hir::Expr<'_>, recv: &hir::Expr<'_>, span: Span) {
if_chain! {
if let Some(method_def_id) = cx.typeck_results().type_dependent_def_id(expr.hir_id);
if match method_name {
"to_os_string" => is_diag_item_method(cx, method_def_id, sym::OsStr),
"to_owned" => is_diag_trait_item(cx, method_def_id, sym::ToOwned),
"to_path_buf" => is_diag_item_method(cx, method_def_id, sym::Path),
"to_vec" => cx.tcx.impl_of_method(method_def_id)
.map(|impl_did| Some(impl_did) == cx.tcx.lang_items().slice_alloc_impl())
== Some(true),
_ => false,
};
if is_clone_like(cx, method_name, method_def_id);
let return_type = cx.typeck_results().expr_ty(expr);
let input_type = cx.typeck_results().expr_ty(recv).peel_refs();
if let Some(ty_name) = input_type.ty_adt_def().map(|adt_def| cx.tcx.item_name(adt_def.did));
Expand All @@ -38,3 +30,22 @@ pub fn check(cx: &LateContext<'_>, method_name: &str, expr: &hir::Expr<'_>, recv
}
}
}

/// Returns true if the named method can be used to clone the receiver.
/// Note that `to_string` is not flagged by `implicit_clone`. So other lints that call
/// `is_clone_like` and that do flag `to_string` must handle it separately. See, e.g.,
/// `is_to_owned_like` in `unnecessary_to_owned.rs`.
pub fn is_clone_like(cx: &LateContext<'_>, method_name: &str, method_def_id: hir::def_id::DefId) -> bool {
match method_name {
"to_os_string" => is_diag_item_method(cx, method_def_id, sym::OsStr),
"to_owned" => is_diag_trait_item(cx, method_def_id, sym::ToOwned),
"to_path_buf" => is_diag_item_method(cx, method_def_id, sym::Path),
"to_vec" => {
cx.tcx
.impl_of_method(method_def_id)
.map(|impl_did| Some(impl_did) == cx.tcx.lang_items().slice_alloc_impl())
== Some(true)
},
_ => false,
}
}
32 changes: 31 additions & 1 deletion clippy_lints/src/methods/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,9 @@ mod suspicious_splitn;
mod uninit_assumed_init;
mod unnecessary_filter_map;
mod unnecessary_fold;
mod unnecessary_iter_cloned;
mod unnecessary_lazy_eval;
mod unnecessary_to_owned;
mod unwrap_or_else_default;
mod unwrap_used;
mod useless_asref;
Expand Down Expand Up @@ -1885,6 +1887,32 @@ declare_clippy_lint! {
"usages of `str::splitn` that can be replaced with `str::split`"
}

declare_clippy_lint! {
/// ### What it does
/// Checks for unnecessary calls to [`ToOwned::to_owned`](https://doc.rust-lang.org/std/borrow/trait.ToOwned.html#tymethod.to_owned)
/// and other `to_owned`-like functions.
///
/// ### Why is this bad?
/// The unnecessary calls result in useless allocations.
///
/// ### Example
/// ```rust
/// let path = std::path::Path::new("x");
/// foo(&path.to_string_lossy().to_string());
/// fn foo(s: &str) {}
/// ```
/// Use instead:
/// ```rust
/// let path = std::path::Path::new("x");
/// foo(&path.to_string_lossy());
/// fn foo(s: &str) {}
/// ```
#[clippy::version = "1.58.0"]
pub UNNECESSARY_TO_OWNED,
perf,
"unnecessary calls to `to_owned`-like functions"
}

pub struct Methods {
avoid_breaking_exported_api: bool,
msrv: Option<RustcVersion>,
Expand Down Expand Up @@ -1964,7 +1992,8 @@ impl_lint_pass!(Methods => [
MANUAL_STR_REPEAT,
EXTEND_WITH_DRAIN,
MANUAL_SPLIT_ONCE,
NEEDLESS_SPLITN
NEEDLESS_SPLITN,
UNNECESSARY_TO_OWNED,
]);

/// Extracts a method call name, args, and `Span` of the method name.
Expand Down Expand Up @@ -2007,6 +2036,7 @@ impl<'tcx> LateLintPass<'tcx> for Methods {
single_char_add_str::check(cx, expr, args);
into_iter_on_ref::check(cx, expr, *method_span, method_call.ident.name, args);
single_char_pattern::check(cx, expr, method_call.ident.name, args);
unnecessary_to_owned::check(cx, expr, method_call.ident.name, args);
},
hir::ExprKind::Binary(op, lhs, rhs) if op.node == hir::BinOpKind::Eq || op.node == hir::BinOpKind::Ne => {
let mut info = BinaryExprInfo {
Expand Down
177 changes: 177 additions & 0 deletions clippy_lints/src/methods/unnecessary_iter_cloned.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
use clippy_utils::diagnostics::span_lint_and_then;
use clippy_utils::higher::ForLoop;
use clippy_utils::source::snippet_opt;
use clippy_utils::ty::{get_associated_type, get_iterator_item_ty, implements_trait};
use clippy_utils::{fn_def_id, get_parent_expr, path_to_local_id, usage};
use rustc_errors::Applicability;
use rustc_hir::intravisit::{walk_expr, NestedVisitorMap, Visitor};
use rustc_hir::{def_id::DefId, BorrowKind, Expr, ExprKind, HirId, LangItem, Mutability, Pat};
use rustc_lint::LateContext;
use rustc_middle::{hir::map::Map, ty};
use rustc_span::{sym, Symbol};

use super::UNNECESSARY_TO_OWNED;

pub fn check(cx: &LateContext<'tcx>, expr: &'tcx Expr<'tcx>, method_name: Symbol, receiver: &'tcx Expr<'tcx>) -> bool {
if_chain! {
if let Some(parent) = get_parent_expr(cx, expr);
if let Some(callee_def_id) = fn_def_id(cx, parent);
if is_into_iter(cx, callee_def_id);
then {
check_for_loop_iter(cx, parent, method_name, receiver)
} else {
false
}
}
}

/// Checks whether `expr` is an iterator in a `for` loop and, if so, determines whether the
/// iterated-over items could be iterated over by reference. The reason why `check` above does not
/// include this code directly is so that it can be called from
/// `unnecessary_into_owned::check_into_iter_call_arg`.
pub fn check_for_loop_iter(
cx: &LateContext<'tcx>,
expr: &'tcx Expr<'tcx>,
method_name: Symbol,
receiver: &'tcx Expr<'tcx>,
) -> bool {
if_chain! {
if let Some(grandparent) = get_parent_expr(cx, expr).and_then(|parent| get_parent_expr(cx, parent));
if let Some(ForLoop { pat, body, .. }) = ForLoop::hir(grandparent);
let (clone_or_copy_needed, addr_of_exprs) = clone_or_copy_needed(cx, pat, body);
if !clone_or_copy_needed;
if let Some(receiver_snippet) = snippet_opt(cx, receiver.span);
then {
let snippet = if_chain! {
if let ExprKind::MethodCall(maybe_iter_method_name, _, [collection], _) = receiver.kind;
if maybe_iter_method_name.ident.name == sym::iter;

if let Some(iterator_trait_id) = cx.tcx.get_diagnostic_item(sym::Iterator);
let receiver_ty = cx.typeck_results().expr_ty(receiver);
if implements_trait(cx, receiver_ty, iterator_trait_id, &[]);
if let Some(iter_item_ty) = get_iterator_item_ty(cx, receiver_ty);

if let Some(into_iterator_trait_id) = cx.tcx.get_diagnostic_item(sym::IntoIterator);
let collection_ty = cx.typeck_results().expr_ty(collection);
if implements_trait(cx, collection_ty, into_iterator_trait_id, &[]);
if let Some(into_iter_item_ty) = get_associated_type(cx, collection_ty, into_iterator_trait_id, "Item");

if iter_item_ty == into_iter_item_ty;
if let Some(collection_snippet) = snippet_opt(cx, collection.span);
then {
collection_snippet
} else {
receiver_snippet
}
};
span_lint_and_then(
cx,
UNNECESSARY_TO_OWNED,
expr.span,
&format!("unnecessary use of `{}`", method_name),
|diag| {
diag.span_suggestion(expr.span, "use", snippet, Applicability::MachineApplicable);
for addr_of_expr in addr_of_exprs {
match addr_of_expr.kind {
ExprKind::AddrOf(_, _, referent) => {
let span = addr_of_expr.span.with_hi(referent.span.lo());
diag.span_suggestion(span, "remove this `&`", String::new(), Applicability::MachineApplicable);
}
_ => unreachable!(),
}
}
}
);
return true;
}
}
false
}

/// The core logic of `check_for_loop_iter` above, this function wraps a use of
/// `CloneOrCopyVisitor`.
fn clone_or_copy_needed(
cx: &LateContext<'tcx>,
pat: &Pat<'tcx>,
body: &'tcx Expr<'tcx>,
) -> (bool, Vec<&'tcx Expr<'tcx>>) {
let mut visitor = CloneOrCopyVisitor {
cx,
binding_hir_ids: pat_bindings(pat),
clone_or_copy_needed: false,
addr_of_exprs: Vec::new(),
};
visitor.visit_expr(body);
(visitor.clone_or_copy_needed, visitor.addr_of_exprs)
}

/// Returns a vector of all `HirId`s bound by the pattern.
fn pat_bindings(pat: &Pat<'_>) -> Vec<HirId> {
let mut collector = usage::ParamBindingIdCollector {
binding_hir_ids: Vec::new(),
};
collector.visit_pat(pat);
collector.binding_hir_ids
}

/// `clone_or_copy_needed` will be false when `CloneOrCopyVisitor` is done visiting if the only
/// operations performed on `binding_hir_ids` are:
/// * to take non-mutable references to them
/// * to use them as non-mutable `&self` in method calls
/// If any of `binding_hir_ids` is used in any other way, then `clone_or_copy_needed` will be true
/// when `CloneOrCopyVisitor` is done visiting.
struct CloneOrCopyVisitor<'cx, 'tcx> {
cx: &'cx LateContext<'tcx>,
binding_hir_ids: Vec<HirId>,
clone_or_copy_needed: bool,
addr_of_exprs: Vec<&'tcx Expr<'tcx>>,
}

impl<'cx, 'tcx> Visitor<'tcx> for CloneOrCopyVisitor<'cx, 'tcx> {
type Map = Map<'tcx>;

fn nested_visit_map(&mut self) -> NestedVisitorMap<Self::Map> {
NestedVisitorMap::OnlyBodies(self.cx.tcx.hir())
}

fn visit_expr(&mut self, expr: &'tcx Expr<'tcx>) {
walk_expr(self, expr);
if self.is_binding(expr) {
if let Some(parent) = get_parent_expr(self.cx, expr) {
match parent.kind {
ExprKind::AddrOf(BorrowKind::Ref, Mutability::Not, _) => {
self.addr_of_exprs.push(parent);
return;
},
ExprKind::MethodCall(_, _, args, _) => {
if_chain! {
if args.iter().skip(1).all(|arg| !self.is_binding(arg));
if let Some(method_def_id) = self.cx.typeck_results().type_dependent_def_id(parent.hir_id);
let method_ty = self.cx.tcx.type_of(method_def_id);
let self_ty = method_ty.fn_sig(self.cx.tcx).input(0).skip_binder();
if matches!(self_ty.kind(), ty::Ref(_, _, Mutability::Not));
then {
return;
}
}
},
_ => {},
}
}
self.clone_or_copy_needed = true;
}
}
}

impl<'cx, 'tcx> CloneOrCopyVisitor<'cx, 'tcx> {
fn is_binding(&self, expr: &Expr<'tcx>) -> bool {
self.binding_hir_ids
.iter()
.any(|hir_id| path_to_local_id(expr, *hir_id))
}
}

/// Returns true if the named method is `IntoIterator::into_iter`.
pub fn is_into_iter(cx: &LateContext<'_>, callee_def_id: DefId) -> bool {
cx.tcx.lang_items().require(LangItem::IntoIterIntoIter) == Ok(callee_def_id)
}
Loading

0 comments on commit 40fd785

Please sign in to comment.