Skip to content

Commit ed22428

Browse files
committed
Auto merge of rust-lang#8717 - Alexendoo:manual-split-once-manual-iter, r=dswij,xFrednet
`manual_split_once`: lint manual iteration of `SplitN` changelog: `manual_split_once`: lint manual iteration of `SplitN` Now lints: ```rust let mut iter = "a.b.c".splitn(2, '.'); let first = iter.next().unwrap(); let second = iter.next().unwrap(); let mut iter = "a.b.c".splitn(2, '.'); let first = iter.next()?; let second = iter.next()?; let mut iter = "a.b.c".rsplitn(2, '.'); let first = iter.next().unwrap(); let second = iter.next().unwrap(); let mut iter = "a.b.c".rsplitn(2, '.'); let first = iter.next()?; let second = iter.next()?; ``` It suggests (minus leftover whitespace): ```rust let (first, second) = "a.b.c".split_once('.').unwrap(); let (first, second) = "a.b.c".split_once('.')?; let (second, first) = "a.b.c".rsplit_once('.').unwrap(); let (second, first) = "a.b.c".rsplit_once('.')?; ``` Currently only lints if the statements are next to each other, as detecting the various kinds of shadowing was tricky, so the following won't lint ```rust let mut iter = "a.b.c".splitn(2, '.'); let something_else = 1; let first = iter.next()?; let second = iter.next()?; ```
2 parents cf68cad + 4424aa4 commit ed22428

File tree

5 files changed

+527
-39
lines changed

5 files changed

+527
-39
lines changed

