]> git.lizzy.rs Git - rust.git/commitdiff
Add lint `manual_split_once`
authorJason Newcomb <jsnewcomb@pm.me>
Sat, 17 Jul 2021 17:52:03 +0000 (13:52 -0400)
committerJason Newcomb <jsnewcomb@pm.me>
Mon, 16 Aug 2021 13:34:58 +0000 (09:34 -0400)
14 files changed:
CHANGELOG.md
clippy_lints/src/lib.rs
clippy_lints/src/methods/manual_split_once.rs [new file with mode: 0644]
clippy_lints/src/methods/mod.rs
clippy_lints/src/methods/suspicious_splitn.rs
clippy_lints/src/utils/conf.rs
clippy_utils/src/diagnostics.rs
clippy_utils/src/msrvs.rs
clippy_utils/src/paths.rs
clippy_utils/src/visitors.rs
tests/compile-test.rs
tests/ui/manual_split_once.fixed [new file with mode: 0644]
tests/ui/manual_split_once.rs [new file with mode: 0644]
tests/ui/manual_split_once.stderr [new file with mode: 0644]

index 2b89170073be513bca6869b916212e05d9fb2935..b9e67d361c0c34cc265eb98307209e64053e1f42 100644 (file)
@@ -2754,6 +2754,7 @@ Released 2018-09-13
 [`manual_ok_or`]: https://rust-lang.github.io/rust-clippy/master/index.html#manual_ok_or
 [`manual_range_contains`]: https://rust-lang.github.io/rust-clippy/master/index.html#manual_range_contains
 [`manual_saturating_arithmetic`]: https://rust-lang.github.io/rust-clippy/master/index.html#manual_saturating_arithmetic
+[`manual_split_once`]: https://rust-lang.github.io/rust-clippy/master/index.html#manual_split_once
 [`manual_str_repeat`]: https://rust-lang.github.io/rust-clippy/master/index.html#manual_str_repeat
 [`manual_strip`]: https://rust-lang.github.io/rust-clippy/master/index.html#manual_strip
 [`manual_swap`]: https://rust-lang.github.io/rust-clippy/master/index.html#manual_swap
