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

Commit 5e60497

Browse files
committed
Auto merge of rust-lang#6038 - mikerite:lint-5734, r=matthiaskrgr
Add `manual-strip` lint Add `manual-strip` lint. changelog: Add `manual-strip` lint
2 parents b08bbe5 + 79a0e51 commit 5e60497

File tree

13 files changed

+473
-16
lines changed

13 files changed

+473
-16
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/doc.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -534,7 +534,7 @@ fn check_word(cx: &LateContext<'_>, word: &str, span: Span) {
534534
return false;
535535
}
536536

537-
let s = if s.ends_with('s') { &s[..s.len() - 1] } else { s };
537+
let s = s.strip_suffix('s').unwrap_or(s);
538538

539539
s.chars().all(char::is_alphanumeric)
540540
&& s.chars().filter(|&c| c.is_uppercase()).take(2).count() > 1

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_err_ignore;
235236
mod map_identity;
@@ -627,6 +628,7 @@ pub fn register_plugins(store: &mut rustc_lint::LintStore, sess: &Session, conf:
627628
&main_recursion::MAIN_RECURSION,
628629
&manual_async_fn::MANUAL_ASYNC_FN,
629630
&manual_non_exhaustive::MANUAL_NON_EXHAUSTIVE,
631+
&manual_strip::MANUAL_STRIP,
630632
&map_clone::MAP_CLONE,
631633
&map_err_ignore::MAP_ERR_IGNORE,
632634
&map_identity::MAP_IDENTITY,
@@ -1113,6 +1115,7 @@ pub fn register_plugins(store: &mut rustc_lint::LintStore, sess: &Session, conf:
11131115
store.register_late_pass(|| box self_assignment::SelfAssignment);
11141116
store.register_late_pass(|| box float_equality_without_abs::FloatEqualityWithoutAbs);
11151117
store.register_late_pass(|| box async_yields_async::AsyncYieldsAsync);
1118+
store.register_late_pass(|| box manual_strip::ManualStrip);
11161119
store.register_late_pass(|| box utils::internal_lints::MatchTypeOnDiagItem);
11171120

11181121
store.register_group(true, "clippy::restriction", Some("clippy_restriction"), vec![
@@ -1342,6 +1345,7 @@ pub fn register_plugins(store: &mut rustc_lint::LintStore, sess: &Session, conf:
13421345
LintId::of(&main_recursion::MAIN_RECURSION),
13431346
LintId::of(&manual_async_fn::MANUAL_ASYNC_FN),
13441347
LintId::of(&manual_non_exhaustive::MANUAL_NON_EXHAUSTIVE),
1348+
LintId::of(&manual_strip::MANUAL_STRIP),
13451349
LintId::of(&map_clone::MAP_CLONE),
13461350
LintId::of(&map_identity::MAP_IDENTITY),
13471351
LintId::of(&map_unit_fn::OPTION_MAP_UNIT_FN),
@@ -1633,6 +1637,7 @@ pub fn register_plugins(store: &mut rustc_lint::LintStore, sess: &Session, conf:
16331637
LintId::of(&loops::EXPLICIT_COUNTER_LOOP),
16341638
LintId::of(&loops::MUT_RANGE_BOUND),
16351639
LintId::of(&loops::WHILE_LET_LOOP),
1640+
LintId::of(&manual_strip::MANUAL_STRIP),
16361641
LintId::of(&map_identity::MAP_IDENTITY),
16371642
LintId::of(&map_unit_fn::OPTION_MAP_UNIT_FN),
16381643
LintId::of(&map_unit_fn::RESULT_MAP_UNIT_FN),

clippy_lints/src/loops.rs

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2601,11 +2601,9 @@ fn check_needless_collect_direct_usage<'tcx>(expr: &'tcx Expr<'_>, cx: &LateCont
26012601
span,
26022602
NEEDLESS_COLLECT_MSG,
26032603
|diag| {
2604-
let (arg, pred) = if contains_arg.starts_with('&') {
2605-
("x", &contains_arg[1..])
2606-
} else {
2607-
("&x", &*contains_arg)
2608-
};
2604+
let (arg, pred) = contains_arg
2605+
.strip_prefix('&')
2606+
.map_or(("&x", &*contains_arg), |s| ("x", s));
26092607
diag.span_suggestion(
26102608
span,
26112609
"replace with",

clippy_lints/src/manual_strip.rs

Lines changed: 245 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,245 @@
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+
} else {
128+
None
129+
}
130+
}
131+
}
132+
133+
// Returns the length of the `expr` if it's a constant string or char.
134+
fn constant_length(cx: &LateContext<'_>, expr: &Expr<'_>) -> Option<u128> {
135+
let (value, _) = constant(cx, cx.typeck_results(), expr)?;
136+
match value {
137+
Constant::Str(value) => Some(value.len() as u128),
138+
Constant::Char(value) => Some(value.len_utf8() as u128),
139+
_ => None,
140+
}
141+
}
142+
143+
// Tests if `expr` equals the length of the pattern.
144+
fn eq_pattern_length<'tcx>(cx: &LateContext<'tcx>, pattern: &Expr<'_>, expr: &'tcx Expr<'_>) -> bool {
145+
if let ExprKind::Lit(Spanned {
146+
node: LitKind::Int(n, _),
147+
..
148+
}) = expr.kind
149+
{
150+
constant_length(cx, pattern).map_or(false, |length| length == n)
151+
} else {
152+
len_arg(cx, expr).map_or(false, |arg| eq_expr_value(cx, pattern, arg))
153+
}
154+
}
155+
156+
// Tests if `expr` is a `&str`.
157+
fn is_ref_str(cx: &LateContext<'_>, expr: &Expr<'_>) -> bool {
158+
match cx.typeck_results().expr_ty_adjusted(&expr).kind() {
159+
ty::Ref(_, ty, _) => ty.is_str(),
160+
_ => false,
161+
}
162+
}
163+
164+
// Removes the outer `AddrOf` expression if needed.
165+
fn peel_ref<'a>(expr: &'a Expr<'_>) -> &'a Expr<'a> {
166+
if let ExprKind::AddrOf(BorrowKind::Ref, _, unref) = &expr.kind {
167+
unref
168+
} else {
169+
expr
170+
}
171+
}
172+
173+
// Find expressions where `target` is stripped using the length of `pattern`.
174+
// We'll suggest replacing these expressions with the result of the `strip_{prefix,suffix}`
175+
// method.
176+
fn find_stripping<'tcx>(
177+
cx: &LateContext<'tcx>,
178+
strip_kind: StripKind,
179+
target: Res,
180+
pattern: &'tcx Expr<'_>,
181+
expr: &'tcx Expr<'_>,
182+
) -> Vec<Span> {
183+
struct StrippingFinder<'a, 'tcx> {
184+
cx: &'a LateContext<'tcx>,
185+
strip_kind: StripKind,
186+
target: Res,
187+
pattern: &'tcx Expr<'tcx>,
188+
results: Vec<Span>,
189+
}
190+
191+
impl<'a, 'tcx> Visitor<'tcx> for StrippingFinder<'a, 'tcx> {
192+
type Map = Map<'tcx>;
193+
fn nested_visit_map(&mut self) -> NestedVisitorMap<Self::Map> {
194+
NestedVisitorMap::None
195+
}
196+
197+
fn visit_expr(&mut self, ex: &'tcx Expr<'_>) {
198+
if_chain! {
199+
if is_ref_str(self.cx, ex);
200+
let unref = peel_ref(ex);
201+
if let ExprKind::Index(indexed, index) = &unref.kind;
202+
if let Some(range) = higher::range(index);
203+
if let higher::Range { start, end, .. } = range;
204+
if let ExprKind::Path(path) = &indexed.kind;
205+
if qpath_res(self.cx, path, ex.hir_id) == self.target;
206+
then {
207+
match (self.strip_kind, start, end) {
208+
(StripKind::Prefix, Some(start), None) => {
209+
if eq_pattern_length(self.cx, self.pattern, start) {
210+
self.results.push(ex.span);
211+
return;
212+
}
213+
},
214+
(StripKind::Suffix, None, Some(end)) => {
215+
if_chain! {
216+
if let ExprKind::Binary(Spanned { node: BinOpKind::Sub, .. }, left, right) = end.kind;
217+
if let Some(left_arg) = len_arg(self.cx, left);
218+
if let ExprKind::Path(left_path) = &left_arg.kind;
219+
if qpath_res(self.cx, left_path, left_arg.hir_id) == self.target;
220+
if eq_pattern_length(self.cx, self.pattern, right);
221+
then {
222+
self.results.push(ex.span);
223+
return;
224+
}
225+
}
226+
},
227+
_ => {}
228+
}
229+
}
230+
}
231+
232+
walk_expr(self, ex);
233+
}
234+
}
235+
236+
let mut finder = StrippingFinder {
237+
cx,
238+
strip_kind,
239+
target,
240+
pattern,
241+
results: vec![],
242+
};
243+
walk_expr(&mut finder, expr);
244+
finder.results
245+
}

clippy_lints/src/misc_early.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -377,16 +377,16 @@ impl EarlyLintPass for MiscEarlyLints {
377377
if let PatKind::Ident(_, ident, None) = arg.pat.kind {
378378
let arg_name = ident.to_string();
379379

380-
if arg_name.starts_with('_') {
381-
if let Some(correspondence) = registered_names.get(&arg_name[1..]) {
380+
if let Some(arg_name) = arg_name.strip_prefix('_') {
381+
if let Some(correspondence) = registered_names.get(arg_name) {
382382
span_lint(
383383
cx,
384384
DUPLICATE_UNDERSCORE_ARGUMENT,
385385
*correspondence,
386386
&format!(
387387
"`{}` already exists, having another argument having almost the same \
388388
name makes code comprehension and documentation more difficult",
389-
arg_name[1..].to_owned()
389+
arg_name
390390
),
391391
);
392392
}

clippy_lints/src/redundant_clone.rs

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -239,10 +239,9 @@ impl<'tcx> LateLintPass<'tcx> for RedundantClone {
239239
);
240240
let mut app = Applicability::MaybeIncorrect;
241241

242-
let mut call_snip = &snip[dot + 1..];
242+
let call_snip = &snip[dot + 1..];
243243
// Machine applicable when `call_snip` looks like `foobar()`
244-
if call_snip.ends_with("()") {
245-
call_snip = call_snip[..call_snip.len()-2].trim();
244+
if let Some(call_snip) = call_snip.strip_suffix("()").map(str::trim) {
246245
if call_snip.as_bytes().iter().all(|b| b.is_ascii_alphabetic() || *b == b'_') {
247246
app = Applicability::MachineApplicable;
248247
}

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/let_if_seq.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ fn issue985_alt() -> i32 {
3333
x
3434
}
3535

36+
#[allow(clippy::manual_strip)]
3637
fn issue975() -> String {
3738
let mut udn = "dummy".to_string();
3839
if udn.starts_with("uuid:") {

0 commit comments

Comments
 (0)