| use clippy_utils::diagnostics::span_lint_hir_and_then; |
| use clippy_utils::source::snippet_opt; |
| use clippy_utils::{fulfill_or_allowed, is_cfg_test, is_from_proc_macro}; |
| use rustc_errors::{Applicability, SuggestionStyle}; |
| use rustc_hir::{HirId, Item, ItemKind, Mod}; |
| use rustc_lint::{LateContext, LateLintPass}; |
| use rustc_session::{declare_lint_pass, declare_tool_lint}; |
| use rustc_span::hygiene::AstPass; |
| use rustc_span::{sym, ExpnKind}; |
| |
| 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 |
| /// ```no_run |
| /// #[cfg(test)] |
| /// mod tests { |
| /// // [...] |
| /// } |
| /// |
| /// fn my_function() { |
| /// // [...] |
| /// } |
| /// ``` |
| /// Use instead: |
| /// ```no_run |
| /// fn my_function() { |
| /// // [...] |
| /// } |
| /// |
| /// #[cfg(test)] |
| /// mod tests { |
| /// // [...] |
| /// } |
| /// ``` |
| #[clippy::version = "1.71.0"] |
| pub ITEMS_AFTER_TEST_MODULE, |
| style, |
| "An item was found after the testing module `tests`" |
| } |
| |
| declare_lint_pass!(ItemsAfterTestModule => [ITEMS_AFTER_TEST_MODULE]); |
| |
| fn cfg_test_module<'tcx>(cx: &LateContext<'tcx>, item: &Item<'tcx>) -> bool { |
| if let ItemKind::Mod(test_mod) = item.kind |
| && item.span.hi() == test_mod.spans.inner_span.hi() |
| && is_cfg_test(cx.tcx, item.hir_id()) |
| && !item.span.from_expansion() |
| && !is_from_proc_macro(cx, item) |
| { |
| true |
| } else { |
| false |
| } |
| } |
| |
| impl LateLintPass<'_> for ItemsAfterTestModule { |
| fn check_mod(&mut self, cx: &LateContext<'_>, module: &Mod<'_>, _: HirId) { |
| let mut items = module.item_ids.iter().map(|&id| cx.tcx.hir().item(id)); |
| |
| let Some((mod_pos, test_mod)) = items.by_ref().enumerate().find(|(_, item)| cfg_test_module(cx, item)) else { |
| return; |
| }; |
| |
| let after: Vec<_> = items |
| .filter(|item| { |
| // Ignore the generated test main function |
| !(item.ident.name == sym::main |
| && item.span.ctxt().outer_expn_data().kind == ExpnKind::AstPass(AstPass::TestHarness)) |
| }) |
| .collect(); |
| |
| if let Some(last) = after.last() |
| && after.iter().all(|&item| { |
| !matches!(item.kind, ItemKind::Mod(_)) && !item.span.from_expansion() && !is_from_proc_macro(cx, item) |
| }) |
| && !fulfill_or_allowed(cx, ITEMS_AFTER_TEST_MODULE, after.iter().map(|item| item.hir_id())) |
| { |
| let def_spans: Vec<_> = std::iter::once(test_mod.owner_id) |
| .chain(after.iter().map(|item| item.owner_id)) |
| .map(|id| cx.tcx.def_span(id)) |
| .collect(); |
| |
| span_lint_hir_and_then( |
| cx, |
| ITEMS_AFTER_TEST_MODULE, |
| test_mod.hir_id(), |
| def_spans, |
| "items after a test module", |
| |diag| { |
| if let Some(prev) = mod_pos.checked_sub(1) |
| && let prev = cx.tcx.hir().item(module.item_ids[prev]) |
| && let items_span = last.span.with_lo(test_mod.span.hi()) |
| && let Some(items) = snippet_opt(cx, items_span) |
| { |
| diag.multipart_suggestion_with_style( |
| "move the items to before the test module was defined", |
| vec![(prev.span.shrink_to_hi(), items), (items_span, String::new())], |
| Applicability::MachineApplicable, |
| SuggestionStyle::HideCodeAlways, |
| ); |
| } |
| }, |
| ); |
| } |
| } |
| } |