Skip to content

Commit a07e887

Browse files
authored
Improve string_to_string lint in case it is in a map call (#14396)
Fixes #14175. There are two "big" cases to handle: `.map(|x| x.to_string())` and `.map(String::to_string)`. If the closure has more than one expression, we should not suggest `.cloned()`. changelog: Improve `string_to_string` lint in case it is in a map call r? @samueltardieu
2 parents 4800557 + 269b913 commit a07e887

File tree

6 files changed

+193
-17
lines changed

6 files changed

+193
-17
lines changed

clippy_lints/src/strings.rs

Lines changed: 84 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ use rustc_session::declare_lint_pass;
1414
use rustc_span::source_map::Spanned;
1515
use rustc_span::sym;
1616

17+
use std::ops::ControlFlow;
18+
1719
declare_clippy_lint! {
1820
/// ### What it does
1921
/// Checks for string appends of the form `x = x + y` (without
@@ -438,27 +440,94 @@ declare_clippy_lint! {
438440

439441
declare_lint_pass!(StringToString => [STRING_TO_STRING]);
440442

443+
fn is_parent_map_like(cx: &LateContext<'_>, expr: &Expr<'_>) -> Option<rustc_span::Span> {
444+
if let Some(parent_expr) = get_parent_expr(cx, expr)
445+
&& let ExprKind::MethodCall(name, _, _, parent_span) = parent_expr.kind
446+
&& name.ident.name == sym::map
447+
&& let Some(caller_def_id) = cx.typeck_results().type_dependent_def_id(parent_expr.hir_id)
448+
&& (clippy_utils::is_diag_item_method(cx, caller_def_id, sym::Result)
449+
|| clippy_utils::is_diag_item_method(cx, caller_def_id, sym::Option)
450+
|| clippy_utils::is_diag_trait_item(cx, caller_def_id, sym::Iterator))
451+
{
452+
Some(parent_span)
453+
} else {
454+
None
455+
}
456+
}
457+
458+
fn is_called_from_map_like(cx: &LateContext<'_>, expr: &Expr<'_>) -> Option<rustc_span::Span> {
459+
// Look for a closure as parent of `expr`, discarding simple blocks
460+
let parent_closure = cx
461+
.tcx
462+
.hir_parent_iter(expr.hir_id)
463+
.try_fold(expr.hir_id, |child_hir_id, (_, node)| match node {
464+
// Check that the child expression is the only expression in the block
465+
Node::Block(block) if block.stmts.is_empty() && block.expr.map(|e| e.hir_id) == Some(child_hir_id) => {
466+
ControlFlow::Continue(block.hir_id)
467+
},
468+
Node::Expr(expr) if matches!(expr.kind, ExprKind::Block(..)) => ControlFlow::Continue(expr.hir_id),
469+
Node::Expr(expr) if matches!(expr.kind, ExprKind::Closure(_)) => ControlFlow::Break(Some(expr)),
470+
_ => ControlFlow::Break(None),
471+
})
472+
.break_value()?;
473+
is_parent_map_like(cx, parent_closure?)
474+
}
475+
476+
fn suggest_cloned_string_to_string(cx: &LateContext<'_>, span: rustc_span::Span) {
477+
span_lint_and_sugg(
478+
cx,
479+
STRING_TO_STRING,
480+
span,
481+
"`to_string()` called on a `String`",
482+
"try",
483+
"cloned()".to_string(),
484+
Applicability::MachineApplicable,
485+
);
486+
}
487+
441488
impl<'tcx> LateLintPass<'tcx> for StringToString {
442489
fn check_expr(&mut self, cx: &LateContext<'tcx>, expr: &Expr<'_>) {
443490
if expr.span.from_expansion() {
444491
return;
445492
}
446493

447-
if let ExprKind::MethodCall(path, self_arg, [], _) = &expr.kind
448-
&& path.ident.name == sym::to_string
449-
&& let ty = cx.typeck_results().expr_ty(self_arg)
450-
&& is_type_lang_item(cx, ty, LangItem::String)
451-
{
452-
#[expect(clippy::collapsible_span_lint_calls, reason = "rust-clippy#7797")]
453-
span_lint_and_then(
454-
cx,
455-
STRING_TO_STRING,
456-
expr.span,
457-
"`to_string()` called on a `String`",
458-
|diag| {
459-
diag.help("consider using `.clone()`");
460-
},
461-
);
494+
match &expr.kind {
495+
ExprKind::MethodCall(path, self_arg, [], _) => {
496+
if path.ident.name == sym::to_string
497+
&& let ty = cx.typeck_results().expr_ty(self_arg)
498+
&& is_type_lang_item(cx, ty.peel_refs(), LangItem::String)
499+
{
500+
if let Some(parent_span) = is_called_from_map_like(cx, expr) {
501+
suggest_cloned_string_to_string(cx, parent_span);
502+
} else {
503+
#[expect(clippy::collapsible_span_lint_calls, reason = "rust-clippy#7797")]
504+
span_lint_and_then(
505+
cx,
506+
STRING_TO_STRING,
507+
expr.span,
508+
"`to_string()` called on a `String`",
509+
|diag| {
510+
diag.help("consider using `.clone()`");
511+
},
512+
);
513+
}
514+
}
515+
},
516+
ExprKind::Path(QPath::TypeRelative(ty, segment)) => {
517+
if segment.ident.name == sym::to_string
518+
&& let rustc_hir::TyKind::Path(QPath::Resolved(_, path)) = ty.peel_refs().kind
519+
&& let rustc_hir::def::Res::Def(_, def_id) = path.res
520+
&& cx
521+
.tcx
522+
.lang_items()
523+
.get(LangItem::String)
524+
.is_some_and(|lang_id| lang_id == def_id)
525+
&& let Some(parent_span) = is_parent_map_like(cx, expr)
526+
{
527+
suggest_cloned_string_to_string(cx, parent_span);
528+
}
529+
},
530+
_ => {},
462531
}
463532
}
464533
}

tests/ui/string_to_string.rs

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,21 @@
11
#![warn(clippy::string_to_string)]
2-
#![allow(clippy::redundant_clone)]
2+
#![allow(clippy::redundant_clone, clippy::unnecessary_literal_unwrap)]
33

44
fn main() {
55
let mut message = String::from("Hello");
66
let mut v = message.to_string();
77
//~^ string_to_string
8+
9+
let variable1 = String::new();
10+
let v = &variable1;
11+
let variable2 = Some(v);
12+
let _ = variable2.map(|x| {
13+
println!();
14+
x.to_string()
15+
});
16+
//~^^ string_to_string
17+
18+
let x = Some(String::new());
19+
let _ = x.unwrap_or_else(|| v.to_string());
20+
//~^ string_to_string
821
}

tests/ui/string_to_string.stderr

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,5 +8,21 @@ LL | let mut v = message.to_string();
88
= note: `-D clippy::string-to-string` implied by `-D warnings`
99
= help: to override `-D warnings` add `#[allow(clippy::string_to_string)]`
1010

11-
error: aborting due to 1 previous error
11+
error: `to_string()` called on a `String`
12+
--> tests/ui/string_to_string.rs:14:9
13+
|
14+
LL | x.to_string()
15+
| ^^^^^^^^^^^^^
16+
|
17+
= help: consider using `.clone()`
18+
19+
error: `to_string()` called on a `String`
20+
--> tests/ui/string_to_string.rs:19:33
21+
|
22+
LL | let _ = x.unwrap_or_else(|| v.to_string());
23+
| ^^^^^^^^^^^^^
24+
|
25+
= help: consider using `.clone()`
26+
27+
error: aborting due to 3 previous errors
1228

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
#![deny(clippy::string_to_string)]
2+
#![allow(clippy::unnecessary_literal_unwrap, clippy::useless_vec, clippy::iter_cloned_collect)]
3+
4+
fn main() {
5+
let variable1 = String::new();
6+
let v = &variable1;
7+
let variable2 = Some(v);
8+
let _ = variable2.cloned();
9+
//~^ string_to_string
10+
let _ = variable2.cloned();
11+
//~^ string_to_string
12+
#[rustfmt::skip]
13+
let _ = variable2.cloned();
14+
//~^ string_to_string
15+
16+
let _ = vec![String::new()].iter().cloned().collect::<Vec<_>>();
17+
//~^ string_to_string
18+
let _ = vec![String::new()].iter().cloned().collect::<Vec<_>>();
19+
//~^ string_to_string
20+
}

tests/ui/string_to_string_in_map.rs

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
#![deny(clippy::string_to_string)]
2+
#![allow(clippy::unnecessary_literal_unwrap, clippy::useless_vec, clippy::iter_cloned_collect)]
3+
4+
fn main() {
5+
let variable1 = String::new();
6+
let v = &variable1;
7+
let variable2 = Some(v);
8+
let _ = variable2.map(String::to_string);
9+
//~^ string_to_string
10+
let _ = variable2.map(|x| x.to_string());
11+
//~^ string_to_string
12+
#[rustfmt::skip]
13+
let _ = variable2.map(|x| { x.to_string() });
14+
//~^ string_to_string
15+
16+
let _ = vec![String::new()].iter().map(String::to_string).collect::<Vec<_>>();
17+
//~^ string_to_string
18+
let _ = vec![String::new()].iter().map(|x| x.to_string()).collect::<Vec<_>>();
19+
//~^ string_to_string
20+
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
error: `to_string()` called on a `String`
2+
--> tests/ui/string_to_string_in_map.rs:8:23
3+
|
4+
LL | let _ = variable2.map(String::to_string);
5+
| ^^^^^^^^^^^^^^^^^^^^^^ help: try: `cloned()`
6+
|
7+
note: the lint level is defined here
8+
--> tests/ui/string_to_string_in_map.rs:1:9
9+
|
10+
LL | #![deny(clippy::string_to_string)]
11+
| ^^^^^^^^^^^^^^^^^^^^^^^^
12+
13+
error: `to_string()` called on a `String`
14+
--> tests/ui/string_to_string_in_map.rs:10:23
15+
|
16+
LL | let _ = variable2.map(|x| x.to_string());
17+
| ^^^^^^^^^^^^^^^^^^^^^^ help: try: `cloned()`
18+
19+
error: `to_string()` called on a `String`
20+
--> tests/ui/string_to_string_in_map.rs:13:23
21+
|
22+
LL | let _ = variable2.map(|x| { x.to_string() });
23+
| ^^^^^^^^^^^^^^^^^^^^^^^^^^ help: try: `cloned()`
24+
25+
error: `to_string()` called on a `String`
26+
--> tests/ui/string_to_string_in_map.rs:16:40
27+
|
28+
LL | let _ = vec![String::new()].iter().map(String::to_string).collect::<Vec<_>>();
29+
| ^^^^^^^^^^^^^^^^^^^^^^ help: try: `cloned()`
30+
31+
error: `to_string()` called on a `String`
32+
--> tests/ui/string_to_string_in_map.rs:18:40
33+
|
34+
LL | let _ = vec![String::new()].iter().map(|x| x.to_string()).collect::<Vec<_>>();
35+
| ^^^^^^^^^^^^^^^^^^^^^^ help: try: `cloned()`
36+
37+
error: aborting due to 5 previous errors
38+

0 commit comments

Comments
 (0)