Skip to content
This repository was archived by the owner on May 28, 2025. It is now read-only.

Commit d1f0f04

Browse files
author
Michael Wright
committed
New lint: manual-strip
Add a new lint, `manual-strip`, that suggests using the `str::strip_prefix` and `str::strip_suffix` methods introduced in Rust 1.45 when the same functionality is performed 'manually'. Closes rust-lang#5734
1 parent 231444d commit d1f0f04

File tree

7 files changed

+453
-0
lines changed

7 files changed

+453
-0
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1672,6 +1672,7 @@ Released 2018-09-13
16721672
[`manual_memcpy`]: https://rust-lang.github.io/rust-clippy/master/index.html#manual_memcpy
16731673
[`manual_non_exhaustive`]: https://rust-lang.github.io/rust-clippy/master/index.html#manual_non_exhaustive
16741674
[`manual_saturating_arithmetic`]: https://rust-lang.github.io/rust-clippy/master/index.html#manual_saturating_arithmetic
1675+
[`manual_strip`]: https://rust-lang.github.io/rust-clippy/master/index.html#manual_strip
16751676
[`manual_swap`]: https://rust-lang.github.io/rust-clippy/master/index.html#manual_swap
16761677
[`many_single_char_names`]: https://rust-lang.github.io/rust-clippy/master/index.html#many_single_char_names
16771678
[`map_clone`]: https://rust-lang.github.io/rust-clippy/master/index.html#map_clone

