Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Avoid emitting assigning_clones when cloned data borrows from the place to clone into #12756

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
78 changes: 77 additions & 1 deletion clippy_lints/src/assigning_clones.rs
Original file line number Diff line number Diff line change
@@ -1,16 +1,18 @@
use clippy_config::msrvs::{self, Msrv};
use clippy_utils::diagnostics::span_lint_and_then;
use clippy_utils::macros::HirNode;
use clippy_utils::mir::{enclosing_mir, PossibleBorrowerMap};
use clippy_utils::sugg::Sugg;
use clippy_utils::{is_trait_method, local_is_initialized, path_to_local};
use rustc_errors::Applicability;
use rustc_hir::{self as hir, Expr, ExprKind};
use rustc_lint::{LateContext, LateLintPass};
use rustc_middle::mir;
use rustc_middle::ty::{self, Instance, Mutability};
use rustc_session::impl_lint_pass;
use rustc_span::def_id::DefId;
use rustc_span::symbol::sym;
use rustc_span::{ExpnKind, SyntaxContext};
use rustc_span::{ExpnKind, Span, SyntaxContext};

declare_clippy_lint! {
/// ### What it does
Expand Down Expand Up @@ -144,6 +146,7 @@ fn extract_call<'tcx>(cx: &LateContext<'tcx>, expr: &'tcx Expr<'tcx>) -> Option<
};

Some(CallCandidate {
span: expr.span,
target,
kind,
method_def_id: resolved_method.def_id(),
Expand Down Expand Up @@ -215,13 +218,85 @@ fn is_ok_to_suggest<'tcx>(cx: &LateContext<'tcx>, lhs: &Expr<'tcx>, call: &CallC
return false;
};

if clone_source_borrows_from_dest(cx, lhs, call.span) {
return false;
}

// Now take a look if the impl block defines an implementation for the method that we're interested
// in. If not, then we're using a default implementation, which is not interesting, so we will
// not suggest the lint.
let implemented_fns = cx.tcx.impl_item_implementor_ids(impl_block);
implemented_fns.contains_key(&provided_fn.def_id)
}

/// Checks if the data being cloned borrows from the place that is being assigned to:
///
/// ```
/// let mut s = String::new();
/// let s2 = &s;
/// s = s2.to_owned();
/// ```
///
/// This cannot be written `s2.clone_into(&mut s)` because it has conflicting borrows.
fn clone_source_borrows_from_dest(cx: &LateContext<'_>, lhs: &Expr<'_>, call_span: Span) -> bool {
/// If this basic block only exists to drop a local as part of an assignment, returns its
/// successor. Otherwise returns the basic block that was passed in.
fn skip_drop_block(mir: &mir::Body<'_>, bb: mir::BasicBlock) -> mir::BasicBlock {
if let mir::TerminatorKind::Drop { target, .. } = mir.basic_blocks[bb].terminator().kind {
target
} else {
bb
}
}

let Some(mir) = enclosing_mir(cx.tcx, lhs.hir_id) else {
return false;
};
let PossibleBorrowerMap { map: borrow_map, .. } = PossibleBorrowerMap::new(cx, mir);

// The operation `dest = src.to_owned()` in MIR is split up across 3 blocks *if* the type has `Drop`
// code. For types that don't, the second basic block is simply skipped.
// For the doc example above that would be roughly:
//
// bb0:
// s2 = &s
// s_temp = ToOwned::to_owned(move s2) -> bb1
//
// bb1:
// drop(s) -> bb2 // drop the old string
//
// bb2:
// s = s_temp
for bb in mir.basic_blocks.iter() {
let terminator = bb.terminator();

// Look for the to_owned/clone call.
if terminator.source_info.span != call_span {
continue;
}

if let mir::TerminatorKind::Call { ref args, target: Some(assign_bb), .. } = terminator.kind
&& let [source] = &**args
&& let mir::Operand::Move(source) = &source.node
&& let assign_bb = skip_drop_block(mir, assign_bb)
// Skip any storage statements as they are just noise
&& let Some(assignment) = mir.basic_blocks[assign_bb].statements
.iter()
.find(|stmt| {
!matches!(stmt.kind, mir::StatementKind::StorageDead(_) | mir::StatementKind::StorageLive(_))
})
&& let mir::StatementKind::Assign(box (borrowed, _)) = &assignment.kind
&& let Some(borrowers) = borrow_map.get(&borrowed.local)
&& borrowers.contains(source.local)
{
return true;
}

return false;
}
false
}

