Skip to content
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

Add items_after_test_module lint #10578

Merged
merged 2 commits into from
Apr 23, 2023
Merged
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 @@ -4730,6 +4730,7 @@ Released 2018-09-13
[`invisible_characters`]: https://rust-lang.github.io/rust-clippy/master/index.html#invisible_characters
[`is_digit_ascii_radix`]: https://rust-lang.github.io/rust-clippy/master/index.html#is_digit_ascii_radix
[`items_after_statements`]: https://rust-lang.github.io/rust-clippy/master/index.html#items_after_statements
[`items_after_test_module`]: https://rust-lang.github.io/rust-clippy/master/index.html#items_after_test_module
[`iter_cloned_collect`]: https://rust-lang.github.io/rust-clippy/master/index.html#iter_cloned_collect
[`iter_count`]: https://rust-lang.github.io/rust-clippy/master/index.html#iter_count
[`iter_kv_map`]: https://rust-lang.github.io/rust-clippy/master/index.html#iter_kv_map
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 @@ -215,6 +215,7 @@ pub(crate) static LINTS: &[&crate::LintInfo] = &[
crate::invalid_upcast_comparisons::INVALID_UPCAST_COMPARISONS_INFO,
crate::invalid_utf8_in_unchecked::INVALID_UTF8_IN_UNCHECKED_INFO,
crate::items_after_statements::ITEMS_AFTER_STATEMENTS_INFO,
crate::items_after_test_module::ITEMS_AFTER_TEST_MODULE_INFO,
crate::iter_not_returning_iterator::ITER_NOT_RETURNING_ITERATOR_INFO,
crate::large_const_arrays::LARGE_CONST_ARRAYS_INFO,
crate::large_enum_variant::LARGE_ENUM_VARIANT_INFO,
Expand Down
83 changes: 83 additions & 0 deletions clippy_lints/src/items_after_test_module.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
use clippy_utils::{diagnostics::span_lint_and_help, is_in_cfg_test};
use rustc_hir::{HirId, ItemId, ItemKind, Mod};
use rustc_lint::{LateContext, LateLintPass, LintContext};
use rustc_middle::lint::in_external_macro;
use rustc_session::{declare_lint_pass, declare_tool_lint};
use rustc_span::{sym, Span};

declare_clippy_lint! {
/// ### What it does
/// Triggers if an item is declared after the testing module marked with `#[cfg(test)]`.
/// ### Why is this bad?
/// Having items declared after the testing module is confusing and may lead to bad test coverage.
/// ### Example
/// ```rust
/// #[cfg(test)]
/// mod tests {
/// // [...]
/// }
///
/// fn my_function() {
/// // [...]
/// }
/// ```
/// Use instead:
/// ```rust
/// fn my_function() {
/// // [...]
/// }
///
/// #[cfg(test)]
/// mod tests {
/// // [...]
/// }
/// ```
#[clippy::version = "1.70.0"]
pub ITEMS_AFTER_TEST_MODULE,
style,
"An item was found after the testing module `tests`"
}

declare_lint_pass!(ItemsAfterTestModule => [ITEMS_AFTER_TEST_MODULE]);

impl LateLintPass<'_> for ItemsAfterTestModule {
fn check_mod(&mut self, cx: &LateContext<'_>, _: &Mod<'_>, _: HirId) {
Copy link
Member

Choose a reason for hiding this comment

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

Some style nit/suggestion dump, feel free to ignore 😅

Maybe we can take out the if_chain check, then do a skip_while iterator on items iter.

let mut was_test_mod_visited = false;
let mut test_mod_span: Option<Span> = None;

let hir = cx.tcx.hir();
let items = hir.items().collect::<Vec<ItemId>>();

for (i, itid) in items.iter().enumerate() {
let item = hir.item(*itid);

if_chain! {
if was_test_mod_visited;
if i == (items.len() - 3 /* Weird magic number (HIR-translation behaviour) */);
Copy link
Member

Choose a reason for hiding this comment

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

Will this ever cause a panic? i.e. will items.len() < 3?

Copy link
Member

Choose a reason for hiding this comment

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

We can use https://doc.rust-lang.org/std/primitive.usize.html#method.checked_sub if we can't guarantee that this won't cause an overflow.

Copy link
Member Author

Choose a reason for hiding this comment

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

Just tested it with this code (The minimum amount of items that meats the requirements for the lint):

#[cfg(test)]
mod test {}

const U: usize = 0;

In an independent crate using this command: cargo dev lint ../<test dir>/ -- -- -W clippy::items_after_test_module --test
Even though the lint, well, lints. It doesn't overflow.

if cx.sess().source_map().lookup_char_pos(item.span.lo()).file.name_hash
== cx.sess().source_map().lookup_char_pos(test_mod_span.unwrap().lo()).file.name_hash; // Will never fail
if !matches!(item.kind, ItemKind::Mod(_));
if !is_in_cfg_test(cx.tcx, itid.hir_id()); // The item isn't in the testing module itself
if !in_external_macro(cx.sess(), item.span);

then {
span_lint_and_help(cx, ITEMS_AFTER_TEST_MODULE, test_mod_span.unwrap().with_hi(item.span.hi()), "items were found after the testing module", None, "move the items to before the testing module was defined");
}};

if matches!(item.kind, ItemKind::Mod(_)) {
for attr in cx.tcx.get_attrs(item.owner_id.to_def_id(), sym::cfg) {
if_chain! {
if attr.has_name(sym::cfg);
if let Some(mitems) = attr.meta_item_list();
if let [mitem] = &*mitems;
if mitem.has_name(sym::test);
then {
was_test_mod_visited = true;
test_mod_span = Some(item.span);
}
}
}
}
}
}
}
2 changes: 2 additions & 0 deletions clippy_lints/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,7 @@ mod int_plus_one;
mod invalid_upcast_comparisons;
mod invalid_utf8_in_unchecked;
mod items_after_statements;
mod items_after_test_module;
mod iter_not_returning_iterator;
mod large_const_arrays;
mod large_enum_variant;
Expand Down Expand Up @@ -961,6 +962,7 @@ pub fn register_plugins(store: &mut rustc_lint::LintStore, sess: &Session, conf:
store.register_late_pass(|_| Box::new(tests_outside_test_module::TestsOutsideTestModule));
store.register_late_pass(|_| Box::new(manual_slice_size_calculation::ManualSliceSizeCalculation));
store.register_early_pass(|| Box::new(suspicious_doc_comments::SuspiciousDocComments));
store.register_late_pass(|_| Box::new(items_after_test_module::ItemsAfterTestModule));
// add lints here, do not remove this comment, it's used in `new_lint`
}

Expand Down
23 changes: 23 additions & 0 deletions tests/ui/items_after_test_module.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
//@compile-flags: --test
#![allow(unused)]
#![warn(clippy::items_after_test_module)]

fn main() {}

fn should_not_lint() {}

#[allow(dead_code)]
#[allow(unused)] // Some attributes to check that span replacement is good enough
#[allow(clippy::allow_attributes)]
#[cfg(test)]
mod tests {
#[test]
fn hi() {}
}

fn should_lint() {}

const SHOULD_ALSO_LINT: usize = 1;
blyxyas marked this conversation as resolved.
Show resolved Hide resolved
macro_rules! should_not_lint {
() => {};
}
17 changes: 17 additions & 0 deletions tests/ui/items_after_test_module.stderr
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
error: items were found after the testing module
--> $DIR/items_after_test_module.rs:13:1
|
LL | / mod tests {
LL | | #[test]
LL | | fn hi() {}
LL | | }
... |
LL | | () => {};
LL | | }
| |_^
dswij marked this conversation as resolved.
Show resolved Hide resolved
|
= help: move the items to before the testing module was defined
= note: `-D clippy::items-after-test-module` implied by `-D warnings`

error: aborting due to previous error