about summary refs log tree commit diff
diff options
context:
space:
mode:
-rw-r--r--CHANGELOG.md1
-rw-r--r--clippy_lints/src/declared_lints.rs1
-rw-r--r--clippy_lints/src/lib.rs2
-rw-r--r--clippy_lints/src/redundant_test_prefix.rs161
-rw-r--r--clippy_utils/src/lib.rs23
-rw-r--r--tests/ui/redundant_test_prefix.fixed158
-rw-r--r--tests/ui/redundant_test_prefix.rs158
-rw-r--r--tests/ui/redundant_test_prefix.stderr119
-rw-r--r--tests/ui/redundant_test_prefix_noautofix.rs288
-rw-r--r--tests/ui/redundant_test_prefix_noautofix.stderr241
10 files changed, 1151 insertions, 1 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md
index a8b703028f2..7977e6ab382 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -6116,6 +6116,7 @@ Released 2018-09-13
 [`redundant_pub_crate`]: https://rust-lang.github.io/rust-clippy/master/index.html#redundant_pub_crate
 [`redundant_slicing`]: https://rust-lang.github.io/rust-clippy/master/index.html#redundant_slicing
 [`redundant_static_lifetimes`]: https://rust-lang.github.io/rust-clippy/master/index.html#redundant_static_lifetimes
+[`redundant_test_prefix`]: https://rust-lang.github.io/rust-clippy/master/index.html#redundant_test_prefix
 [`redundant_type_annotations`]: https://rust-lang.github.io/rust-clippy/master/index.html#redundant_type_annotations
 [`ref_as_ptr`]: https://rust-lang.github.io/rust-clippy/master/index.html#ref_as_ptr
 [`ref_binding_to_reference`]: https://rust-lang.github.io/rust-clippy/master/index.html#ref_binding_to_reference
