about summary refs log tree commit diff
diff options
context:
space:
mode:
-rw-r--r--clippy_lints/src/methods/collapsible_str_replace.rs154
-rw-r--r--clippy_lints/src/methods/mod.rs2
-rw-r--r--tests/ui/collapsible_str_replace.rs27
3 files changed, 167 insertions, 16 deletions
diff --git a/clippy_lints/src/methods/collapsible_str_replace.rs b/clippy_lints/src/methods/collapsible_str_replace.rs
index e5547b50471..e2477fb06bd 100644
--- a/clippy_lints/src/methods/collapsible_str_replace.rs
+++ b/clippy_lints/src/methods/collapsible_str_replace.rs
@@ -1,6 +1,154 @@
+use clippy_utils::diagnostics::span_lint_and_sugg;
+// use clippy_utils::source::snippet_with_context;
+use clippy_utils::visitors::for_each_expr;
+use core::ops::ControlFlow;
+use if_chain::if_chain;
+use rustc_ast::ast::LitKind;
+use rustc_errors::Applicability;
 use rustc_hir as hir;
 use rustc_hir::*;
-use rustc_lint::{LateContext, LateLintPass};
-use rustc_session::{declare_lint_pass, declare_tool_lint};
+use rustc_lint::LateContext;
+use rustc_middle::ty;
+use rustc_span::source_map::Spanned;
+use std::unreachable;
+// use rustc_span::Span;
 
