Skip to content

Commit 01d7a32

Browse files
committed
manual_strip: use existing identifier instead of placeholder
When the manually stripped entity receives a name as the first use through a simple `let` statement, this name can be used in the generated `if let Some(…)` expression instead of a placeholder.
1 parent a8b1782 commit 01d7a32

6 files changed

+188
-21
lines changed

clippy_lints/src/manual_strip.rs

+55-18
Original file line numberDiff line numberDiff line change
@@ -2,19 +2,21 @@ use clippy_config::Conf;
22
use clippy_utils::consts::{ConstEvalCtxt, Constant};
33
use clippy_utils::diagnostics::span_lint_and_then;
44
use clippy_utils::msrvs::{self, Msrv};
5-
use clippy_utils::source::snippet;
5+
use clippy_utils::source::snippet_with_applicability;
66
use clippy_utils::usage::mutated_variables;
77
use clippy_utils::{eq_expr_value, higher};
8+
use rustc_ast::BindingMode;
89
use rustc_ast::ast::LitKind;
10+
use rustc_data_structures::fx::FxHashMap;
911
use rustc_errors::Applicability;
1012
use rustc_hir::def::Res;
11-
use rustc_hir::intravisit::{Visitor, walk_expr};
12-
use rustc_hir::{BinOpKind, BorrowKind, Expr, ExprKind};
13-
use rustc_lint::{LateContext, LateLintPass};
13+
use rustc_hir::intravisit::{Visitor, walk_expr, walk_pat};
14+
use rustc_hir::{BinOpKind, BorrowKind, Expr, ExprKind, Node, PatKind};
15+
use rustc_lint::{LateContext, LateLintPass, LintContext as _};
1416
use rustc_middle::ty;
1517
use rustc_session::impl_lint_pass;
1618
use rustc_span::source_map::Spanned;
17-
use rustc_span::{Span, sym};
19+
use rustc_span::{Symbol, sym};
1820
use std::iter;
1921

