From 85e7f5964dda9bff5edebe20e8ea556b499fc766 Mon Sep 17 00:00:00 2001 From: Johannes Altmanninger Date: Thu, 23 Oct 2025 19:55:18 +0200 Subject: [PATCH 1/4] hir-def attr: reuse cfgs() It's even inlined though that probably only makes a difference in cross-crate calls. --- crates/hir-def/src/attr.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/hir-def/src/attr.rs b/crates/hir-def/src/attr.rs index b4fcfa11aea7..f5a46a821e83 100644 --- a/crates/hir-def/src/attr.rs +++ b/crates/hir-def/src/attr.rs @@ -180,7 +180,7 @@ impl Attrs { #[inline] pub fn cfg(&self) -> Option { - let mut cfgs = self.by_key(sym::cfg).tt_values().map(CfgExpr::parse); + let mut cfgs = self.cfgs(); let first = cfgs.next()?; match cfgs.next() { Some(second) => { From e00ee90bf03434536a08fe2f9544bf40532d8aaa Mon Sep 17 00:00:00 2001 From: Johannes Altmanninger Date: Thu, 23 Oct 2025 19:55:18 +0200 Subject: [PATCH 2/4] ide-db search: extract predicate for test functions Helps a following commit. --- crates/ide-db/src/search.rs | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/crates/ide-db/src/search.rs b/crates/ide-db/src/search.rs index f1d076e874d5..4ee4fc6af134 100644 --- a/crates/ide-db/src/search.rs +++ b/crates/ide-db/src/search.rs @@ -1370,8 +1370,11 @@ fn is_name_ref_in_import(name_ref: &ast::NameRef) -> bool { } fn is_name_ref_in_test(sema: &Semantics<'_, RootDatabase>, name_ref: &ast::NameRef) -> bool { - name_ref.syntax().ancestors().any(|node| match ast::Fn::cast(node) { - Some(it) => sema.to_def(&it).is_some_and(|func| func.is_test(sema.db)), - None => false, - }) + name_ref.syntax().ancestors().any(|node|is_test_function(sema, &node)) +} + +/// Returns true if the node is a function with the `#[test]` attribute. +fn is_test_function(sema: &Semantics<'_, RootDatabase>, node: &syntax::SyntaxNode) -> bool { + ast::Fn::cast(node.clone()) + .is_some_and(|func| sema.to_def(&func).is_some_and(|func| func.is_test(sema.db))) } From 10d8d66534c42eb26e02cffda11b53340e53f02b Mon Sep 17 00:00:00 2001 From: Johannes Altmanninger Date: Thu, 23 Oct 2025 19:55:18 +0200 Subject: [PATCH 3/4] ide-db search: extract function for detecting cfg(test) attribute For the next commit. While at it, handle #[cfg(all(test,foo))]. Also take the attribute by reference since we don't need to consume it. Not sure if that's the convention here. --- Cargo.lock | 1 + crates/ide-db/Cargo.toml | 1 + crates/ide-db/src/search.rs | 21 ++++++++++++++++++--- crates/ide/src/runnables.rs | 19 ++++++------------- 4 files changed, 26 insertions(+), 16 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index b557b10e5c77..269a7d21844a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -969,6 +969,7 @@ dependencies = [ "arrayvec", "base-db", "bitflags 2.9.4", + "cfg", "cov-mark", "crossbeam-channel", "either", diff --git a/crates/ide-db/Cargo.toml b/crates/ide-db/Cargo.toml index b7148160182c..10cecbcc8a57 100644 --- a/crates/ide-db/Cargo.toml +++ b/crates/ide-db/Cargo.toml @@ -14,6 +14,7 @@ doctest = false [dependencies] cov-mark = "2.0.0" +cfg.workspace = true crossbeam-channel.workspace = true tracing.workspace = true rayon.workspace = true diff --git a/crates/ide-db/src/search.rs b/crates/ide-db/src/search.rs index 4ee4fc6af134..01fb5df95ac7 100644 --- a/crates/ide-db/src/search.rs +++ b/crates/ide-db/src/search.rs @@ -10,9 +10,9 @@ use std::{cell::LazyCell, cmp::Reverse}; use base_db::{RootQueryDb, SourceDatabase}; use either::Either; use hir::{ - Adt, AsAssocItem, DefWithBody, EditionedFileId, FileRange, FileRangeWrapper, HasAttrs, - HasContainer, HasSource, InFile, InFileWrapper, InRealFile, InlineAsmOperand, ItemContainer, - ModuleSource, PathResolution, Semantics, Visibility, sym, + Adt, AsAssocItem, AttrsWithOwner, CfgExpr, DefWithBody, EditionedFileId, FileRange, + FileRangeWrapper, HasAttrs, HasContainer, HasSource, InFile, InFileWrapper, InRealFile, + InlineAsmOperand, ItemContainer, ModuleSource, PathResolution, Semantics, Visibility, sym, }; use memchr::memmem::Finder; use parser::SyntaxKind; @@ -1378,3 +1378,18 @@ fn is_test_function(sema: &Semantics<'_, RootDatabase>, node: &syntax::SyntaxNod ast::Fn::cast(node.clone()) .is_some_and(|func| sema.to_def(&func).is_some_and(|func| func.is_test(sema.db))) } + +/// Returns true if the given attributes enable code only in test configurations. +pub fn has_cfg_test(attrs: &AttrsWithOwner) -> bool { + fn is_cfg_test(cfg_expr: &CfgExpr) -> bool { + use CfgExpr::*; + use cfg::CfgAtom; + match cfg_expr { + Atom(CfgAtom::Flag(flag)) => *flag == sym::test, + All(exprs) => exprs.iter().any(is_cfg_test), + // N.B. possible false negatives here. + Invalid | Atom(CfgAtom::KeyValue { .. }) | Any(_) | Not(_) => false, + } + } + attrs.cfgs().any(|cfg_expr| is_cfg_test(&cfg_expr)) +} diff --git a/crates/ide/src/runnables.rs b/crates/ide/src/runnables.rs index 494701d97def..eccec1ae8dd8 100644 --- a/crates/ide/src/runnables.rs +++ b/crates/ide/src/runnables.rs @@ -2,11 +2,8 @@ use std::{fmt, sync::OnceLock}; use arrayvec::ArrayVec; use ast::HasName; -use cfg::{CfgAtom, CfgExpr}; -use hir::{ - AsAssocItem, AttrsWithOwner, HasAttrs, HasCrate, HasSource, Semantics, Symbol, db::HirDatabase, - sym, -}; +use cfg::CfgExpr; +use hir::{AsAssocItem, HasAttrs, HasCrate, HasSource, Semantics, Symbol, db::HirDatabase}; use ide_assists::utils::{has_test_related_attribute, test_related_attribute_syn}; use ide_db::impl_empty_upmap_from_ra_fixture; use ide_db::{ @@ -15,7 +12,7 @@ use ide_db::{ defs::Definition, documentation::docs_from_attrs, helpers::visit_file_defs, - search::{FileReferenceNode, SearchScope}, + search::{FileReferenceNode, SearchScope, has_cfg_test}, }; use itertools::Itertools; use macros::UpmapFromRaFixture; @@ -323,7 +320,7 @@ pub(crate) fn runnable_fn( def: hir::Function, ) -> Option { let edition = def.krate(sema.db).edition(sema.db); - let under_cfg_test = has_cfg_test(def.module(sema.db).attrs(sema.db)); + let under_cfg_test = has_cfg_test(&def.module(sema.db).attrs(sema.db)); let kind = if !under_cfg_test && def.is_main(sema.db) { RunnableKind::Bin } else { @@ -366,7 +363,7 @@ pub(crate) fn runnable_mod( sema: &Semantics<'_, RootDatabase>, def: hir::Module, ) -> Option { - if !has_test_function_or_multiple_test_submodules(sema, &def, has_cfg_test(def.attrs(sema.db))) + if !has_test_function_or_multiple_test_submodules(sema, &def, has_cfg_test(&def.attrs(sema.db))) { return None; } @@ -442,10 +439,6 @@ pub(crate) fn runnable_impl( }) } -fn has_cfg_test(attrs: AttrsWithOwner) -> bool { - attrs.cfgs().any(|cfg| matches!(&cfg, CfgExpr::Atom(CfgAtom::Flag(s)) if *s == sym::test)) -} - /// Creates a test mod runnable for outline modules at the top of their definition. fn runnable_mod_outline_definition( sema: &Semantics<'_, RootDatabase>, @@ -453,7 +446,7 @@ fn runnable_mod_outline_definition( ) -> Option { def.as_source_file_id(sema.db)?; - if !has_test_function_or_multiple_test_submodules(sema, &def, has_cfg_test(def.attrs(sema.db))) + if !has_test_function_or_multiple_test_submodules(sema, &def, has_cfg_test(&def.attrs(sema.db))) { return None; } From 44dd5b89f29eb2b07e852ac1ca44d3a9d1905345 Mon Sep 17 00:00:00 2001 From: Johannes Altmanninger Date: Thu, 23 Oct 2025 19:55:18 +0200 Subject: [PATCH 4/4] references.excludeTests to exclude #[cfg(test)]-gated refs The references.excludeTests setting fails to exclude references from test code in various scenarios. For example when using textDocument/references on "foo" in pub fn foo() {} #[cfg(test)] mod tests { use crate::foo; // Should be excluded } same when the test module lives in a different file: #[cfg(test)] mod tests; Try to fix that for most real-world cases. TODO: the unit test does not actually test this because whereas full rust-analyzer finds references in code that's disabled due to #[cfg(test)] directives, the test logic doesn't; "ModuleData::children" is always empty. Would appreciate help with that. changelog fix Closes #18573 --- crates/ide-db/src/search.rs | 53 +++++++++++++++++++++++++++++++++++- crates/ide/src/references.rs | 22 ++++++++++++--- 2 files changed, 70 insertions(+), 5 deletions(-) diff --git a/crates/ide-db/src/search.rs b/crates/ide-db/src/search.rs index 01fb5df95ac7..41a2ea8fe0b9 100644 --- a/crates/ide-db/src/search.rs +++ b/crates/ide-db/src/search.rs @@ -1370,7 +1370,11 @@ fn is_name_ref_in_import(name_ref: &ast::NameRef) -> bool { } fn is_name_ref_in_test(sema: &Semantics<'_, RootDatabase>, name_ref: &ast::NameRef) -> bool { - name_ref.syntax().ancestors().any(|node|is_test_function(sema, &node)) + name_ref + .syntax() + .ancestors() + .any(|node| is_test_function(sema, &node) || node_has_cfg_test(sema, &node)) + || current_module_is_declared_in_cfg_test(sema, name_ref) } /// Returns true if the node is a function with the `#[test]` attribute. @@ -1379,6 +1383,42 @@ fn is_test_function(sema: &Semantics<'_, RootDatabase>, node: &syntax::SyntaxNod .is_some_and(|func| sema.to_def(&func).is_some_and(|func| func.is_test(sema.db))) } +/// Returns true if the node is only enabled in test configurations. +fn node_has_cfg_test(sema: &Semantics<'_, RootDatabase>, node: &syntax::SyntaxNode) -> bool { + macro_rules! attrs { + ($it:expr) => { + sema.to_def($it).map(|node| node.attrs(sema.db)) + }; + } + use ast::*; + let attrs = match_ast! { + match node { + Adt(it) => attrs!(&it), + Const(it) => attrs!(&it), + ConstParam(it) => attrs!(&it), + Enum(it) => attrs!(&it), + ExternCrate(it) => attrs!(&it), + Fn(it) => attrs!(&it), + GenericParam(it) => attrs!(&it), + Impl(it) => attrs!(&it), + LifetimeParam(it) => attrs!(&it), + Macro(it) => attrs!(&it), + Module(it) => attrs!(&it), + RecordField(it) => attrs!(&it), + SourceFile(it) => attrs!(&it), + Static(it) => attrs!(&it), + Struct(it) => attrs!(&it), + Trait(it) => attrs!(&it), + TypeAlias(it) => attrs!(&it), + TypeParam(it) => attrs!(&it), + Union(it) => attrs!(&it), + Variant(it) => attrs!(&it), + _ => None, + } + }; + attrs.as_ref().is_some_and(has_cfg_test) +} + /// Returns true if the given attributes enable code only in test configurations. pub fn has_cfg_test(attrs: &AttrsWithOwner) -> bool { fn is_cfg_test(cfg_expr: &CfgExpr) -> bool { @@ -1393,3 +1433,14 @@ pub fn has_cfg_test(attrs: &AttrsWithOwner) -> bool { } attrs.cfgs().any(|cfg_expr| is_cfg_test(&cfg_expr)) } + +/// Returns true if this reference's enclosing module is declared conditional on `cfg(test)`. +/// E.g. it's declared like `#[cfg(test)] mod tests;` in its parent module. +fn current_module_is_declared_in_cfg_test( + sema: &Semantics<'_, RootDatabase>, + name_ref: &ast::NameRef, +) -> bool { + let file_id = sema.original_range(name_ref.syntax()).file_id; + sema.file_to_module_def(file_id.file_id(sema.db)) + .is_some_and(|mod_def| has_cfg_test(&mod_def.attrs(sema.db))) +} diff --git a/crates/ide/src/references.rs b/crates/ide/src/references.rs index a53a19299727..64759e1f92b1 100644 --- a/crates/ide/src/references.rs +++ b/crates/ide/src/references.rs @@ -467,7 +467,7 @@ mod tests { fn exclude_tests() { check( r#" -fn test_func() {} +pub fn test_func() {} fn func() { test_func$0(); @@ -477,12 +477,26 @@ fn func() { fn test() { test_func(); } +#[cfg(test)] +fn cfg_test_fn() { + test_func(); +} +#[cfg(test)] +mod cfg_test_mod { + use super::test_func; + fn test() { + test_func(); + } +} "#, + // TODO The last two should be "import test" and "test". expect![[r#" - test_func Function FileId(0) 0..17 3..12 + test_func Function FileId(0) 0..21 7..16 - FileId(0) 35..44 - FileId(0) 75..84 test + FileId(0) 39..48 + FileId(0) 79..88 test + FileId(0) 130..139 + FileId(0) 227..236 "#]], );