Skip to content

Commit 2d4d8e1

Browse files
committed
Auto merge of rust-lang#8984 - xanathar:pr/suspicious_to_owned, r=llogiq
Implemented `suspicious_to_owned` lint to check if `to_owned` is called on a `Cow` changelog: Add lint ``[`suspicious_to_owned`]`` ----------------- Hi, posting this unsolicited PR as I've been burned by this issue :) Being unsolicited, feel free to reject it or reassign a different lint level etc. This lint checks whether `to_owned` is called on `Cow<'_, _>`. This is done because `to_owned` is very similarly named to `into_owned`, but the effect of calling those two methods is completely different (one makes the `Cow::Borrowed` into a `Cow::Owned`, the other just clones the `Cow`). If the cow is then passed to code for which the type is not checked (e.g. generics, closures, etc.) it might slip through and if the cow data is coming from an unsafe context there is the potential for accidentally cause undefined behavior. Even if not falling into this painful case, there's really no reason to call `to_owned` on a `Cow` other than confusing people reading the code: either `into_owned` or `clone` should be called. Note that this overlaps perfectly with `implicit_clone` as a warning, but `implicit_clone` is classified pedantic (while the consequences for `Cow` might be of a wider blast radius than just pedantry); given the overlap, I set-up the lint so that if `suspicious_to_owned` triggers `implicit_clone` will not trigger. I'm not 100% sure this is done in the correct way (I tried to copy what other lints were doing) so please provide feedback on it if it isn't. ### Checklist - \[x] Followed [lint naming conventions][lint_naming] - \[x] Added passing UI tests (including committed `.stderr` file) - \[x] `cargo test` passes locally - \[x] Executed `cargo dev update_lints` - \[x] Added lint documentation - \[x] Run `cargo dev fmt`
2 parents be8bd60 + de028e2 commit 2d4d8e1

File tree

9 files changed

+202
-1
lines changed

9 files changed

+202
-1
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4082,6 +4082,7 @@ Released 2018-09-13
40824082
[`suspicious_op_assign_impl`]: https://rust-lang.github.io/rust-clippy/master/index.html#suspicious_op_assign_impl
40834083
[`suspicious_operation_groupings`]: https://rust-lang.github.io/rust-clippy/master/index.html#suspicious_operation_groupings
40844084
[`suspicious_splitn`]: https://rust-lang.github.io/rust-clippy/master/index.html#suspicious_splitn
4085+
[`suspicious_to_owned`]: https://rust-lang.github.io/rust-clippy/master/index.html#suspicious_to_owned
40854086
[`suspicious_unary_op_formatting`]: https://rust-lang.github.io/rust-clippy/master/index.html#suspicious_unary_op_formatting
40864087
[`swap_ptr_to_ref`]: https://rust-lang.github.io/rust-clippy/master/index.html#swap_ptr_to_ref
40874088
[`tabs_in_doc_comments`]: https://rust-lang.github.io/rust-clippy/master/index.html#tabs_in_doc_comments

clippy_lints/src/lib.register_all.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -207,6 +207,7 @@ store.register_group(true, "clippy::all", Some("clippy_all"), vec![
207207
LintId::of(methods::STRING_EXTEND_CHARS),
208208
LintId::of(methods::SUSPICIOUS_MAP),
209209
LintId::of(methods::SUSPICIOUS_SPLITN),
210+
LintId::of(methods::SUSPICIOUS_TO_OWNED),
210211
LintId::of(methods::UNINIT_ASSUMED_INIT),
211212
LintId::of(methods::UNIT_HASH),
212213
LintId::of(methods::UNNECESSARY_FILTER_MAP),

clippy_lints/src/lib.register_lints.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -358,6 +358,7 @@ store.register_lints(&[
358358
methods::STRING_EXTEND_CHARS,
359359
methods::SUSPICIOUS_MAP,
360360
methods::SUSPICIOUS_SPLITN,
361+
methods::SUSPICIOUS_TO_OWNED,
361362
methods::UNINIT_ASSUMED_INIT,
362363
methods::UNIT_HASH,
363364
methods::UNNECESSARY_FILTER_MAP,

clippy_lints/src/lib.register_suspicious.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ store.register_group(true, "clippy::suspicious", Some("clippy_suspicious"), vec!
2424
LintId::of(loops::MUT_RANGE_BOUND),
2525
LintId::of(methods::NO_EFFECT_REPLACE),
2626
LintId::of(methods::SUSPICIOUS_MAP),
27+
LintId::of(methods::SUSPICIOUS_TO_OWNED),
2728
LintId::of(multi_assignments::MULTI_ASSIGNMENTS),
2829
LintId::of(mut_key::MUTABLE_KEY_TYPE),
2930
LintId::of(octal_escapes::OCTAL_ESCAPES),

clippy_lints/src/methods/mod.rs

Lines changed: 57 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@ mod str_splitn;
7878
mod string_extend_chars;
7979
mod suspicious_map;
8080
mod suspicious_splitn;
81+
mod suspicious_to_owned;
8182
mod uninit_assumed_init;
8283
mod unit_hash;
8384
mod unnecessary_filter_map;
@@ -2053,6 +2054,55 @@ declare_clippy_lint! {
20532054
"replace `.iter().count()` with `.len()`"
20542055
}
20552056

2057+
declare_clippy_lint! {
2058+
/// ### What it does
2059+
/// Checks for the usage of `_.to_owned()`, on a `Cow<'_, _>`.
2060+
///
2061+
/// ### Why is this bad?
2062+
/// Calling `to_owned()` on a `Cow` creates a clone of the `Cow`
2063+
/// itself, without taking ownership of the `Cow` contents (i.e.
2064+
/// it's equivalent to calling `Cow::clone`).
2065+
/// The similarly named `into_owned` method, on the other hand,
2066+
/// clones the `Cow` contents, effectively turning any `Cow::Borrowed`
2067+
/// into a `Cow::Owned`.
2068+
///
2069+
/// Given the potential ambiguity, consider replacing `to_owned`
2070+
/// with `clone` for better readability or, if getting a `Cow::Owned`
2071+
/// was the original intent, using `into_owned` instead.
2072+
///
2073+
/// ### Example
2074+
/// ```rust
2075+
/// # use std::borrow::Cow;
2076+
/// let s = "Hello world!";
2077+
/// let cow = Cow::Borrowed(s);
2078+
///
2079+
/// let data = cow.to_owned();
2080+
/// assert!(matches!(data, Cow::Borrowed(_)))
2081+
/// ```
2082+
/// Use instead:
2083+
/// ```rust
2084+
/// # use std::borrow::Cow;
2085+
/// let s = "Hello world!";
2086+
/// let cow = Cow::Borrowed(s);
2087+
///
2088+
/// let data = cow.clone();
2089+
/// assert!(matches!(data, Cow::Borrowed(_)))
2090+
/// ```
2091+
/// or
2092+
/// ```rust
2093+
/// # use std::borrow::Cow;
2094+
/// let s = "Hello world!";
2095+
/// let cow = Cow::Borrowed(s);
2096+
///
2097+
/// let data = cow.into_owned();
2098+
/// assert!(matches!(data, String))
2099+
/// ```
2100+
#[clippy::version = "1.65.0"]
2101+
pub SUSPICIOUS_TO_OWNED,
2102+
suspicious,
2103+
"calls to `to_owned` on a `Cow<'_, _>` might not do what they are expected"
2104+
}
2105+
20562106
declare_clippy_lint! {
20572107
/// ### What it does
20582108
/// Checks for calls to [`splitn`]
@@ -3075,6 +3125,7 @@ impl_lint_pass!(Methods => [
30753125
FROM_ITER_INSTEAD_OF_COLLECT,
30763126
INSPECT_FOR_EACH,
30773127
IMPLICIT_CLONE,
3128+
SUSPICIOUS_TO_OWNED,
30783129
SUSPICIOUS_SPLITN,
30793130
MANUAL_STR_REPEAT,
30803131
EXTEND_WITH_DRAIN,
@@ -3553,7 +3604,12 @@ impl Methods {
35533604
}
35543605
unnecessary_lazy_eval::check(cx, expr, recv, arg, "then_some");
35553606
},
3556-
("to_os_string" | "to_owned" | "to_path_buf" | "to_vec", []) => {
3607+
("to_owned", []) => {
3608+
if !suspicious_to_owned::check(cx, expr, recv) {
3609+
implicit_clone::check(cx, name, expr, recv);
3610+
}
3611+
},
3612+
("to_os_string" | "to_path_buf" | "to_vec", []) => {
35573613
implicit_clone::check(cx, name, expr, recv);
35583614
},
35593615
("unwrap", []) => {
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
use clippy_utils::diagnostics::span_lint_and_sugg;
2+
use clippy_utils::is_diag_trait_item;
3+
use clippy_utils::source::snippet_with_context;
4+
use if_chain::if_chain;
5+
use rustc_errors::Applicability;
6+
use rustc_hir as hir;
7+
use rustc_lint::LateContext;
8+
use rustc_middle::ty;
9+
use rustc_span::sym;
10+
11+
use super::SUSPICIOUS_TO_OWNED;
12+
13+
pub fn check(cx: &LateContext<'_>, expr: &hir::Expr<'_>, recv: &hir::Expr<'_>) -> bool {
14+
if_chain! {
15+
if let Some(method_def_id) = cx.typeck_results().type_dependent_def_id(expr.hir_id);
16+
if is_diag_trait_item(cx, method_def_id, sym::ToOwned);
17+
let input_type = cx.typeck_results().expr_ty(expr);
18+
if let ty::Adt(adt, _) = cx.typeck_results().expr_ty(expr).kind();
19+
if cx.tcx.is_diagnostic_item(sym::Cow, adt.did());
20+
then {
21+
let mut app = Applicability::MaybeIncorrect;
22+
let recv_snip = snippet_with_context(cx, recv.span, expr.span.ctxt(), "..", &mut app).0;
23+
span_lint_and_sugg(
24+
cx,
25+
SUSPICIOUS_TO_OWNED,
26+
expr.span,
27+
&format!("this `to_owned` call clones the {0} itself and does not cause the {0} contents to become owned", input_type),
28+
"consider using, depending on intent",
29+
format!("{0}.clone()` or `{0}.into_owned()", recv_snip),
30+
app,
31+
);
32+
return true;
33+
}
34+
}
35+
false
36+
}

tests/compile-test.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -393,6 +393,7 @@ const RUSTFIX_COVERAGE_KNOWN_EXCEPTIONS: &[&str] = &[
393393
"search_is_some.rs",
394394
"single_component_path_imports_nested_first.rs",
395395
"string_add.rs",
396+
"suspicious_to_owned.rs",
396397
"toplevel_ref_arg_non_rustfix.rs",
397398
"unit_arg.rs",
398399
"unnecessary_clone.rs",

tests/ui/suspicious_to_owned.rs

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
#![warn(clippy::suspicious_to_owned)]
2+
#![warn(clippy::implicit_clone)]
3+
#![allow(clippy::redundant_clone)]
4+
use std::borrow::Cow;
5+
use std::ffi::CStr;
6+
7+
fn main() {
8+
let moo = "Moooo";
9+
let c_moo = b"Moooo\0";
10+
let c_moo_ptr = c_moo.as_ptr() as *const i8;
11+
let moos = ['M', 'o', 'o'];
12+
let moos_vec = moos.to_vec();
13+
14+
// we expect this to be linted
15+
let cow = Cow::Borrowed(moo);
16+
let _ = cow.to_owned();
17+
// we expect no lints for this
18+
let cow = Cow::Borrowed(moo);
19+
let _ = cow.into_owned();
20+
// we expect no lints for this
21+
let cow = Cow::Borrowed(moo);
22+
let _ = cow.clone();
23+
24+
// we expect this to be linted
25+
let cow = Cow::Borrowed(&moos);
26+
let _ = cow.to_owned();
27+
// we expect no lints for this
28+
let cow = Cow::Borrowed(&moos);
29+
let _ = cow.into_owned();
30+
// we expect no lints for this
31+
let cow = Cow::Borrowed(&moos);
32+
let _ = cow.clone();
33+
34+
// we expect this to be linted
35+
let cow = Cow::Borrowed(&moos_vec);
36+
let _ = cow.to_owned();
37+
// we expect no lints for this
38+
let cow = Cow::Borrowed(&moos_vec);
39+
let _ = cow.into_owned();
40+
// we expect no lints for this
41+
let cow = Cow::Borrowed(&moos_vec);
42+
let _ = cow.clone();
43+
44+
// we expect this to be linted
45+
let cow = unsafe { CStr::from_ptr(c_moo_ptr) }.to_string_lossy();
46+
let _ = cow.to_owned();
47+
// we expect no lints for this
48+
let cow = unsafe { CStr::from_ptr(c_moo_ptr) }.to_string_lossy();
49+
let _ = cow.into_owned();
50+
// we expect no lints for this
51+
let cow = unsafe { CStr::from_ptr(c_moo_ptr) }.to_string_lossy();
52+
let _ = cow.clone();
53+
54+
// we expect no lints for these
55+
let _ = moo.to_owned();
56+
let _ = c_moo.to_owned();
57+
let _ = moos.to_owned();
58+
59+
// we expect implicit_clone lints for these
60+
let _ = String::from(moo).to_owned();
61+
let _ = moos_vec.to_owned();
62+
}

tests/ui/suspicious_to_owned.stderr

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
error: this `to_owned` call clones the std::borrow::Cow<str> itself and does not cause the std::borrow::Cow<str> contents to become owned
2+
--> $DIR/suspicious_to_owned.rs:16:13
3+
|
4+
LL | let _ = cow.to_owned();
5+
| ^^^^^^^^^^^^^^ help: consider using, depending on intent: `cow.clone()` or `cow.into_owned()`
6+
|
7+
= note: `-D clippy::suspicious-to-owned` implied by `-D warnings`
8+
9+
error: this `to_owned` call clones the std::borrow::Cow<[char; 3]> itself and does not cause the std::borrow::Cow<[char; 3]> contents to become owned
10+
--> $DIR/suspicious_to_owned.rs:26:13
11+
|
12+
LL | let _ = cow.to_owned();
13+
| ^^^^^^^^^^^^^^ help: consider using, depending on intent: `cow.clone()` or `cow.into_owned()`
14+
15+
error: this `to_owned` call clones the std::borrow::Cow<std::vec::Vec<char>> itself and does not cause the std::borrow::Cow<std::vec::Vec<char>> contents to become owned
16+
--> $DIR/suspicious_to_owned.rs:36:13
17+
|
18+
LL | let _ = cow.to_owned();
19+
| ^^^^^^^^^^^^^^ help: consider using, depending on intent: `cow.clone()` or `cow.into_owned()`
20+
21+
error: this `to_owned` call clones the std::borrow::Cow<str> itself and does not cause the std::borrow::Cow<str> contents to become owned
22+
--> $DIR/suspicious_to_owned.rs:46:13
23+
|
24+
LL | let _ = cow.to_owned();
25+
| ^^^^^^^^^^^^^^ help: consider using, depending on intent: `cow.clone()` or `cow.into_owned()`
26+
27+
error: implicitly cloning a `String` by calling `to_owned` on its dereferenced type
28+
--> $DIR/suspicious_to_owned.rs:60:13
29+
|
30+
LL | let _ = String::from(moo).to_owned();
31+
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ help: consider using: `String::from(moo).clone()`
32+
|
33+
= note: `-D clippy::implicit-clone` implied by `-D warnings`
34+
35+
error: implicitly cloning a `Vec` by calling `to_owned` on its dereferenced type
36+
--> $DIR/suspicious_to_owned.rs:61:13
37+
|
38+
LL | let _ = moos_vec.to_owned();
39+
| ^^^^^^^^^^^^^^^^^^^ help: consider using: `moos_vec.clone()`
40+
41+
error: aborting due to 6 previous errors
42+

0 commit comments

Comments
 (0)