Skip to content

Commit 1eee413

Browse files
committed
Fix items_after_test_module for non root modules, add applicable suggestion
1 parent 331d01e commit 1eee413

File tree

11 files changed

+177
-59
lines changed

11 files changed

+177
-59
lines changed
Lines changed: 68 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
1-
use clippy_utils::diagnostics::span_lint_and_help;
2-
use clippy_utils::{is_from_proc_macro, is_in_cfg_test};
3-
use rustc_hir::{HirId, ItemId, ItemKind, Mod};
4-
use rustc_lint::{LateContext, LateLintPass, LintContext};
5-
use rustc_middle::lint::in_external_macro;
1+
use clippy_utils::diagnostics::span_lint_hir_and_then;
2+
use clippy_utils::source::snippet_opt;
3+
use clippy_utils::{fulfill_or_allowed, is_cfg_test, is_from_proc_macro};
4+
use rustc_errors::{Applicability, SuggestionStyle};
5+
use rustc_hir::{HirId, Item, ItemKind, Mod};
6+
use rustc_lint::{LateContext, LateLintPass};
67
use rustc_session::{declare_lint_pass, declare_tool_lint};
7-
use rustc_span::{sym, Span};
8+
use rustc_span::hygiene::AstPass;
9+
use rustc_span::{sym, ExpnKind};
810

