Skip to content

Lint to prevent redundant test_ prefix in test names. #12861

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5697,6 +5697,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
Expand Down
2 changes: 1 addition & 1 deletion clippy_dev/src/new_lint.rs
Original file line number Diff line number Diff line change
Expand Up @@ -581,7 +581,7 @@ fn setup_mod_file(path: &Path, lint: &LintData<'_>) -> io::Result<&'static str>
}

#[test]
fn test_camel_case() {
fn camel_case() {
let s = "a_lint";
let s2 = to_camel_case(s);
assert_eq!(s2, "ALint");
Expand Down
16 changes: 8 additions & 8 deletions clippy_dev/src/update_lints.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1042,7 +1042,7 @@ mod tests {
use super::*;

#[test]
fn test_parse_contents() {
fn parse_contents() {
static CONTENTS: &str = r#"
declare_clippy_lint! {
#[clippy::version = "Hello Clippy!"]
Expand All @@ -1060,7 +1060,7 @@ mod tests {
}
"#;
let mut result = Vec::new();
parse_contents(CONTENTS, "module_name", &mut result);
super::parse_contents(CONTENTS, "module_name", &mut result);
for r in &mut result {
r.declaration_range = Range::default();
}
Expand All @@ -1085,7 +1085,7 @@ mod tests {
}

#[test]
fn test_parse_deprecated_contents() {
fn parse_deprecated_contents() {
static DEPRECATED_CONTENTS: &str = r#"
/// some doc comment
declare_deprecated_lint! {
Expand All @@ -1096,7 +1096,7 @@ mod tests {
"#;

let mut result = Vec::new();
parse_deprecated_contents(DEPRECATED_CONTENTS, &mut result);
super::parse_deprecated_contents(DEPRECATED_CONTENTS, &mut result);
for r in &mut result {
r.declaration_range = Range::default();
}
Expand All @@ -1110,7 +1110,7 @@ mod tests {
}

#[test]
fn test_usable_lints() {
fn usable_lints() {
let lints = vec![
Lint::new(
"should_assert_eq2",
Expand Down Expand Up @@ -1145,7 +1145,7 @@ mod tests {
}

#[test]
fn test_by_lint_group() {
fn by_lint_group() {
let lints = vec![
Lint::new("should_assert_eq", "group1", "\"abc\"", "module_name", Range::default()),
Lint::new(
Expand Down Expand Up @@ -1179,7 +1179,7 @@ mod tests {
}

#[test]
fn test_gen_deprecated() {
fn gen_deprecated() {
let lints = vec![
DeprecatedLint::new(
"should_assert_eq",
Expand All @@ -1205,6 +1205,6 @@ mod tests {
.join("\n")
+ "\n";

assert_eq!(expected, gen_deprecated(&lints));
assert_eq!(expected, super::gen_deprecated(&lints));
}
}
2 changes: 1 addition & 1 deletion clippy_lints/src/cargo/feature_name.rs
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ fn lint(cx: &LateContext<'_>, feature: &str, substring: &str, is_prefix: bool) {
}

#[test]
fn test_prefixes_sorted() {
fn prefixes_sorted() {
let mut sorted_prefixes = PREFIXES;
sorted_prefixes.sort_unstable();
assert_eq!(PREFIXES, sorted_prefixes);
Expand Down
1 change: 1 addition & 0 deletions clippy_lints/src/declared_lints.rs
Original file line number Diff line number Diff line change
Expand Up @@ -624,6 +624,7 @@ pub(crate) 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,
Expand Down
2 changes: 1 addition & 1 deletion clippy_lints/src/doc/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -223,7 +223,7 @@ declare_clippy_lint! {
/// /// assert_eq!(1_u8, 1);
/// /// }
/// /// ```
/// fn test_attr_in_doctest() {
/// fn attr_in_doctest() {
/// unimplemented!();
/// }
/// ```
Expand Down
11 changes: 5 additions & 6 deletions clippy_lints/src/empty_with_brackets.rs
Original file line number Diff line number Diff line change
Expand Up @@ -144,23 +144,22 @@ fn has_no_fields(cx: &EarlyContext<'_>, var_data: &VariantData, braces_span: Spa

#[cfg(test)]
mod unit_test {
use super::*;

#[test]
fn test_has_no_ident_token() {
fn has_no_ident_token() {
let input = "{ field: u8 }";
assert!(!has_no_ident_token(input));
assert!(!super::has_no_ident_token(input));

let input = "(u8, String);";
assert!(!has_no_ident_token(input));
assert!(!super::has_no_ident_token(input));

let input = " {
// test = 5
}
";
assert!(has_no_ident_token(input));
assert!(super::has_no_ident_token(input));

let input = " ();";
assert!(has_no_ident_token(input));
assert!(super::has_no_ident_token(input));
}
}
2 changes: 2 additions & 0 deletions clippy_lints/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -301,6 +301,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;
Expand Down Expand Up @@ -1165,6 +1166,7 @@ pub fn register_lints(store: &mut rustc_lint::LintStore, conf: &'static Conf) {
..Default::default()
})
});
store.register_late_pass(|_| Box::new(redundant_test_prefix::RedundantTestPrefix));
// add lints here, do not remove this comment, it's used in `new_lint`
}

Expand Down
75 changes: 40 additions & 35 deletions clippy_lints/src/matches/overlapping_arms.rs
Original file line number Diff line number Diff line change
Expand Up @@ -148,39 +148,44 @@ where
None
}

#[test]
fn test_overlapping() {
use rustc_span::DUMMY_SP;

let sp = |s, e| SpannedRange {
span: DUMMY_SP,
node: (s, e),
};

assert_eq!(None, overlapping::<u8>(&[]));
assert_eq!(None, overlapping(&[sp(1, EndBound::Included(4))]));
assert_eq!(
None,
overlapping(&[sp(1, EndBound::Included(4)), sp(5, EndBound::Included(6))])
);
assert_eq!(
None,
overlapping(&[
sp(1, EndBound::Included(4)),
sp(5, EndBound::Included(6)),
sp(10, EndBound::Included(11))
],)
);
assert_eq!(
Some((&sp(1, EndBound::Included(4)), &sp(3, EndBound::Included(6)))),
overlapping(&[sp(1, EndBound::Included(4)), sp(3, EndBound::Included(6))])
);
assert_eq!(
Some((&sp(5, EndBound::Included(6)), &sp(6, EndBound::Included(11)))),
overlapping(&[
sp(1, EndBound::Included(4)),
sp(5, EndBound::Included(6)),
sp(6, EndBound::Included(11))
],)
);
#[cfg(test)]
mod test {
use super::{EndBound, SpannedRange};

#[test]
fn overlapping() {
use rustc_span::DUMMY_SP;

let sp = |s, e| SpannedRange {
span: DUMMY_SP,
node: (s, e),
};

assert_eq!(None, super::overlapping::<u8>(&[]));
assert_eq!(None, super::overlapping(&[sp(1, EndBound::Included(4))]));
assert_eq!(
None,
super::overlapping(&[sp(1, EndBound::Included(4)), sp(5, EndBound::Included(6))])
);
assert_eq!(
None,
super::overlapping(&[
sp(1, EndBound::Included(4)),
sp(5, EndBound::Included(6)),
sp(10, EndBound::Included(11))
],)
);
assert_eq!(
Some((&sp(1, EndBound::Included(4)), &sp(3, EndBound::Included(6)))),
super::overlapping(&[sp(1, EndBound::Included(4)), sp(3, EndBound::Included(6))])
);
assert_eq!(
Some((&sp(5, EndBound::Included(6)), &sp(6, EndBound::Included(11)))),
super::overlapping(&[
sp(1, EndBound::Included(4)),
sp(5, EndBound::Included(6)),
sp(6, EndBound::Included(11))
],)
);
}
}
9 changes: 4 additions & 5 deletions clippy_lints/src/needless_continue.rs
Original file line number Diff line number Diff line change
Expand Up @@ -440,11 +440,10 @@ fn span_of_first_expr_in_block(block: &ast::Block) -> Option<Span> {

#[cfg(test)]
mod test {
use super::erode_from_back;

#[test]
#[rustfmt::skip]
fn test_erode_from_back() {
fn erode_from_back() {
let input = "\
{
let x = 5;
Expand All @@ -456,19 +455,19 @@ mod test {
let x = 5;
let y = format!(\"{}\", 42);";

let got = erode_from_back(input);
let got = super::erode_from_back(input);
assert_eq!(expected, got);
}

#[test]
#[rustfmt::skip]
fn test_erode_from_back_no_brace() {
fn erode_from_back_no_brace() {
let input = "\
let x = 5;
let y = something();
";
let expected = input;
let got = erode_from_back(input);
let got = super::erode_from_back(input);
assert_eq!(expected, got);
}
}
74 changes: 74 additions & 0 deletions clippy_lints/src/redundant_test_prefix.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
use clippy_utils::diagnostics::span_lint_and_help;
use clippy_utils::{is_in_cfg_test, is_in_test_function};
use rustc_hir::intravisit::FnKind;
use rustc_hir::{Body, FnDecl};
use rustc_lint::{LateContext, LateLintPass};
use rustc_session::declare_lint_pass;
use rustc_span::def_id::LocalDefId;
use rustc_span::Span;

declare_clippy_lint! {
/// ### What it does
/// Triggers when a function annotated with the `#[test]` macro begins with `test_`
/// ### Why is this bad?
/// This is redundant, since the annotations already denotes the function a test.
/// Tests are executed using `cargo test `module::test-name-here::`.
/// It's clear that a test is a test, without prefixing `test_` to the test name.
/// ### Example
/// ```no_run
/// #[test]
/// fn my_test_case() {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
/// fn my_test_case() {
/// fn test_my_test_case() {

I assume you're meaning to show a 'bad' example here that should trigger the lint.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, right, I need to update this.

/// // test logic here...
/// }
/// ```
/// Use instead:
/// ```no_run
/// #[test]
/// fn my_test_case() {
/// // test logic here...
/// }
/// ```
#[clippy::version = "1.79.0"]
pub REDUNDANT_TEST_PREFIX,
pedantic,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I feel like this shouldn't be in pedantic but in restriction instead. The usage of the test_ prefix doesn't seem like an antipattern to me. There are good arguments against it, but the number of changes required in existing changes shows that the usage is still quite common.

I'll include this decision in the group discussion before we merge this PR :)

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are arguments to be made for several categories: style, pedantic, restriction, etc.

The usage of test_ seems to be pedantic to me, because it's clearly unnecessary in almost every case, including some of the ones mentioned in the threads in this PR. For example, test names that have test_ + method_name, should just be super::method_name IMO, following the same like of reasoning for as everywhere else.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One argument I always consider is the number of lint triggers that happen in current code. Clippy can be a very pedantic, but the number of lint emissions has to be reasonable. If users get 100+ warnings of this lint with the next Clippy release it's way more likely that users get annoyed and simply allow the lint and that would help nobody.

This is not meant as a hard no, but just an explanation of my suggestion. I'm not sure where to place it and would leave this up to a group decision.

"A test name is prefixed with `test`"
}

declare_lint_pass!(RedundantTestPrefix => [REDUNDANT_TEST_PREFIX]);

impl LateLintPass<'_> for RedundantTestPrefix {
fn check_fn(
&mut self,
cx: &LateContext<'_>,
fn_kind: FnKind<'_>,
_: &FnDecl<'_>,
body: &Body<'_>,
span: Span,
_: LocalDefId,
) {
if is_prefixed_with_test_fn(fn_kind)
&& is_in_test_function(cx.tcx, body.id().hir_id)
&& is_in_cfg_test(cx.tcx, body.id().hir_id)
{
span_lint_and_help(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would be nicer if this lint could be auto-fixed, so, maybe use span_lint_and_sugg instead?

But then it'll make things a bit more complicated when such test function is called in another test function, such as:

#[cfg(test)]
mod tests {
    #[test]
    fn test_a() {
        // ...
    }

    #[test]
    fn test_b() {
        // ...
    }

    #[test]
    fn test_a_and_b() {
        test_a();
        test_b();
    }
}

therefore, you'll need to check for the function calls within the test function bodies, then adjust the applicability to Applicability::MaybeIncorrect or something better... suggest changing all those names as well.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm interested in learning more about how to get this done, I'm pretty new to the clippy tooling as a contributor, and I was wondering how to plumb up fixing this automatically. Perhaps we could pair on this if you have some time.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Was in a family trip few days ago so I didn't see this message, sorry~

...and I was wondering how to plumb up fixing this automatically...

Well, what I meant to say was instead of using span_lint_and_help, try using span_lint_and_sugg. Therefore you can offer a suggestion with the lint, in this case, a new function name without the test_ prefix, as well as an applicablity tell it's machine applicable, then people can run clippy with --fix to automatically apply the suggestion.

Something like...:

if let FnKind::ItemFn(ident, ..) = fn_kind &&
    let Some(non_prefixed) = ident.name.as_str()
{
    span_lint_and_sugg(
        cx,
        REDUNDANT_TEST_PREFIX,
        span,
        "this test is prefixed redundantly with `test`",
        "consider removing the redundant `test` prefix from the test name",
        non_prefixed,
        Applicability::MachineApplicable
    );
}

But then it'll make things a bit more complicated when such test function is called in another test function

As for this, it might just me being too paranoid 😅, as I haven't seen any real world example doing that afaik, so you can ignore it for now.

But if you are interested, here's a little hint:

Expand Me! You can check each `expr` if it `is_in_test_function`, then if it is `Call`ing a function that have `test_` prefixed name, you can have the following info stored in a map:
  • the function's DefId (key)
  • Spans where it was called
  • a suggested name without prefix
  • a bool indicates whether this function is a test function or not

for example

FxHashMap<DefId, (Vec<Span>, String, bool)>

Then you can check_fn as usual but don't call span_lint* yet, just updating the bool value (or insert new entry to) the map you had and continue.

Finally, having a check_crate_post function, you can go over that map you have, picking only the ones marked as test function and call emit lint, if the Vec<Span> is not empty, you can then use multipart_suggestion.

I think this is one way to do it? idk, I'm writing this at almost 2am so most of them might not make sense lol 😅 .

cx,
REDUNDANT_TEST_PREFIX,
span,
"this test is prefixed redundantly with `test`",
None,
"consider removing the redundant `test` prefix from the test name",
);
}
}
}

fn is_prefixed_with_test_fn(fn_kind: FnKind<'_>) -> bool {
match fn_kind {
FnKind::ItemFn(ident, ..) => {
// check if `fn` name is prefixed with `test_`
ident.name.as_str().starts_with("test_")
},
// ignore closures
_ => false,
}
}
Loading
Loading