Skip to content

Commit 51024fb

Browse files
committed
Add lint manual_split_once
1 parent bdbc608 commit 51024fb

14 files changed

+452
-26
lines changed

CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -2754,6 +2754,7 @@ Released 2018-09-13
27542754
[`manual_ok_or`]: https://rust-lang.github.io/rust-clippy/master/index.html#manual_ok_or
27552755
[`manual_range_contains`]: https://rust-lang.github.io/rust-clippy/master/index.html#manual_range_contains
27562756
[`manual_saturating_arithmetic`]: https://rust-lang.github.io/rust-clippy/master/index.html#manual_saturating_arithmetic
2757+
[`manual_split_once`]: https://rust-lang.github.io/rust-clippy/master/index.html#manual_split_once
27572758
[`manual_str_repeat`]: https://rust-lang.github.io/rust-clippy/master/index.html#manual_str_repeat
27582759
[`manual_strip`]: https://rust-lang.github.io/rust-clippy/master/index.html#manual_strip
27592760
[`manual_swap`]: https://rust-lang.github.io/rust-clippy/master/index.html#manual_swap

clippy_lints/src/lib.rs

+3
Original file line numberDiff line numberDiff line change
@@ -773,6 +773,7 @@ pub fn register_plugins(store: &mut rustc_lint::LintStore, sess: &Session, conf:
773773
methods::MANUAL_FILTER_MAP,
774774
methods::MANUAL_FIND_MAP,
775775
methods::MANUAL_SATURATING_ARITHMETIC,
776+
methods::MANUAL_SPLIT_ONCE,
776777
methods::MANUAL_STR_REPEAT,
777778
methods::MAP_COLLECT_RESULT_UNIT,
778779
methods::MAP_FLATTEN,
@@ -1320,6 +1321,7 @@ pub fn register_plugins(store: &mut rustc_lint::LintStore, sess: &Session, conf:
13201321
LintId::of(methods::MANUAL_FILTER_MAP),
13211322
LintId::of(methods::MANUAL_FIND_MAP),
13221323
LintId::of(methods::MANUAL_SATURATING_ARITHMETIC),
1324+
LintId::of(methods::MANUAL_SPLIT_ONCE),
13231325
LintId::of(methods::MANUAL_STR_REPEAT),
13241326
LintId::of(methods::MAP_COLLECT_RESULT_UNIT),
13251327
LintId::of(methods::MAP_IDENTITY),
@@ -1618,6 +1620,7 @@ pub fn register_plugins(store: &mut rustc_lint::LintStore, sess: &Session, conf:
16181620
LintId::of(methods::ITER_COUNT),
16191621
LintId::of(methods::MANUAL_FILTER_MAP),
16201622
LintId::of(methods::MANUAL_FIND_MAP),
1623+
LintId::of(methods::MANUAL_SPLIT_ONCE),
16211624
LintId::of(methods::MAP_IDENTITY),
16221625
LintId::of(methods::OPTION_AS_REF_DEREF),
16231626
LintId::of(methods::OPTION_FILTER_MAP),
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,206 @@
1+
use clippy_utils::consts::{constant, Constant};
2+
use clippy_utils::diagnostics::span_lint_and_sugg;
3+
use clippy_utils::source::snippet_with_context;
4+
use clippy_utils::{is_diag_item_method, match_def_path, paths};
5+
use if_chain::if_chain;
6+
use rustc_errors::Applicability;
7+
use rustc_hir::{Expr, ExprKind, HirId, LangItem, Node, QPath};
8+
use rustc_lint::LateContext;
9+
use rustc_middle::ty::{self, adjustment::Adjust};
10+
use rustc_span::{symbol::sym, Span, SyntaxContext};
11+
12+
use super::MANUAL_SPLIT_ONCE;
13+
14+
pub(super) fn check(cx: &LateContext<'_>, method_name: &str, expr: &Expr<'_>, self_arg: &Expr<'_>, pat_arg: &Expr<'_>) {
15+
if !cx.typeck_results().expr_ty_adjusted(self_arg).peel_refs().is_str() {
16+
return;
17+
}
18+
19+
let ctxt = expr.span.ctxt();
20+
let usage = match parse_iter_usage(cx, ctxt, cx.tcx.hir().parent_iter(expr.hir_id)) {
21+
Some(x) => x,
22+
None => return,
23+
};
24+
let (method_name, msg) = if method_name == "splitn" {
25+
("split_once", "manual implementation of `split_once`")
26+
} else {
27+
("rsplit_once", "manual implementation of `rsplit_once`")
28+
};
29+
30+
let mut app = Applicability::MachineApplicable;
31+
let self_snip = snippet_with_context(cx, self_arg.span, ctxt, "..", &mut app).0;
32+
let pat_snip = snippet_with_context(cx, pat_arg.span, ctxt, "..", &mut app).0;
33+
34+
match usage.kind {
35+
IterUsageKind::NextTuple => {
36+
span_lint_and_sugg(
37+
cx,
38+
MANUAL_SPLIT_ONCE,
39+
usage.span,
40+
msg,
41+
"try this",
42+
format!("{}.{}({})", self_snip, method_name, pat_snip),
43+
app,
44+
);
45+
},
46+
IterUsageKind::Next => {
47+
let self_deref = {
48+
let adjust = cx.typeck_results().expr_adjustments(self_arg);
49+
if adjust.is_empty() {
50+
String::new()
51+
} else if cx.typeck_results().expr_ty(self_arg).is_box()
52+
|| adjust
53+
.iter()
54+
.any(|a| matches!(a.kind, Adjust::Deref(Some(_))) || a.target.is_box())
55+
{
56+
format!("&{}", "*".repeat(adjust.len() - 1))
57+
} else {
58+
"*".repeat(adjust.len() - 2)
59+
}
60+
};
61+
let sugg = if usage.unwrap_kind.is_some() {
62+
format!(
63+
"{}.{}({}).map_or({}{}, |x| x.0)",
64+
&self_snip, method_name, pat_snip, self_deref, &self_snip
65+
)
66+
} else {
67+
format!(
68+
"Some({}.{}({}).map_or({}{}, |x| x.0))",
69+
&self_snip, method_name, pat_snip, self_deref, &self_snip
70+
)
71+
};
72+
73+
span_lint_and_sugg(cx, MANUAL_SPLIT_ONCE, usage.span, msg, "try this", sugg, app);
74+
},
75+
IterUsageKind::Second => {
76+
let access_str = match usage.unwrap_kind {
77+
Some(UnwrapKind::Unwrap) => ".unwrap().1",
78+
Some(UnwrapKind::QuestionMark) => "?.1",
79+
None => ".map(|x| x.1)",
80+
};
81+
span_lint_and_sugg(
82+
cx,
83+
MANUAL_SPLIT_ONCE,
84+
usage.span,
85+
msg,
86+
"try this",
87+
format!("{}.{}({}){}", self_snip, method_name, pat_snip, access_str),
88+
app,
89+
);
90+
},
91+
}
92+
}
93+
94+
enum IterUsageKind {
95+
Next,
96+
Second,
97+
NextTuple,
98+
}
99+
100+
enum UnwrapKind {
101+
Unwrap,
102+
QuestionMark,
103+
}
104+
105+
struct IterUsage {
106+
kind: IterUsageKind,
107+
unwrap_kind: Option<UnwrapKind>,
108+
span: Span,
109+
}
110+
111+
fn parse_iter_usage(
112+
cx: &LateContext<'tcx>,
113+
ctxt: SyntaxContext,
114+
mut iter: impl Iterator<Item = (HirId, Node<'tcx>)>,
115+
) -> Option<IterUsage> {
116+
let (kind, span) = match iter.next() {
117+
Some((_, Node::Expr(e))) if e.span.ctxt() == ctxt => {
118+
let (name, args) = if let ExprKind::MethodCall(name, _, [_, args @ ..], _) = e.kind {
119+
(name, args)
120+
} else {
121+
return None;
122+
};
123+
let did = cx.typeck_results().type_dependent_def_id(e.hir_id)?;
124+
let iter_id = cx.tcx.get_diagnostic_item(sym::Iterator)?;
125+
126+
match (&*name.ident.as_str(), args) {
127+
("next", []) if cx.tcx.trait_of_item(did) == Some(iter_id) => (IterUsageKind::Next, e.span),
128+
("next_tuple", []) => {
129+
if_chain! {
130+
if match_def_path(cx, did, &paths::ITERTOOLS_NEXT_TUPLE);
131+
if let ty::Adt(adt_def, subs) = cx.typeck_results().expr_ty(e).kind();
132+
if cx.tcx.is_diagnostic_item(sym::option_type, adt_def.did);
133+
if let ty::Tuple(subs) = subs.type_at(0).kind();
134+
if subs.len() == 2;
135+
then {
136+
return Some(IterUsage { kind: IterUsageKind::NextTuple, span: e.span, unwrap_kind: None });
137+
} else {
138+
return None;
139+
}
140+
}
141+
},
142+
("nth" | "skip", [idx_expr]) if cx.tcx.trait_of_item(did) == Some(iter_id) => {
143+
if let Some((Constant::Int(idx), _)) = constant(cx, cx.typeck_results(), idx_expr) {
144+
let span = if name.ident.as_str() == "nth" {
145+
e.span
146+
} else {
147+
if_chain! {
148+
if let Some((_, Node::Expr(next_expr))) = iter.next();
149+
if let ExprKind::MethodCall(next_name, _, [_], _) = next_expr.kind;
150+
if next_name.ident.name == sym::next;
151+
if next_expr.span.ctxt() == ctxt;
152+
if let Some(next_id) = cx.typeck_results().type_dependent_def_id(next_expr.hir_id);
153+
if cx.tcx.trait_of_item(next_id) == Some(iter_id);
154+
then {
155+
next_expr.span
156+
} else {
157+
return None;
158+
}
159+
}
160+
};
161+
match idx {
162+
0 => (IterUsageKind::Next, span),
163+
1 => (IterUsageKind::Second, span),
164+
_ => return None,
165+
}
166+
} else {
167+
return None;
168+
}
169+
},
170+
_ => return None,
171+
}
172+
},
173+
_ => return None,
174+
};
175+
176+
let (unwrap_kind, span) = if let Some((_, Node::Expr(e))) = iter.next() {
177+
match e.kind {
178+
ExprKind::Call(
179+
Expr {
180+
kind: ExprKind::Path(QPath::LangItem(LangItem::TryTraitBranch, _)),
181+
..
182+
},
183+
_,
184+
) if e.span.parent().unwrap().ctxt() == ctxt => (Some(UnwrapKind::QuestionMark), e.span),
185+
_ if e.span.ctxt() != ctxt => (None, span),
186+
ExprKind::MethodCall(name, _, [_], _)
187+
if name.ident.name == sym::unwrap
188+
&& cx
189+
.typeck_results()
190+
.type_dependent_def_id(e.hir_id)
191+
.map_or(false, |id| is_diag_item_method(cx, id, sym::option_type)) =>
192+
{
193+
(Some(UnwrapKind::Unwrap), e.span)
194+
},
195+
_ => (None, span),
196+
}
197+
} else {
198+
(None, span)
199+
};
200+
201+
Some(IterUsage {
202+
kind,
203+
unwrap_kind,
204+
span,
205+
})
206+
}

clippy_lints/src/methods/mod.rs

+41-3
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ mod iter_nth_zero;
3333
mod iter_skip_next;
3434
mod iterator_step_by_zero;
3535
mod manual_saturating_arithmetic;
36+
mod manual_split_once;
3637
mod manual_str_repeat;
3738
mod map_collect_result_unit;
3839
mod map_flatten;
@@ -64,6 +65,7 @@ mod wrong_self_convention;
6465
mod zst_offset;
6566

6667
use bind_instead_of_map::BindInsteadOfMap;
68+
use clippy_utils::consts::{constant, Constant};
6769
use clippy_utils::diagnostics::{span_lint, span_lint_and_help};
6870
use clippy_utils::ty::{contains_adt_constructor, contains_ty, implements_trait, is_copy, is_type_diagnostic_item};
6971
use clippy_utils::{contains_return, get_trait_def_id, in_macro, iter_input_pats, meets_msrv, msrvs, paths, return_ty};
@@ -1771,6 +1773,31 @@ declare_clippy_lint! {
17711773
"manual implementation of `str::repeat`"
17721774
}
17731775

1776+
declare_clippy_lint! {
1777+
/// **What it does:** Checks for usages of `splitn(2, _)`
1778+
///
1779+
/// **Why is this bad?** `split_once` is clearer.
1780+
///
1781+
/// **Known problems:** None.
1782+
///
1783+
/// **Example:**
1784+
///
1785+
/// ```rust
1786+
/// // Bad
1787+
/// let some_str = "name=value";
1788+
/// let mut iter = some_str.splitn(2, '=');
1789+
/// let name = iter.next().unwrap();
1790+
/// let value = iter.next().unwrap_or("");
1791+
///
1792+
/// // Good
1793+
/// let some_str = "name=value";
1794+
/// let (name, value) = some_str.split_once('=').unwrap_or((some_str, ""));
1795+
/// ```
1796+
pub MANUAL_SPLIT_ONCE,
1797+
complexity,
1798+
"replace `.splitn(2, pat)` with `.split_once(pat)`"
1799+
}
1800+
17741801
pub struct Methods {
17751802
avoid_breaking_exported_api: bool,
17761803
msrv: Option<RustcVersion>,
@@ -1848,7 +1875,8 @@ impl_lint_pass!(Methods => [
18481875
IMPLICIT_CLONE,
18491876
SUSPICIOUS_SPLITN,
18501877
MANUAL_STR_REPEAT,
1851-
EXTEND_WITH_DRAIN
1878+
EXTEND_WITH_DRAIN,
1879+
MANUAL_SPLIT_ONCE
18521880
]);
18531881

18541882
/// Extracts a method call name, args, and `Span` of the method name.
@@ -2176,8 +2204,18 @@ fn check_methods<'tcx>(cx: &LateContext<'tcx>, expr: &'tcx Expr<'_>, msrv: Optio
21762204
unnecessary_lazy_eval::check(cx, expr, recv, arg, "or");
21772205
}
21782206
},
2179-
("splitn" | "splitn_mut" | "rsplitn" | "rsplitn_mut", [count_arg, _]) => {
2180-
suspicious_splitn::check(cx, name, expr, recv, count_arg);
2207+
("splitn" | "rsplitn", [count_arg, pat_arg]) => {
2208+
if let Some((Constant::Int(count), _)) = constant(cx, cx.typeck_results(), count_arg) {
2209+
suspicious_splitn::check(cx, name, expr, recv, count);
2210+
if count == 2 && meets_msrv(msrv, &msrvs::STR_SPLIT_ONCE) {
2211+
manual_split_once::check(cx, name, expr, recv, pat_arg);
2212+
}
2213+
}
2214+
},
2215+
("splitn_mut" | "rsplitn_mut", [count_arg, _]) => {
2216+
if let Some((Constant::Int(count), _)) = constant(cx, cx.typeck_results(), count_arg) {
2217+
suspicious_splitn::check(cx, name, expr, recv, count);
2218+
}
21812219
},
21822220
("step_by", [arg]) => iterator_step_by_zero::check(cx, expr, arg),
21832221
("to_os_string" | "to_owned" | "to_path_buf" | "to_vec", []) => {

clippy_lints/src/methods/suspicious_splitn.rs

+3-11
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
use clippy_utils::consts::{constant, Constant};
21
use clippy_utils::diagnostics::span_lint_and_note;
32
use if_chain::if_chain;
43
use rustc_ast::LitKind;
@@ -8,25 +7,18 @@ use rustc_span::source_map::Spanned;
87

98
use super::SUSPICIOUS_SPLITN;
109

11-
pub(super) fn check(
12-
cx: &LateContext<'_>,
13-
method_name: &str,
14-
expr: &Expr<'_>,
15-
self_arg: &Expr<'_>,
16-
count_arg: &Expr<'_>,
17-
) {
10+
pub(super) fn check(cx: &LateContext<'_>, method_name: &str, expr: &Expr<'_>, self_arg: &Expr<'_>, count: u128) {
1811
if_chain! {
19-
if let Some((Constant::Int(count), _)) = constant(cx, cx.typeck_results(), count_arg);
2012
if count <= 1;
2113
if let Some(call_id) = cx.typeck_results().type_dependent_def_id(expr.hir_id);
2214
if let Some(impl_id) = cx.tcx.impl_of_method(call_id);
2315
let lang_items = cx.tcx.lang_items();
2416
if lang_items.slice_impl() == Some(impl_id) || lang_items.str_impl() == Some(impl_id);
2517
then {
2618
// Ignore empty slice and string literals when used with a literal count.
27-
if (matches!(self_arg.kind, ExprKind::Array([]))
19+
if matches!(self_arg.kind, ExprKind::Array([]))
2820
|| matches!(self_arg.kind, ExprKind::Lit(Spanned { node: LitKind::Str(s, _), .. }) if s.is_empty())
29-
) && matches!(count_arg.kind, ExprKind::Lit(_))
21+
3022
{
3123
return;
3224
}

clippy_lints/src/utils/conf.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -136,7 +136,7 @@ define_Conf! {
136136
///
137137
/// Suppress lints whenever the suggested change would cause breakage for other crates.
138138
(avoid_breaking_exported_api: bool = true),
139-
/// Lint: MANUAL_STR_REPEAT, CLONED_INSTEAD_OF_COPIED, REDUNDANT_FIELD_NAMES, REDUNDANT_STATIC_LIFETIMES, FILTER_MAP_NEXT, CHECKED_CONVERSIONS, MANUAL_RANGE_CONTAINS, USE_SELF, MEM_REPLACE_WITH_DEFAULT, MANUAL_NON_EXHAUSTIVE, OPTION_AS_REF_DEREF, MAP_UNWRAP_OR, MATCH_LIKE_MATCHES_MACRO, MANUAL_STRIP, MISSING_CONST_FOR_FN, UNNESTED_OR_PATTERNS, FROM_OVER_INTO, PTR_AS_PTR, IF_THEN_SOME_ELSE_NONE.
139+
/// Lint: MANUAL_SPLIT_ONCE, MANUAL_STR_REPEAT, CLONED_INSTEAD_OF_COPIED, REDUNDANT_FIELD_NAMES, REDUNDANT_STATIC_LIFETIMES, FILTER_MAP_NEXT, CHECKED_CONVERSIONS, MANUAL_RANGE_CONTAINS, USE_SELF, MEM_REPLACE_WITH_DEFAULT, MANUAL_NON_EXHAUSTIVE, OPTION_AS_REF_DEREF, MAP_UNWRAP_OR, MATCH_LIKE_MATCHES_MACRO, MANUAL_STRIP, MISSING_CONST_FOR_FN, UNNESTED_OR_PATTERNS, FROM_OVER_INTO, PTR_AS_PTR, IF_THEN_SOME_ELSE_NONE.
140140
///
141141
/// The minimum rust version that the project supports
142142
(msrv: Option<String> = None),

clippy_utils/src/diagnostics.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ fn docs_link(diag: &mut DiagnosticBuilder<'_>, lint: &'static Lint) {
2121
"for further information visit https://rust-lang.github.io/rust-clippy/{}/index.html#{}",
2222
&option_env!("RUST_RELEASE_NUM").map_or("master".to_string(), |n| {
2323
// extract just major + minor version and ignore patch versions
24-
format!("rust-{}", n.rsplitn(2, '.').nth(1).unwrap())
24+
format!("rust-{}", n.rsplit_once('.').unwrap().1)
2525
}),
2626
lint
2727
));

clippy_utils/src/msrvs.rs

+1
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ macro_rules! msrv_aliases {
1313
// names may refer to stabilized feature flags or library items
1414
msrv_aliases! {
1515
1,53,0 { OR_PATTERNS }
16+
1,52,0 { STR_SPLIT_ONCE }
1617
1,50,0 { BOOL_THEN }
1718
1,46,0 { CONST_IF_MATCH }
1819
1,45,0 { STR_STRIP_PREFIX }

clippy_utils/src/paths.rs

+1
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ pub const IO_WRITE: [&str; 3] = ["std", "io", "Write"];
6868
pub const IPADDR_V4: [&str; 5] = ["std", "net", "ip", "IpAddr", "V4"];
6969
pub const IPADDR_V6: [&str; 5] = ["std", "net", "ip", "IpAddr", "V6"];
7070
pub const ITER_REPEAT: [&str; 5] = ["core", "iter", "sources", "repeat", "repeat"];
71+
pub const ITERTOOLS_NEXT_TUPLE: [&str; 3] = ["itertools", "Itertools", "next_tuple"];
7172
#[cfg(feature = "internal-lints")]
7273
pub const KW_MODULE: [&str; 3] = ["rustc_span", "symbol", "kw"];
7374
#[cfg(feature = "internal-lints")]

0 commit comments

Comments
 (0)