911
declare_clippy_lint! {
1012
/// ### What it does
@@ -41,46 +43,72 @@ declare_clippy_lint! {
4143

4244
declare_lint_pass!(ItemsAfterTestModule => [ITEMS_AFTER_TEST_MODULE]);
4345

44-
impl LateLintPass<'_> for ItemsAfterTestModule {
45-
fn check_mod(&mut self, cx: &LateContext<'_>, _: &Mod<'_>, _: HirId) {
46-
let mut was_test_mod_visited = false;
47-
let mut test_mod_span: Option<Span> = None;
46+
fn cfg_test_module<'tcx>(cx: &LateContext<'tcx>, item: &Item<'tcx>) -> bool {
47+
if let ItemKind::Mod(test_mod) = item.kind
48+
&& item.span.hi() == test_mod.spans.inner_span.hi()
49+
&& is_cfg_test(cx.tcx, item.hir_id())
50+
&& !item.span.from_expansion()
51+
&& !is_from_proc_macro(cx, item)
52+
{
53+
true
54+
} else {
55+
false
56+
}
57+
}
4858

49-
let hir = cx.tcx.hir();
50-
let items = hir.items().collect::<Vec<ItemId>>();
59+
impl LateLintPass<'_> for ItemsAfterTestModule {
60+
fn check_mod(&mut self, cx: &LateContext<'_>, r#mod: &Mod<'_>, _: HirId) {
61+
let mut items = r#mod.item_ids.iter().map(|&id| cx.tcx.hir().item(id));
5162

52-
for (i, itid) in items.iter().enumerate() {
53-
let item = hir.item(*itid);
63+
let Some((mod_pos, test_mod)) = items.by_ref().enumerate().find(|(_, item)| cfg_test_module(cx, item)) else {
64+
return;
65+
};
5466

55-
if_chain! {
56-
if was_test_mod_visited;
57-
if i == (items.len() - 3 /* Weird magic number (HIR-translation behaviour) */);
58-
if cx.sess().source_map().lookup_char_pos(item.span.lo()).file.name_hash
59-
== cx.sess().source_map().lookup_char_pos(test_mod_span.unwrap().lo()).file.name_hash; // Will never fail
60-
if !matches!(item.kind, ItemKind::Mod(_));
61-
if !is_in_cfg_test(cx.tcx, itid.hir_id()); // The item isn't in the testing module itself
62-
if !in_external_macro(cx.sess(), item.span);
63-
if !is_from_proc_macro(cx, item);
67+
let after: Vec<_> = items
68+
.filter(|item| {
69+
// Ignore the generated test main function
70+
!(item.ident.name == sym::main
71+
&& item.span.ctxt().outer_expn_data().kind == ExpnKind::AstPass(AstPass::TestHarness))
72+
})
73+
.collect();
6474

65-
then {
66-
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");
67-
}};
75+
if let Some(last) = after.last()
76+
&& after.iter().all(|&item| {
77+
!matches!(item.kind, ItemKind::Mod(_))
78+
&& !item.span.from_expansion()
79+
&& !is_from_proc_macro(cx, item)
80+
})
81+
&& !fulfill_or_allowed(cx, ITEMS_AFTER_TEST_MODULE, after.iter().map(|item| item.hir_id()))
82+
{
83+
let def_spans: Vec<_> = std::iter::once(test_mod.owner_id)
84+
.chain(after.iter().map(|item| item.owner_id))
85+
.map(|id| cx.tcx.def_span(id))
86+
.collect();
6887

69-
if let ItemKind::Mod(module) = item.kind && item.span.hi() == module.spans.inner_span.hi() {
70-
// Check that it works the same way, the only I way I've found for #10713
71-
for attr in cx.tcx.get_attrs(item.owner_id.to_def_id(), sym::cfg) {
72-
if_chain! {
73-
if attr.has_name(sym::cfg);
74-
if let Some(mitems) = attr.meta_item_list();
75-
if let [mitem] = &*mitems;
76-
if mitem.has_name(sym::test);
77-
then {
78-
was_test_mod_visited = true;
79-
test_mod_span = Some(item.span);
80-
}
88+
span_lint_hir_and_then(
89+
cx,
90+
ITEMS_AFTER_TEST_MODULE,
91+
test_mod.hir_id(),
92+
def_spans,
93+
"items after a test module",
94+
|diag| {
95+
if let Some(prev) = mod_pos.checked_sub(1)
96+
&& let prev = cx.tcx.hir().item(r#mod.item_ids[prev])
97+
&& let items_span = last.span.with_lo(test_mod.span.hi())
98+
&& let Some(items) = snippet_opt(cx, items_span)
99+
{
100+
diag.multipart_suggestion_with_style(
101+
"move the items to before the test module was defined",
102+
vec![
103+
(prev.span.shrink_to_hi(), items),
104+
(items_span, String::new())
105+
],
106+
Applicability::MachineApplicable,
107+
SuggestionStyle::HideCodeAlways,
108+
);
81109
}
82-
}
83-
}
110+
},
111+
);
84112
}
85113
}
86114
}

clippy_utils/src/lib.rs

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,6 @@ use std::sync::{Mutex, MutexGuard, OnceLock};
8080
use if_chain::if_chain;
8181
use itertools::Itertools;
8282
use rustc_ast::ast::{self, LitKind, RangeLimits};
83-
use rustc_ast::Attribute;
8483
use rustc_data_structures::fx::FxHashMap;
8584
use rustc_data_structures::unhash::UnhashMap;
8685
use rustc_hir::def::{DefKind, Res};
@@ -2452,11 +2451,12 @@ pub fn is_in_test_function(tcx: TyCtxt<'_>, id: hir::HirId) -> bool {
24522451
})
24532452
}
24542453

2455-
/// Checks if the item containing the given `HirId` has `#[cfg(test)]` attribute applied
2454+
/// Checks if `id` has a `#[cfg(test)]` attribute applied
24562455
///
2457-
/// Note: Add `//@compile-flags: --test` to UI tests with a `#[cfg(test)]` function
2458-
pub fn is_in_cfg_test(tcx: TyCtxt<'_>, id: hir::HirId) -> bool {
2459-
fn is_cfg_test(attr: &Attribute) -> bool {
2456+
/// This only checks directly applied attributes, to see if a node is inside a `#[cfg(test)]` parent
2457+
/// use [`is_in_cfg_test`]
2458+
pub fn is_cfg_test(tcx: TyCtxt<'_>, id: hir::HirId) -> bool {
2459+
tcx.hir().attrs(id).iter().any(|attr| {
24602460
if attr.has_name(sym::cfg)
24612461
&& let Some(items) = attr.meta_item_list()
24622462
&& let [item] = &*items
@@ -2466,11 +2466,14 @@ pub fn is_in_cfg_test(tcx: TyCtxt<'_>, id: hir::HirId) -> bool {
24662466
} else {
24672467
false
24682468
}
2469-
}
2469+
})
2470+
}
2471+
2472+
/// Checks if the item containing the given `HirId` has `#[cfg(test)]` attribute applied
2473+
pub fn is_in_cfg_test(tcx: TyCtxt<'_>, id: hir::HirId) -> bool {
24702474
tcx.hir()
2471-
.parent_iter(id)
2472-
.flat_map(|(parent_id, _)| tcx.hir().attrs(parent_id))
2473-
.any(is_cfg_test)
2475+
.parent_id_iter(id)
2476+
.any(|parent_id| is_cfg_test(tcx, parent_id))
24742477
}
24752478

24762479
/// Checks if the item of any of its parents has `#[cfg(...)]` attribute applied.
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
//@aux-build:../auxiliary/proc_macros.rs
2+
extern crate proc_macros;
3+
4+
proc_macros::with_span! {
5+
span
6+
#[cfg(test)]
7+
mod tests {}
8+
}
9+
10+
#[test]
11+
fn f() {}
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
#[cfg(test)]
2+
mod tests {}
3+
4+
fn in_submodule() {}

tests/ui/items_after_test_module/block_module.stderr

Lines changed: 0 additions & 2 deletions
This file was deleted.
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
#[path = "auxiliary/submodule.rs"]
2+
mod submodule;
3+
4+
#[cfg(test)]
5+
mod tests {
6+
#[test]
7+
fn t() {}
8+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
error: items after a test module
2+
--> $DIR/auxiliary/submodule.rs:2:1
3+
|
4+
LL | mod tests {}
5+
| ^^^^^^^^^
6+
LL |
7+
LL | fn in_submodule() {}
8+
| ^^^^^^^^^^^^^^^^^
9+
|
10+
= note: `-D clippy::items-after-test-module` implied by `-D warnings`
11+
= help: to override `-D warnings` add `#[allow(clippy::items_after_test_module)]`
12+
13+
error: aborting due to previous error
14+
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
#[cfg(test)]
2+
mod tests {
3+
#[test]
4+
fn f() {}
5+
}
6+
7+
#[cfg(test)]
8+
mod more_tests {
9+
#[test]
10+
fn g() {}
11+
}
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,17 @@
1-
//@compile-flags: --test
21
#![allow(unused)]
32
#![warn(clippy::items_after_test_module)]
43

54
fn main() {}
65

76
fn should_not_lint() {}
87

8+
fn should_lint() {}
9+
10+
const SHOULD_ALSO_LINT: usize = 1;
11+
macro_rules! should_lint {
12+
() => {};
13+
}
14+
915
#[allow(dead_code)]
1016
#[allow(unused)] // Some attributes to check that span replacement is good enough
1117
#[allow(clippy::allow_attributes)]
@@ -14,10 +20,3 @@ mod tests {
1420
#[test]
1521
fn hi() {}
1622
}
17-
18-
fn should_lint() {}
19-
20-
const SHOULD_ALSO_LINT: usize = 1;
21-
macro_rules! should_not_lint {
22-
() => {};
23-
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
#![allow(unused)]
2+
#![warn(clippy::items_after_test_module)]
3+
4+
fn main() {}
5+
6+
fn should_not_lint() {}
7+
8+
#[allow(dead_code)]
9+
#[allow(unused)] // Some attributes to check that span replacement is good enough
10+
#[allow(clippy::allow_attributes)]
11+
#[cfg(test)]
12+
mod tests {
13+
#[test]
14+
fn hi() {}
15+
}
16+
17+
fn should_lint() {}
18+
19+
const SHOULD_ALSO_LINT: usize = 1;
20+
macro_rules! should_lint {
21+
() => {};
22+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
error: items after a test module
2+
--> $DIR/root_module.rs:12:1
3+
|
4+
LL | mod tests {
5+
| ^^^^^^^^^
6+
...
7+
LL | fn should_lint() {}
8+
| ^^^^^^^^^^^^^^^^
9+
LL |
10+
LL | const SHOULD_ALSO_LINT: usize = 1;
11+
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
12+
LL | macro_rules! should_lint {
13+
| ^^^^^^^^^^^^^^^^^^^^^^^^
14+
|
15+
= note: `-D clippy::items-after-test-module` implied by `-D warnings`
16+
= help: to override `-D warnings` add `#[allow(clippy::items_after_test_module)]`
17+
= help: move the items to before the test module was defined
18+
19+
error: aborting due to previous error
20+

0 commit comments

Comments
 (0)