diff options
Diffstat (limited to 'crates/ide/src/runnables.rs')
-rw-r--r-- | crates/ide/src/runnables.rs | 445 |
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 @@ | |||
1 | use std::fmt; | 1 | use std::fmt; |
2 | 2 | ||
3 | use ast::NameOwner; | ||
3 | use cfg::CfgExpr; | 4 | use cfg::CfgExpr; |
4 | use hir::{AsAssocItem, HasAttrs, HasSource, Semantics}; | 5 | use either::Either; |
6 | use hir::{AsAssocItem, HasAttrs, HasSource, HirDisplay, Semantics}; | ||
5 | use ide_assists::utils::test_related_attribute; | 7 | use ide_assists::utils::test_related_attribute; |
6 | use ide_db::{defs::Definition, RootDatabase, SymbolKind}; | 8 | use ide_db::{ |
9 | base_db::{FilePosition, FileRange}, | ||
10 | defs::Definition, | ||
11 | helpers::visit_file_defs, | ||
12 | search::SearchScope, | ||
13 | RootDatabase, SymbolKind, | ||
14 | }; | ||
7 | use itertools::Itertools; | 15 | use itertools::Itertools; |
16 | use rustc_hash::FxHashSet; | ||
8 | use syntax::{ | 17 | use 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 | ||
13 | use crate::{ | 22 | use 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)] |
19 | pub struct Runnable { | 28 | pub 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)] |
26 | pub enum TestId { | 35 | pub 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)] |
41 | pub enum RunnableKind { | 50 | pub 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 | // |=== |
96 | pub(crate) fn runnables(db: &RootDatabase, file_id: FileId) -> Vec<Runnable> { | 105 | pub(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 | ||
108 | fn 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 | // |=== | ||
144 | pub(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()), | 157 | fn 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 | ||
187 | fn 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 | |||
208 | fn 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 | |||
217 | fn 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 | |||
141 | pub(crate) fn runnable_fn(sema: &Semantics<RootDatabase>, def: hir::Function) -> Option<Runnable> { | 230 | pub(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)] |
260 | pub struct TestAttr { | 359 | pub 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 | ||
1187 | fn 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 | ||
1200 | fn foo$0() { }; | ||
1201 | |||
1202 | mod 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 | ||
1241 | struct Fo$0o; | ||
1242 | fn foo(arg: &Foo) { }; | ||
1243 | |||
1244 | mod 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 | ||
1285 | fn foo$0() { }; | ||
1286 | |||
1287 | mod 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 | ||
1336 | fn foo$0() { }; | ||
1337 | |||
1338 | mod 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 | ||
1408 | struct Foo<T, U>; | ||
1409 | |||
1410 | impl<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 | } |