index 8372d681078dfb8719a775320a0d33e44e445e2f..1fadaf4770a811cb6803e4a66de21e41c7262361 100644 (file)
@@ -773,6 +773,7 @@ pub fn register_plugins(store: &mut rustc_lint::LintStore, sess: &Session, conf:
         methods::MANUAL_FILTER_MAP,
         methods::MANUAL_FIND_MAP,
         methods::MANUAL_SATURATING_ARITHMETIC,
+        methods::MANUAL_SPLIT_ONCE,
         methods::MANUAL_STR_REPEAT,
         methods::MAP_COLLECT_RESULT_UNIT,
         methods::MAP_FLATTEN,
@@ -1319,6 +1320,7 @@ pub fn register_plugins(store: &mut rustc_lint::LintStore, sess: &Session, conf:
         LintId::of(methods::MANUAL_FILTER_MAP),
         LintId::of(methods::MANUAL_FIND_MAP),
         LintId::of(methods::MANUAL_SATURATING_ARITHMETIC),
+        LintId::of(methods::MANUAL_SPLIT_ONCE),
         LintId::of(methods::MANUAL_STR_REPEAT),
         LintId::of(methods::MAP_COLLECT_RESULT_UNIT),
         LintId::of(methods::MAP_IDENTITY),
@@ -1617,6 +1619,7 @@ pub fn register_plugins(store: &mut rustc_lint::LintStore, sess: &Session, conf:
         LintId::of(methods::ITER_COUNT),
         LintId::of(methods::MANUAL_FILTER_MAP),
         LintId::of(methods::MANUAL_FIND_MAP),
+        LintId::of(methods::MANUAL_SPLIT_ONCE),
         LintId::of(methods::MAP_IDENTITY),
         LintId::of(methods::OPTION_AS_REF_DEREF),
         LintId::of(methods::OPTION_FILTER_MAP),
diff --git a/clippy_lints/src/methods/manual_split_once.rs b/clippy_lints/src/methods/manual_split_once.rs
new file mode 100644 (file)
index 0000000..e273186
--- /dev/null
@@ -0,0 +1,213 @@
+use clippy_utils::consts::{constant, Constant};
+use clippy_utils::diagnostics::span_lint_and_sugg;
+use clippy_utils::source::snippet_with_context;
+use clippy_utils::{is_diag_item_method, match_def_path, paths};
+use if_chain::if_chain;
+use rustc_errors::Applicability;
+use rustc_hir::{Expr, ExprKind, HirId, LangItem, Node, QPath};
+use rustc_lint::LateContext;
+use rustc_middle::ty::{self, adjustment::Adjust};
+use rustc_span::{symbol::sym, Span, SyntaxContext};
+
+use super::MANUAL_SPLIT_ONCE;
+
+pub(super) fn check(cx: &LateContext<'_>, method_name: &str, expr: &Expr<'_>, self_arg: &Expr<'_>, pat_arg: &Expr<'_>) {
+    if !cx.typeck_results().expr_ty_adjusted(self_arg).peel_refs().is_str() {
+        return;
+    }
+
+    let ctxt = expr.span.ctxt();
+    let usage = match parse_iter_usage(cx, ctxt, cx.tcx.hir().parent_iter(expr.hir_id)) {
+        Some(x) => x,
+        None => return,
+    };
+    let (method_name, msg) = if method_name == "splitn" {
+        ("split_once", "manual implementation of `split_once`")
+    } else {
+        ("rsplit_once", "manual implementation of `rsplit_once`")
+    };
+
+    let mut app = Applicability::MachineApplicable;
+    let self_snip = snippet_with_context(cx, self_arg.span, ctxt, "..", &mut app).0;
+    let pat_snip = snippet_with_context(cx, pat_arg.span, ctxt, "..", &mut app).0;
+
+    match usage.kind {
+        IterUsageKind::NextTuple => {
+            span_lint_and_sugg(
+                cx,
+                MANUAL_SPLIT_ONCE,
+                usage.span,
+                msg,
+                "try this",
+                format!("{}.{}({})", self_snip, method_name, pat_snip),
+                app,
+            );
+        },
+        IterUsageKind::Next => {
+            let self_deref = {
+                let adjust = cx.typeck_results().expr_adjustments(self_arg);
+                if adjust.is_empty() {
+                    String::new()
+                } else if cx.typeck_results().expr_ty(self_arg).is_box()
+                    || adjust
+                        .iter()
+                        .any(|a| matches!(a.kind, Adjust::Deref(Some(_))) || a.target.is_box())
+                {
+                    format!("&{}", "*".repeat(adjust.len() - 1))
+                } else {
+                    "*".repeat(adjust.len() - 2)
+                }
+            };
+            let sugg = if usage.unwrap_kind.is_some() {
+                format!(
+                    "{}.{}({}).map_or({}{}, |x| x.0)",
+                    &self_snip, method_name, pat_snip, self_deref, &self_snip
+                )
+            } else {
+                format!(
+                    "Some({}.{}({}).map_or({}{}, |x| x.0))",
+                    &self_snip, method_name, pat_snip, self_deref, &self_snip
+                )
+            };
+
+            span_lint_and_sugg(cx, MANUAL_SPLIT_ONCE, usage.span, msg, "try this", sugg, app);
+        },
+        IterUsageKind::Second => {
+            let access_str = match usage.unwrap_kind {
+                Some(UnwrapKind::Unwrap) => ".unwrap().1",
+                Some(UnwrapKind::QuestionMark) => "?.1",
+                None => ".map(|x| x.1)",
+            };
+            span_lint_and_sugg(
+                cx,
+                MANUAL_SPLIT_ONCE,
+                usage.span,
+                msg,
+                "try this",
+                format!("{}.{}({}){}", self_snip, method_name, pat_snip, access_str),
+                app,
+            );
+        },
+    }
+}
+
+enum IterUsageKind {
+    Next,
+    Second,
+    NextTuple,
+}
+
+enum UnwrapKind {
+    Unwrap,
+    QuestionMark,
+}
+
+struct IterUsage {
+    kind: IterUsageKind,
+    unwrap_kind: Option<UnwrapKind>,
+    span: Span,
+}
+
+fn parse_iter_usage(
+    cx: &LateContext<'tcx>,
+    ctxt: SyntaxContext,
+    mut iter: impl Iterator<Item = (HirId, Node<'tcx>)>,
+) -> Option<IterUsage> {
+    let (kind, span) = match iter.next() {
+        Some((_, Node::Expr(e))) if e.span.ctxt() == ctxt => {
+            let (name, args) = if let ExprKind::MethodCall(name, _, [_, args @ ..], _) = e.kind {
+                (name, args)
+            } else {
+                return None;
+            };
+            let did = cx.typeck_results().type_dependent_def_id(e.hir_id)?;
+            let iter_id = cx.tcx.get_diagnostic_item(sym::Iterator)?;
+
+            match (&*name.ident.as_str(), args) {
+                ("next", []) if cx.tcx.trait_of_item(did) == Some(iter_id) => (IterUsageKind::Next, e.span),
+                ("next_tuple", []) => {
+                    if_chain! {
+                        if match_def_path(cx, did, &paths::ITERTOOLS_NEXT_TUPLE);
+                        if let ty::Adt(adt_def, subs) = cx.typeck_results().expr_ty(e).kind();
+                        if cx.tcx.is_diagnostic_item(sym::option_type, adt_def.did);
+                        if let ty::Tuple(subs) = subs.type_at(0).kind();
+                        if subs.len() == 2;
+                        then {
+                            return Some(IterUsage { kind: IterUsageKind::NextTuple, span: e.span, unwrap_kind: None });
+                        } else {
+                            return None;
+                        }
+                    }
+                },
+                ("nth" | "skip", [idx_expr]) if cx.tcx.trait_of_item(did) == Some(iter_id) => {
+                    if let Some((Constant::Int(idx), _)) = constant(cx, cx.typeck_results(), idx_expr) {
+                        let span = if name.ident.as_str() == "nth" {
+                            e.span
+                        } else {
+                            if_chain! {
+                                if let Some((_, Node::Expr(next_expr))) = iter.next();
+                                if let ExprKind::MethodCall(next_name, _, [_], _) = next_expr.kind;
+                                if next_name.ident.name == sym::next;
+                                if next_expr.span.ctxt() == ctxt;
+                                if let Some(next_id) = cx.typeck_results().type_dependent_def_id(next_expr.hir_id);
+                                if cx.tcx.trait_of_item(next_id) == Some(iter_id);
+                                then {
+                                    next_expr.span
+                                } else {
+                                    return None;
+                                }
+                            }
+                        };
+                        match idx {
+                            0 => (IterUsageKind::Next, span),
+                            1 => (IterUsageKind::Second, span),
+                            _ => return None,
+                        }
+                    } else {
+                        return None;
+                    }
+                },
+                _ => return None,
+            }
+        },
+        _ => return None,
+    };
+
+    let (unwrap_kind, span) = if let Some((_, Node::Expr(e))) = iter.next() {
+        match e.kind {
+            ExprKind::Call(
+                Expr {
+                    kind: ExprKind::Path(QPath::LangItem(LangItem::TryTraitBranch, _)),
+                    ..
+                },
+                _,
+            ) => {
+                let parent_span = e.span.parent().unwrap();
+                if parent_span.ctxt() == ctxt {
+                    (Some(UnwrapKind::QuestionMark), parent_span)
+                } else {
+                    (None, span)
+                }
+            },
+            _ if e.span.ctxt() != ctxt => (None, span),
+            ExprKind::MethodCall(name, _, [_], _)
+                if name.ident.name == sym::unwrap
+                    && cx
+                        .typeck_results()
+                        .type_dependent_def_id(e.hir_id)
+                        .map_or(false, |id| is_diag_item_method(cx, id, sym::option_type)) =>
+            {
+                (Some(UnwrapKind::Unwrap), e.span)
+            },
+            _ => (None, span),
+        }
+    } else {
+        (None, span)
+    };
+
+    Some(IterUsage {
+        kind,
+        unwrap_kind,
+        span,
+    })
+}
index 91606ed3b2bb0d9bc83dfddb0085208451d78415..15ad0325006eb62dcc31de8151b091c71cd8bcf8 100644 (file)
@@ -33,6 +33,7 @@
 mod iter_skip_next;
 mod iterator_step_by_zero;
 mod manual_saturating_arithmetic;
+mod manual_split_once;
 mod manual_str_repeat;
 mod map_collect_result_unit;
 mod map_flatten;
@@ -64,6 +65,7 @@
 mod zst_offset;
 
 use bind_instead_of_map::BindInsteadOfMap;
+use clippy_utils::consts::{constant, Constant};
 use clippy_utils::diagnostics::{span_lint, span_lint_and_help};
 use clippy_utils::ty::{contains_adt_constructor, contains_ty, implements_trait, is_copy, is_type_diagnostic_item};
 use clippy_utils::{contains_return, get_trait_def_id, in_macro, iter_input_pats, meets_msrv, msrvs, paths, return_ty};
     "manual implementation of `str::repeat`"
 }
 
+declare_clippy_lint! {
+    /// **What it does:** Checks for usages of `splitn(2, _)`
+    ///
+    /// **Why is this bad?** `split_once` is clearer.
+    ///
+    /// **Known problems:** None.
+    ///
+    /// **Example:**
+    ///
+    /// ```rust
+    /// // Bad
+    /// let some_str = "name=value";
+    /// let mut iter = some_str.splitn(2, '=');
+    /// let name = iter.next().unwrap();
+    /// let value = iter.next().unwrap_or("");
+    ///
+    /// // Good
+    /// let some_str = "name=value";
+    /// let (name, value) = some_str.split_once('=').unwrap_or((some_str, ""));
+    /// ```
+    pub MANUAL_SPLIT_ONCE,
+    complexity,
+    "replace `.splitn(2, pat)` with `.split_once(pat)`"
+}
+
 pub struct Methods {
     avoid_breaking_exported_api: bool,
     msrv: Option<RustcVersion>,
@@ -1848,7 +1875,8 @@ pub fn new(avoid_breaking_exported_api: bool, msrv: Option<RustcVersion>) -> Sel
     IMPLICIT_CLONE,
     SUSPICIOUS_SPLITN,
     MANUAL_STR_REPEAT,
-    EXTEND_WITH_DRAIN
+    EXTEND_WITH_DRAIN,
+    MANUAL_SPLIT_ONCE
 ]);
 
 /// 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
                     unnecessary_lazy_eval::check(cx, expr, recv, arg, "or");
                 }
             },