diff --git a/clippy_lints/src/declared_lints.rs b/clippy_lints/src/declared_lints.rs
index 3e9469256e5..cc8f7573957 100644
--- a/clippy_lints/src/declared_lints.rs
+++ b/clippy_lints/src/declared_lints.rs
@@ -667,6 +667,7 @@ pub static LINTS: &[&crate::LintInfo] = &[
     crate::redundant_slicing::DEREF_BY_SLICING_INFO,
     crate::redundant_slicing::REDUNDANT_SLICING_INFO,
     crate::redundant_static_lifetimes::REDUNDANT_STATIC_LIFETIMES_INFO,
+    crate::redundant_test_prefix::REDUNDANT_TEST_PREFIX_INFO,
     crate::redundant_type_annotations::REDUNDANT_TYPE_ANNOTATIONS_INFO,
     crate::ref_option_ref::REF_OPTION_REF_INFO,
     crate::ref_patterns::REF_PATTERNS_INFO,
diff --git a/clippy_lints/src/lib.rs b/clippy_lints/src/lib.rs
index dfcf548e8db..2be5e620f1d 100644
--- a/clippy_lints/src/lib.rs
+++ b/clippy_lints/src/lib.rs
@@ -320,6 +320,7 @@ mod redundant_locals;
 mod redundant_pub_crate;
 mod redundant_slicing;
 mod redundant_static_lifetimes;
+mod redundant_test_prefix;
 mod redundant_type_annotations;
 mod ref_option_ref;
 mod ref_patterns;
@@ -984,5 +985,6 @@ pub fn register_lints(store: &mut rustc_lint::LintStore, conf: &'static Conf) {
     store.register_late_pass(move |_| Box::new(non_std_lazy_statics::NonStdLazyStatic::new(conf)));
     store.register_late_pass(|_| Box::new(manual_option_as_slice::ManualOptionAsSlice::new(conf)));
     store.register_late_pass(|_| Box::new(single_option_map::SingleOptionMap));
+    store.register_late_pass(move |_| Box::new(redundant_test_prefix::RedundantTestPrefix));
     // add lints here, do not remove this comment, it's used in `new_lint`
 }
diff --git a/clippy_lints/src/redundant_test_prefix.rs b/clippy_lints/src/redundant_test_prefix.rs
new file mode 100644
index 00000000000..84276e32165
--- /dev/null
+++ b/clippy_lints/src/redundant_test_prefix.rs
@@ -0,0 +1,161 @@
+use clippy_utils::diagnostics::span_lint_and_then;
+use clippy_utils::is_test_function;
+use clippy_utils::visitors::for_each_expr;
+use rustc_errors::Applicability;
+use rustc_hir::intravisit::FnKind;
+use rustc_hir::{self as hir, Body, ExprKind, FnDecl};
+use rustc_lexer::is_ident;
+use rustc_lint::{LateContext, LateLintPass};
+use rustc_session::declare_lint_pass;
+use rustc_span::def_id::LocalDefId;
+use rustc_span::{Span, Symbol, edition};
+use std::borrow::Cow;
+use std::ops::ControlFlow;
+
+declare_clippy_lint! {
+    /// ### What it does
+    /// Checks for test functions (functions annotated with `#[test]`) that are prefixed
+    /// with `test_` which is redundant.
+    ///
+    /// ### Why is this bad?
+    /// This is redundant because test functions are already annotated with `#[test]`.
+    /// Moreover, it clutters the output of `cargo test` since test functions are expanded as
+    /// `module::tests::test_use_case` in the output. Without the redundant prefix, the output
+    /// becomes `module::tests::use_case`, which is more readable.
+    ///
+    /// ### Example
+    /// ```no_run
+    /// #[cfg(test)]
+    /// mod tests {
+    ///   use super::*;
+    ///
+    ///   #[test]
+    ///   fn test_use_case() {
+    ///       // test code
+    ///   }
+    /// }
+    /// ```
+    /// Use instead:
+    /// ```no_run
+    /// #[cfg(test)]
+    /// mod tests {
+    ///   use super::*;
+    ///
+    ///   #[test]
+    ///   fn use_case() {
+    ///       // test code
+    ///   }
+    /// }
+    /// ```
+    #[clippy::version = "1.88.0"]
+    pub REDUNDANT_TEST_PREFIX,
+    restriction,
+    "redundant `test_` prefix in test function name"
+}
+
+declare_lint_pass!(RedundantTestPrefix => [REDUNDANT_TEST_PREFIX]);
+
+impl<'tcx> LateLintPass<'tcx> for RedundantTestPrefix {
+    fn check_fn(
+        &mut self,
+        cx: &LateContext<'tcx>,
+        kind: FnKind<'_>,
+        _decl: &FnDecl<'_>,
+        body: &'tcx Body<'_>,
+        _span: Span,
+        fn_def_id: LocalDefId,
+    ) {
+        // Ignore methods and closures.
+        let FnKind::ItemFn(ref ident, ..) = kind else {
+            return;
+        };
+
+        // Skip the lint if the function is within a macro expansion.
+        if ident.span.from_expansion() {
+            return;
+        }
+
+        // Skip if the function name does not start with `test_`.
+        if !ident.as_str().starts_with("test_") {
+            return;
+        }
+
+        // If the function is not a test function, skip the lint.
+        if !is_test_function(cx.tcx, fn_def_id) {
+            return;
+        }
+
+        span_lint_and_then(
+            cx,
+            REDUNDANT_TEST_PREFIX,
+            ident.span,
+            "redundant `test_` prefix in test function name",
+            |diag| {
+                let non_prefixed = Symbol::intern(ident.as_str().trim_start_matches("test_"));
+                if is_invalid_ident(non_prefixed) {
+                    // If the prefix-trimmed name is not a valid function name, do not provide an
+                    // automatic fix, just suggest renaming the function.
+                    diag.help(
+                        "consider function renaming (just removing `test_` prefix will produce invalid function name)",
+                    );
+                } else {
+                    let (sugg, msg): (Cow<'_, str>, _) = if name_conflicts(cx, body, non_prefixed) {
+                        // If `non_prefixed` conflicts with another function in the same module/scope,
+                        // do not provide an automatic fix, but still emit a fix suggestion.
+                        (
+                            format!("{non_prefixed}_works").into(),
+                            "consider function renaming (just removing `test_` prefix will cause a name conflict)",
+                        )
+                    } else {
+                        // If `non_prefixed` is a valid identifier and does not conflict with another function,
+                        // so we can suggest an auto-fix.
+                        (non_prefixed.as_str().into(), "consider removing the `test_` prefix")
+                    };
+                    diag.span_suggestion(ident.span, msg, sugg, Applicability::MaybeIncorrect);
+                }
+            },
+        );
+    }
+}
+
+/// Checks whether removal of the `_test` prefix from the function name will cause a name conflict.
+///
+/// There should be no other function with the same name in the same module/scope. Also, there
+/// should not be any function call with the same name within the body of the function, to avoid
+/// recursion.
+fn name_conflicts<'tcx>(cx: &LateContext<'tcx>, body: &'tcx Body<'_>, fn_name: Symbol) -> bool {
+    let tcx = cx.tcx;
+    let id = body.id().hir_id;
+
+    // Iterate over items in the same module/scope
+    let (module, _module_span, _module_hir) = tcx.hir_get_module(tcx.parent_module(id));
+    if module
+        .item_ids
+        .iter()
+        .any(|item| matches!(tcx.hir_item(*item).kind, hir::ItemKind::Fn { ident, .. } if ident.name == fn_name))
+    {
+        // Name conflict found
+        return true;
+    }
+
+    // Also check that within the body of the function there is also no function call
+    // with the same name (since it will result in recursion)
+    for_each_expr(cx, body, |expr| {
+        if let ExprKind::Path(qpath) = &expr.kind
+            && let Some(def_id) = cx.qpath_res(qpath, expr.hir_id).opt_def_id()
+            && let Some(name) = tcx.opt_item_name(def_id)
+            && name == fn_name
+        {
+            // Function call with the same name found
+            ControlFlow::Break(())
+        } else {
+            ControlFlow::Continue(())
+        }
+    })
+    .is_some()
+}
+
+fn is_invalid_ident(ident: Symbol) -> bool {
+    // The identifier is either a reserved keyword, or starts with an invalid sequence.
+    ident.is_reserved(|| edition::LATEST_STABLE_EDITION) || !is_ident(ident.as_str())
+}
diff --git a/clippy_utils/src/lib.rs b/clippy_utils/src/lib.rs
index bba4c3179e4..551266da029 100644
--- a/clippy_utils/src/lib.rs
+++ b/clippy_utils/src/lib.rs
@@ -2641,7 +2641,9 @@ pub fn is_hir_ty_cfg_dependant(cx: &LateContext<'_>, ty: &hir::Ty<'_>) -> bool {
 
 static TEST_ITEM_NAMES_CACHE: OnceLock<Mutex<FxHashMap<LocalModDefId, Vec<Symbol>>>> = OnceLock::new();
 
-fn with_test_item_names(tcx: TyCtxt<'_>, module: LocalModDefId, f: impl Fn(&[Symbol]) -> bool) -> bool {
+/// Apply `f()` to the set of test item names.
+/// The names are sorted using the default `Symbol` ordering.
+fn with_test_item_names(tcx: TyCtxt<'_>, module: LocalModDefId, f: impl FnOnce(&[Symbol]) -> bool) -> bool {
     let cache = TEST_ITEM_NAMES_CACHE.get_or_init(|| Mutex::new(FxHashMap::default()));
     let mut map: MutexGuard<'_, FxHashMap<LocalModDefId, Vec<Symbol>>> = cache.lock().unwrap();
     let value = map.entry(module);
@@ -2695,6 +2697,25 @@ pub fn is_in_test_function(tcx: TyCtxt<'_>, id: HirId) -> bool {
     })
 }
 
+/// Checks if `fn_def_id` has a `#[test]` attribute applied
+///
+/// This only checks directly applied attributes. To see if a node has a parent function marked with
+/// `#[test]` use [`is_in_test_function`].
+///
+/// Note: Add `//@compile-flags: --test` to UI tests with a `#[test]` function
+pub fn is_test_function(tcx: TyCtxt<'_>, fn_def_id: LocalDefId) -> bool {
+    let id = tcx.local_def_id_to_hir_id(fn_def_id);
+    if let Node::Item(item) = tcx.hir_node(id)
+        && let ItemKind::Fn { ident, .. } = item.kind
+    {
+        with_test_item_names(tcx, tcx.parent_module(id), |names| {
+            names.binary_search(&ident.name).is_ok()
+        })
+    } else {
+        false
+    }
+}
+
 /// Checks if `id` has a `#[cfg(test)]` attribute applied
 ///
 /// This only checks directly applied attributes, to see if a node is inside a `#[cfg(test)]` parent
diff --git a/tests/ui/redundant_test_prefix.fixed b/tests/ui/redundant_test_prefix.fixed
new file mode 100644
index 00000000000..b99771f0640
--- /dev/null
+++ b/tests/ui/redundant_test_prefix.fixed
@@ -0,0 +1,158 @@
+#![allow(dead_code)]
+#![warn(clippy::redundant_test_prefix)]
+
+fn main() {
+    // Normal function, no redundant prefix.
+}
+
+fn f1() {
+    // Normal function, no redundant prefix.
+}
+
+fn test_f2() {
+    // Has prefix, but no `#[test]` attribute, ignore.
+}
+
+#[test]
+fn f3() {
+    //~^ redundant_test_prefix
+
+    // Has prefix, has `#[test]` attribute. Not within a `#[cfg(test)]`.
+    // No collision with other functions, should emit warning.
+}
+
+#[cfg(test)]
+#[test]
+fn f4() {
+    //~^ redundant_test_prefix
+
+    // Has prefix, has `#[test]` attribute, within a `#[cfg(test)]`.
+    // No collision with other functions, should emit warning.
+}
+
+mod m1 {
+    pub fn f5() {}
+}
+
+#[cfg(test)]
+#[test]
+fn f6() {
+    //~^ redundant_test_prefix
+
+    use m1::f5;
+
+    f5();
+    // Has prefix, has `#[test]` attribute, within a `#[cfg(test)]`.
+    // No collision, has function call, but it will not result in recursion.
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+
+    #[test]
+    fn foo() {
+        //~^ redundant_test_prefix
+    }
+
+    #[test]
+    fn foo_with_call() {
+        //~^ redundant_test_prefix
+
+        main();
+    }
+
+    #[test]
+    fn f1() {
+        //~^ redundant_test_prefix
+    }
+
+    #[test]
+    fn f2() {
+        //~^ redundant_test_prefix
+    }
+
+    #[test]
+    fn f3() {
+        //~^ redundant_test_prefix
+    }
+
+    #[test]
+    fn f4() {
+        //~^ redundant_test_prefix
+    }
+
+    #[test]
+    fn f5() {
+        //~^ redundant_test_prefix
+    }
+
+    #[test]
+    fn f6() {
+        //~^ redundant_test_prefix
+    }
+}
+
+mod tests_no_annotations {
+    use super::*;
+
+    #[test]
+    fn foo() {
+        //~^ redundant_test_prefix
+    }
+
+    #[test]
+    fn foo_with_call() {
+        //~^ redundant_test_prefix
+
+        main();
+    }
+
+    #[test]
+    fn f1() {
+        //~^ redundant_test_prefix
+    }
+
+    #[test]
+    fn f2() {
+        //~^ redundant_test_prefix
+    }
+
+    #[test]
+    fn f3() {
+        //~^ redundant_test_prefix
+    }
+
+    #[test]
+    fn f4() {
+        //~^ redundant_test_prefix
+    }
+
+    #[test]
+    fn f5() {
+        //~^ redundant_test_prefix
+    }
+
+    #[test]
+    fn f6() {
+        //~^ redundant_test_prefix
+    }
+}
+
+// This test is inspired by real test in `clippy_utils/src/sugg.rs`.
+// The `is_in_test_function()` checks whether any identifier within a given node's parents is
+// marked with `#[test]` attribute. Thus flagging false positives when nested functions are
+// prefixed with `test_`. Therefore `is_test_function()` has been defined in `clippy_utils`,
+// allowing to select only functions that are immediately marked with `#[test]` annotation.
+//
+// This test case ensures that for such nested functions no error is emitted.
+#[test]
+fn not_op() {
+    fn test_not(foo: bool) {
+        assert!(foo);
+    }
+
+    // Use helper function
+    test_not(true);
+    test_not(false);
+}
diff --git a/tests/ui/redundant_test_prefix.rs b/tests/ui/redundant_test_prefix.rs
new file mode 100644
index 00000000000..3aec577cffa
--- /dev/null
+++ b/tests/ui/redundant_test_prefix.rs
@@ -0,0 +1,158 @@
+#![allow(dead_code)]
+#![warn(clippy::redundant_test_prefix)]
+
+fn main() {
+    // Normal function, no redundant prefix.
+}
+
+fn f1() {
+    // Normal function, no redundant prefix.
+}
+
+fn test_f2() {
+    // Has prefix, but no `#[test]` attribute, ignore.
+}
+
+#[test]
+fn test_f3() {
+    //~^ redundant_test_prefix
+
+    // Has prefix, has `#[test]` attribute. Not within a `#[cfg(test)]`.
+    // No collision with other functions, should emit warning.
+}
+
+#[cfg(test)]
+#[test]
+fn test_f4() {
+    //~^ redundant_test_prefix
+
+    // Has prefix, has `#[test]` attribute, within a `#[cfg(test)]`.
+    // No collision with other functions, should emit warning.
+}
+
+mod m1 {
+    pub fn f5() {}
+}
+
+#[cfg(test)]
+#[test]
+fn test_f6() {
+    //~^ redundant_test_prefix
+
+    use m1::f5;
+
+    f5();
+    // Has prefix, has `#[test]` attribute, within a `#[cfg(test)]`.
+    // No collision, has function call, but it will not result in recursion.
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+
+    #[test]
+    fn test_foo() {
+        //~^ redundant_test_prefix
+    }
+
+    #[test]
+    fn test_foo_with_call() {
+        //~^ redundant_test_prefix
+
+        main();
+    }
+
+    #[test]
+    fn test_f1() {
+        //~^ redundant_test_prefix
+    }
+
+    #[test]
+    fn test_f2() {
+        //~^ redundant_test_prefix
+    }
+
+    #[test]
+    fn test_f3() {
+        //~^ redundant_test_prefix
+    }
+
+    #[test]
+    fn test_f4() {
+        //~^ redundant_test_prefix
+    }
+
+    #[test]
+    fn test_f5() {
+        //~^ redundant_test_prefix
+    }
+
+    #[test]
+    fn test_f6() {
+        //~^ redundant_test_prefix
+    }
+}
+
+mod tests_no_annotations {
+    use super::*;
+
+    #[test]
+    fn test_foo() {
+        //~^ redundant_test_prefix
+    }
+
+    #[test]
+    fn test_foo_with_call() {
+        //~^ redundant_test_prefix
+
+        main();
+    }
+
+    #[test]
+    fn test_f1() {
+        //~^ redundant_test_prefix
+    }
+
+    #[test]
+    fn test_f2() {
+        //~^ redundant_test_prefix
+    }
+
+    #[test]
+    fn test_f3() {
+        //~^ redundant_test_prefix
+    }
+
+    #[test]
+    fn test_f4() {
+        //~^ redundant_test_prefix
+    }
+
+    #[test]
+    fn test_f5() {
+        //~^ redundant_test_prefix
+    }
+
+    #[test]
+    fn test_f6() {
+        //~^ redundant_test_prefix
+    }
+}
+
+// This test is inspired by real test in `clippy_utils/src/sugg.rs`.
+// The `is_in_test_function()` checks whether any identifier within a given node's parents is
+// marked with `#[test]` attribute. Thus flagging false positives when nested functions are
+// prefixed with `test_`. Therefore `is_test_function()` has been defined in `clippy_utils`,
+// allowing to select only functions that are immediately marked with `#[test]` annotation.
+//
+// This test case ensures that for such nested functions no error is emitted.
+#[test]
+fn not_op() {
+    fn test_not(foo: bool) {
+        assert!(foo);
+    }
+
+    // Use helper function
+    test_not(true);
+    test_not(false);
+}
diff --git a/tests/ui/redundant_test_prefix.stderr b/tests/ui/redundant_test_prefix.stderr
new file mode 100644
index 00000000000..d156af586df
--- /dev/null
+++ b/tests/ui/redundant_test_prefix.stderr
@@ -0,0 +1,119 @@
+error: redundant `test_` prefix in test function name
+  --> tests/ui/redundant_test_prefix.rs:17:4
+   |
+LL | fn test_f3() {
+   |    ^^^^^^^ help: consider removing the `test_` prefix: `f3`
+   |
+   = note: `-D clippy::redundant-test-prefix` implied by `-D warnings`
+   = help: to override `-D warnings` add `#[allow(clippy::redundant_test_prefix)]`
+
+error: redundant `test_` prefix in test function name
+  --> tests/ui/redundant_test_prefix.rs:26:4
+   |
+LL | fn test_f4() {
+   |    ^^^^^^^ help: consider removing the `test_` prefix: `f4`
+
+error: redundant `test_` prefix in test function name
+  --> tests/ui/redundant_test_prefix.rs:39:4
+   |
+LL | fn test_f6() {
+   |    ^^^^^^^ help: consider removing the `test_` prefix: `f6`
+
+error: redundant `test_` prefix in test function name
+  --> tests/ui/redundant_test_prefix.rs:54:8
+   |
+LL |     fn test_foo() {
+   |        ^^^^^^^^ help: consider removing the `test_` prefix: `foo`
+
+error: redundant `test_` prefix in test function name
+  --> tests/ui/redundant_test_prefix.rs:59:8
+   |
+LL |     fn test_foo_with_call() {
+   |        ^^^^^^^^^^^^^^^^^^ help: consider removing the `test_` prefix: `foo_with_call`
+
+error: redundant `test_` prefix in test function name
+  --> tests/ui/redundant_test_prefix.rs:66:8
+   |
+LL |     fn test_f1() {
+   |        ^^^^^^^ help: consider removing the `test_` prefix: `f1`
+
+error: redundant `test_` prefix in test function name
+  --> tests/ui/redundant_test_prefix.rs:71:8
+   |
+LL |     fn test_f2() {
+   |        ^^^^^^^ help: consider removing the `test_` prefix: `f2`
+
+error: redundant `test_` prefix in test function name
+  --> tests/ui/redundant_test_prefix.rs:76:8
+   |
+LL |     fn test_f3() {
+   |        ^^^^^^^ help: consider removing the `test_` prefix: `f3`
+
+error: redundant `test_` prefix in test function name
+  --> tests/ui/redundant_test_prefix.rs:81:8
+   |
+LL |     fn test_f4() {
+   |        ^^^^^^^ help: consider removing the `test_` prefix: `f4`
+
+error: redundant `test_` prefix in test function name
+  --> tests/ui/redundant_test_prefix.rs:86:8
+   |
+LL |     fn test_f5() {
+   |        ^^^^^^^ help: consider removing the `test_` prefix: `f5`
+
+error: redundant `test_` prefix in test function name
+  --> tests/ui/redundant_test_prefix.rs:91:8
+   |
+LL |     fn test_f6() {
+   |        ^^^^^^^ help: consider removing the `test_` prefix: `f6`
+
+error: redundant `test_` prefix in test function name
+  --> tests/ui/redundant_test_prefix.rs:100:8
+   |
+LL |     fn test_foo() {
+   |        ^^^^^^^^ help: consider removing the `test_` prefix: `foo`
+
+error: redundant `test_` prefix in test function name
+  --> tests/ui/redundant_test_prefix.rs:105:8
+   |
+LL |     fn test_foo_with_call() {
+   |        ^^^^^^^^^^^^^^^^^^ help: consider removing the `test_` prefix: `foo_with_call`
+
+error: redundant `test_` prefix in test function name
+  --> tests/ui/redundant_test_prefix.rs:112:8
+   |
+LL |     fn test_f1() {
+   |        ^^^^^^^ help: consider removing the `test_` prefix: `f1`
+
+error: redundant `test_` prefix in test function name
+  --> tests/ui/redundant_test_prefix.rs:117:8
+   |
+LL |     fn test_f2() {
+   |        ^^^^^^^ help: consider removing the `test_` prefix: `f2`
+
+error: redundant `test_` prefix in test function name
+  --> tests/ui/redundant_test_prefix.rs:122:8
+   |
+LL |     fn test_f3() {
+   |        ^^^^^^^ help: consider removing the `test_` prefix: `f3`
+
+error: redundant `test_` prefix in test function name
+  --> tests/ui/redundant_test_prefix.rs:127:8
+   |
+LL |     fn test_f4() {
+   |        ^^^^^^^ help: consider removing the `test_` prefix: `f4`
+
+error: redundant `test_` prefix in test function name
+  --> tests/ui/redundant_test_prefix.rs:132:8
+   |
+LL |     fn test_f5() {
+   |        ^^^^^^^ help: consider removing the `test_` prefix: `f5`
+
+error: redundant `test_` prefix in test function name
+  --> tests/ui/redundant_test_prefix.rs:137:8
+   |
+LL |     fn test_f6() {
+   |        ^^^^^^^ help: consider removing the `test_` prefix: `f6`
+
+error: aborting due to 19 previous errors
+
diff --git a/tests/ui/redundant_test_prefix_noautofix.rs b/tests/ui/redundant_test_prefix_noautofix.rs
new file mode 100644
index 00000000000..6ad5d011d8b
--- /dev/null
+++ b/tests/ui/redundant_test_prefix_noautofix.rs
@@ -0,0 +1,288 @@
+//@no-rustfix: name conflicts
+
+#![allow(dead_code)]
+#![warn(clippy::redundant_test_prefix)]
+
+fn main() {
+    // Normal function, no redundant prefix.
+}
+
+fn f1() {
+    // Normal function, no redundant prefix.
+}
+
+fn test_f2() {
+    // Has prefix, but no `#[test]` attribute, ignore.
+}
+
+#[test]
+fn test_f3() {
+    //~^ redundant_test_prefix
+
+    // Has prefix, has `#[test]` attribute. Not within a `#[cfg(test)]`.
+    // No collision with other functions, should emit warning.
+}
+
+#[cfg(test)]
+#[test]
+fn test_f4() {
+    //~^ redundant_test_prefix
+
+    // Has prefix, has `#[test]` attribute, within a `#[cfg(test)]`.
+    // No collision with other functions, should emit warning.
+}
+
+fn f5() {}
+
+#[cfg(test)]
+#[test]
+fn test_f5() {
+    //~^ redundant_test_prefix
+
+    // Has prefix, has `#[test]` attribute, within a `#[cfg(test)]`.
+    // Collision with existing function.
+}
+
+mod m1 {
+    pub fn f6() {}
+    pub fn f7() {}
+}
+
+#[cfg(test)]
+#[test]
+fn test_f6() {
+    //~^ redundant_test_prefix
+
+    use m1::f6;
+
+    f6();
+    // Has prefix, has `#[test]` attribute, within a `#[cfg(test)]`.
+    // No collision, but has a function call that will result in recursion.
+}
+
+#[cfg(test)]
+#[test]
+fn test_f8() {
+    //~^ redundant_test_prefix
+
+    use m1::f7;
+
+    f7();
+    // Has prefix, has `#[test]` attribute, within a `#[cfg(test)]`.
+    // No collision, has function call, but it will not result in recursion.
+}
+
+// Although there's no direct call of `f` in the test, name collision still exists,
+// since all `m3` functions are imported and then `map` is used to call `f`.
+mod m2 {
+    mod m3 {
+        pub fn f(_: i32) -> i32 {
+            0
+        }
+    }
+
+    use m3::*;
+
+    #[cfg(test)]
+    #[test]
+    fn test_f() {
+        //~^ redundant_test_prefix
+        let a = Some(3);
+        let _ = a.map(f);
+    }
+}
+
+mod m3 {
+    fn test_m3_1() {
+        // Has prefix, but no `#[test]` attribute, ignore.
+    }
+
+    #[test]
+    fn test_m3_2() {
+        //~^ redundant_test_prefix
+
+        // Has prefix, has `#[test]` attribute. Not within a `#[cfg(test)]`.
+        // No collision with other functions, should emit warning.
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+
+    #[test]
+    fn test_foo() {
+        //~^ redundant_test_prefix
+    }
+
+    #[test]
+    fn test_foo_with_call() {
+        //~^ redundant_test_prefix
+
+        main();
+    }
+
+    #[test]
+    fn test_f1() {
+        //~^ redundant_test_prefix
+    }
+
+    #[test]
+    fn test_f2() {
+        //~^ redundant_test_prefix
+    }
+
+    #[test]
+    fn test_f3() {
+        //~^ redundant_test_prefix
+    }
+
+    #[test]
+    fn test_f4() {
+        //~^ redundant_test_prefix
+    }
+
+    #[test]
+    fn test_f5() {
+        //~^ redundant_test_prefix
+    }
+
+    #[test]
+    fn test_f6() {
+        //~^ redundant_test_prefix
+    }
+
+    #[test]
+    fn test_1() {
+        //~^ redundant_test_prefix
+
+        // `1` is invalid function name, so suggestion to rename is emitted
+    }
+
+    #[test]
+    fn test_const() {
+        //~^ redundant_test_prefix
+
+        // `const` is reserved keyword, so suggestion to rename is emitted
+    }
+
+    #[test]
+    fn test_async() {
+        //~^ redundant_test_prefix
+
+        // `async` is reserved keyword, so suggestion to rename is emitted
+    }
+
+    #[test]
+    fn test_yield() {
+        //~^ redundant_test_prefix
+
+        // `yield` is reserved keyword for future use, so suggestion to rename is emitted
+    }
+
+    #[test]
+    fn test_() {
+        //~^ redundant_test_prefix
+
+        // `` is invalid function name, so suggestion to rename is emitted
+    }
+}
+
+mod tests_no_annotations {
+    use super::*;
+
+    #[test]
+    fn test_foo() {
+        //~^ redundant_test_prefix
+    }
+
+    #[test]
+    fn test_foo_with_call() {
+        //~^ redundant_test_prefix
+
+        main();
+    }
+
+    #[test]
+    fn test_f1() {
+        //~^ redundant_test_prefix
+    }
+
+    #[test]
+    fn test_f2() {
+        //~^ redundant_test_prefix
+    }
+
+    #[test]
+    fn test_f3() {
+        //~^ redundant_test_prefix
+    }
+
+    #[test]
+    fn test_f4() {
+        //~^ redundant_test_prefix
+    }
+
+    #[test]
+    fn test_f5() {
+        //~^ redundant_test_prefix
+    }
+
+    #[test]
+    fn test_f6() {
+        //~^ redundant_test_prefix
+    }
+
+    #[test]
+    fn test_1() {
+        //~^ redundant_test_prefix
+
+        // `1` is invalid function name, so suggestion to rename is emitted
+    }
+
+    #[test]
+    fn test_const() {
+        //~^ redundant_test_prefix
+
+        // `const` is reserved keyword, so suggestion to rename is emitted
+    }
+
+    #[test]
+    fn test_async() {
+        //~^ redundant_test_prefix
+
+        // `async` is reserved keyword, so suggestion to rename is emitted
+    }
+
+    #[test]
+    fn test_yield() {
+        //~^ redundant_test_prefix
+
+        // `yield` is reserved keyword for future use, so suggestion to rename is emitted
+    }
+
+    #[test]
+    fn test_() {
+        //~^ redundant_test_prefix
+
+        // `` is invalid function name, so suggestion to rename is emitted
+    }
+}
+
+// This test is inspired by real test in `clippy_utils/src/sugg.rs`.
+// The `is_in_test_function()` checks whether any identifier within a given node's parents is
+// marked with `#[test]` attribute. Thus flagging false positives when nested functions are
+// prefixed with `test_`. Therefore `is_test_function()` has been defined in `clippy_utils`,
+// allowing to select only functions that are immediately marked with `#[test]` annotation.
+//
+// This test case ensures that for such nested functions no error is emitted.
+#[test]
+fn not_op() {
+    fn test_not(foo: bool) {
+        assert!(foo);
+    }
+
+    // Use helper function
+    test_not(true);
+    test_not(false);
+}
diff --git a/tests/ui/redundant_test_prefix_noautofix.stderr b/tests/ui/redundant_test_prefix_noautofix.stderr
new file mode 100644
index 00000000000..6440faf1b3c
--- /dev/null
+++ b/tests/ui/redundant_test_prefix_noautofix.stderr
@@ -0,0 +1,241 @@
+error: redundant `test_` prefix in test function name
+  --> tests/ui/redundant_test_prefix_noautofix.rs:19:4
+   |
+LL | fn test_f3() {
+   |    ^^^^^^^ help: consider removing the `test_` prefix: `f3`
+   |
+   = note: `-D clippy::redundant-test-prefix` implied by `-D warnings`
+   = help: to override `-D warnings` add `#[allow(clippy::redundant_test_prefix)]`
+
+error: redundant `test_` prefix in test function name
+  --> tests/ui/redundant_test_prefix_noautofix.rs:28:4
+   |
+LL | fn test_f4() {
+   |    ^^^^^^^ help: consider removing the `test_` prefix: `f4`
+
+error: redundant `test_` prefix in test function name
+  --> tests/ui/redundant_test_prefix_noautofix.rs:39:4
+   |
+LL | fn test_f5() {
+   |    ^^^^^^^
+   |
+help: consider function renaming (just removing `test_` prefix will cause a name conflict)
+   |
+LL - fn test_f5() {
+LL + fn f5_works() {
+   |
+
+error: redundant `test_` prefix in test function name
+  --> tests/ui/redundant_test_prefix_noautofix.rs:53:4
+   |
+LL | fn test_f6() {
+   |    ^^^^^^^
+   |
+help: consider function renaming (just removing `test_` prefix will cause a name conflict)
+   |
+LL - fn test_f6() {
+LL + fn f6_works() {
+   |
+
+error: redundant `test_` prefix in test function name
+  --> tests/ui/redundant_test_prefix_noautofix.rs:65:4
+   |
+LL | fn test_f8() {
+   |    ^^^^^^^ help: consider removing the `test_` prefix: `f8`
+
+error: redundant `test_` prefix in test function name
+  --> tests/ui/redundant_test_prefix_noautofix.rs:88:8
+   |
+LL |     fn test_f() {
+   |        ^^^^^^
+   |
+help: consider function renaming (just removing `test_` prefix will cause a name conflict)
+   |
+LL -     fn test_f() {
+LL +     fn f_works() {
+   |
+
+error: redundant `test_` prefix in test function name
+  --> tests/ui/redundant_test_prefix_noautofix.rs:101:8
+   |
+LL |     fn test_m3_2() {
+   |        ^^^^^^^^^ help: consider removing the `test_` prefix: `m3_2`
+
+error: redundant `test_` prefix in test function name
+  --> tests/ui/redundant_test_prefix_noautofix.rs:114:8
+   |
+LL |     fn test_foo() {
+   |        ^^^^^^^^ help: consider removing the `test_` prefix: `foo`
+
+error: redundant `test_` prefix in test function name
+  --> tests/ui/redundant_test_prefix_noautofix.rs:119:8
+   |
+LL |     fn test_foo_with_call() {
+   |        ^^^^^^^^^^^^^^^^^^ help: consider removing the `test_` prefix: `foo_with_call`
+
+error: redundant `test_` prefix in test function name
+  --> tests/ui/redundant_test_prefix_noautofix.rs:126:8
+   |
+LL |     fn test_f1() {
+   |        ^^^^^^^ help: consider removing the `test_` prefix: `f1`
+
+error: redundant `test_` prefix in test function name
+  --> tests/ui/redundant_test_prefix_noautofix.rs:131:8
+   |
+LL |     fn test_f2() {
+   |        ^^^^^^^ help: consider removing the `test_` prefix: `f2`
+
+error: redundant `test_` prefix in test function name
+  --> tests/ui/redundant_test_prefix_noautofix.rs:136:8
+   |
+LL |     fn test_f3() {
+   |        ^^^^^^^ help: consider removing the `test_` prefix: `f3`
+
+error: redundant `test_` prefix in test function name
+  --> tests/ui/redundant_test_prefix_noautofix.rs:141:8
+   |
+LL |     fn test_f4() {
+   |        ^^^^^^^ help: consider removing the `test_` prefix: `f4`
+
+error: redundant `test_` prefix in test function name
+  --> tests/ui/redundant_test_prefix_noautofix.rs:146:8
+   |
+LL |     fn test_f5() {
+   |        ^^^^^^^ help: consider removing the `test_` prefix: `f5`
+
+error: redundant `test_` prefix in test function name
+  --> tests/ui/redundant_test_prefix_noautofix.rs:151:8
+   |
+LL |     fn test_f6() {
+   |        ^^^^^^^ help: consider removing the `test_` prefix: `f6`
+
+error: redundant `test_` prefix in test function name
+  --> tests/ui/redundant_test_prefix_noautofix.rs:156:8
+   |
+LL |     fn test_1() {
+   |        ^^^^^^
+   |
+   = help: consider function renaming (just removing `test_` prefix will produce invalid function name)
+
+error: redundant `test_` prefix in test function name
+  --> tests/ui/redundant_test_prefix_noautofix.rs:163:8
+   |
+LL |     fn test_const() {
+   |        ^^^^^^^^^^
+   |
+   = help: consider function renaming (just removing `test_` prefix will produce invalid function name)
+
+error: redundant `test_` prefix in test function name
+  --> tests/ui/redundant_test_prefix_noautofix.rs:170:8
+   |
+LL |     fn test_async() {
+   |        ^^^^^^^^^^
+   |
+   = help: consider function renaming (just removing `test_` prefix will produce invalid function name)
+
+error: redundant `test_` prefix in test function name
+  --> tests/ui/redundant_test_prefix_noautofix.rs:177:8
+   |
+LL |     fn test_yield() {
+   |        ^^^^^^^^^^
+   |
+   = help: consider function renaming (just removing `test_` prefix will produce invalid function name)
+
+error: redundant `test_` prefix in test function name
+  --> tests/ui/redundant_test_prefix_noautofix.rs:184:8
+   |
+LL |     fn test_() {
+   |        ^^^^^
+   |
+   = help: consider function renaming (just removing `test_` prefix will produce invalid function name)
+
+error: redundant `test_` prefix in test function name
+  --> tests/ui/redundant_test_prefix_noautofix.rs:195:8
+   |
+LL |     fn test_foo() {
+   |        ^^^^^^^^ help: consider removing the `test_` prefix: `foo`
+
+error: redundant `test_` prefix in test function name
+  --> tests/ui/redundant_test_prefix_noautofix.rs:200:8
+   |
+LL |     fn test_foo_with_call() {
+   |        ^^^^^^^^^^^^^^^^^^ help: consider removing the `test_` prefix: `foo_with_call`
+
+error: redundant `test_` prefix in test function name
+  --> tests/ui/redundant_test_prefix_noautofix.rs:207:8
+   |
+LL |     fn test_f1() {
+   |        ^^^^^^^ help: consider removing the `test_` prefix: `f1`
+
+error: redundant `test_` prefix in test function name
+  --> tests/ui/redundant_test_prefix_noautofix.rs:212:8
+   |
+LL |     fn test_f2() {
+   |        ^^^^^^^ help: consider removing the `test_` prefix: `f2`
+
+error: redundant `test_` prefix in test function name
+  --> tests/ui/redundant_test_prefix_noautofix.rs:217:8
+   |
+LL |     fn test_f3() {
+   |        ^^^^^^^ help: consider removing the `test_` prefix: `f3`
+
+error: redundant `test_` prefix in test function name
+  --> tests/ui/redundant_test_prefix_noautofix.rs:222:8
+   |
+LL |     fn test_f4() {
+   |        ^^^^^^^ help: consider removing the `test_` prefix: `f4`
+
+error: redundant `test_` prefix in test function name
+  --> tests/ui/redundant_test_prefix_noautofix.rs:227:8
+   |
+LL |     fn test_f5() {
+   |        ^^^^^^^ help: consider removing the `test_` prefix: `f5`
+
+error: redundant `test_` prefix in test function name
+  --> tests/ui/redundant_test_prefix_noautofix.rs:232:8
+   |
+LL |     fn test_f6() {
+   |        ^^^^^^^ help: consider removing the `test_` prefix: `f6`
+
+error: redundant `test_` prefix in test function name
+  --> tests/ui/redundant_test_prefix_noautofix.rs:237:8
+   |
+LL |     fn test_1() {
+   |        ^^^^^^
+   |
+   = help: consider function renaming (just removing `test_` prefix will produce invalid function name)
+
+error: redundant `test_` prefix in test function name
+  --> tests/ui/redundant_test_prefix_noautofix.rs:244:8
+   |
+LL |     fn test_const() {
+   |        ^^^^^^^^^^
+   |
+   = help: consider function renaming (just removing `test_` prefix will produce invalid function name)
+
+error: redundant `test_` prefix in test function name
+  --> tests/ui/redundant_test_prefix_noautofix.rs:251:8
+   |
+LL |     fn test_async() {
+   |        ^^^^^^^^^^
+   |
+   = help: consider function renaming (just removing `test_` prefix will produce invalid function name)
+
+error: redundant `test_` prefix in test function name
+  --> tests/ui/redundant_test_prefix_noautofix.rs:258:8
+   |
+LL |     fn test_yield() {
+   |        ^^^^^^^^^^
+   |
+   = help: consider function renaming (just removing `test_` prefix will produce invalid function name)
+
+error: redundant `test_` prefix in test function name
+  --> tests/ui/redundant_test_prefix_noautofix.rs:265:8
+   |
+LL |     fn test_() {
+   |        ^^^^^
+   |
+   = help: consider function renaming (just removing `test_` prefix will produce invalid function name)
+
+error: aborting due to 33 previous errors
+