Skip to content

Commit 10de1a6

Browse files
committed
new lint: redundant_test_prefix
1 parent ec105ba commit 10de1a6

10 files changed

+1156
-0
lines changed

CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -6116,6 +6116,7 @@ Released 2018-09-13
61166116
[`redundant_pub_crate`]: https://rust-lang.github.io/rust-clippy/master/index.html#redundant_pub_crate
61176117
[`redundant_slicing`]: https://rust-lang.github.io/rust-clippy/master/index.html#redundant_slicing
61186118
[`redundant_static_lifetimes`]: https://rust-lang.github.io/rust-clippy/master/index.html#redundant_static_lifetimes
6119+
[`redundant_test_prefix`]: https://rust-lang.github.io/rust-clippy/master/index.html#redundant_test_prefix
61196120
[`redundant_type_annotations`]: https://rust-lang.github.io/rust-clippy/master/index.html#redundant_type_annotations
61206121
[`ref_as_ptr`]: https://rust-lang.github.io/rust-clippy/master/index.html#ref_as_ptr
61216122
[`ref_binding_to_reference`]: https://rust-lang.github.io/rust-clippy/master/index.html#ref_binding_to_reference

clippy_lints/src/declared_lints.rs

+1
Original file line numberDiff line numberDiff line change
@@ -668,6 +668,7 @@ pub static LINTS: &[&crate::LintInfo] = &[
668668
crate::redundant_slicing::DEREF_BY_SLICING_INFO,
669669
crate::redundant_slicing::REDUNDANT_SLICING_INFO,
670670
crate::redundant_static_lifetimes::REDUNDANT_STATIC_LIFETIMES_INFO,
671+
crate::redundant_test_prefix::REDUNDANT_TEST_PREFIX_INFO,
671672
crate::redundant_type_annotations::REDUNDANT_TYPE_ANNOTATIONS_INFO,
672673
crate::ref_option_ref::REF_OPTION_REF_INFO,
673674
crate::ref_patterns::REF_PATTERNS_INFO,

clippy_lints/src/lib.rs

+2
Original file line numberDiff line numberDiff line change
@@ -320,6 +320,7 @@ mod redundant_locals;
320320
mod redundant_pub_crate;
321321
mod redundant_slicing;
322322
mod redundant_static_lifetimes;
323+
mod redundant_test_prefix;
323324
mod redundant_type_annotations;
324325
mod ref_option_ref;
325326
mod ref_patterns;
@@ -984,5 +985,6 @@ pub fn register_lints(store: &mut rustc_lint::LintStore, conf: &'static Conf) {
984985
store.register_late_pass(move |_| Box::new(non_std_lazy_statics::NonStdLazyStatic::new(conf)));
985986
store.register_late_pass(|_| Box::new(manual_option_as_slice::ManualOptionAsSlice::new(conf)));
986987
store.register_late_pass(|_| Box::new(single_option_map::SingleOptionMap));
988+
store.register_late_pass(move |_| Box::new(redundant_test_prefix::RedundantTestPrefix));
987989
// add lints here, do not remove this comment, it's used in `new_lint`
988990
}
+168
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,168 @@
1+
use clippy_utils::diagnostics::{span_lint_and_help, span_lint_and_sugg};
2+
use clippy_utils::is_test_function;
3+
use clippy_utils::visitors::for_each_expr;
4+
use rustc_errors::Applicability;
5+
use rustc_hir::intravisit::FnKind;
6+
use rustc_hir::{self as hir, Body, ExprKind, FnDecl};
7+
use rustc_lexer::is_ident;
8+
use rustc_lint::{LateContext, LateLintPass};
9+
use rustc_session::{declare_lint_pass};
10+
use rustc_span::def_id::LocalDefId;
11+
use rustc_span::{Span, Symbol, edition};
12+
use std::ops::ControlFlow;
13+
14+
declare_clippy_lint! {
15+
/// ### What it does
16+
/// Checks for test functions (functions annotated with `#[test]`) that are prefixed
17+
/// with `test_` which is redundant.
18+
///
19+
/// ### Why is this bad?
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.
24+
///
25+
/// ### Example
26+
/// ```no_run
27+
/// #[cfg(test)]
28+
/// mod tests {
29+
/// use super::*;
30+
///
31+
/// #[test]
32+
/// fn test_use_case() {
33+
/// // test code
34+
/// }
35+
/// }
36+
/// ```
37+
/// Use instead:
38+
/// ```no_run
39+
/// #[cfg(test)]
40+
/// mod tests {
41+
/// use super::*;
42+
///
43+
/// #[test]
44+
/// fn use_case() {
45+
/// // test code
46+
/// }
47+
/// }
48+
/// ```
49+
#[clippy::version = "1.88.0"]
50+
pub REDUNDANT_TEST_PREFIX,
51+
restriction,
52+
"redundant `test_` prefix in test function name"
53+
}
54+
55+
declare_lint_pass!(RedundantTestPrefix => [REDUNDANT_TEST_PREFIX]);
56+
57+
impl<'tcx> LateLintPass<'tcx> for RedundantTestPrefix {
58+
fn check_fn(
59+
&mut self,
60+
cx: &LateContext<'tcx>,
61+
kind: FnKind<'_>,
62+
_decl: &FnDecl<'_>,
63+
body: &'tcx Body<'_>,
64+
_span: Span,
65+
fn_def_id: LocalDefId,
66+
) {
67+
// Ignore methods and closures.
68+
let FnKind::ItemFn(ref ident, ..) = kind else {
69+
return;
70+
};
71+
72+
// Skip the lint if the function is within a macro expansion.
73+
if ident.span.from_expansion() {
74+
return;
75+
}
76+
77+
// Skip if the function name does not start with `test_`.
78+
if !ident.as_str().starts_with("test_") {
79+
return;
80+
}
81+
82+
if is_test_function(cx.tcx, cx.tcx.local_def_id_to_hir_id(fn_def_id)) {
83+
let msg = "redundant `test_` prefix in test function name";
84+
let mut non_prefixed = ident.as_str().trim_start_matches("test_").to_string();
85+
86+
if is_invalid_ident(&non_prefixed) {
87+
// If the prefix-trimmed name is not a valid function name, do not provide an
88+
// automatic fix, just suggest renaming the function.
89+
span_lint_and_help(
90+
cx,
91+
REDUNDANT_TEST_PREFIX,
92+
ident.span,
93+
msg,
94+
None,
95+
"consider function renaming (just removing `test_` prefix will produce invalid function name)",
96+
);
97+
} else if name_conflicts(cx, body, &non_prefixed) {
98+
// If `non_prefixed` conflicts with another function in the same module/scope,
99+
// do not provide an automatic fix, but still emit a fix suggestion.
100+
non_prefixed.push_str("_works");
101+
span_lint_and_sugg(
102+
cx,
103+
REDUNDANT_TEST_PREFIX,
104+
ident.span,
105+
msg,
106+
"consider function renaming (just removing `test_` prefix will cause a name conflict)",
107+
non_prefixed,
108+
Applicability::MaybeIncorrect,
109+
);
110+
} else {
111+
// If `non_prefixed` is a valid identifier and does not conflict with another function,
112+
// so we can suggest an auto-fix.
113+
span_lint_and_sugg(
114+
cx,
115+
REDUNDANT_TEST_PREFIX,
116+
ident.span,
117+
msg,
118+
"consider removing the `test_` prefix",
119+
non_prefixed,
120+
Applicability::MaybeIncorrect,
121+
);
122+
}
123+
}
124+
}
125+
}
126+
127+
/// Checks whether removal of the `_test` prefix from the function name will cause a name conflict.
128+
///
129+
/// There should be no other function with the same name in the same module/scope. Also, there
130+
/// should not be any function call with the same name within the body of the function, to avoid
131+
/// recursion.
132+
fn name_conflicts<'tcx>(cx: &LateContext<'tcx>, body: &'tcx Body<'_>, fn_name: &str) -> bool {
133+
let tcx = cx.tcx;
134+
let id = body.id().hir_id;
135+
136+
// Iterate over items in the same module/scope
137+
let (module, _module_span, _module_hir) = tcx.hir_get_module(tcx.parent_module(id));
138+
for item in module.item_ids {
139+
let item = tcx.hir_item(*item);
140+
if let hir::ItemKind::Fn { ident, .. } = item.kind
141+
&& ident.name.as_str() == fn_name
142+
{
143+
// Name conflict found
144+
return true;
145+
}
146+
}
147+
148+
// Also check that within the body of the function there is also no function call
149+
// with the same name (since it will result in recursion)
150+
for_each_expr(cx, body, |expr| {
151+
if let ExprKind::Path(qpath) = &expr.kind
152+
&& let Some(def_id) = cx.qpath_res(qpath, expr.hir_id).opt_def_id()
153+
&& let Some(name) = tcx.opt_item_name(def_id)
154+
&& name.as_str() == fn_name
155+
{
156+
// Function call with the same name found
157+
return ControlFlow::Break(());
158+
}
159+
160+
ControlFlow::Continue(())
161+
})
162+
.is_some()
163+
}
164+
165+
fn is_invalid_ident(ident: &str) -> bool {
166+
// The identifier is either a reserved keyword, or starts with an invalid sequence.
167+
Symbol::intern(ident).is_reserved(|| edition::LATEST_STABLE_EDITION) || !is_ident(ident)
168+
}