clippy_lints/src/lib.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -230,6 +230,7 @@ mod macro_use;
230230
mod main_recursion;
231231
mod manual_async_fn;
232232
mod manual_non_exhaustive;
233+
mod manual_strip;
233234
mod map_clone;
234235
mod map_identity;
235236
mod map_unit_fn;
@@ -626,6 +627,7 @@ pub fn register_plugins(store: &mut rustc_lint::LintStore, sess: &Session, conf:
626627
&main_recursion::MAIN_RECURSION,
627628
&manual_async_fn::MANUAL_ASYNC_FN,
628629
&manual_non_exhaustive::MANUAL_NON_EXHAUSTIVE,
630+
&manual_strip::MANUAL_STRIP,
629631
&map_clone::MAP_CLONE,
630632
&map_identity::MAP_IDENTITY,
631633
&map_unit_fn::OPTION_MAP_UNIT_FN,
@@ -1109,6 +1111,7 @@ pub fn register_plugins(store: &mut rustc_lint::LintStore, sess: &Session, conf:
11091111
store.register_late_pass(|| box self_assignment::SelfAssignment);
11101112
store.register_late_pass(|| box float_equality_without_abs::FloatEqualityWithoutAbs);
11111113
store.register_late_pass(|| box async_yields_async::AsyncYieldsAsync);
1114+
store.register_late_pass(|| box manual_strip::ManualStrip);
11121115

11131116
store.register_group(true, "clippy::restriction", Some("clippy_restriction"), vec![
11141117
LintId::of(&arithmetic::FLOAT_ARITHMETIC),
@@ -1335,6 +1338,7 @@ pub fn register_plugins(store: &mut rustc_lint::LintStore, sess: &Session, conf:
13351338
LintId::of(&main_recursion::MAIN_RECURSION),
13361339
LintId::of(&manual_async_fn::MANUAL_ASYNC_FN),
13371340
LintId::of(&manual_non_exhaustive::MANUAL_NON_EXHAUSTIVE),
1341+
LintId::of(&manual_strip::MANUAL_STRIP),
13381342
LintId::of(&map_clone::MAP_CLONE),
13391343
LintId::of(&map_identity::MAP_IDENTITY),
13401344
LintId::of(&map_unit_fn::OPTION_MAP_UNIT_FN),
@@ -1626,6 +1630,7 @@ pub fn register_plugins(store: &mut rustc_lint::LintStore, sess: &Session, conf:
16261630
LintId::of(&loops::EXPLICIT_COUNTER_LOOP),
16271631
LintId::of(&loops::MUT_RANGE_BOUND),
16281632
LintId::of(&loops::WHILE_LET_LOOP),
1633+
LintId::of(&manual_strip::MANUAL_STRIP),
16291634
LintId::of(&map_identity::MAP_IDENTITY),
16301635
LintId::of(&map_unit_fn::OPTION_MAP_UNIT_FN),
16311636
LintId::of(&map_unit_fn::RESULT_MAP_UNIT_FN),

clippy_lints/src/manual_strip.rs

Lines changed: 246 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,246 @@
1+
use crate::consts::{constant, Constant};
2+
use crate::utils::usage::mutated_variables;
3+
use crate::utils::{
4+
eq_expr_value, higher, match_def_path, multispan_sugg, paths, qpath_res, snippet, span_lint_and_then,
5+
};
6+
7+
use if_chain::if_chain;
8+
use rustc_ast::ast::LitKind;
9+
use rustc_hir::def::Res;
10+
use rustc_hir::intravisit::{walk_expr, NestedVisitorMap, Visitor};
11+
use rustc_hir::BinOpKind;
12+
use rustc_hir::{BorrowKind, Expr, ExprKind};
13+
use rustc_lint::{LateContext, LateLintPass};
14+
use rustc_middle::hir::map::Map;
15+
use rustc_middle::ty;
16+
use rustc_session::{declare_lint_pass, declare_tool_lint};
17+
use rustc_span::source_map::Spanned;
18+
use rustc_span::Span;
19+
20+
declare_clippy_lint! {
21+
/// **What it does:**
22+
/// Suggests using `strip_{prefix,suffix}` over `str::{starts,ends}_with` and slicing using
23+
/// the pattern's length.
24+
///
25+
/// **Why is this bad?**
26+
/// Using `str:strip_{prefix,suffix}` is safer and may have better performance as there is no
27+
/// slicing which may panic and the compiler does not need to insert this panic code. It is
28+
/// also sometimes more readable as it removes the need for duplicating or storing the pattern
29+
/// used by `str::{starts,ends}_with` and in the slicing.
30+
///
31+
/// **Known problems:**
32+
/// None.
33+
///
34+
/// **Example:**
35+
///
36+
/// ```rust
37+
/// let s = "hello, world!";
38+
/// if s.starts_with("hello, ") {
39+
/// assert_eq!(s["hello, ".len()..].to_uppercase(), "WORLD!");
40+
/// }
41+
/// ```
42+
/// Use instead:
43+
/// ```rust
44+
/// let s = "hello, world!";
45+
/// if let Some(end) = s.strip_prefix("hello, ") {
46+
/// assert_eq!(end.to_uppercase(), "WORLD!");
47+
/// }
48+
/// ```
49+
pub MANUAL_STRIP,
50+
complexity,
51+
"suggests using `strip_{prefix,suffix}` over `str::{starts,ends}_with` and slicing"
52+
}
53+
54+
declare_lint_pass!(ManualStrip => [MANUAL_STRIP]);
55+
56+
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
57+
enum StripKind {
58+
Prefix,
59+
Suffix,
60+
}
61+
62+
impl<'tcx> LateLintPass<'tcx> for ManualStrip {
63+
fn check_expr(&mut self, cx: &LateContext<'tcx>, expr: &'tcx Expr<'_>) {
64+
if_chain! {
65+
if let Some((cond, then, _)) = higher::if_block(&expr);
66+
if let ExprKind::MethodCall(_, _, [target_arg, pattern], _) = cond.kind;
67+
if let Some(method_def_id) = cx.typeck_results().type_dependent_def_id(cond.hir_id);
68+
if let ExprKind::Path(target_path) = &target_arg.kind;
69+
then {
70+
let strip_kind = if match_def_path(cx, method_def_id, &paths::STR_STARTS_WITH) {
71+
StripKind::Prefix
72+
} else if match_def_path(cx, method_def_id, &paths::STR_ENDS_WITH) {
73+
StripKind::Suffix
74+
} else {
75+
return;
76+
};
77+
let target_res = qpath_res(cx, &target_path, target_arg.hir_id);
78+
if target_res == Res::Err {
79+
return;
80+
};
81+
82+
if_chain! {
83+
if let Res::Local(hir_id) = target_res;
84+
if let Some(used_mutably) = mutated_variables(then, cx);
85+
if used_mutably.contains(&hir_id);
86+
then {
87+
return;
88+
}
89+
}
90+
91+
let strippings = find_stripping(cx, strip_kind, target_res, pattern, then);
92+
if !strippings.is_empty() {
93+
94+
let kind_word = match strip_kind {
95+
StripKind::Prefix => "prefix",
96+
StripKind::Suffix => "suffix",
97+
};
98+
99+
let test_span = expr.span.until(then.span);
100+
span_lint_and_then(cx, MANUAL_STRIP, strippings[0], &format!("stripping a {} manually", kind_word), |diag| {
101+
diag.span_note(test_span, &format!("the {} was tested here", kind_word));
102+
multispan_sugg(
103+
diag,
104+
&format!("try using the `strip_{}` method", kind_word),
105+
vec![(test_span,
106+
format!("if let Some(<stripped>) = {}.strip_{}({}) ",
107+
snippet(cx, target_arg.span, ".."),
108+
kind_word,
109+
snippet(cx, pattern.span, "..")))]
110+
.into_iter().chain(strippings.into_iter().map(|span| (span, "<stripped>".into()))),
111+
)
112+
});
113+
}
114+
}
115+
}
116+
}
117+
}
118+
119+
// Returns `Some(arg)` if `expr` matches `arg.len()` and `None` otherwise.
120+
fn len_arg<'tcx>(cx: &LateContext<'tcx>, expr: &'tcx Expr<'_>) -> Option<&'tcx Expr<'tcx>> {
121+
if_chain! {
122+
if let ExprKind::MethodCall(_, _, [arg], _) = expr.kind;
123+
if let Some(method_def_id) = cx.typeck_results().type_dependent_def_id(expr.hir_id);
124+
if match_def_path(cx, method_def_id, &paths::STR_LEN);
125+
then {
126+
Some(arg)
127+
}
128+
else {
129+
None
130+
}
131+
}
132+
}
133+
134+
// Returns the length of the `expr` if it's a constant string or char.
135+
fn constant_length(cx: &LateContext<'_>, expr: &Expr<'_>) -> Option<u128> {
136+
let (value, _) = constant(cx, cx.typeck_results(), expr)?;
137+
match value {
138+
Constant::Str(value) => Some(value.len() as u128),
139+
Constant::Char(value) => Some(value.len_utf8() as u128),
140+
_ => None,
141+
}
142+
}
143+
144+
// Tests if `expr` equals the length of the pattern.
145+
fn eq_pattern_length<'tcx>(cx: &LateContext<'tcx>, pattern: &Expr<'_>, expr: &'tcx Expr<'_>) -> bool {
146+
if let ExprKind::Lit(Spanned {
147+
node: LitKind::Int(n, _),
148+
..
149+
}) = expr.kind
150+
{
151+
constant_length(cx, pattern).map_or(false, |length| length == n)
152+
} else {
153+
len_arg(cx, expr).map_or(false, |arg| eq_expr_value(cx, pattern, arg))
154+
}
155+
}
156+
157+
// Tests if `expr` is a `&str`.
158+
fn is_ref_str(cx: &LateContext<'_>, expr: &Expr<'_>) -> bool {
159+
match cx.typeck_results().expr_ty_adjusted(&expr).kind() {
160+
ty::Ref(_, ty, _) => ty.is_str(),
161+
_ => false,
162+
}
163+
}
164+
165+
// Removes the outer `AddrOf` expression if needed.
166+
fn peel_ref<'a>(expr: &'a Expr<'_>) -> &'a Expr<'a> {
167+
if let ExprKind::AddrOf(BorrowKind::Ref, _, unref) = &expr.kind {
168+
unref
169+
} else {
170+
expr
171+
}
172+
}
173+
174+
// Find expressions where `target` is stripped using the length of `pattern`.
175+
// We'll suggest replacing these expressions with the result of the `strip_{prefix,suffix}`
176+
// method.
177+
fn find_stripping<'tcx>(
178+
cx: &LateContext<'tcx>,
179+
strip_kind: StripKind,
180+
target: Res,
181+
pattern: &'tcx Expr<'_>,
182+
expr: &'tcx Expr<'_>,
183+
) -> Vec<Span> {
184+
struct StrippingFinder<'a, 'tcx> {
185+
cx: &'a LateContext<'tcx>,
186+
strip_kind: StripKind,
187+
target: Res,
188+
pattern: &'tcx Expr<'tcx>,
189+
results: Vec<Span>,
190+
}
191+
192+
impl<'a, 'tcx> Visitor<'tcx> for StrippingFinder<'a, 'tcx> {
193+
type Map = Map<'tcx>;
194+
fn nested_visit_map(&mut self) -> NestedVisitorMap<Self::Map> {
195+
NestedVisitorMap::None
196+
}
197+
198+
fn visit_expr(&mut self, ex: &'tcx Expr<'_>) {
199+
if_chain! {
200+
if is_ref_str(self.cx, ex);
201+
let unref = peel_ref(ex);
202+
if let ExprKind::Index(indexed, index) = &unref.kind;
203+
if let Some(range) = higher::range(index);
204+
if let higher::Range { start, end, .. } = range;
205+
if let ExprKind::Path(path) = &indexed.kind;
206+
if qpath_res(self.cx, path, ex.hir_id) == self.target;
207+
then {
208+
match (self.strip_kind, start, end) {
209+
(StripKind::Prefix, Some(start), None) => {
210+
if eq_pattern_length(self.cx, self.pattern, start) {
211+
self.results.push(ex.span);
212+
return;
213+
}
214+
},
215+
(StripKind::Suffix, None, Some(end)) => {
216+
if_chain! {
217+
if let ExprKind::Binary(Spanned { node: BinOpKind::Sub, .. }, left, right) = end.kind;
218+
if let Some(left_arg) = len_arg(self.cx, left);
219+
if let ExprKind::Path(left_path) = &left_arg.kind;
220+
if qpath_res(self.cx, left_path, left_arg.hir_id) == self.target;
221+
if eq_pattern_length(self.cx, self.pattern, right);
222+
then {
223+
self.results.push(ex.span);
224+
return;
225+
}
226+
}
227+
},
228+
_ => {}
229+
}
230+
}
231+
}
232+
233+
walk_expr(self, ex);
234+
}
235+
}
236+
237+
let mut finder = StrippingFinder {
238+
cx,
239+
strip_kind,
240+
target,
241+
pattern,
242+
results: vec![],
243+
};
244+
walk_expr(&mut finder, expr);
245+
finder.results
246+
}

clippy_lints/src/utils/paths.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,9 @@ pub const STD_MEM_TRANSMUTE: [&str; 3] = ["std", "mem", "transmute"];
115115
pub const STD_PTR_NULL: [&str; 3] = ["std", "ptr", "null"];
116116
pub const STRING_AS_MUT_STR: [&str; 4] = ["alloc", "string", "String", "as_mut_str"];
117117
pub const STRING_AS_STR: [&str; 4] = ["alloc", "string", "String", "as_str"];
118+
pub const STR_ENDS_WITH: [&str; 4] = ["core", "str", "<impl str>", "ends_with"];
119+
pub const STR_LEN: [&str; 4] = ["core", "str", "<impl str>", "len"];
120+
pub const STR_STARTS_WITH: [&str; 4] = ["core", "str", "<impl str>", "starts_with"];
118121
pub const SYNTAX_CONTEXT: [&str; 3] = ["rustc_span", "hygiene", "SyntaxContext"];
119122
pub const TO_OWNED: [&str; 3] = ["alloc", "borrow", "ToOwned"];
120123
pub const TO_OWNED_METHOD: [&str; 4] = ["alloc", "borrow", "ToOwned", "to_owned"];

src/lintlist/mod.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1144,6 +1144,13 @@ pub static ref ALL_LINTS: Vec<Lint> = vec![
11441144
deprecation: None,
11451145
module: "methods",
11461146
},
1147+
Lint {
1148+
name: "manual_strip",
1149+
group: "complexity",
1150+
desc: "suggests using `strip_{prefix,suffix}` over `str::{starts,ends}_with` and slicing",
1151+
deprecation: None,
1152+
module: "manual_strip",
1153+
},
11471154
Lint {
11481155
name: "manual_swap",
11491156
group: "complexity",

tests/ui/manual_strip.rs

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
#![warn(clippy::manual_strip)]
2+
3+
fn main() {
4+
let s = "abc";
5+
6+
if s.starts_with("ab") {
7+
str::to_string(&s["ab".len()..]);
8+
s["ab".len()..].to_string();
9+
10+
str::to_string(&s[2..]);
11+
s[2..].to_string();
12+
}
13+
14+
if s.ends_with("bc") {
15+
str::to_string(&s[..s.len() - "bc".len()]);
16+
s[..s.len() - "bc".len()].to_string();
17+
18+
str::to_string(&s[..s.len() - 2]);
19+
s[..s.len() - 2].to_string();
20+
}
21+
22+
// Character patterns
23+
if s.starts_with('a') {
24+
str::to_string(&s[1..]);
25+
s[1..].to_string();
26+
}
27+
28+
// Variable prefix
29+
let prefix = "ab";
30+
if s.starts_with(prefix) {
31+
str::to_string(&s[prefix.len()..]);
32+
}
33+
34+
// Constant prefix
35+
const PREFIX: &str = "ab";
36+
if s.starts_with(PREFIX) {
37+
str::to_string(&s[PREFIX.len()..]);
38+
str::to_string(&s[2..]);
39+
}
40+
41+
// Constant target
42+
const TARGET: &str = "abc";
43+
if TARGET.starts_with(prefix) {
44+
str::to_string(&TARGET[prefix.len()..]);
45+
}
46+
47+
// String target - not mutated.
48+
let s1: String = "abc".into();
49+
if s1.starts_with("ab") {
50+
s1[2..].to_uppercase();
51+
}
52+
53+
// String target - mutated. (Don't lint.)
54+
let mut s2: String = "abc".into();
55+
if s2.starts_with("ab") {
56+
s2.push('d');
57+
s2[2..].to_uppercase();
58+
}
59+
}

0 commit comments

Comments
 (0)