fn suggest<'tcx>(
cx: &LateContext<'tcx>,
ctxt: SyntaxContext,
Expand Down Expand Up @@ -255,6 +330,7 @@ enum TargetTrait {

#[derive(Debug)]
struct CallCandidate<'tcx> {
span: Span,
target: TargetTrait,
kind: CallKind<'tcx>,
// DefId of the called method from an impl block that implements the target trait
Expand Down
2 changes: 1 addition & 1 deletion clippy_utils/src/mir/possible_borrower.rs
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ impl<'a, 'b, 'tcx> mir::visit::Visitor<'tcx> for PossibleBorrowerVisitor<'a, 'b,
fn visit_assign(&mut self, place: &mir::Place<'tcx>, rvalue: &mir::Rvalue<'_>, _location: mir::Location) {
let lhs = place.local;
match rvalue {
mir::Rvalue::Ref(_, _, borrowed) => {
mir::Rvalue::Ref(_, _, borrowed) | mir::Rvalue::CopyForDeref(borrowed) => {
self.possible_borrower.add(borrowed.local, lhs);
},
other => {
Expand Down
56 changes: 56 additions & 0 deletions tests/ui/assigning_clones.fixed
Original file line number Diff line number Diff line change
Expand Up @@ -272,3 +272,59 @@ impl<'a> Add for &'a mut HasCloneFrom {
self
}
}

mod borrowck_conflicts {
//! Cases where clone_into and friends cannot be used because the src/dest have conflicting
//! borrows.
use std::path::PathBuf;

fn issue12444(mut name: String) {
let parts = name.split(", ").collect::<Vec<_>>();
let first = *parts.first().unwrap();
name = first.to_owned();
}

fn issue12444_simple() {
let mut s = String::new();
let s2 = &s;
s = s2.to_owned();
}

fn issue12444_nodrop_projections() {
struct NoDrop;

impl Clone for NoDrop {
fn clone(&self) -> Self {
todo!()
}
fn clone_from(&mut self, other: &Self) {
todo!()
}
}

let mut s = NoDrop;
let s2 = &s;
s = s2.clone();

let mut s = (NoDrop, NoDrop);
let s2 = &s.0;
s.0 = s2.clone();

// This *could* emit a warning, but PossibleBorrowerMap only works with locals so it
// considers `s` fully borrowed
let mut s = (NoDrop, NoDrop);
let s2 = &s.1;
s.0 = s2.clone();
}

fn issue12460(mut name: String) {
if let Some(stripped_name) = name.strip_prefix("baz-") {
name = stripped_name.to_owned();
}
}

fn issue12749() {
let mut path = PathBuf::from("/a/b/c");
path = path.components().as_path().to_owned();
}
}
56 changes: 56 additions & 0 deletions tests/ui/assigning_clones.rs
Original file line number Diff line number Diff line change
Expand Up @@ -272,3 +272,59 @@ impl<'a> Add for &'a mut HasCloneFrom {
self
}
}

mod borrowck_conflicts {
//! Cases where clone_into and friends cannot be used because the src/dest have conflicting
//! borrows.
use std::path::PathBuf;

fn issue12444(mut name: String) {
let parts = name.split(", ").collect::<Vec<_>>();
let first = *parts.first().unwrap();
name = first.to_owned();
}

fn issue12444_simple() {
let mut s = String::new();
let s2 = &s;
s = s2.to_owned();
}

fn issue12444_nodrop_projections() {
struct NoDrop;

impl Clone for NoDrop {
fn clone(&self) -> Self {
todo!()
}
fn clone_from(&mut self, other: &Self) {
todo!()
}
}

let mut s = NoDrop;
let s2 = &s;
s = s2.clone();

let mut s = (NoDrop, NoDrop);
let s2 = &s.0;
s.0 = s2.clone();

// This *could* emit a warning, but PossibleBorrowerMap only works with locals so it
// considers `s` fully borrowed
let mut s = (NoDrop, NoDrop);
let s2 = &s.1;
s.0 = s2.clone();
Comment on lines +315 to +317
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a false negative now but fixing it seems like it'd require large changes to PossibleBorrowerMap to teach it disjoint borrows, but I suppose this FN might be better than a FP

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In a sense, a performance lint is about reducing time wasted by the computer, but as human time is more expensive, it's surely more costly to waste human time if no computing performance is likely to be gained in the process.

}

fn issue12460(mut name: String) {
if let Some(stripped_name) = name.strip_prefix("baz-") {
name = stripped_name.to_owned();
}
}

fn issue12749() {
let mut path = PathBuf::from("/a/b/c");
path = path.components().as_path().to_owned();
}
}