Skip to content

Commit f0a6022

Browse files
committed
Add tests_outside_test_module lint
1 parent d43714a commit f0a6022

6 files changed

+134
-0
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4935,6 +4935,7 @@ Released 2018-09-13
49354935
[`tabs_in_doc_comments`]: https://rust-lang.github.io/rust-clippy/master/index.html#tabs_in_doc_comments
49364936
[`temporary_assignment`]: https://rust-lang.github.io/rust-clippy/master/index.html#temporary_assignment
49374937
[`temporary_cstring_as_ptr`]: https://rust-lang.github.io/rust-clippy/master/index.html#temporary_cstring_as_ptr
4938+
[`tests_outside_test_module`]: https://rust-lang.github.io/rust-clippy/master/index.html#tests_outside_test_module
49384939
[`to_digit_is_some`]: https://rust-lang.github.io/rust-clippy/master/index.html#to_digit_is_some
49394940
[`to_string_in_display`]: https://rust-lang.github.io/rust-clippy/master/index.html#to_string_in_display
49404941
[`to_string_in_format_args`]: https://rust-lang.github.io/rust-clippy/master/index.html#to_string_in_format_args

clippy_lints/src/declared_lints.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -576,6 +576,7 @@ pub(crate) static LINTS: &[&crate::LintInfo] = &[
576576
crate::swap_ptr_to_ref::SWAP_PTR_TO_REF_INFO,
577577
crate::tabs_in_doc_comments::TABS_IN_DOC_COMMENTS_INFO,
578578
crate::temporary_assignment::TEMPORARY_ASSIGNMENT_INFO,
579+
crate::tests_outside_test_module::TESTS_OUTSIDE_TEST_MODULE_INFO,
579580
crate::to_digit_is_some::TO_DIGIT_IS_SOME_INFO,
580581
crate::trailing_empty_array::TRAILING_EMPTY_ARRAY_INFO,
581582
crate::trait_bounds::TRAIT_DUPLICATION_IN_BOUNDS_INFO,

clippy_lints/src/lib.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -290,6 +290,7 @@ mod swap;
290290
mod swap_ptr_to_ref;
291291
mod tabs_in_doc_comments;
292292
mod temporary_assignment;
293+
mod tests_outside_test_module;
293294
mod to_digit_is_some;
294295
mod trailing_empty_array;
295296
mod trait_bounds;
@@ -949,6 +950,7 @@ pub fn register_plugins(store: &mut rustc_lint::LintStore, sess: &Session, conf:
949950
avoid_breaking_exported_api,
950951
))
951952
});
953+
store.register_late_pass(|_| Box::new(tests_outside_test_module::TestsOutsideTestModule::new()));
952954
// add lints here, do not remove this comment, it's used in `new_lint`
953955
}
954956

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
use clippy_utils::{diagnostics::span_lint_and_note, is_in_cfg_test, is_in_test_function, is_test_module_or_function};
2+
use rustc_data_structures::sync::par_for_each_in;
3+
use rustc_hir::{intravisit::FnKind, Body, FnDecl, HirId, ItemKind, Mod};
4+
use rustc_lint::{LateContext, LateLintPass};
5+
use rustc_session::{declare_tool_lint, impl_lint_pass};
6+
use rustc_span::{def_id::LocalDefId, Span};
7+
8+
declare_clippy_lint! {
9+
/// ### What it does
10+
///
11+
/// Triggers when a testing function (marked with the `#[test]` attribute) isn't inside a testing module (marked with `#[cfg(test)]`).
12+
///
13+
/// ### Why is this bad?
14+
///
15+
/// The idiomatic (and more performant) way of writing tests is inside a testing module (flagged with `#[cfg(test)]`), having test functions outside of this module is confusing and may lead to them being "hidden".
16+
///
17+
/// ### Example
18+
/// ```rust
19+
/// #[test]
20+
/// fn my_cool_test() {
21+
/// // [...]
22+
/// }
23+
///
24+
/// #[cfg(test)]
25+
/// mod tests {
26+
/// // [...]
27+
/// }
28+
///
29+
/// ```
30+
/// Use instead:
31+
/// ```rust
32+
/// #[cfg(test)]
33+
/// mod tests {
34+
/// #[test]
35+
/// fn my_cool_test() {
36+
/// // [...]
37+
/// }
38+
/// }
39+
/// ```
40+
#[clippy::version = "1.70.0"]
41+
pub TESTS_OUTSIDE_TEST_MODULE,
42+
restriction,
43+
"The test function `my_cool_test` is outside the testing module `tests`."
44+
}
45+
46+
pub(crate) struct TestsOutsideTestModule {
47+
pub test_mod_exists: bool,
48+
}
49+
50+
impl TestsOutsideTestModule {
51+
pub fn new() -> Self {
52+
Self { test_mod_exists: false }
53+
}
54+
}
55+
56+
impl_lint_pass!(TestsOutsideTestModule => [TESTS_OUTSIDE_TEST_MODULE]);
57+
58+
impl LateLintPass<'_> for TestsOutsideTestModule {
59+
fn check_mod(&mut self, cx: &LateContext<'_>, _: &Mod<'_>, _: HirId) {
60+
self.test_mod_exists = false;
61+
62+
// par_for_each_item uses Fn, while par_for_each_in uses FnMut
63+
par_for_each_in(cx.tcx.hir_crate_items(()).items(), |itemid| {
64+
let item = cx.tcx.hir().item(itemid);
65+
if_chain! {
66+
if matches!(item.kind, ItemKind::Mod(_));
67+
if is_test_module_or_function(cx.tcx, item);
68+
then {
69+
self.test_mod_exists = true;
70+
}
71+
}
72+
});
73+
}
74+
75+
fn check_fn(
76+
&mut self,
77+
cx: &LateContext<'_>,
78+
kind: FnKind<'_>,
79+
_: &FnDecl<'_>,
80+
body: &Body<'_>,
81+
sp: Span,
82+
_: LocalDefId,
83+
) {
84+
if_chain! {
85+
if !matches!(kind, FnKind::Closure);
86+
if self.test_mod_exists;
87+
if is_in_test_function(cx.tcx, body.id().hir_id);
88+
if !is_in_cfg_test(cx.tcx, body.id().hir_id);
89+
then {
90+
span_lint_and_note(
91+
cx,
92+
TESTS_OUTSIDE_TEST_MODULE,
93+
sp,
94+
"this function marked with #[test] is outside a #[cfg(test)] module",
95+
None,
96+
"move it to a testing module marked with #[cfg(test)]",
97+
);
98+
}
99+
}
100+
}
101+
}

tests/ui/tests_outside_test_module.rs

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
// compile-flags: --test
2+
#![allow(unused)]
3+
#![warn(clippy::tests_outside_test_module)]
4+
5+
fn main() {
6+
// test code goes here
7+
}
8+
9+
// Should lint
10+
#[test]
11+
fn my_test() {}
12+
13+
#[cfg(test)]
14+
mod tests {
15+
// Should not lint
16+
#[test]
17+
fn my_test() {}
18+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
error: this function marked with #[test] is outside a #[cfg(test)] module
2+
--> $DIR/tests_outside_test_module.rs:11:1
3+
|
4+
LL | fn my_test() {}
5+
| ^^^^^^^^^^^^^^^
6+
|
7+
= note: move it to a testing module marked with #[cfg(test)]
8+
= note: `-D clippy::tests-outside-test-module` implied by `-D warnings`
9+
10+
error: aborting due to previous error
11+

0 commit comments

Comments
 (0)