Skip to content

Commit f8a4553

Browse files
committed
Auto merge of rust-lang#17014 - Veykril:runnables-exported-main-test, r=Veykril
fix: Consider `exported_name="main"` functions in test modules as tests Fixes rust-lang/rust-analyzer#17011
2 parents 8e581ac + 5957835 commit f8a4553

File tree

6 files changed

+94
-22
lines changed

6 files changed

+94
-22
lines changed

crates/hir-def/src/attr.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,10 @@ impl Attrs {
141141
}
142142
}
143143

144+
pub fn cfgs(&self) -> impl Iterator<Item = CfgExpr> + '_ {
145+
self.by_key("cfg").tt_values().map(CfgExpr::parse)
146+
}
147+
144148
pub(crate) fn is_cfg_enabled(&self, cfg_options: &CfgOptions) -> bool {
145149
match self.cfg() {
146150
None => true,

crates/hir/src/lib.rs

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2006,12 +2006,15 @@ impl Function {
20062006

20072007
/// is this a `fn main` or a function with an `export_name` of `main`?
20082008
pub fn is_main(self, db: &dyn HirDatabase) -> bool {
2009-
if !self.module(db).is_crate_root() {
2010-
return false;
2011-
}
20122009
let data = db.function_data(self.id);
2010+
data.attrs.export_name() == Some("main")
2011+
|| self.module(db).is_crate_root() && data.name.to_smol_str() == "main"
2012+
}
20132013

2014-
data.name.to_smol_str() == "main" || data.attrs.export_name() == Some("main")
2014+
/// Is this a function with an `export_name` of `main`?
2015+
pub fn exported_main(self, db: &dyn HirDatabase) -> bool {
2016+
let data = db.function_data(self.id);
2017+
data.attrs.export_name() == Some("main")
20152018
}
20162019

20172020
/// Does this function have the ignore attribute?

crates/ide-assists/src/handlers/toggle_ignore.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ use syntax::{
33
AstNode, AstToken,
44
};
55

6-
use crate::{utils::test_related_attribute, AssistContext, AssistId, AssistKind, Assists};
6+
use crate::{utils::test_related_attribute_syn, AssistContext, AssistId, AssistKind, Assists};
77

88
// Assist: toggle_ignore
99
//
@@ -26,7 +26,7 @@ use crate::{utils::test_related_attribute, AssistContext, AssistId, AssistKind,
2626
pub(crate) fn toggle_ignore(acc: &mut Assists, ctx: &AssistContext<'_>) -> Option<()> {
2727
let attr: ast::Attr = ctx.find_node_at_offset()?;
2828
let func = attr.syntax().parent().and_then(ast::Fn::cast)?;
29-
let attr = test_related_attribute(&func)?;
29+
let attr = test_related_attribute_syn(&func)?;
3030

3131
match has_ignore_attribute(&func) {
3232
None => acc.add(

crates/ide-assists/src/utils.rs

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ pub fn extract_trivial_expression(block_expr: &ast::BlockExpr) -> Option<ast::Ex
7171
///
7272
/// It may produce false positives, for example, `#[wasm_bindgen_test]` requires a different command to run the test,
7373
/// but it's better than not to have the runnables for the tests at all.
74-
pub fn test_related_attribute(fn_def: &ast::Fn) -> Option<ast::Attr> {
74+
pub fn test_related_attribute_syn(fn_def: &ast::Fn) -> Option<ast::Attr> {
7575
fn_def.attrs().find_map(|attr| {
7676
let path = attr.path()?;
7777
let text = path.syntax().text().to_string();
@@ -83,6 +83,19 @@ pub fn test_related_attribute(fn_def: &ast::Fn) -> Option<ast::Attr> {
8383
})
8484
}
8585

86+
pub fn has_test_related_attribute(attrs: &hir::AttrsWithOwner) -> bool {
87+
attrs.iter().any(|attr| {
88+
let path = attr.path();
89+
(|| {
90+
Some(
91+
path.segments().first()?.as_text()?.starts_with("test")
92+
|| path.segments().last()?.as_text()?.ends_with("test"),
93+
)
94+
})()
95+
.unwrap_or_default()
96+
})
97+
}
98+
8699
#[derive(Clone, Copy, PartialEq)]
87100
pub enum IgnoreAssocItems {
88101
DocHiddenAttrPresent,

crates/ide/src/annotations/fn_references.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
//! We have to skip tests, so cannot reuse file_structure module.
33
44
use hir::Semantics;
5-
use ide_assists::utils::test_related_attribute;
5+
use ide_assists::utils::test_related_attribute_syn;
66
use ide_db::RootDatabase;
77
use syntax::{ast, ast::HasName, AstNode, SyntaxNode, TextRange};
88

@@ -19,7 +19,7 @@ pub(super) fn find_all_methods(
1919

2020
fn method_range(item: SyntaxNode) -> Option<(TextRange, Option<TextRange>)> {
2121
ast::Fn::cast(item).and_then(|fn_def| {
22-
if test_related_attribute(&fn_def).is_some() {
22+
if test_related_attribute_syn(&fn_def).is_some() {
2323
None
2424
} else {
2525
Some((

crates/ide/src/runnables.rs

Lines changed: 65 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
use std::fmt;
22

33
use ast::HasName;
4-
use cfg::CfgExpr;
5-
use hir::{db::HirDatabase, AsAssocItem, HasAttrs, HasSource, HirFileIdExt, Semantics};
6-
use ide_assists::utils::test_related_attribute;
4+
use cfg::{CfgAtom, CfgExpr};
5+
use hir::{
6+
db::HirDatabase, AsAssocItem, AttrsWithOwner, HasAttrs, HasSource, HirFileIdExt, Semantics,
7+
};
8+
use ide_assists::utils::{has_test_related_attribute, test_related_attribute_syn};
79
use ide_db::{
810
base_db::{FilePosition, FileRange},
911
defs::Definition,
@@ -280,7 +282,7 @@ fn find_related_tests_in_module(
280282
}
281283

282284
fn as_test_runnable(sema: &Semantics<'_, RootDatabase>, fn_def: &ast::Fn) -> Option<Runnable> {
283-
if test_related_attribute(fn_def).is_some() {
285+
if test_related_attribute_syn(fn_def).is_some() {
284286
let function = sema.to_def(fn_def)?;
285287
runnable_fn(sema, function)
286288
} else {
@@ -293,7 +295,7 @@ fn parent_test_module(sema: &Semantics<'_, RootDatabase>, fn_def: &ast::Fn) -> O
293295
let module = ast::Module::cast(node)?;
294296
let module = sema.to_def(&module)?;
295297

296-
if has_test_function_or_multiple_test_submodules(sema, &module) {
298+
if has_test_function_or_multiple_test_submodules(sema, &module, false) {
297299
Some(module)
298300
} else {
299301
None
@@ -305,7 +307,8 @@ pub(crate) fn runnable_fn(
305307
sema: &Semantics<'_, RootDatabase>,
306308
def: hir::Function,
307309
) -> Option<Runnable> {
308-
let kind = if def.is_main(sema.db) {
310+
let under_cfg_test = has_cfg_test(def.module(sema.db).attrs(sema.db));
311+
let kind = if !under_cfg_test && def.is_main(sema.db) {
309312
RunnableKind::Bin
310313
} else {
311314
let test_id = || {
@@ -342,7 +345,8 @@ pub(crate) fn runnable_mod(
342345
sema: &Semantics<'_, RootDatabase>,
343346
def: hir::Module,
344347
) -> Option<Runnable> {
345-
if !has_test_function_or_multiple_test_submodules(sema, &def) {
348+
if !has_test_function_or_multiple_test_submodules(sema, &def, has_cfg_test(def.attrs(sema.db)))
349+
{
346350
return None;
347351
}
348352
let path = def
@@ -384,12 +388,17 @@ pub(crate) fn runnable_impl(
384388
Some(Runnable { use_name_in_title: false, nav, kind: RunnableKind::DocTest { test_id }, cfg })
385389
}
386390

391+
fn has_cfg_test(attrs: AttrsWithOwner) -> bool {
392+
attrs.cfgs().any(|cfg| matches!(cfg, CfgExpr::Atom(CfgAtom::Flag(s)) if s == "test"))
393+
}
394+
387395
/// Creates a test mod runnable for outline modules at the top of their definition.
388396
fn runnable_mod_outline_definition(
389397
sema: &Semantics<'_, RootDatabase>,
390398
def: hir::Module,
391399
) -> Option<Runnable> {
392-
if !has_test_function_or_multiple_test_submodules(sema, &def) {
400+
if !has_test_function_or_multiple_test_submodules(sema, &def, has_cfg_test(def.attrs(sema.db)))
401+
{
393402
return None;
394403
}
395404
let path = def
@@ -522,20 +531,28 @@ fn has_runnable_doc_test(attrs: &hir::Attrs) -> bool {
522531
fn has_test_function_or_multiple_test_submodules(
523532
sema: &Semantics<'_, RootDatabase>,
524533
module: &hir::Module,
534+
consider_exported_main: bool,
525535
) -> bool {
526536
let mut number_of_test_submodules = 0;
527537

528538
for item in module.declarations(sema.db) {
529539
match item {
530540
hir::ModuleDef::Function(f) => {
531-
if let Some(it) = f.source(sema.db) {
532-
if test_related_attribute(&it.value).is_some() {
533-
return true;
534-
}
541+
if has_test_related_attribute(&f.attrs(sema.db)) {
542+
return true;
543+
}
544+
if consider_exported_main && f.exported_main(sema.db) {
545+
// an exported main in a test module can be considered a test wrt to custom test
546+
// runners
547+
return true;
535548
}
536549
}
537550
hir::ModuleDef::Module(submodule) => {
538-
if has_test_function_or_multiple_test_submodules(sema, &submodule) {
551+
if has_test_function_or_multiple_test_submodules(
552+
sema,
553+
&submodule,
554+
consider_exported_main,
555+
) {
539556
number_of_test_submodules += 1;
540557
}
541558
}
@@ -1484,4 +1501,39 @@ mod r#mod {
14841501
"#]],
14851502
)
14861503
}
1504+
1505+
#[test]
1506+
fn exported_main_is_test_in_cfg_test_mod() {
1507+
check(
1508+
r#"
1509+
//- /lib.rs crate:foo cfg:test
1510+
$0
1511+
mod not_a_test_module_inline {
1512+
#[export_name = "main"]
1513+
fn exp_main() {}
1514+
}
1515+
#[cfg(test)]
1516+
mod test_mod_inline {
1517+
#[export_name = "main"]
1518+
fn exp_main() {}
1519+
}
1520+
mod not_a_test_module;
1521+
#[cfg(test)]
1522+
mod test_mod;
1523+
//- /not_a_test_module.rs
1524+
#[export_name = "main"]
1525+
fn exp_main() {}
1526+
//- /test_mod.rs
1527+
#[export_name = "main"]
1528+
fn exp_main() {}
1529+
"#,
1530+
expect![[r#"
1531+
[
1532+
"(Bin, NavigationTarget { file_id: FileId(0), full_range: 36..80, focus_range: 67..75, name: \"exp_main\", kind: Function })",
1533+
"(TestMod, NavigationTarget { file_id: FileId(0), full_range: 83..168, focus_range: 100..115, name: \"test_mod_inline\", kind: Module, description: \"mod test_mod_inline\" }, Atom(Flag(\"test\")))",
1534+
"(TestMod, NavigationTarget { file_id: FileId(0), full_range: 192..218, focus_range: 209..217, name: \"test_mod\", kind: Module, description: \"mod test_mod\" }, Atom(Flag(\"test\")))",
1535+
]
1536+
"#]],
1537+
)
1538+
}
14871539
}

0 commit comments

Comments
 (0)