diff --git a/clippy_lints/src/tests_outside_test_module.rs b/clippy_lints/src/tests_outside_test_module.rs index 25d0a16e2ab4..46327d171743 100644 --- a/clippy_lints/src/tests_outside_test_module.rs +++ b/clippy_lints/src/tests_outside_test_module.rs @@ -5,7 +5,9 @@ use rustc_hir::{Body, FnDecl}; use rustc_lint::{LateContext, LateLintPass}; use rustc_session::declare_lint_pass; use rustc_span::def_id::LocalDefId; -use rustc_span::Span; +use rustc_span::source_map::SourceMap; +use rustc_span::{FileName, Span}; +use std::path::PathBuf; declare_clippy_lint! { /// ### What it does @@ -15,6 +17,7 @@ declare_clippy_lint! { /// ### Why restrict this? /// The idiomatic (and more performant) way of writing tests is inside a testing module (flagged with `#[cfg(test)]`), /// having test functions outside of this module is confusing and may lead to them being "hidden". + /// This does not apply to integration tests though, and this lint will ignore those. /// /// ### Example /// ```no_run @@ -59,6 +62,7 @@ impl LateLintPass<'_> for TestsOutsideTestModule { ) { if !matches!(kind, FnKind::Closure) && is_in_test_function(cx.tcx, body.id().hir_id) + && !is_integration_test(cx.tcx.sess.source_map(), sp) && !is_in_cfg_test(cx.tcx, body.id().hir_id) { #[expect(clippy::collapsible_span_lint_calls, reason = "rust-clippy#7797")] @@ -74,3 +78,24 @@ impl LateLintPass<'_> for TestsOutsideTestModule { } } } + +fn is_integration_test(sm: &SourceMap, sp: Span) -> bool { + // This part is from https://github.com/rust-lang/rust/blob/a91f7d72f12efcc00ecf71591f066c534d45ddf7/compiler/rustc_expand/src/expand.rs#L402-L409 (fn expand_crate) + // Extract crate base path from the filename path. + let file_path = match sm.span_to_filename(sp) { + FileName::Real(name) => name + .into_local_path() + .expect("attempting to resolve a file path in an external file"), + other => PathBuf::from(other.prefer_local().to_string()), + }; + + // Root path contains the topmost sources directory of the crate. + // I.e., for `project` with sources in `src` and tests in `tests` folders + // (no matter how many nested folders lie inside), + // there will be two different root paths: `/project/src` and `/project/tests`. + let root_path = file_path.parent().unwrap_or(&file_path).to_owned(); + + // The next part matches logic in https://github.com/rust-lang/rust/blob/a91f7d72f12efcc00ecf71591f066c534d45ddf7/compiler/rustc_builtin_macros/src/test.rs#L526 (fn test_type) + // Integration tests are under /tests directory in the crate root_path we determined above. + root_path.ends_with("tests") +} diff --git a/tests/ui/tests_outside_test_module.rs b/tests/ui/tests_outside_test_module.rs index 0abde4a57bf7..d340b7afe988 100644 --- a/tests/ui/tests_outside_test_module.rs +++ b/tests/ui/tests_outside_test_module.rs @@ -5,11 +5,10 @@ fn main() { // test code goes here } -// Should lint +// Should not lint +// Because we're inside an integration test #[test] fn my_test() {} -//~^ ERROR: this function marked with #[test] is outside a #[cfg(test)] module -//~| NOTE: move it to a testing module marked with #[cfg(test)] #[cfg(test)] mod tests { diff --git a/tests/ui/tests_outside_test_module.stderr b/tests/ui/tests_outside_test_module.stderr deleted file mode 100644 index 09feae6bf2aa..000000000000 --- a/tests/ui/tests_outside_test_module.stderr +++ /dev/null @@ -1,12 +0,0 @@ -error: this function marked with #[test] is outside a #[cfg(test)] module - --> tests/ui/tests_outside_test_module.rs:10:1 - | -LL | fn my_test() {} - | ^^^^^^^^^^^^^^^ - | - = note: move it to a testing module marked with #[cfg(test)] - = note: `-D clippy::tests-outside-test-module` implied by `-D warnings` - = help: to override `-D warnings` add `#[allow(clippy::tests_outside_test_module)]` - -error: aborting due to 1 previous error -