-pub(super) fn check<'tcx>(cx: &LateContext<'tcx>, expr: &hir::Expr<'_>) {}
+use super::method_call;
+use super::COLLAPSIBLE_STR_REPLACE;
+
+pub(super) fn check<'tcx>(
+    cx: &LateContext<'tcx>,
+    expr: &'tcx hir::Expr<'tcx>,
+    name: &str,
+    recv: &'tcx hir::Expr<'tcx>,
+    args: &'tcx [hir::Expr<'tcx>],
+) {
+    match (name, args) {
+        ("replace", [from, to]) => {
+            // Check for `str::replace` calls with char slice for linting
+            let original_recv = find_original_recv(recv);
+            let original_recv_ty = cx.typeck_results().expr_ty(original_recv).peel_refs();
+            if_chain! {
+                // Check the receiver of the method call is `str` type
+                if matches!(original_recv_ty.kind(), ty::Str);
+                let from_ty = cx.typeck_results().expr_ty(from).peel_refs();
+                if let ty::Array(array_ty, _) = from_ty.kind();
+                if matches!(array_ty.kind(), ty::Char);
+                then {
+                    check_replace_call_with_char_slice(cx, from, to);
+                }
+            }
+
+            match method_call(recv) {
+                // Check if there's an earlier `str::replace` call
+                Some(("replace", [prev_recv, prev_from, prev_to], prev_span)) => {
+                    println!("Consecutive replace calls");
+                    // Check that the original receiver is of `ty::Str` type
+                    // Check that all the `from` args are char literals
+                    // Check that all the `to` args are the same variable or has the same &str value
+                    // If so, then lint
+                },
+                _ => {},
+            }
+        },
+        _ => {},
+    }
+}
+
+fn find_original_recv<'tcx>(recv: &'tcx hir::Expr<'tcx>) -> &'tcx hir::Expr<'tcx> {
+    let mut original_recv = recv;
+
+    let _: Option<()> = for_each_expr(recv, |e| {
+        if let Some((name, [prev_recv, args @ ..], _)) = method_call(e) {
+            match (name, args) {
+                ("replace", [_, _]) => {
+                    original_recv = prev_recv;
+                    ControlFlow::Continue(())
+                },
+                _ => ControlFlow::BREAK,
+            }
+        } else {
+            ControlFlow::Continue(())
+        }
+    });
+
+    original_recv
+}
+
+fn check_replace_call_with_char_slice<'tcx>(
+    cx: &LateContext<'tcx>,
+    from_arg: &'tcx hir::Expr<'tcx>,
+    to_arg: &'tcx hir::Expr<'tcx>,
+) {
+    let mut has_no_var = true;
+    let mut char_list: Vec<char> = Vec::new();
+    // Go through the `from_arg` to collect all char literals
+    let _: Option<()> = for_each_expr(from_arg, |e| {
+        if let ExprKind::Lit(Spanned {
+            node: LitKind::Char(val),
+            ..
+        }) = e.kind
+        {
+            char_list.push(val);
+            ControlFlow::Continue(())
+        } else if let ExprKind::Path(..) = e.kind {
+            // If a variable is found in the char slice, no lint for first version of this lint
+            has_no_var = false;
+            ControlFlow::BREAK
+        } else {
+            ControlFlow::Continue(())
+        }
+    });
+
+    if has_no_var {
+        let to_arg_repr = match to_arg.kind {
+            ExprKind::Lit(Spanned {
+                node: LitKind::Str(to_arg_val, _),
+                ..
+            }) => {
+                let repr = to_arg_val.as_str();
+                let double_quote = "\"";
+                double_quote.to_owned() + repr + double_quote
+            },
+            ExprKind::Path(QPath::Resolved(
+                _,
+                Path {
+                    segments: path_segments,
+                    ..
+                },
+            )) => {
+                // join the path_segments values by "::"
+                let path_segment_ident_names: Vec<&str> = path_segments
+                    .iter()
+                    .map(|path_seg| path_seg.ident.name.as_str())
+                    .collect();
+
+                path_segment_ident_names.join("::")
+            },
+            _ => unreachable!(),
+        };
+
+        let app = Applicability::MachineApplicable;
+        span_lint_and_sugg(
+            cx,
+            COLLAPSIBLE_STR_REPLACE,
+            from_arg.span,
+            "used slice of chars in `str::replace` call",
+            "replace with",
+            format!(
+                "replace(|c| matches!(c, {}), {})",
+                format_slice_of_chars_for_sugg(&char_list),
+                to_arg_repr,
+            ),
+            app,
+        );
+    }
+}
+
+fn format_slice_of_chars_for_sugg(chars: &Vec<char>) -> String {
+    let single_quoted_chars: Vec<String> = chars
+        .iter()
+        .map(|c| "'".to_owned() + &c.to_string() + &"'".to_owned())
+        .collect();
+    single_quoted_chars.join(" | ")
+}
diff --git a/clippy_lints/src/methods/mod.rs b/clippy_lints/src/methods/mod.rs
index 8f7cb105c22..ec5d5402b0e 100644
--- a/clippy_lints/src/methods/mod.rs
+++ b/clippy_lints/src/methods/mod.rs
@@ -3519,7 +3519,7 @@ impl Methods {
                 ("sort_unstable_by", [arg]) => {
                     unnecessary_sort_by::check(cx, expr, recv, arg, true);
                 },
-                ("replace", [_, _]) => collapsible_str_replace::check(cx, expr, recv, args),
+                ("replace", [_, _]) => collapsible_str_replace::check(cx, expr, name, recv, args),
                 ("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);
diff --git a/tests/ui/collapsible_str_replace.rs b/tests/ui/collapsible_str_replace.rs
index 0f4bdf09480..943fb4473bb 100644
--- a/tests/ui/collapsible_str_replace.rs
+++ b/tests/ui/collapsible_str_replace.rs
@@ -10,6 +10,7 @@ fn main() {
     let p = 'p';
     let s = 's';
     let u = 'u';
+    let l = "l";
 
     // LINT CASES
     // If the first argument to a single `str::replace` call is a slice and none of the chars
@@ -17,6 +18,20 @@ fn main() {
     let replacement = misspelled.replace(&['s', 'u', 'p'], "l");
     println!("{replacement}");
 
+    let replacement = misspelled.replace(&['s', 'u', 'p'], l);
+    println!("{replacement}");
+
+    // If multiple `str::replace` calls contain slices and none of the chars are variables,
+    // recommend `collapsible_str_replace`
+    let replacement = misspelled.replace(&['s', 'u'], "l").replace(&['u', 'p'], "l");
+    println!("{replacement}");
+
+    let replacement = misspelled.replace('s', "l").replace(&['u', 'p'], "l");
+    println!("{replacement}");
+
+    let replacement = misspelled.replace(&['s', 'u'], "l").replace('p', "l");
+    println!("replacement");
+
     // If there are consecutive calls to `str::replace` and none of the chars are variables,
     // recommend `collapsible_str_replace`
     let replacement = misspelled.replace('s', "l").replace('u', "l");
@@ -38,18 +53,6 @@ fn main() {
     let replacement = misspelled.replace(&get_filter(), "l");
 
     // NO LINT TIL IMPROVEMENT
-    // If multiple `str::replace` calls contain slices and none of the chars are variables,
-    // the first iteration does not recommend `collapsible_str_replace`
-    let replacement = misspelled.replace(&['s', 'u', 'p'], "l").replace(&['s', 'p'], "l");
-    println!("{replacement}");
-
-    // If a mixture of `str::replace` calls with slice and char arguments are used for `from` arg,
-    // the first iteration does not recommend `collapsible_str_replace`
-    let replacement = misspelled.replace(&['s', 'u'], "l").replace('p', "l");
-    println!("replacement");
-
-    let replacement = misspelled.replace('p', "l").replace(&['s', 'u'], "l");
-
     // The first iteration of `collapsible_str_replace` will not create lint if the first argument to
     // a single `str::replace` call is a slice and one or more of its chars are variables
     let replacement = misspelled.replace(&['s', u, 'p'], "l");