2022
declare_clippy_lint! {
@@ -95,18 +97,37 @@ impl<'tcx> LateLintPass<'tcx> for ManualStrip {
9597
return;
9698
}
9799

98-
let strippings = find_stripping(cx, strip_kind, target_res, pattern, then);
100+
let (strippings, bindings) = find_stripping(cx, strip_kind, target_res, pattern, then);
99101
if !strippings.is_empty() {
100102
let kind_word = match strip_kind {
101103
StripKind::Prefix => "prefix",
102104
StripKind::Suffix => "suffix",
103105
};
104106

105107
let test_span = expr.span.until(then.span);
108+
109+
// If the first use is a simple `let` statement, reuse its identifier in the `if let Some(…)` and
110+
// remove the `let` statement as long as the identifier is never bound again within the lexical
111+
// scope of interest.
112+
let (ident_name, let_stmt_span, skip, mut app) = if let Node::LetStmt(let_stmt) =
113+
cx.tcx.parent_hir_node(strippings[0].hir_id)
114+
&& let PatKind::Binding(BindingMode::NONE, _, ident, None) = &let_stmt.pat.kind
115+
&& bindings.get(&ident.name) == Some(&1)
116+
{
117+
(
118+
ident.name.as_str(),
119+
Some(cx.sess().source_map().span_extend_while_whitespace(let_stmt.span)),
120+
1,
121+
Applicability::MachineApplicable,
122+
)
123+
} else {
124+
("<stripped>", None, 0, Applicability::HasPlaceholders)
125+
};
126+
106127
span_lint_and_then(
107128
cx,
108129
MANUAL_STRIP,
109-
strippings[0],
130+
strippings[0].span,
110131
format!("stripping a {kind_word} manually"),
111132
|diag| {
112133
diag.span_note(test_span, format!("the {kind_word} was tested here"));
@@ -115,14 +136,20 @@ impl<'tcx> LateLintPass<'tcx> for ManualStrip {
115136
iter::once((
116137
test_span,
117138
format!(
118-
"if let Some(<stripped>) = {}.strip_{kind_word}({}) ",
119-
snippet(cx, target_arg.span, ".."),
120-
snippet(cx, pattern.span, "..")
139+
"if let Some({ident_name}) = {}.strip_{kind_word}({}) ",
140+
snippet_with_applicability(cx, target_arg.span, "_", &mut app),
141+
snippet_with_applicability(cx, pattern.span, "_", &mut app)
121142
),
122143
))
123-
.chain(strippings.into_iter().map(|span| (span, "<stripped>".into())))
144+
.chain(let_stmt_span.map(|span| (span, String::new())))
145+
.chain(
146+
strippings
147+
.into_iter()
148+
.skip(skip)
149+
.map(|expr| (expr.span, ident_name.into())),
150+
)
124151
.collect(),
125-
Applicability::HasPlaceholders,
152+
app,
126153
);
127154
},
128155
);
@@ -188,19 +215,21 @@ fn peel_ref<'a>(expr: &'a Expr<'_>) -> &'a Expr<'a> {
188215
/// Find expressions where `target` is stripped using the length of `pattern`.
189216
/// We'll suggest replacing these expressions with the result of the `strip_{prefix,suffix}`
190217
/// method.
218+
/// Also, all bindings found during the visit are counted and returned.
191219
fn find_stripping<'tcx>(
192220
cx: &LateContext<'tcx>,
193221
strip_kind: StripKind,
194222
target: Res,
195223
pattern: &'tcx Expr<'_>,
196-
expr: &'tcx Expr<'_>,
197-
) -> Vec<Span> {
224+
expr: &'tcx Expr<'tcx>,
225+
) -> (Vec<&'tcx Expr<'tcx>>, FxHashMap<Symbol, usize>) {
198226
struct StrippingFinder<'a, 'tcx> {
199227
cx: &'a LateContext<'tcx>,
200228
strip_kind: StripKind,
201229
target: Res,
202230
pattern: &'tcx Expr<'tcx>,
203-
results: Vec<Span>,
231+
results: Vec<&'tcx Expr<'tcx>>,
232+
bindings: FxHashMap<Symbol, usize>,
204233
}
205234

206235
impl<'tcx> Visitor<'tcx> for StrippingFinder<'_, 'tcx> {
@@ -215,7 +244,7 @@ fn find_stripping<'tcx>(
215244
match (self.strip_kind, start, end) {
216245
(StripKind::Prefix, Some(start), None) => {
217246
if eq_pattern_length(self.cx, self.pattern, start) {
218-
self.results.push(ex.span);
247+
self.results.push(ex);
219248
return;
220249
}
221250
},
@@ -232,7 +261,7 @@ fn find_stripping<'tcx>(
232261
&& self.cx.qpath_res(left_path, left_arg.hir_id) == self.target
233262
&& eq_pattern_length(self.cx, self.pattern, right)
234263
{
235-
self.results.push(ex.span);
264+
self.results.push(ex);
236265
return;
237266
}
238267
},
@@ -242,6 +271,13 @@ fn find_stripping<'tcx>(
242271

243272
walk_expr(self, ex);
244273
}
274+
275+
fn visit_pat(&mut self, pat: &'tcx rustc_hir::Pat<'tcx>) -> Self::Result {
276+
if let PatKind::Binding(_, _, ident, _) = pat.kind {
277+
*self.bindings.entry(ident.name).or_default() += 1;
278+
}
279+
walk_pat(self, pat);
280+
}
245281
}
246282

247283
let mut finder = StrippingFinder {
@@ -250,7 +286,8 @@ fn find_stripping<'tcx>(
250286
target,
251287
pattern,
252288
results: vec![],
289+
bindings: FxHashMap::default(),
253290
};
254291
walk_expr(&mut finder, expr);
255-
finder.results
292+
(finder.results, finder.bindings)
256293
}

tests/ui/manual_strip.rs

+17
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,23 @@ fn main() {
7474
if s3.starts_with("ab") {
7575
s4[2..].to_string();
7676
}
77+
78+
// Don't propose to reuse the `stripped` identifier as it is overriden
79+
if s.starts_with("ab") {
80+
let stripped = &s["ab".len()..];
81+
//~^ ERROR: stripping a prefix manually
82+
let stripped = format!("{stripped}-");
83+
println!("{stripped}{}", &s["ab".len()..]);
84+
}
85+
86+
// Don't propose to reuse the `stripped` identifier as it is mutable
87+
if s.starts_with("ab") {
88+
let mut stripped = &s["ab".len()..];
89+
//~^ ERROR: stripping a prefix manually
90+
stripped = "";
91+
let stripped = format!("{stripped}-");
92+
println!("{stripped}{}", &s["ab".len()..]);
93+
}
7794
}
7895

7996
#[clippy::msrv = "1.44"]

tests/ui/manual_strip.stderr

+44-3
Original file line numberDiff line numberDiff line change
@@ -138,13 +138,54 @@ LL ~ <stripped>.to_uppercase();
138138
|
139139