clippy_utils/src/lib.rs

+20
Original file line numberDiff line numberDiff line change
@@ -2695,6 +2695,26 @@ pub fn is_in_test_function(tcx: TyCtxt<'_>, id: HirId) -> bool {
26952695
})
26962696
}
26972697

2698+
/// Checks if `id` has a `#[test]` attribute applied
2699+
///
2700+
/// This only checks directly applied attributes. To see if a node has a parent function marked with
2701+
/// `#[test]` use [`is_in_test_function`].
2702+
///
2703+
/// Note: Add `//@compile-flags: --test` to UI tests with a `#[test]` function
2704+
pub fn is_test_function(tcx: TyCtxt<'_>, id: HirId) -> bool {
2705+
with_test_item_names(tcx, tcx.parent_module(id), |names| {
2706+
if let Node::Item(item) = tcx.hir_node(id)
2707+
&& let ItemKind::Fn { ident, .. } = item.kind
2708+
{
2709+
// Note that we have sorted the item names in the visitor,
2710+
// so the binary_search gets the same as `contains`, but faster.
2711+
names.binary_search(&ident.name).is_ok()
2712+
} else {
2713+
false
2714+
}
2715+
})
2716+
}
2717+
26982718
/// Checks if `id` has a `#[cfg(test)]` attribute applied
26992719
///
27002720
/// This only checks directly applied attributes, to see if a node is inside a `#[cfg(test)]` parent

