11use 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;
34use clippy_utils:: { is_in_cfg_test, is_in_test_function} ;
5+ use rustc_errors:: Applicability ;
46use rustc_hir:: intravisit:: FnKind ;
5- use rustc_hir:: { Body , FnDecl } ;
7+ use rustc_hir:: { self as hir , Body , ExprKind , FnDecl } ;
68use rustc_lint:: { LateContext , LateLintPass } ;
79use rustc_session:: impl_lint_pass;
810use rustc_span:: Span ;
911use rustc_span:: def_id:: LocalDefId ;
12+ use std:: ops:: ControlFlow ;
1013
1114declare_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
5256impl_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}
0 commit comments