-            ("splitn" | "splitn_mut" | "rsplitn" | "rsplitn_mut", [count_arg, _]) => {
-                suspicious_splitn::check(cx, name, expr, recv, count_arg);
+            ("splitn" | "rsplitn", [count_arg, pat_arg]) => {
+                if let Some((Constant::Int(count), _)) = constant(cx, cx.typeck_results(), count_arg) {
+                    suspicious_splitn::check(cx, name, expr, recv, count);
+                    if count == 2 && meets_msrv(msrv, &msrvs::STR_SPLIT_ONCE) {
+                        manual_split_once::check(cx, name, expr, recv, pat_arg);
+                    }
+                }
+            },
+            ("splitn_mut" | "rsplitn_mut", [count_arg, _]) => {
+                if let Some((Constant::Int(count), _)) = constant(cx, cx.typeck_results(), count_arg) {
+                    suspicious_splitn::check(cx, name, expr, recv, count);
+                }
             },
             ("step_by", [arg]) => iterator_step_by_zero::check(cx, expr, arg),
             ("to_os_string" | "to_owned" | "to_path_buf" | "to_vec", []) => {
index a271df60572a22f0969910f22a5bc1c1828dabca..1c546a15bf62b331a5993e378aee11e7c832dc72 100644 (file)
@@ -1,4 +1,3 @@
-use clippy_utils::consts::{constant, Constant};
 use clippy_utils::diagnostics::span_lint_and_note;
 use if_chain::if_chain;
 use rustc_ast::LitKind;
@@ -8,15 +7,8 @@
 
 use super::SUSPICIOUS_SPLITN;
 
-pub(super) fn check(
-    cx: &LateContext<'_>,
-    method_name: &str,
-    expr: &Expr<'_>,
-    self_arg: &Expr<'_>,
-    count_arg: &Expr<'_>,
-) {
+pub(super) fn check(cx: &LateContext<'_>, method_name: &str, expr: &Expr<'_>, self_arg: &Expr<'_>, count: u128) {
     if_chain! {
-        if let Some((Constant::Int(count), _)) = constant(cx, cx.typeck_results(), count_arg);
         if count <= 1;
         if let Some(call_id) = cx.typeck_results().type_dependent_def_id(expr.hir_id);
         if let Some(impl_id) = cx.tcx.impl_of_method(call_id);
@@ -24,9 +16,9 @@ pub(super) fn check(
         if lang_items.slice_impl() == Some(impl_id) || lang_items.str_impl() == Some(impl_id);
         then {
             // Ignore empty slice and string literals when used with a literal count.
-            if (matches!(self_arg.kind, ExprKind::Array([]))
+            if matches!(self_arg.kind, ExprKind::Array([]))
                 || matches!(self_arg.kind, ExprKind::Lit(Spanned { node: LitKind::Str(s, _), .. }) if s.is_empty())
-            ) && matches!(count_arg.kind, ExprKind::Lit(_))
+
             {
                 return;
             }
index 9ee2e3024520bda298fb1d33d73a66d8dc39051e..211ffdeca2000e53d749507206e8e960464f9cef 100644 (file)
@@ -136,7 +136,7 @@ pub(crate) fn get_configuration_metadata() -> Vec<ClippyConfiguration> {
     ///
     /// Suppress lints whenever the suggested change would cause breakage for other crates.
     (avoid_breaking_exported_api: bool = true),
-    /// 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.
+    /// 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.
     ///
     /// The minimum rust version that the project supports
     (msrv: Option<String> = None),
index 71cfa196fc335f93d7b62e5d0234e08a08f33780..9302e5c21faa4f7606d485726bcbf256aba1cfb8 100644 (file)
@@ -21,7 +21,7 @@ fn docs_link(diag: &mut DiagnosticBuilder<'_>, lint: &'static Lint) {
                 "for further information visit https://rust-lang.github.io/rust-clippy/{}/index.html#{}",
                 &option_env!("RUST_RELEASE_NUM").map_or("master".to_string(), |n| {
                     // extract just major + minor version and ignore patch versions
-                    format!("rust-{}", n.rsplitn(2, '.').nth(1).unwrap())
+                    format!("rust-{}", n.rsplit_once('.').unwrap().1)
                 }),
                 lint
             ));
index 4a9c4fd0276b378074b0b9895512f22a2e5a65d1..14234d9c9cbf0d6ce4384f7ae8b97a9294afc387 100644 (file)
@@ -13,6 +13,7 @@ macro_rules! msrv_aliases {
 // names may refer to stabilized feature flags or library items
 msrv_aliases! {
     1,53,0 { OR_PATTERNS }
+    1,52,0 { STR_SPLIT_ONCE }
     1,50,0 { BOOL_THEN }
     1,46,0 { CONST_IF_MATCH }
     1,45,0 { STR_STRIP_PREFIX }
index b0c3fe1e5a7121d56cafc32e45a0f872536d633b..d7e46c2d3eb9d2826966f19fc3d8f98b058a150d 100644 (file)
@@ -68,6 +68,7 @@
 pub const IPADDR_V4: [&str; 5] = ["std", "net", "ip", "IpAddr", "V4"];
 pub const IPADDR_V6: [&str; 5] = ["std", "net", "ip", "IpAddr", "V6"];
 pub const ITER_REPEAT: [&str; 5] = ["core", "iter", "sources", "repeat", "repeat"];
+pub const ITERTOOLS_NEXT_TUPLE: [&str; 3] = ["itertools", "Itertools", "next_tuple"];
 #[cfg(feature = "internal-lints")]
 pub const KW_MODULE: [&str; 3] = ["rustc_span", "symbol", "kw"];
 #[cfg(feature = "internal-lints")]
index 52f95ff34f505f46f725d29bdb3b64a44263496d..503effbdad5725069eb4ddc268a08c80ae7b9014 100644 (file)
@@ -147,22 +147,23 @@ fn visit<V: Visitor<'tcx>>(self, visitor: &mut V) {
             }
         }
     };
-    ([$t:ident], $f:ident) => {
-        impl Visitable<'tcx> for &'tcx [$t<'tcx>] {
-            fn visit<V: Visitor<'tcx>>(self, visitor: &mut V) {
-                for x in self {
-                    visitor.$f(x);
-                }
-            }
-        }
-    };
 }
 visitable_ref!(Arm, visit_arm);
 visitable_ref!(Block, visit_block);
 visitable_ref!(Body, visit_body);
 visitable_ref!(Expr, visit_expr);
 visitable_ref!(Stmt, visit_stmt);
-visitable_ref!([Stmt], visit_stmt);
+
+// impl<'tcx, I: IntoIterator> Visitable<'tcx> for I
+// where
+//     I::Item: Visitable<'tcx>,
+// {
+//     fn visit<V: Visitor<'tcx>>(self, visitor: &mut V) {
+//         for x in self {
+//             x.visit(visitor);
+//         }
+//     }
+// }
 
 /// Calls the given function for each break expression.
 pub fn visit_break_exprs<'tcx>(
index 0a82377a10e4247eaebac2c19ab4696b96673fc0..6116acffe07f4a4db75c12de2600a2f7547e8587 100644 (file)
@@ -39,6 +39,7 @@ fn third_party_crates() -> String {
         "clippy_lints",
         "clippy_utils",
         "if_chain",
+        "itertools",
         "quote",
         "regex",
         "serde",
diff --git a/tests/ui/manual_split_once.fixed b/tests/ui/manual_split_once.fixed
new file mode 100644 (file)
index 0000000..3a03329
--- /dev/null
@@ -0,0 +1,50 @@
+// run-rustfix
+
+#![feature(custom_inner_attributes)]
+#![warn(clippy::manual_split_once)]
+#![allow(clippy::iter_skip_next, clippy::iter_nth_zero)]
+
+extern crate itertools;
+
+#[allow(unused_imports)]
+use itertools::Itertools;
+
+fn main() {
+    let _ = Some("key=value".split_once('=').map_or("key=value", |x| x.0));
+    let _ = "key=value".splitn(2, '=').nth(2);
+    let _ = "key=value".split_once('=').map_or("key=value", |x| x.0);
+    let _ = "key=value".split_once('=').map_or("key=value", |x| x.0);
+    let _ = "key=value".split_once('=').unwrap().1;
+    let _ = "key=value".split_once('=').unwrap().1;
+    let (_, _) = "key=value".split_once('=').unwrap();
+
+    let s = String::from("key=value");
+    let _ = s.split_once('=').map_or(&*s, |x| x.0);
+
+    let s = Box::<str>::from("key=value");
+    let _ = s.split_once('=').map_or(&*s, |x| x.0);
+
+    let s = &"key=value";
+    let _ = s.split_once('=').map_or(*s, |x| x.0);
+
+    fn _f(s: &str) -> Option<&str> {
+        let _ = s.split_once("key=value").map_or(s, |x| x.0);
+        let _ = s.split_once("key=value")?.1;
+        let _ = s.split_once("key=value")?.1;
+        None
+    }
+
+    // Don't lint, slices don't have `split_once`
+    let _ = [0, 1, 2].splitn(2, |&x| x == 1).nth(1).unwrap();
+}
+
+fn _msrv_1_51() {
+    #![clippy::msrv = "1.51"]
+    // `str::split_once` was stabilized in 1.16. Do not lint this
+    let _ = "key=value".splitn(2, '=').nth(1).unwrap();
+}
+
+fn _msrv_1_52() {
+    #![clippy::msrv = "1.52"]
+    let _ = "key=value".split_once('=').unwrap().1;
+}
diff --git a/tests/ui/manual_split_once.rs b/tests/ui/manual_split_once.rs
new file mode 100644 (file)
index 0000000..e6093b6
--- /dev/null
@@ -0,0 +1,50 @@
+// run-rustfix
+
+#![feature(custom_inner_attributes)]
+#![warn(clippy::manual_split_once)]
+#![allow(clippy::iter_skip_next, clippy::iter_nth_zero)]
+
+extern crate itertools;
+
+#[allow(unused_imports)]
+use itertools::Itertools;
+
+fn main() {
+    let _ = "key=value".splitn(2, '=').next();
+    let _ = "key=value".splitn(2, '=').nth(2);
+    let _ = "key=value".splitn(2, '=').next().unwrap();
+    let _ = "key=value".splitn(2, '=').nth(0).unwrap();
+    let _ = "key=value".splitn(2, '=').nth(1).unwrap();
+    let _ = "key=value".splitn(2, '=').skip(1).next().unwrap();
+    let (_, _) = "key=value".splitn(2, '=').next_tuple().unwrap();
+
+    let s = String::from("key=value");
+    let _ = s.splitn(2, '=').next().unwrap();
+
+    let s = Box::<str>::from("key=value");
+    let _ = s.splitn(2, '=').nth(0).unwrap();
+
+    let s = &"key=value";
+    let _ = s.splitn(2, '=').skip(0).next().unwrap();
+
+    fn _f(s: &str) -> Option<&str> {
+        let _ = s.splitn(2, "key=value").next()?;
+        let _ = s.splitn(2, "key=value").nth(1)?;
+        let _ = s.splitn(2, "key=value").skip(1).next()?;
+        None
+    }
+
+    // Don't lint, slices don't have `split_once`
+    let _ = [0, 1, 2].splitn(2, |&x| x == 1).nth(1).unwrap();
+}
+
+fn _msrv_1_51() {
+    #![clippy::msrv = "1.51"]
+    // `str::split_once` was stabilized in 1.16. Do not lint this
+    let _ = "key=value".splitn(2, '=').nth(1).unwrap();
+}
+
+fn _msrv_1_52() {
+    #![clippy::msrv = "1.52"]
+    let _ = "key=value".splitn(2, '=').nth(1).unwrap();
+}
diff --git a/tests/ui/manual_split_once.stderr b/tests/ui/manual_split_once.stderr
new file mode 100644 (file)
index 0000000..4f15196
--- /dev/null
@@ -0,0 +1,82 @@
+error: manual implementation of `split_once`
+  --> $DIR/manual_split_once.rs:13:13
+   |
+LL |     let _ = "key=value".splitn(2, '=').next();
+   |             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ help: try this: `Some("key=value".split_once('=').map_or("key=value", |x| x.0))`
+   |
+   = note: `-D clippy::manual-split-once` implied by `-D warnings`
+
+error: manual implementation of `split_once`
+  --> $DIR/manual_split_once.rs:15:13
+   |
+LL |     let _ = "key=value".splitn(2, '=').next().unwrap();
+   |             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ help: try this: `"key=value".split_once('=').map_or("key=value", |x| x.0)`
+
+error: manual implementation of `split_once`
+  --> $DIR/manual_split_once.rs:16:13
+   |
+LL |     let _ = "key=value".splitn(2, '=').nth(0).unwrap();
+   |             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ help: try this: `"key=value".split_once('=').map_or("key=value", |x| x.0)`
+
+error: manual implementation of `split_once`
+  --> $DIR/manual_split_once.rs:17:13
+   |
+LL |     let _ = "key=value".splitn(2, '=').nth(1).unwrap();
+   |             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ help: try this: `"key=value".split_once('=').unwrap().1`
+
+error: manual implementation of `split_once`
+  --> $DIR/manual_split_once.rs:18:13
+   |
+LL |     let _ = "key=value".splitn(2, '=').skip(1).next().unwrap();
+   |             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ help: try this: `"key=value".split_once('=').unwrap().1`
+
+error: manual implementation of `split_once`
+  --> $DIR/manual_split_once.rs:19:18
+   |
+LL |     let (_, _) = "key=value".splitn(2, '=').next_tuple().unwrap();
+   |                  ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ help: try this: `"key=value".split_once('=')`
+
+error: manual implementation of `split_once`
+  --> $DIR/manual_split_once.rs:22:13
+   |
+LL |     let _ = s.splitn(2, '=').next().unwrap();
+   |             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ help: try this: `s.split_once('=').map_or(&*s, |x| x.0)`
+
+error: manual implementation of `split_once`
+  --> $DIR/manual_split_once.rs:25:13
+   |
+LL |     let _ = s.splitn(2, '=').nth(0).unwrap();
+   |             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ help: try this: `s.split_once('=').map_or(&*s, |x| x.0)`
+
+error: manual implementation of `split_once`
+  --> $DIR/manual_split_once.rs:28:13
+   |
+LL |     let _ = s.splitn(2, '=').skip(0).next().unwrap();
+   |             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ help: try this: `s.split_once('=').map_or(*s, |x| x.0)`
+
+error: manual implementation of `split_once`
+  --> $DIR/manual_split_once.rs:31:17
+   |
+LL |         let _ = s.splitn(2, "key=value").next()?;
+   |                 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ help: try this: `s.split_once("key=value").map_or(s, |x| x.0)`
+
+error: manual implementation of `split_once`
+  --> $DIR/manual_split_once.rs:32:17
+   |
+LL |         let _ = s.splitn(2, "key=value").nth(1)?;
+   |                 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ help: try this: `s.split_once("key=value")?.1`
+
+error: manual implementation of `split_once`
+  --> $DIR/manual_split_once.rs:33:17
+   |
+LL |         let _ = s.splitn(2, "key=value").skip(1).next()?;
+   |                 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ help: try this: `s.split_once("key=value")?.1`
+
+error: manual implementation of `split_once`
+  --> $DIR/manual_split_once.rs:49:13
+   |
+LL |     let _ = "key=value".splitn(2, '=').nth(1).unwrap();
+   |             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ help: try this: `"key=value".split_once('=').unwrap().1`
+
+error: aborting due to 13 previous errors
+