tests/ui/redundant_test_prefix.fixed

+158
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
#![allow(dead_code)]
2+
#![warn(clippy::redundant_test_prefix)]
3+
4+
fn main() {
5+
// Normal function, no redundant prefix.
6+
}
7+
8+
fn f1() {
9+
// Normal function, no redundant prefix.
10+
}
11+
12+
fn test_f2() {
13+
// Has prefix, but no `#[test]` attribute, ignore.
14+
}
15+
16+
#[test]
17+
fn f3() {
18+
//~^ redundant_test_prefix
19+
20+
// Has prefix, has `#[test]` attribute. Not within a `#[cfg(test)]`.
21+
// No collision with other functions, should emit warning.
22+
}
23+
24+
#[cfg(test)]
25+
#[test]
26+
fn f4() {
27+
//~^ redundant_test_prefix
28+
29+
// Has prefix, has `#[test]` attribute, within a `#[cfg(test)]`.
30+
// No collision with other functions, should emit warning.
31+
}
32+
33+
mod m1 {
34+
pub fn f5() {}
35+
}
36+
37+
#[cfg(test)]
38+
#[test]
39+
fn f6() {
40+
//~^ redundant_test_prefix
41+
42+
use m1::f5;
43+
44+
f5();
45+
// Has prefix, has `#[test]` attribute, within a `#[cfg(test)]`.
46+
// No collision, has function call, but it will not result in recursion.
47+
}
48+
49+
#[cfg(test)]
50+
mod tests {
51+
use super::*;
52+
53+
#[test]
54+
fn foo() {
55+
//~^ redundant_test_prefix
56+
}
57+
58+
#[test]
59+
fn foo_with_call() {
60+
//~^ redundant_test_prefix
61+
62+
main();
63+
}
64+
65+
#[test]
66+
fn f1() {
67+
//~^ redundant_test_prefix
68+
}
69+
70+
#[test]
71+
fn f2() {
72+
//~^ redundant_test_prefix
73+
}
74+
75+
#[test]
76+
fn f3() {
77+
//~^ redundant_test_prefix
78+
}
79+
80+
#[test]
81+
fn f4() {
82+
//~^ redundant_test_prefix
83+
}
84+
85+
#[test]
86+
fn f5() {
87+
//~^ redundant_test_prefix
88+
}
89+
90+
#[test]
91+
fn f6() {
92+
//~^ redundant_test_prefix
93+
}
94+
}
95+
96+
mod tests_no_annotations {
97+
use super::*;
98+
99+
#[test]
100+
fn foo() {
101+
//~^ redundant_test_prefix
102+
}
103+
104+
#[test]
105+
fn foo_with_call() {
106+
//~^ redundant_test_prefix
107+
108+
main();
109+
}
110+
111+
#[test]
112+
fn f1() {
113+
//~^ redundant_test_prefix
114+
}
115+
116+
#[test]
117+
fn f2() {
118+
//~^ redundant_test_prefix
119+
}
120+
121+
#[test]
122+
fn f3() {
123+
//~^ redundant_test_prefix
124+
}
125+
126+
#[test]
127+
fn f4() {
128+
//~^ redundant_test_prefix
129+
}
130+
131+
#[test]
132+
fn f5() {
133+
//~^ redundant_test_prefix
134+
}
135+
136+
#[test]
137+
fn f6() {
138+
//~^ redundant_test_prefix
139+
}
140+
}
141+
142+
// This test is inspired by real test in `clippy_utils/src/sugg.rs`.
143+
// The `is_in_test_function()` checks whether any identifier within a given node's parents is
144+
// marked with `#[test]` attribute. Thus flagging false positives when nested functions are
145+
// prefixed with `test_`. Therefore `is_test_function()` has been defined in `clippy_utils`,
146+
// allowing to select only functions that are immediately marked with `#[test]` annotation.
147+
//
148+
// This test case ensures that for such nested functions no error is emitted.
149+
#[test]
150+
fn not_op() {
151+
fn test_not(foo: bool) {
152+
assert!(foo);
153+
}
154+
155+
// Use helper function
156+
test_not(true);
157+
test_not(false);
158+
}

0 commit comments

Comments
 (0)