Skip to content

Commit ac7cbf3

Browse files
committed
refactor: redundant_test_prefix rustfix
1 parent eb39382 commit ac7cbf3

File tree

6 files changed

+581
-45
lines changed

6 files changed

+581
-45
lines changed
Lines changed: 78 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,37 @@
11
use clippy_config::Conf;
2-
use clippy_utils::diagnostics::span_lint_and_help;
2+
use clippy_utils::diagnostics::span_lint_and_sugg;
3+
use clippy_utils::visitors::for_each_expr;
34
use clippy_utils::{is_in_cfg_test, is_in_test_function};
5+
use rustc_errors::Applicability;
46
use rustc_hir::intravisit::FnKind;
5-
use rustc_hir::{Body, FnDecl};
7+
use rustc_hir::{self as hir, Body, ExprKind, FnDecl};
68
use rustc_lint::{LateContext, LateLintPass};
79
use rustc_session::impl_lint_pass;
810
use rustc_span::Span;
911
use rustc_span::def_id::LocalDefId;
12+
use std::ops::ControlFlow;
1013

1114
declare_clippy_lint! {
1215
/// ### What it does
13-
/// Checks for test functions (functions annotated with `#[test]`) that prefixed with `test_`
14-
/// which is redundant.
16+
/// Checks for test functions (functions annotated with `#[test]`) that are prefixed
17+
/// with `test_` which is redundant.
1518
///
1619
/// ### Why is this bad?
17-
/// This is redundant because the test functions are already annotated with `#[test]`.
18-
/// Moreover, it clutters the output of `cargo test` as test functions are expanded as
19-
/// `module::tests::test_name` in the output.
20+
/// This is redundant because test functions are already annotated with `#[test]`.
21+
/// Moreover, it clutters the output of `cargo test` since test functions are expanded as
22+
/// `module::tests::test_use_case` in the output. Without the redundant prefix, the output
23+
/// becomes `module::tests::use_case`, which is more readable.
2024
///
2125
/// ### Example
2226
/// ```no_run
2327
/// #[test]
24-
/// fn test_my_use_case() {
28+
/// fn test_use_case() {
2529
/// // test code
2630
/// }
2731
/// ```
2832
/// Use instead:
2933
/// ```no_run
30-
/// fn my_use_case() {
34+
/// fn use_case() {
3135
/// // test code
3236
/// }
3337
/// ```
@@ -51,15 +55,15 @@ impl RedundantTestPrefix {
5155

5256
impl_lint_pass!(RedundantTestPrefix => [REDUNDANT_TEST_PREFIX]);
5357

54-
impl LateLintPass<'_> for RedundantTestPrefix {
58+
impl<'tcx> LateLintPass<'tcx> for RedundantTestPrefix {
5559
fn check_fn(
5660
&mut self,
57-
cx: &LateContext<'_>,
58-
fn_kind: FnKind<'_>,
59-
_: &FnDecl<'_>,
60-
body: &Body<'_>,
61-
span: Span,
62-
_: LocalDefId,
61+
cx: &LateContext<'tcx>,
62+
kind: FnKind<'_>,
63+
_decl: &FnDecl<'_>,
64+
body: &'tcx Body<'_>,
65+
_span: Span,
66+
_fn_def_id: LocalDefId,
6367
) {
6468
// Skip the lint if the function is not within a node with `#[cfg(test)]` attribute,
6569
// which is true for integration tests. If the lint is enabled for integration tests,
@@ -68,20 +72,70 @@ impl LateLintPass<'_> for RedundantTestPrefix {
6872
return;
6973
}
7074

71-
if is_in_test_function(cx.tcx, body.id().hir_id) && has_redundant_test_prefix(fn_kind) {
72-
span_lint_and_help(
75+
// Ignore methods and closures.
76+
let FnKind::ItemFn(ref ident, ..) = kind else {
77+
return;
78+
};
79+
80+
if is_in_test_function(cx.tcx, body.id().hir_id) && ident.as_str().starts_with("test_") {
81+
let mut non_prefixed = ident.as_str().trim_start_matches("test_").to_string();
82+
let mut help_msg = "consider removing the `test_` prefix";
83+
// If `non_prefixed` conflicts with another function in the same module/scope,
84+
// add extra suffix to avoid name conflict.
85+
if name_conflicts(cx, body, &non_prefixed) {
86+
non_prefixed.push_str("_works");
87+
help_msg = "consider removing the `test_` prefix (suffix avoids name conflict)";
88+
}
89+
span_lint_and_sugg(
7390
cx,
7491
REDUNDANT_TEST_PREFIX,
75-
span,
92+
ident.span,
7693
"redundant `test_` prefix in test function name",
77-
None,
78-
"consider removing the `test_` prefix",
94+
help_msg,
95+
non_prefixed,
96+
Applicability::MachineApplicable,
7997
);
8098
}
8199
}
82100
}
83101

84-
/// Checks if the function has redundant `test_` prefix in its name.
85-
fn has_redundant_test_prefix(fn_kind: FnKind<'_>) -> bool {
86-
matches!(fn_kind, FnKind::ItemFn(ident, ..) if ident.name.as_str().starts_with("test_"))
102+
/// Checks whether removal of the `_test` prefix from the function name will cause a name conflict.
103+
///
104+
/// There should be no other function with the same name in the same module/scope. Also, there
105+
/// should not be any function call with the same name within the body of the function, to avoid
106+
/// recursion.
107+
fn name_conflicts<'tcx>(cx: &LateContext<'tcx>, body: &'tcx Body<'_>, fn_name: &str) -> bool {
108+
let tcx = cx.tcx;
109+
let id = body.id().hir_id;
110+
111+
// Iterate over items in the same module/scope
112+
let (module, _module_span, _module_hir) = tcx.hir().get_module(tcx.parent_module(id));
113+
for item in module.item_ids {
114+
let item = tcx.hir().item(*item);
115+
if let hir::ItemKind::Fn(..) = item.kind {
116+
if item.ident.name.as_str() == fn_name {
117+
// Name conflict found
118+
return true;
119+
}
120+
}
121+
}
122+
123+
// Also check that within the body of the function there is also no function call
124+
// with the same name (since it will result in recursion)
125+
for_each_expr(cx, body, |expr| {
126+
if let ExprKind::Call(path_expr, _) = expr.kind {
127+
if let ExprKind::Path(qpath) = &path_expr.kind {
128+
if let Some(def_id) = cx.qpath_res(qpath, path_expr.hir_id).opt_def_id() {
129+
if let Some(name) = tcx.opt_item_name(def_id) {
130+
if name.as_str() == fn_name {
131+
// Function call with the same name found
132+
return ControlFlow::Break(());
133+
}
134+
}
135+
}
136+
}
137+
}
138+
ControlFlow::Continue(())
139+
})
140+
.is_some()
87141
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
//@revisions: default integ_tests
2+
//@[default] rustc-env:CLIPPY_CONF_DIR=tests/ui-toml/redundant_test_prefix/default
3+
//@[integ_tests] rustc-env:CLIPPY_CONF_DIR=tests/ui-toml/redundant_test_prefix/in_integration_tests
4+
//@compile-flags: --test
5+
#![allow(dead_code)]
6+
#![warn(clippy::redundant_test_prefix)]
7+
8+
fn main() {}
9+
10+
mod tests_no_annotations {
11+
use super::*;
12+
13+
#[test]
14+
fn has_annotation() {
15+
//~[integ_tests]^ redundant_test_prefix
16+
}
17+
18+
fn no_annotation() {}
19+
}
20+
21+
#[test]
22+
fn main_module_has_annotation() {
23+
//~[integ_tests]^ redundant_test_prefix
24+
}
25+
26+
fn test_main_module_no_annotation() {}
27+
28+
#[cfg(test)]
29+
mod tests {}
Lines changed: 6 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,17 @@
11
error: redundant `test_` prefix in test function name
2-
--> tests/ui-toml/redundant_test_prefix/redundant_test_prefix.rs:14:5
2+
--> tests/ui-toml/redundant_test_prefix/redundant_test_prefix.rs:14:8
33
|
4-
LL | / fn test_has_annotation() {
5-
LL | |
6-
LL | | }
7-
| |_____^
4+
LL | fn test_has_annotation() {
5+
| ^^^^^^^^^^^^^^^^^^^ help: consider removing the `test_` prefix: `has_annotation`
86
|
9-
= help: consider removing the `test_` prefix
107
= note: `-D clippy::redundant-test-prefix` implied by `-D warnings`
118
= help: to override `-D warnings` add `#[allow(clippy::redundant_test_prefix)]`
129

1310
error: redundant `test_` prefix in test function name
14-
--> tests/ui-toml/redundant_test_prefix/redundant_test_prefix.rs:22:1
11+
--> tests/ui-toml/redundant_test_prefix/redundant_test_prefix.rs:22:4
1512
|
16-
LL | / fn test_main_module_has_annotation() {
17-
LL | |
18-
LL | | }
19-
| |_^
20-
|
21-
= help: consider removing the `test_` prefix
13+
LL | fn test_main_module_has_annotation() {
14+
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ help: consider removing the `test_` prefix: `main_module_has_annotation`
2215

2316
error: aborting due to 2 previous errors
2417

0 commit comments

Comments
 (0)