clippy_lints/src/methods/mod.rs

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2009,13 +2009,27 @@ declare_clippy_lint! {
20092009
/// ### Example
20102010
/// ```rust,ignore
20112011
/// // Bad
2012-
/// let (key, value) = _.splitn(2, '=').next_tuple()?;
2013-
/// let value = _.splitn(2, '=').nth(1)?;
2012+
/// let s = "key=value=add";
2013+
/// let (key, value) = s.splitn(2, '=').next_tuple()?;
2014+
/// let value = s.splitn(2, '=').nth(1)?;
20142015
///
2016+
/// let mut parts = s.splitn(2, '=');
2017+
/// let key = parts.next()?;
2018+
/// let value = parts.next()?;
2019+
/// ```
2020+
/// Use instead:
2021+
/// ```rust,ignore
20152022
/// // Good
2016-
/// let (key, value) = _.split_once('=')?;
2017-
/// let value = _.split_once('=')?.1;
2023+
/// let s = "key=value=add";
2024+
/// let (key, value) = s.split_once('=')?;
2025+
/// let value = s.split_once('=')?.1;
2026+
///
2027+
/// let (key, value) = s.split_once('=')?;
20182028
/// ```
2029+
///
2030+
/// ### Limitations
2031+
/// The multiple statement variant currently only detects `iter.next()?`/`iter.next().unwrap()`
2032+
/// in two separate `let` statements that immediately follow the `splitn()`
20192033
#[clippy::version = "1.57.0"]
20202034
pub MANUAL_SPLIT_ONCE,
20212035
complexity,

clippy_lints/src/methods/str_splitn.rs

Lines changed: 192 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,19 @@
11
use clippy_utils::consts::{constant, Constant};
2-
use clippy_utils::diagnostics::span_lint_and_sugg;
2+
use clippy_utils::diagnostics::{span_lint_and_sugg, span_lint_and_then};
33
use clippy_utils::source::snippet_with_context;
4-
use clippy_utils::{is_diag_item_method, match_def_path, meets_msrv, msrvs, paths};
4+
use clippy_utils::usage::local_used_after_expr;
5+
use clippy_utils::visitors::expr_visitor;
6+
use clippy_utils::{is_diag_item_method, match_def_path, meets_msrv, msrvs, path_to_local_id, paths};
57
use if_chain::if_chain;
68
use rustc_errors::Applicability;
7-
use rustc_hir::{Expr, ExprKind, HirId, LangItem, Node, QPath};
9+
use rustc_hir::intravisit::Visitor;
10+
use rustc_hir::{
11+
BindingAnnotation, Expr, ExprKind, HirId, LangItem, Local, MatchSource, Node, Pat, PatKind, QPath, Stmt, StmtKind,
12+
};
813
use rustc_lint::LateContext;
914
use rustc_middle::ty;
1015
use rustc_semver::RustcVersion;
11-
use rustc_span::{symbol::sym, Span, SyntaxContext};
16+
use rustc_span::{sym, Span, Symbol, SyntaxContext};
1217

1318
use super::{MANUAL_SPLIT_ONCE, NEEDLESS_SPLITN};
1419

@@ -25,40 +30,41 @@ pub(super) fn check(
2530
return;
2631
}
2732

28-
let ctxt = expr.span.ctxt();
29-
let Some(usage) = parse_iter_usage(cx, ctxt, cx.tcx.hir().parent_iter(expr.hir_id)) else { return };
30-
31-
let needless = match usage.kind {
33+
let needless = |usage_kind| match usage_kind {
3234
IterUsageKind::Nth(n) => count > n + 1,
3335
IterUsageKind::NextTuple => count > 2,
3436
};
37+
let manual = count == 2 && meets_msrv(msrv, &msrvs::STR_SPLIT_ONCE);
3538

36-
if needless {
37-
let mut app = Applicability::MachineApplicable;
38-
let (r, message) = if method_name == "splitn" {
39-
("", "unnecessary use of `splitn`")
40-
} else {
41-
("r", "unnecessary use of `rsplitn`")
42-
};
43-
44-
span_lint_and_sugg(
45-
cx,
46-
NEEDLESS_SPLITN,
47-
expr.span,
48-
message,
49-
"try this",
50-
format!(
51-
"{}.{r}split({})",
52-
snippet_with_context(cx, self_arg.span, ctxt, "..", &mut app).0,
53-
snippet_with_context(cx, pat_arg.span, ctxt, "..", &mut app).0,
54-
),
55-
app,
56-
);
57-
} else if count == 2 && meets_msrv(msrv, &msrvs::STR_SPLIT_ONCE) {
58-
check_manual_split_once(cx, method_name, expr, self_arg, pat_arg, &usage);
39+
match parse_iter_usage(cx, expr.span.ctxt(), cx.tcx.hir().parent_iter(expr.hir_id)) {
40+
Some(usage) if needless(usage.kind) => lint_needless(cx, method_name, expr, self_arg, pat_arg),
41+
Some(usage) if manual => check_manual_split_once(cx, method_name, expr, self_arg, pat_arg, &usage),
42+
None if manual => {
43+
check_manual_split_once_indirect(cx, method_name, expr, self_arg, pat_arg);
44+
},
45+
_ => {},
5946
}
6047
}
6148

49+
fn lint_needless(cx: &LateContext<'_>, method_name: &str, expr: &Expr<'_>, self_arg: &Expr<'_>, pat_arg: &Expr<'_>) {
50+
let mut app = Applicability::MachineApplicable;
51+
let r = if method_name == "splitn" { "" } else { "r" };
52+
53+
span_lint_and_sugg(
54+
cx,
55+
NEEDLESS_SPLITN,
56+
expr.span,
57+
&format!("unnecessary use of `{r}splitn`"),
58+
"try this",
59+
format!(
60+
"{}.{r}split({})",
61+
snippet_with_context(cx, self_arg.span, expr.span.ctxt(), "..", &mut app).0,
62+
snippet_with_context(cx, pat_arg.span, expr.span.ctxt(), "..", &mut app).0,
63+
),
64+
app,
65+
);
66+
}
67+
6268
fn check_manual_split_once(
6369
cx: &LateContext<'_>,
6470
method_name: &str,
@@ -107,16 +113,171 @@ fn check_manual_split_once(
107113
span_lint_and_sugg(cx, MANUAL_SPLIT_ONCE, usage.span, msg, "try this", sugg, app);
108114
}
109115

116+
/// checks for
117+
///
118+
/// ```
119+
/// let mut iter = "a.b.c".splitn(2, '.');
120+
/// let a = iter.next();
121+
/// let b = iter.next();
122+
/// ```
123+
fn check_manual_split_once_indirect(
124+
cx: &LateContext<'_>,
125+
method_name: &str,
126+
expr: &Expr<'_>,
127+
self_arg: &Expr<'_>,
128+
pat_arg: &Expr<'_>,
129+
) -> Option<()> {
130+
let ctxt = expr.span.ctxt();
131+
let mut parents = cx.tcx.hir().parent_iter(expr.hir_id);
132+
if let (_, Node::Local(local)) = parents.next()?
133+
&& let PatKind::Binding(BindingAnnotation::Mutable, iter_binding_id, iter_ident, None) = local.pat.kind
134+
&& let (iter_stmt_id, Node::Stmt(_)) = parents.next()?
135+
&& let (_, Node::Block(enclosing_block)) = parents.next()?
136+
137+
&& let mut stmts = enclosing_block
138+
.stmts
139+
.iter()
140+
.skip_while(|stmt| stmt.hir_id != iter_stmt_id)
141+
.skip(1)
142+
143+
&& let first = indirect_usage(cx, stmts.next()?, iter_binding_id, ctxt)?
144+
&& let second = indirect_usage(cx, stmts.next()?, iter_binding_id, ctxt)?
145+
&& first.unwrap_kind == second.unwrap_kind
146+
&& first.name != second.name
147+
&& !local_used_after_expr(cx, iter_binding_id, second.init_expr)
148+
{
149+
let (r, lhs, rhs) = if method_name == "splitn" {
150+
("", first.name, second.name)
151+
} else {
152+
("r", second.name, first.name)
153+
};
154+
let msg = format!("manual implementation of `{r}split_once`");
155+
156+
let mut app = Applicability::MachineApplicable;
157+
let self_snip = snippet_with_context(cx, self_arg.span, ctxt, "..", &mut app).0;
158+
let pat_snip = snippet_with_context(cx, pat_arg.span, ctxt, "..", &mut app).0;
159+
160+
span_lint_and_then(cx, MANUAL_SPLIT_ONCE, local.span, &msg, |diag| {
161+
diag.span_label(first.span, "first usage here");
162+
diag.span_label(second.span, "second usage here");
163+
164+
let unwrap = match first.unwrap_kind {
165+
UnwrapKind::Unwrap => ".unwrap()",
166+
UnwrapKind::QuestionMark => "?",
167+
};
168+
diag.span_suggestion_verbose(
169+
local.span,
170+
&format!("try `{r}split_once`"),
171+
format!("let ({lhs}, {rhs}) = {self_snip}.{r}split_once({pat_snip}){unwrap};"),
172+
app,
173+
);
174+
175+
let remove_msg = format!("remove the `{iter_ident}` usages");
176+
diag.span_suggestion(
177+
first.span,
178+
&remove_msg,
179+
String::new(),
180+
app,
181+
);
182+
diag.span_suggestion(
183+
second.span,
184+
&remove_msg,
185+
String::new(),
186+
app,
187+
);
188+
});
189+
}
190+
191+
Some(())
192+
}
193+
194+
#[derive(Debug)]
195+
struct IndirectUsage<'a> {
196+
name: Symbol,
197+
span: Span,
198+
init_expr: &'a Expr<'a>,
199+
unwrap_kind: UnwrapKind,
200+
}
201+
202+
/// returns `Some(IndirectUsage)` for e.g.
203+
///
204+
/// ```ignore
205+
/// let name = binding.next()?;
206+
/// let name = binding.next().unwrap();
207+
/// ```
208+
fn indirect_usage<'tcx>(
209+
cx: &LateContext<'tcx>,
210+
stmt: &Stmt<'tcx>,
211+
binding: HirId,
212+
ctxt: SyntaxContext,
213+
) -> Option<IndirectUsage<'tcx>> {
214+
if let StmtKind::Local(Local {
215+
pat:
216+
Pat {
217+
kind: PatKind::Binding(BindingAnnotation::Unannotated, _, ident, None),
218+
..
219+
},
220+
init: Some(init_expr),
221+
hir_id: local_hir_id,
222+
..
223+
}) = stmt.kind
224+
{
225+
let mut path_to_binding = None;
226+
expr_visitor(cx, |expr| {
227+
if path_to_local_id(expr, binding) {
228+
path_to_binding = Some(expr);
229+
}
230+
231+
path_to_binding.is_none()
232+
})
233+
.visit_expr(init_expr);
234+
235+
let mut parents = cx.tcx.hir().parent_iter(path_to_binding?.hir_id);
236+
let iter_usage = parse_iter_usage(cx, ctxt, &mut parents)?;
237+
238+
let (parent_id, _) = parents.find(|(_, node)| {
239+
!matches!(
240+
node,
241+
Node::Expr(Expr {
242+
kind: ExprKind::Match(.., MatchSource::TryDesugar),
243+
..
244+
})
245+
)
246+
})?;
247+
248+
if let IterUsage {
249+
kind: IterUsageKind::Nth(0),
250+
unwrap_kind: Some(unwrap_kind),
251+
..
252+
} = iter_usage
253+
{
254+
if parent_id == *local_hir_id {
255+
return Some(IndirectUsage {
256+
name: ident.name,
257+
span: stmt.span,
258+
init_expr,
259+
unwrap_kind,
260+
});
261+
}
262+
}
263+
}
264+
265+
None
266+
}
267+
268+
#[derive(Debug, Clone, Copy)]
110269
enum IterUsageKind {
111270
Nth(u128),
112271
NextTuple,
113272
}
114273

274+
#[derive(Debug, PartialEq)]
115275
enum UnwrapKind {
116276
Unwrap,
117277
QuestionMark,
118278
}
119279

280+
#[derive(Debug)]
120281
struct IterUsage {
121282
kind: IterUsageKind,
122283
unwrap_kind: Option<UnwrapKind>,

0 commit comments

Comments
 (0)