rust-analyzer/crates/ra_ide/src/runnables.rs

759 lines
21 KiB
Rust
Raw Normal View History

2020-06-02 15:22:23 +00:00
use std::fmt;
use hir::{AsAssocItem, Attrs, HirFileId, InFile, Semantics};
2019-01-08 19:33:36 +00:00
use itertools::Itertools;
2020-06-02 15:22:23 +00:00
use ra_cfg::CfgExpr;
2020-02-06 11:52:32 +00:00
use ra_ide_db::RootDatabase;
2019-01-08 19:33:36 +00:00
use ra_syntax::{
2020-06-02 15:22:23 +00:00
ast::{self, AstNode, AttrsOwner, DocCommentsOwner, ModuleItemOwner, NameOwner},
match_ast, SyntaxNode,
2019-01-08 19:33:36 +00:00
};
2020-06-02 15:22:23 +00:00
use crate::{display::ToNav, FileId, NavigationTarget};
2019-01-08 19:33:36 +00:00
#[derive(Debug)]
pub struct Runnable {
2020-06-02 15:22:23 +00:00
pub nav: NavigationTarget,
2019-01-08 19:33:36 +00:00
pub kind: RunnableKind,
pub cfg_exprs: Vec<CfgExpr>,
2019-01-08 19:33:36 +00:00
}
#[derive(Debug)]
pub enum TestId {
Name(String),
Path(String),
}
2020-06-02 15:22:23 +00:00
impl fmt::Display for TestId {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
TestId::Name(name) => write!(f, "{}", name),
TestId::Path(path) => write!(f, "{}", path),
}
}
}
2019-01-08 19:33:36 +00:00
#[derive(Debug)]
pub enum RunnableKind {
2020-04-22 20:52:12 +00:00
Test { test_id: TestId, attr: TestAttr },
2019-01-08 19:33:36 +00:00
TestMod { path: String },
Bench { test_id: TestId },
DocTest { test_id: TestId },
2019-01-08 19:33:36 +00:00
Bin,
}
2020-05-31 07:45:41 +00:00
// Feature: Run
//
// Shows a popup suggesting to run a test/benchmark/binary **at the current cursor
// location**. Super useful for repeatedly running just a single test. Do bind this
// to a shortcut!
//
// |===
// | Editor | Action Name
//
// | VS Code | **Rust Analyzer: Run**
// |===
2019-01-15 18:02:42 +00:00
pub(crate) fn runnables(db: &RootDatabase, file_id: FileId) -> Vec<Runnable> {
let sema = Semantics::new(db);
let source_file = sema.parse(file_id);
source_file.syntax().descendants().filter_map(|i| runnable(&sema, i, file_id)).collect()
2019-01-08 19:33:36 +00:00
}
fn runnable(sema: &Semantics<RootDatabase>, item: SyntaxNode, file_id: FileId) -> Option<Runnable> {
2019-10-30 20:09:16 +00:00
match_ast! {
match item {
ast::FnDef(it) => runnable_fn(sema, it, file_id),
ast::Module(it) => runnable_mod(sema, it, file_id),
_ => None,
2019-10-30 20:09:16 +00:00
}
2019-01-08 19:33:36 +00:00
}
}
fn runnable_fn(
sema: &Semantics<RootDatabase>,
fn_def: ast::FnDef,
file_id: FileId,
) -> Option<Runnable> {
let name_string = fn_def.name()?.text().to_string();
let kind = if name_string == "main" {
2019-01-08 19:33:36 +00:00
RunnableKind::Bin
} else {
2020-05-24 13:47:05 +00:00
let test_id = match sema.to_def(&fn_def).map(|def| def.module(sema.db)) {
Some(module) => {
let def = sema.to_def(&fn_def)?;
let impl_trait_name = def.as_assoc_item(sema.db).and_then(|assoc_item| {
match assoc_item.container(sema.db) {
hir::AssocItemContainer::Trait(trait_item) => {
Some(trait_item.name(sema.db).to_string())
}
hir::AssocItemContainer::ImplDef(impl_def) => impl_def
.target_ty(sema.db)
.as_adt()
.map(|adt| adt.name(sema.db).to_string()),
}
});
2020-05-24 13:47:05 +00:00
let path_iter = module
.path_to_root(sema.db)
.into_iter()
.rev()
.filter_map(|it| it.name(sema.db))
.map(|name| name.to_string());
let path = if let Some(impl_trait_name) = impl_trait_name {
path_iter
.chain(std::iter::once(impl_trait_name))
.chain(std::iter::once(name_string))
.join("::")
} else {
path_iter.chain(std::iter::once(name_string)).join("::")
};
TestId::Path(path)
}
None => TestId::Name(name_string),
};
if has_test_related_attribute(&fn_def) {
2020-04-22 20:52:12 +00:00
let attr = TestAttr::from_fn(&fn_def);
RunnableKind::Test { test_id, attr }
} else if fn_def.has_atom_attr("bench") {
RunnableKind::Bench { test_id }
} else if has_doc_test(&fn_def) {
RunnableKind::DocTest { test_id }
} else {
return None;
}
2019-01-08 19:33:36 +00:00
};
let attrs = Attrs::from_attrs_owner(sema.db, InFile::new(HirFileId::from(file_id), &fn_def));
let cfg_exprs =
attrs.by_key("cfg").tt_values().map(|subtree| ra_cfg::parse_cfg(subtree)).collect();
2020-06-02 15:22:23 +00:00
let nav = NavigationTarget::from_named(sema.db, InFile::new(file_id.into(), &fn_def));
Some(Runnable { nav, kind, cfg_exprs })
2019-01-08 19:33:36 +00:00
}
2020-04-22 20:52:12 +00:00
#[derive(Debug)]
pub struct TestAttr {
pub ignore: bool,
}
impl TestAttr {
fn from_fn(fn_def: &ast::FnDef) -> TestAttr {
let ignore = fn_def
.attrs()
.filter_map(|attr| attr.simple_name())
.any(|attribute_text| attribute_text == "ignore");
TestAttr { ignore }
}
}
/// This is a method with a heuristics to support test methods annotated with custom test annotations, such as
/// `#[test_case(...)]`, `#[tokio::test]` and similar.
/// Also a regular `#[test]` annotation is supported.
///
/// It may produce false positives, for example, `#[wasm_bindgen_test]` requires a different command to run the test,
/// but it's better than not to have the runnables for the tests at all.
fn has_test_related_attribute(fn_def: &ast::FnDef) -> bool {
fn_def
.attrs()
.filter_map(|attr| attr.path())
.map(|path| path.syntax().to_string().to_lowercase())
.any(|attribute_text| attribute_text.contains("test"))
}
fn has_doc_test(fn_def: &ast::FnDef) -> bool {
fn_def.doc_comment_text().map_or(false, |comment| comment.contains("```"))
}
fn runnable_mod(
sema: &Semantics<RootDatabase>,
module: ast::Module,
file_id: FileId,
) -> Option<Runnable> {
2019-01-08 19:33:36 +00:00
let has_test_function = module
.item_list()?
.items()
2019-08-19 11:13:58 +00:00
.filter_map(|it| match it {
ast::ModuleItem::FnDef(it) => Some(it),
2019-01-08 19:33:36 +00:00
_ => None,
})
.any(|f| has_test_related_attribute(&f));
2019-01-08 19:33:36 +00:00
if !has_test_function {
return None;
}
let module_def = sema.to_def(&module)?;
let path = module_def
.path_to_root(sema.db)
.into_iter()
.rev()
.filter_map(|it| it.name(sema.db))
.join("::");
let attrs = Attrs::from_attrs_owner(sema.db, InFile::new(HirFileId::from(file_id), &module));
let cfg_exprs =
attrs.by_key("cfg").tt_values().map(|subtree| ra_cfg::parse_cfg(subtree)).collect();
2019-01-08 19:33:36 +00:00
2020-06-02 15:22:23 +00:00
let nav = module_def.to_nav(sema.db);
Some(Runnable { nav, kind: RunnableKind::TestMod { path }, cfg_exprs })
2019-01-08 19:33:36 +00:00
}
2019-01-14 13:27:08 +00:00
#[cfg(test)]
mod tests {
2019-08-29 13:49:10 +00:00
use insta::assert_debug_snapshot;
2019-01-14 13:27:08 +00:00
use crate::mock_analysis::analysis_and_position;
#[test]
fn test_runnables() {
let (analysis, pos) = analysis_and_position(
r#"
//- /lib.rs
<|> //empty
fn main() {}
#[test]
fn test_foo() {}
#[test]
#[ignore]
fn test_foo() {}
"#,
);
let runnables = analysis.runnables(pos.file_id).unwrap();
2019-08-29 13:49:10 +00:00
assert_debug_snapshot!(&runnables,
2019-11-15 09:56:24 +00:00
@r###"
[
Runnable {
2020-06-02 16:02:58 +00:00
nav: NavigationTarget {
file_id: FileId(
1,
),
full_range: 1..21,
name: "main",
kind: FN_DEF,
focus_range: Some(
12..16,
),
container_name: None,
description: None,
docs: None,
},
2019-11-15 09:56:24 +00:00
kind: Bin,
cfg_exprs: [],
2019-11-15 09:56:24 +00:00
},
Runnable {
2020-06-02 16:02:58 +00:00
nav: NavigationTarget {
file_id: FileId(
1,
),
full_range: 22..46,
name: "test_foo",
kind: FN_DEF,
focus_range: Some(
33..41,
),
container_name: None,
description: None,
docs: None,
},
2019-11-15 09:56:24 +00:00
kind: Test {
2020-02-16 16:39:58 +00:00
test_id: Path(
"test_foo",
),
2020-04-22 20:52:12 +00:00
attr: TestAttr {
ignore: false,
},
2019-11-15 09:56:24 +00:00
},
cfg_exprs: [],
2019-11-15 09:56:24 +00:00
},
Runnable {
2020-06-02 16:02:58 +00:00
nav: NavigationTarget {
file_id: FileId(
1,
),
full_range: 47..81,
name: "test_foo",
kind: FN_DEF,
focus_range: Some(
68..76,
),
container_name: None,
description: None,
docs: None,
},
2019-11-15 09:56:24 +00:00
kind: Test {
2020-02-16 16:39:58 +00:00
test_id: Path(
"test_foo",
),
2020-04-22 20:52:12 +00:00
attr: TestAttr {
ignore: true,
},
2019-11-15 09:56:24 +00:00
},
cfg_exprs: [],
2019-11-15 09:56:24 +00:00
},
]
"###
2019-07-28 11:08:06 +00:00
);
2019-01-14 13:27:08 +00:00
}
#[test]
fn test_runnables_doc_test() {
let (analysis, pos) = analysis_and_position(
r#"
//- /lib.rs
<|> //empty
fn main() {}
/// ```
/// let x = 5;
/// ```
fn foo() {}
"#,
);
let runnables = analysis.runnables(pos.file_id).unwrap();
assert_debug_snapshot!(&runnables,
@r###"
[
Runnable {
2020-06-02 16:02:58 +00:00
nav: NavigationTarget {
file_id: FileId(
1,
),
full_range: 1..21,
name: "main",
kind: FN_DEF,
focus_range: Some(
12..16,
),
container_name: None,
description: None,
docs: None,
},
kind: Bin,
cfg_exprs: [],
},
Runnable {
2020-06-02 16:02:58 +00:00
nav: NavigationTarget {
file_id: FileId(
1,
),
full_range: 22..64,
name: "foo",
kind: FN_DEF,
focus_range: Some(
56..59,
),
container_name: None,
description: None,
docs: None,
},
kind: DocTest {
test_id: Path(
"foo",
),
},
cfg_exprs: [],
},
]
"###
);
}
#[test]
fn test_runnables_doc_test_in_impl() {
let (analysis, pos) = analysis_and_position(
r#"
//- /lib.rs
<|> //empty
fn main() {}
struct Data;
impl Data {
/// ```
/// let x = 5;
/// ```
fn foo() {}
}
"#,
);
let runnables = analysis.runnables(pos.file_id).unwrap();
assert_debug_snapshot!(&runnables,
@r###"
[
Runnable {
2020-06-02 16:02:58 +00:00
nav: NavigationTarget {
file_id: FileId(
1,
),
full_range: 1..21,
name: "main",
kind: FN_DEF,
focus_range: Some(
12..16,
),
container_name: None,
description: None,
docs: None,
},
kind: Bin,
cfg_exprs: [],
},
Runnable {
2020-06-02 16:02:58 +00:00
nav: NavigationTarget {
file_id: FileId(
1,
),
full_range: 51..105,
name: "foo",
kind: FN_DEF,
focus_range: Some(
97..100,
),
container_name: None,
description: None,
docs: None,
},
kind: DocTest {
test_id: Path(
"Data::foo",
),
},
cfg_exprs: [],
},
]
"###
);
}
2019-01-14 13:27:08 +00:00
#[test]
fn test_runnables_module() {
let (analysis, pos) = analysis_and_position(
r#"
//- /lib.rs
<|> //empty
mod test_mod {
#[test]
fn test_foo1() {}
}
"#,
);
let runnables = analysis.runnables(pos.file_id).unwrap();
2019-08-29 13:49:10 +00:00
assert_debug_snapshot!(&runnables,
2019-11-15 09:56:24 +00:00
@r###"
[
Runnable {
2020-06-02 16:02:58 +00:00
nav: NavigationTarget {
file_id: FileId(
1,
),
full_range: 1..59,
name: "test_mod",
kind: MODULE,
focus_range: Some(
13..21,
),
container_name: None,
description: None,
docs: None,
},
2019-11-15 09:56:24 +00:00
kind: TestMod {
path: "test_mod",
},
cfg_exprs: [],
2019-11-15 09:56:24 +00:00
},
Runnable {
2020-06-02 16:02:58 +00:00
nav: NavigationTarget {
file_id: FileId(
1,
),
full_range: 28..57,
name: "test_foo1",
kind: FN_DEF,
focus_range: Some(
43..52,
),
container_name: None,
description: None,
docs: None,
},
2019-11-15 09:56:24 +00:00
kind: Test {
test_id: Path(
"test_mod::test_foo1",
),
2020-04-22 20:52:12 +00:00
attr: TestAttr {
ignore: false,
},
2019-11-15 09:56:24 +00:00
},
cfg_exprs: [],
2019-11-15 09:56:24 +00:00
},
]
"###
2019-07-28 11:08:06 +00:00
);
2019-01-14 13:27:08 +00:00
}
#[test]
fn test_runnables_one_depth_layer_module() {
let (analysis, pos) = analysis_and_position(
r#"
//- /lib.rs
<|> //empty
mod foo {
mod test_mod {
#[test]
fn test_foo1() {}
}
}
"#,
);
let runnables = analysis.runnables(pos.file_id).unwrap();
2019-08-29 13:49:10 +00:00
assert_debug_snapshot!(&runnables,
2019-11-15 09:56:24 +00:00
@r###"
[
Runnable {
2020-06-02 16:02:58 +00:00
nav: NavigationTarget {
file_id: FileId(
1,
),
full_range: 23..85,
name: "test_mod",
kind: MODULE,
focus_range: Some(
27..35,
),
container_name: None,
description: None,
docs: None,
},
2019-11-15 09:56:24 +00:00
kind: TestMod {
path: "foo::test_mod",
},
cfg_exprs: [],
2019-11-15 09:56:24 +00:00
},
Runnable {
2020-06-02 16:02:58 +00:00
nav: NavigationTarget {
file_id: FileId(
1,
),
full_range: 46..79,
name: "test_foo1",
kind: FN_DEF,
focus_range: Some(
65..74,
),
container_name: None,
description: None,
docs: None,
},
2019-11-15 09:56:24 +00:00
kind: Test {
test_id: Path(
"foo::test_mod::test_foo1",
),
2020-04-22 20:52:12 +00:00
attr: TestAttr {
ignore: false,
},
2019-11-15 09:56:24 +00:00
},
cfg_exprs: [],
2019-11-15 09:56:24 +00:00
},
]
"###
2019-07-28 11:08:06 +00:00
);
2019-01-14 13:27:08 +00:00
}
#[test]
fn test_runnables_multiple_depth_module() {
let (analysis, pos) = analysis_and_position(
r#"
//- /lib.rs
<|> //empty
mod foo {
mod bar {
mod test_mod {
#[test]
fn test_foo1() {}
}
}
}
"#,
);
let runnables = analysis.runnables(pos.file_id).unwrap();
2019-08-29 13:49:10 +00:00
assert_debug_snapshot!(&runnables,
2019-11-15 09:56:24 +00:00
@r###"
[
Runnable {
2020-06-02 16:02:58 +00:00
nav: NavigationTarget {
file_id: FileId(
1,
),
full_range: 41..115,
name: "test_mod",
kind: MODULE,
focus_range: Some(
45..53,
),
container_name: None,
description: None,
docs: None,
},
2019-11-15 09:56:24 +00:00
kind: TestMod {
path: "foo::bar::test_mod",
},
cfg_exprs: [],
2019-11-15 09:56:24 +00:00
},
Runnable {
2020-06-02 16:02:58 +00:00
nav: NavigationTarget {
file_id: FileId(
1,
),
full_range: 68..105,
name: "test_foo1",
kind: FN_DEF,
focus_range: Some(
91..100,
),
container_name: None,
description: None,
docs: None,
},
2019-11-15 09:56:24 +00:00
kind: Test {
test_id: Path(
"foo::bar::test_mod::test_foo1",
),
2020-04-22 20:52:12 +00:00
attr: TestAttr {
ignore: false,
},
2019-11-15 09:56:24 +00:00
},
cfg_exprs: [],
},
]
"###
);
}
#[test]
fn test_runnables_with_feature() {
let (analysis, pos) = analysis_and_position(
r#"
//- /lib.rs crate:foo cfg:feature=foo
<|> //empty
#[test]
#[cfg(feature = "foo")]
fn test_foo1() {}
"#,
);
let runnables = analysis.runnables(pos.file_id).unwrap();
assert_debug_snapshot!(&runnables,
@r###"
[
Runnable {
2020-06-02 16:02:58 +00:00
nav: NavigationTarget {
file_id: FileId(
1,
),
full_range: 1..58,
name: "test_foo1",
kind: FN_DEF,
focus_range: Some(
44..53,
),
container_name: None,
description: None,
docs: None,
},
kind: Test {
2020-05-24 13:47:05 +00:00
test_id: Path(
"test_foo1",
),
attr: TestAttr {
ignore: false,
},
},
cfg_exprs: [
KeyValue {
key: "feature",
value: "foo",
},
],
},
]
"###
);
}
#[test]
fn test_runnables_with_features() {
let (analysis, pos) = analysis_and_position(
r#"
//- /lib.rs crate:foo cfg:feature=foo,feature=bar
<|> //empty
#[test]
#[cfg(all(feature = "foo", feature = "bar"))]
fn test_foo1() {}
"#,
);
let runnables = analysis.runnables(pos.file_id).unwrap();
assert_debug_snapshot!(&runnables,
@r###"
[
Runnable {
2020-06-02 16:02:58 +00:00
nav: NavigationTarget {
file_id: FileId(
1,
),
full_range: 1..80,
name: "test_foo1",
kind: FN_DEF,
focus_range: Some(
66..75,
),
container_name: None,
description: None,
docs: None,
},
kind: Test {
2020-05-24 13:47:05 +00:00
test_id: Path(
"test_foo1",
),
attr: TestAttr {
ignore: false,
},
},
cfg_exprs: [
All(
[
KeyValue {
key: "feature",
value: "foo",
},
KeyValue {
key: "feature",
value: "bar",
},
],
),
],
2019-11-15 09:56:24 +00:00
},
]
"###
2019-07-28 11:08:06 +00:00
);
2019-01-14 13:27:08 +00:00
}
#[test]
fn test_runnables_no_test_function_in_module() {
let (analysis, pos) = analysis_and_position(
r#"
//- /lib.rs
<|> //empty
mod test_mod {
fn foo1() {}
}
"#,
);
let runnables = analysis.runnables(pos.file_id).unwrap();
assert!(runnables.is_empty())
}
}