-
Notifications
You must be signed in to change notification settings - Fork 1.6k
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
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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) { | ||
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) */); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Will this ever cause a panic? i.e. will There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. There was a problem hiding this comment. Choose a reason for hiding this commentThe 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):
In an independent crate using this command: |
||
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); | ||
} | ||
} | ||
} | ||
} | ||
} | ||
} | ||
} |
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 { | ||
() => {}; | ||
} |
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 | ||
|
There was a problem hiding this comment.
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 askip_while
iterator onitems
iter.