140140
error: stripping a prefix manually
141-
--> tests/ui/manual_strip.rs:91:9
141+
--> tests/ui/manual_strip.rs:80:24
142+
|
143+
LL | let stripped = &s["ab".len()..];
144+
| ^^^^^^^^^^^^^^^^
145+
|
146+
note: the prefix was tested here
147+
--> tests/ui/manual_strip.rs:79:5
148+
|
149+
LL | if s.starts_with("ab") {
150+
| ^^^^^^^^^^^^^^^^^^^^^^^
151+
help: try using the `strip_prefix` method
152+
|
153+
LL ~ if let Some(<stripped>) = s.strip_prefix("ab") {
154+
LL ~ let stripped = <stripped>;
155+
LL |
156+
LL | let stripped = format!("{stripped}-");
157+
LL ~ println!("{stripped}{}", <stripped>);
158+
|
159+
160+
error: stripping a prefix manually
161+
--> tests/ui/manual_strip.rs:88:28
162+
|
163+
LL | let mut stripped = &s["ab".len()..];
164+
| ^^^^^^^^^^^^^^^^
165+
|
166+
note: the prefix was tested here
167+
--> tests/ui/manual_strip.rs:87:5
168+
|
169+
LL | if s.starts_with("ab") {
170+
| ^^^^^^^^^^^^^^^^^^^^^^^
171+
help: try using the `strip_prefix` method
172+
|
173+
LL ~ if let Some(<stripped>) = s.strip_prefix("ab") {
174+
LL ~ let mut stripped = <stripped>;
175+
LL |
176+
LL | stripped = "";
177+
LL | let stripped = format!("{stripped}-");
178+
LL ~ println!("{stripped}{}", <stripped>);
179+
|
180+
181+
error: stripping a prefix manually
182+
--> tests/ui/manual_strip.rs:108:9
142183
|
143184
LL | s[1..].to_string();
144185
| ^^^^^^
145186
|
146187
note: the prefix was tested here
147-
--> tests/ui/manual_strip.rs:90:5
188+
--> tests/ui/manual_strip.rs:107:5
148189
|
149190
LL | if s.starts_with('a') {
150191
| ^^^^^^^^^^^^^^^^^^^^^^
@@ -154,5 +195,5 @@ LL ~ if let Some(<stripped>) = s.strip_prefix('a') {
154195
LL ~ <stripped>.to_string();
155196
|
156197

157-
error: aborting due to 8 previous errors
198+
error: aborting due to 10 previous errors
158199

tests/ui/manual_strip_fixable.fixed

+15
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
#![warn(clippy::manual_strip)]
2+
3+
fn main() {
4+
let s = "abc";
5+
6+
if let Some(stripped) = s.strip_prefix("ab") {
7+
//~^ ERROR: stripping a prefix manually
8+
println!("{stripped}{}", stripped);
9+
}
10+
11+
if let Some(stripped) = s.strip_suffix("bc") {
12+
//~^ ERROR: stripping a suffix manually
13+
println!("{stripped}{}", stripped);
14+
}
15+
}

tests/ui/manual_strip_fixable.rs

+17
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
#![warn(clippy::manual_strip)]
2+
3+
fn main() {
4+
let s = "abc";
5+
6+
if s.starts_with("ab") {
7+
let stripped = &s["ab".len()..];
8+
//~^ ERROR: stripping a prefix manually
9+
println!("{stripped}{}", &s["ab".len()..]);
10+
}
11+
12+
if s.ends_with("bc") {
13+
let stripped = &s[..s.len() - "bc".len()];
14+
//~^ ERROR: stripping a suffix manually
15+
println!("{stripped}{}", &s[..s.len() - "bc".len()]);
16+
}
17+
}

tests/ui/manual_strip_fixable.stderr

+40
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
error: stripping a prefix manually
2+
--> tests/ui/manual_strip_fixable.rs:7:24
3+
|
4+
LL | let stripped = &s["ab".len()..];
5+
| ^^^^^^^^^^^^^^^^
6+
|
7+
note: the prefix was tested here
8+
--> tests/ui/manual_strip_fixable.rs:6:5
9+
|
10+
LL | if s.starts_with("ab") {
11+
| ^^^^^^^^^^^^^^^^^^^^^^^
12+
= note: `-D clippy::manual-strip` implied by `-D warnings`
13+
= help: to override `-D warnings` add `#[allow(clippy::manual_strip)]`
14+
help: try using the `strip_prefix` method
15+
|
16+
LL ~ if let Some(stripped) = s.strip_prefix("ab") {
17+
LL ~
18+
LL ~ println!("{stripped}{}", stripped);
19+
|
20+
21+
error: stripping a suffix manually
22+
--> tests/ui/manual_strip_fixable.rs:13:24
23+
|
24+
LL | let stripped = &s[..s.len() - "bc".len()];
25+
| ^^^^^^^^^^^^^^^^^^^^^^^^^^
26+
|
27+
note: the suffix was tested here
28+
--> tests/ui/manual_strip_fixable.rs:12:5
29+
|
30+
LL | if s.ends_with("bc") {
31+
| ^^^^^^^^^^^^^^^^^^^^^
32+
help: try using the `strip_suffix` method
33+
|
34+
LL ~ if let Some(stripped) = s.strip_suffix("bc") {
35+
LL ~
36+
LL ~ println!("{stripped}{}", stripped);
37+
|
38+
39+
error: aborting due to 2 previous errors
40+

0 commit comments

Comments
 (0)