aboutsummaryrefslogtreecommitdiff
path: root/crates/ide/src/runnables.rs
diff options
context:
space:
mode:
Diffstat (limited to 'crates/ide/src/runnables.rs')
-rw-r--r--crates/ide/src/runnables.rs445
1 files changed, 403 insertions, 42 deletions
diff --git a/crates/ide/src/runnables.rs b/crates/ide/src/runnables.rs
index 280565563..0c7a8fbf8 100644
--- a/crates/ide/src/runnables.rs
+++ b/crates/ide/src/runnables.rs
@@ -1,10 +1,19 @@
1use std::fmt; 1use std::fmt;
2 2
3use ast::NameOwner;
3use cfg::CfgExpr; 4use cfg::CfgExpr;
4use hir::{AsAssocItem, HasAttrs, HasSource, Semantics}; 5use either::Either;
6use hir::{AsAssocItem, HasAttrs, HasSource, HirDisplay, Semantics};
5use ide_assists::utils::test_related_attribute; 7use ide_assists::utils::test_related_attribute;
6use ide_db::{defs::Definition, RootDatabase, SymbolKind}; 8use ide_db::{
9 base_db::{FilePosition, FileRange},
10 defs::Definition,
11 helpers::visit_file_defs,
12 search::SearchScope,
13 RootDatabase, SymbolKind,
14};
7use itertools::Itertools; 15use itertools::Itertools;
16use rustc_hash::FxHashSet;
8use syntax::{ 17use syntax::{
9 ast::{self, AstNode, AttrsOwner}, 18 ast::{self, AstNode, AttrsOwner},
10 match_ast, SyntaxNode, 19 match_ast, SyntaxNode,
@@ -12,17 +21,17 @@ use syntax::{
12 21
13use crate::{ 22use crate::{
14 display::{ToNav, TryToNav}, 23 display::{ToNav, TryToNav},
15 FileId, NavigationTarget, 24 references, FileId, NavigationTarget,
16}; 25};
17 26
18#[derive(Debug, Clone)] 27#[derive(Debug, Clone, Hash, PartialEq, Eq)]
19pub struct Runnable { 28pub struct Runnable {
20 pub nav: NavigationTarget, 29 pub nav: NavigationTarget,
21 pub kind: RunnableKind, 30 pub kind: RunnableKind,
22 pub cfg: Option<CfgExpr>, 31 pub cfg: Option<CfgExpr>,
23} 32}
24 33
25#[derive(Debug, Clone)] 34#[derive(Debug, Clone, Hash, PartialEq, Eq)]
26pub enum TestId { 35pub enum TestId {
27 Name(String), 36 Name(String),
28 Path(String), 37 Path(String),
@@ -37,7 +46,7 @@ impl fmt::Display for TestId {
37 } 46 }
38} 47}
39 48
40#[derive(Debug, Clone)] 49#[derive(Debug, Clone, Hash, PartialEq, Eq)]
41pub enum RunnableKind { 50pub enum RunnableKind {
42 Test { test_id: TestId, attr: TestAttr }, 51 Test { test_id: TestId, attr: TestAttr },
43 TestMod { path: String }, 52 TestMod { path: String },
@@ -95,49 +104,129 @@ impl Runnable {
95// |=== 104// |===
96pub(crate) fn runnables(db: &RootDatabase, file_id: FileId) -> Vec<Runnable> { 105pub(crate) fn runnables(db: &RootDatabase, file_id: FileId) -> Vec<Runnable> {
97 let sema = Semantics::new(db); 106 let sema = Semantics::new(db);
98 let module = match sema.to_module_def(file_id) {
99 None => return Vec::new(),
100 Some(it) => it,
101 };
102 107
103 let mut res = Vec::new(); 108 let mut res = Vec::new();
104 runnables_mod(&sema, &mut res, module); 109 visit_file_defs(&sema, file_id, &mut |def| match def {
110 Either::Left(def) => {
111 let runnable = match def {
112 hir::ModuleDef::Module(it) => runnable_mod(&sema, it),
113 hir::ModuleDef::Function(it) => runnable_fn(&sema, it),
114 _ => None,
115 };
116 res.extend(runnable.or_else(|| module_def_doctest(&sema, def)))
117 }
118 Either::Right(impl_) => {
119 res.extend(impl_.items(db).into_iter().filter_map(|assoc| match assoc {
120 hir::AssocItem::Function(it) => {
121 runnable_fn(&sema, it).or_else(|| module_def_doctest(&sema, it.into()))
122 }
123 hir::AssocItem::Const(it) => module_def_doctest(&sema, it.into()),
124 hir::AssocItem::TypeAlias(it) => module_def_doctest(&sema, it.into()),
125 }))
126 }
127 });
105 res 128 res
106} 129}
107 130
108fn runnables_mod(sema: &Semantics<RootDatabase>, acc: &mut Vec<Runnable>, module: hir::Module) { 131// Feature: Related Tests
109 acc.extend(module.declarations(sema.db).into_iter().filter_map(|def| { 132//
110 let runnable = match def { 133// Provides a sneak peek of all tests where the current item is used.
111 hir::ModuleDef::Module(it) => runnable_mod(&sema, it), 134//
112 hir::ModuleDef::Function(it) => runnable_fn(&sema, it), 135// The simplest way to use this feature is via the context menu:
113 _ => None, 136// - Right-click on the selected item. The context menu opens.
114 }; 137// - Select **Peek related tests**
115 runnable.or_else(|| module_def_doctest(&sema, def)) 138//
116 })); 139// |===
140// | Editor | Action Name
141//
142// | VS Code | **Rust Analyzer: Peek related tests**
143// |===
144pub(crate) fn related_tests(
145 db: &RootDatabase,
146 position: FilePosition,
147 search_scope: Option<SearchScope>,
148) -> Vec<Runnable> {
149 let sema = Semantics::new(db);
150 let mut res: FxHashSet<Runnable> = FxHashSet::default();
117 151
118 acc.extend(module.impl_defs(sema.db).into_iter().flat_map(|it| it.items(sema.db)).filter_map( 152 find_related_tests(&sema, position, search_scope, &mut res);
119 |def| match def { 153
120 hir::AssocItem::Function(it) => { 154 res.into_iter().collect_vec()
121 runnable_fn(&sema, it).or_else(|| module_def_doctest(&sema, it.into())) 155}
122 } 156
123 hir::AssocItem::Const(it) => module_def_doctest(&sema, it.into()), 157fn find_related_tests(
124 hir::AssocItem::TypeAlias(it) => module_def_doctest(&sema, it.into()), 158 sema: &Semantics<RootDatabase>,
125 }, 159 position: FilePosition,
126 )); 160 search_scope: Option<SearchScope>,
127 161 tests: &mut FxHashSet<Runnable>,
128 for def in module.declarations(sema.db) { 162) {
129 if let hir::ModuleDef::Module(submodule) = def { 163 if let Some(refs) = references::find_all_refs(&sema, position, search_scope) {
130 match submodule.definition_source(sema.db).value { 164 for (file_id, refs) in refs.references {
131 hir::ModuleSource::Module(_) => runnables_mod(sema, acc, submodule), 165 let file = sema.parse(file_id);
132 hir::ModuleSource::SourceFile(_) => { 166 let file = file.syntax();
133 cov_mark::hit!(dont_recurse_in_outline_submodules) 167 let functions = refs.iter().filter_map(|(range, _)| {
168 let token = file.token_at_offset(range.start()).next()?;
169 let token = sema.descend_into_macros(token);
170 let syntax = token.parent();
171 syntax.ancestors().find_map(ast::Fn::cast)
172 });
173
174 for fn_def in functions {
175 if let Some(runnable) = as_test_runnable(&sema, &fn_def) {
176 // direct test
177 tests.insert(runnable);
178 } else if let Some(module) = parent_test_module(&sema, &fn_def) {
179 // indirect test
180 find_related_tests_in_module(sema, &fn_def, &module, tests);
134 } 181 }
135 hir::ModuleSource::BlockExpr(_) => {} // inner items aren't runnable
136 } 182 }
137 } 183 }
138 } 184 }
139} 185}
140 186
187fn find_related_tests_in_module(
188 sema: &Semantics<RootDatabase>,
189 fn_def: &ast::Fn,
190 parent_module: &hir::Module,
191 tests: &mut FxHashSet<Runnable>,
192) {
193 if let Some(fn_name) = fn_def.name() {
194 let mod_source = parent_module.definition_source(sema.db);
195 let range = match mod_source.value {
196 hir::ModuleSource::Module(m) => m.syntax().text_range(),
197 hir::ModuleSource::BlockExpr(b) => b.syntax().text_range(),
198 hir::ModuleSource::SourceFile(f) => f.syntax().text_range(),
199 };
200
201 let file_id = mod_source.file_id.original_file(sema.db);
202 let mod_scope = SearchScope::file_range(FileRange { file_id, range });
203 let fn_pos = FilePosition { file_id, offset: fn_name.syntax().text_range().start() };
204 find_related_tests(sema, fn_pos, Some(mod_scope), tests)
205 }
206}
207
208fn as_test_runnable(sema: &Semantics<RootDatabase>, fn_def: &ast::Fn) -> Option<Runnable> {
209 if test_related_attribute(&fn_def).is_some() {
210 let function = sema.to_def(fn_def)?;
211 runnable_fn(sema, function)
212 } else {
213 None
214 }
215}
216
217fn parent_test_module(sema: &Semantics<RootDatabase>, fn_def: &ast::Fn) -> Option<hir::Module> {
218 fn_def.syntax().ancestors().find_map(|node| {
219 let module = ast::Module::cast(node)?;
220 let module = sema.to_def(&module)?;
221
222 if has_test_function_or_multiple_test_submodules(sema, &module) {
223 Some(module)
224 } else {
225 None
226 }
227 })
228}
229
141pub(crate) fn runnable_fn(sema: &Semantics<RootDatabase>, def: hir::Function) -> Option<Runnable> { 230pub(crate) fn runnable_fn(sema: &Semantics<RootDatabase>, def: hir::Function) -> Option<Runnable> {
142 let func = def.source(sema.db)?; 231 let func = def.source(sema.db)?;
143 let name_string = def.name(sema.db).to_string(); 232 let name_string = def.name(sema.db).to_string();
@@ -234,11 +323,21 @@ fn module_def_doctest(sema: &Semantics<RootDatabase>, def: hir::ModuleDef) -> Op
234 // FIXME: this also looks very wrong 323 // FIXME: this also looks very wrong
235 if let Some(assoc_def) = assoc_def { 324 if let Some(assoc_def) = assoc_def {
236 if let hir::AssocItemContainer::Impl(imp) = assoc_def.container(sema.db) { 325 if let hir::AssocItemContainer::Impl(imp) = assoc_def.container(sema.db) {
237 if let Some(adt) = imp.target_ty(sema.db).as_adt() { 326 let ty = imp.target_ty(sema.db);
238 let name = adt.name(sema.db).to_string(); 327 if let Some(adt) = ty.as_adt() {
328 let name = adt.name(sema.db);
239 let idx = path.rfind(':').map_or(0, |idx| idx + 1); 329 let idx = path.rfind(':').map_or(0, |idx| idx + 1);
240 let (prefix, suffix) = path.split_at(idx); 330 let (prefix, suffix) = path.split_at(idx);
241 return format!("{}{}::{}", prefix, name, suffix); 331 let mut ty_params = ty.type_parameters().peekable();
332 let params = if ty_params.peek().is_some() {
333 format!(
334 "<{}>",
335 ty_params.format_with(", ", |ty, cb| cb(&ty.display(sema.db)))
336 )
337 } else {
338 String::new()
339 };
340 return format!("{}{}{}::{}", prefix, name, params, suffix);
242 } 341 }
243 } 342 }
244 } 343 }
@@ -256,7 +355,7 @@ fn module_def_doctest(sema: &Semantics<RootDatabase>, def: hir::ModuleDef) -> Op
256 Some(res) 355 Some(res)
257} 356}
258 357
259#[derive(Debug, Copy, Clone)] 358#[derive(Debug, Copy, Clone, Hash, PartialEq, Eq)]
260pub struct TestAttr { 359pub struct TestAttr {
261 pub ignore: bool, 360 pub ignore: bool,
262} 361}
@@ -349,6 +448,12 @@ mod tests {
349 ); 448 );
350 } 449 }
351 450
451 fn check_tests(ra_fixture: &str, expect: Expect) {
452 let (analysis, position) = fixture::position(ra_fixture);
453 let tests = analysis.related_tests(position, None).unwrap();
454 expect.assert_debug_eq(&tests);
455 }
456
352 #[test] 457 #[test]
353 fn test_runnables() { 458 fn test_runnables() {
354 check( 459 check(
@@ -1056,7 +1161,6 @@ mod tests {
1056 1161
1057 #[test] 1162 #[test]
1058 fn dont_recurse_in_outline_submodules() { 1163 fn dont_recurse_in_outline_submodules() {
1059 cov_mark::check!(dont_recurse_in_outline_submodules);
1060 check( 1164 check(
1061 r#" 1165 r#"
1062//- /lib.rs 1166//- /lib.rs
@@ -1074,4 +1178,261 @@ mod tests {
1074 "#]], 1178 "#]],
1075 ); 1179 );
1076 } 1180 }
1181
1182 #[test]
1183 fn find_no_tests() {
1184 check_tests(
1185 r#"
1186//- /lib.rs
1187fn foo$0() { };
1188"#,
1189 expect![[r#"
1190 []
1191 "#]],
1192 );
1193 }
1194
1195 #[test]
1196 fn find_direct_fn_test() {
1197 check_tests(
1198 r#"
1199//- /lib.rs
1200fn foo$0() { };
1201
1202mod tests {
1203 #[test]
1204 fn foo_test() {
1205 super::foo()
1206 }
1207}
1208"#,
1209 expect![[r#"
1210 [
1211 Runnable {
1212 nav: NavigationTarget {
1213 file_id: FileId(
1214 0,
1215 ),
1216 full_range: 31..85,
1217 focus_range: 46..54,
1218 name: "foo_test",
1219 kind: Function,
1220 },
1221 kind: Test {
1222 test_id: Path(
1223 "tests::foo_test",
1224 ),
1225 attr: TestAttr {
1226 ignore: false,
1227 },
1228 },
1229 cfg: None,
1230 },
1231 ]
1232 "#]],
1233 );
1234 }
1235
1236 #[test]
1237 fn find_direct_struct_test() {
1238 check_tests(
1239 r#"
1240//- /lib.rs
1241struct Fo$0o;
1242fn foo(arg: &Foo) { };
1243
1244mod tests {
1245 use super::*;
1246
1247 #[test]
1248 fn foo_test() {
1249 foo(Foo);
1250 }
1251}
1252"#,
1253 expect![[r#"
1254 [
1255 Runnable {
1256 nav: NavigationTarget {
1257 file_id: FileId(
1258 0,
1259 ),
1260 full_range: 71..122,
1261 focus_range: 86..94,
1262 name: "foo_test",
1263 kind: Function,
1264 },
1265 kind: Test {
1266 test_id: Path(
1267 "tests::foo_test",
1268 ),
1269 attr: TestAttr {
1270 ignore: false,
1271 },
1272 },
1273 cfg: None,
1274 },
1275 ]
1276 "#]],
1277 );
1278 }
1279
1280 #[test]
1281 fn find_indirect_fn_test() {
1282 check_tests(
1283 r#"
1284//- /lib.rs
1285fn foo$0() { };
1286
1287mod tests {
1288 use super::foo;
1289
1290 fn check1() {
1291 check2()
1292 }
1293
1294 fn check2() {
1295 foo()
1296 }
1297
1298 #[test]
1299 fn foo_test() {
1300 check1()
1301 }
1302}
1303"#,
1304 expect![[r#"
1305 [
1306 Runnable {
1307 nav: NavigationTarget {
1308 file_id: FileId(
1309 0,
1310 ),
1311 full_range: 133..183,
1312 focus_range: 148..156,
1313 name: "foo_test",
1314 kind: Function,
1315 },
1316 kind: Test {
1317 test_id: Path(
1318 "tests::foo_test",
1319 ),
1320 attr: TestAttr {
1321 ignore: false,
1322 },
1323 },
1324 cfg: None,
1325 },
1326 ]
1327 "#]],
1328 );
1329 }
1330
1331 #[test]
1332 fn tests_are_unique() {
1333 check_tests(
1334 r#"
1335//- /lib.rs
1336fn foo$0() { };
1337
1338mod tests {
1339 use super::foo;
1340
1341 #[test]
1342 fn foo_test() {
1343 foo();
1344 foo();
1345 }
1346
1347 #[test]
1348 fn foo2_test() {
1349 foo();
1350 foo();
1351 }
1352
1353}
1354"#,
1355 expect![[r#"
1356 [
1357 Runnable {
1358 nav: NavigationTarget {
1359 file_id: FileId(
1360 0,
1361 ),
1362 full_range: 52..115,
1363 focus_range: 67..75,
1364 name: "foo_test",
1365 kind: Function,
1366 },
1367 kind: Test {
1368 test_id: Path(
1369 "tests::foo_test",
1370 ),
1371 attr: TestAttr {
1372 ignore: false,
1373 },
1374 },
1375 cfg: None,
1376 },
1377 Runnable {
1378 nav: NavigationTarget {
1379 file_id: FileId(
1380 0,
1381 ),
1382 full_range: 121..185,
1383 focus_range: 136..145,
1384 name: "foo2_test",
1385 kind: Function,
1386 },
1387 kind: Test {
1388 test_id: Path(
1389 "tests::foo2_test",
1390 ),
1391 attr: TestAttr {
1392 ignore: false,
1393 },
1394 },
1395 cfg: None,
1396 },
1397 ]
1398 "#]],
1399 );
1400 }
1401
1402 #[test]
1403 fn doc_test_type_params() {
1404 check(
1405 r#"
1406//- /lib.rs
1407$0
1408struct Foo<T, U>;
1409
1410impl<T, U> Foo<T, U> {
1411 /// ```rust
1412 /// ````
1413 fn t() {}
1414}
1415"#,
1416 &[&DOCTEST],
1417 expect![[r#"
1418 [
1419 Runnable {
1420 nav: NavigationTarget {
1421 file_id: FileId(
1422 0,
1423 ),
1424 full_range: 47..85,
1425 name: "t",
1426 },
1427 kind: DocTest {
1428 test_id: Path(
1429 "Foo<T, U>::t",
1430 ),
1431 },
1432 cfg: None,
1433 },
1434 ]
1435 "#]],
1436 );
1437 }
1077} 1438}