• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 use clippy_utils::{diagnostics::span_lint_and_help, is_from_proc_macro, is_in_cfg_test};
2 use rustc_hir::{HirId, ItemId, ItemKind, Mod};
3 use rustc_lint::{LateContext, LateLintPass, LintContext};
4 use rustc_middle::lint::in_external_macro;
5 use rustc_session::{declare_lint_pass, declare_tool_lint};
6 use rustc_span::{sym, Span};
7 
8 declare_clippy_lint! {
9     /// ### What it does
10     /// Triggers if an item is declared after the testing module marked with `#[cfg(test)]`.
11     /// ### Why is this bad?
12     /// Having items declared after the testing module is confusing and may lead to bad test coverage.
13     /// ### Example
14     /// ```rust
15     /// #[cfg(test)]
16     /// mod tests {
17     ///     // [...]
18     /// }
19     ///
20     /// fn my_function() {
21     ///     // [...]
22     /// }
23     /// ```
24     /// Use instead:
25     /// ```rust
26     /// fn my_function() {
27     ///     // [...]
28     /// }
29     ///
30     /// #[cfg(test)]
31     /// mod tests {
32     ///     // [...]
33     /// }
34     /// ```
35     #[clippy::version = "1.70.0"]
36     pub ITEMS_AFTER_TEST_MODULE,
37     style,
38     "An item was found after the testing module `tests`"
39 }
40 
41 declare_lint_pass!(ItemsAfterTestModule => [ITEMS_AFTER_TEST_MODULE]);
42 
43 impl LateLintPass<'_> for ItemsAfterTestModule {
check_mod(&mut self, cx: &LateContext<'_>, _: &Mod<'_>, _: HirId)44     fn check_mod(&mut self, cx: &LateContext<'_>, _: &Mod<'_>, _: HirId) {
45         let mut was_test_mod_visited = false;
46         let mut test_mod_span: Option<Span> = None;
47 
48         let hir = cx.tcx.hir();
49         let items = hir.items().collect::<Vec<ItemId>>();
50 
51         for (i, itid) in items.iter().enumerate() {
52             let item = hir.item(*itid);
53 
54             if_chain! {
55             if was_test_mod_visited;
56             if i == (items.len() - 3 /* Weird magic number (HIR-translation behaviour) */);
57             if cx.sess().source_map().lookup_char_pos(item.span.lo()).file.name_hash
58             == cx.sess().source_map().lookup_char_pos(test_mod_span.unwrap().lo()).file.name_hash; // Will never fail
59             if !matches!(item.kind, ItemKind::Mod(_));
60             if !is_in_cfg_test(cx.tcx, itid.hir_id()); // The item isn't in the testing module itself
61             if !in_external_macro(cx.sess(), item.span);
62             if !is_from_proc_macro(cx, item);
63 
64             then {
65                 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");
66             }};
67 
68             if let ItemKind::Mod(module) = item.kind && item.span.hi() == module.spans.inner_span.hi() {
69 			// Check that it works the same way, the only I way I've found for #10713
70 				for attr in cx.tcx.get_attrs(item.owner_id.to_def_id(), sym::cfg) {
71 					if_chain! {
72 						if attr.has_name(sym::cfg);
73                         if let Some(mitems) = attr.meta_item_list();
74                         if let [mitem] = &*mitems;
75                         if mitem.has_name(sym::test);
76                         then {
77 							was_test_mod_visited = true;
78                             test_mod_span = Some(item.span);
79                         }
80                     }
81                 }
82 			}
83         }
84     }
85 }
86