diff options
Diffstat (limited to 'crates/ide/src')
-rw-r--r-- | crates/ide/src/lib.rs | 9 | ||||
-rw-r--r-- | crates/ide/src/runnables.rs | 338 |
2 files changed, 341 insertions, 6 deletions
diff --git a/crates/ide/src/lib.rs b/crates/ide/src/lib.rs index b600178ee..baa80cf43 100644 --- a/crates/ide/src/lib.rs +++ b/crates/ide/src/lib.rs | |||
@@ -445,6 +445,15 @@ impl Analysis { | |||
445 | self.with_db(|db| runnables::runnables(db, file_id)) | 445 | self.with_db(|db| runnables::runnables(db, file_id)) |
446 | } | 446 | } |
447 | 447 | ||
448 | /// Returns the set of tests for the given file position. | ||
449 | pub fn related_tests( | ||
450 | &self, | ||
451 | position: FilePosition, | ||
452 | search_scope: Option<SearchScope>, | ||
453 | ) -> Cancelable<Vec<Runnable>> { | ||
454 | self.with_db(|db| runnables::related_tests(db, position, search_scope)) | ||
455 | } | ||
456 | |||
448 | /// Computes syntax highlighting for the given file | 457 | /// Computes syntax highlighting for the given file |
449 | pub fn highlight(&self, file_id: FileId) -> Cancelable<Vec<HlRange>> { | 458 | pub fn highlight(&self, file_id: FileId) -> Cancelable<Vec<HlRange>> { |
450 | self.with_db(|db| syntax_highlighting::highlight(db, file_id, None, false)) | 459 | self.with_db(|db| syntax_highlighting::highlight(db, file_id, None, false)) |
diff --git a/crates/ide/src/runnables.rs b/crates/ide/src/runnables.rs index 1e7baed20..ce3a2e7ba 100644 --- a/crates/ide/src/runnables.rs +++ b/crates/ide/src/runnables.rs | |||
@@ -1,10 +1,14 @@ | |||
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 hir::{AsAssocItem, HasAttrs, HasSource, Semantics}; |
5 | use ide_assists::utils::test_related_attribute; | 6 | use ide_assists::utils::test_related_attribute; |
6 | use ide_db::{defs::Definition, RootDatabase, SymbolKind}; | 7 | use ide_db::{ |
8 | base_db::FilePosition, defs::Definition, search::SearchScope, RootDatabase, SymbolKind, | ||
9 | }; | ||
7 | use itertools::Itertools; | 10 | use itertools::Itertools; |
11 | use rustc_hash::FxHashSet; | ||
8 | use syntax::{ | 12 | use syntax::{ |
9 | ast::{self, AstNode, AttrsOwner}, | 13 | ast::{self, AstNode, AttrsOwner}, |
10 | match_ast, SyntaxNode, | 14 | match_ast, SyntaxNode, |
@@ -13,17 +17,17 @@ use test_utils::mark; | |||
13 | 17 | ||
14 | use crate::{ | 18 | use crate::{ |
15 | display::{ToNav, TryToNav}, | 19 | display::{ToNav, TryToNav}, |
16 | FileId, NavigationTarget, | 20 | references, FileId, NavigationTarget, |
17 | }; | 21 | }; |
18 | 22 | ||
19 | #[derive(Debug, Clone)] | 23 | #[derive(Debug, Clone, Hash, PartialEq, Eq)] |
20 | pub struct Runnable { | 24 | pub struct Runnable { |
21 | pub nav: NavigationTarget, | 25 | pub nav: NavigationTarget, |
22 | pub kind: RunnableKind, | 26 | pub kind: RunnableKind, |
23 | pub cfg: Option<CfgExpr>, | 27 | pub cfg: Option<CfgExpr>, |
24 | } | 28 | } |
25 | 29 | ||
26 | #[derive(Debug, Clone)] | 30 | #[derive(Debug, Clone, Hash, PartialEq, Eq)] |
27 | pub enum TestId { | 31 | pub enum TestId { |
28 | Name(String), | 32 | Name(String), |
29 | Path(String), | 33 | Path(String), |
@@ -38,7 +42,7 @@ impl fmt::Display for TestId { | |||
38 | } | 42 | } |
39 | } | 43 | } |
40 | 44 | ||
41 | #[derive(Debug, Clone)] | 45 | #[derive(Debug, Clone, Hash, PartialEq, Eq)] |
42 | pub enum RunnableKind { | 46 | pub enum RunnableKind { |
43 | Test { test_id: TestId, attr: TestAttr }, | 47 | Test { test_id: TestId, attr: TestAttr }, |
44 | TestMod { path: String }, | 48 | TestMod { path: String }, |
@@ -106,6 +110,102 @@ pub(crate) fn runnables(db: &RootDatabase, file_id: FileId) -> Vec<Runnable> { | |||
106 | res | 110 | res |
107 | } | 111 | } |
108 | 112 | ||
113 | // Feature: Run Test | ||
114 | // | ||
115 | // Shows a popup suggesting to run a test in which the item **at the current cursor | ||
116 | // location** is used (if any). | ||
117 | // | ||
118 | // |=== | ||
119 | // | Editor | Action Name | ||
120 | // | ||
121 | // | VS Code | **Rust Analyzer: Run Test** | ||
122 | // |=== | ||
123 | pub(crate) fn related_tests( | ||
124 | db: &RootDatabase, | ||
125 | position: FilePosition, | ||
126 | search_scope: Option<SearchScope>, | ||
127 | ) -> Vec<Runnable> { | ||
128 | let sema = Semantics::new(db); | ||
129 | let mut res: FxHashSet<Runnable> = FxHashSet::default(); | ||
130 | |||
131 | find_related_tests(&sema, position, search_scope, &mut res); | ||
132 | |||
133 | res.into_iter().collect_vec() | ||
134 | } | ||
135 | |||
136 | fn find_related_tests( | ||
137 | sema: &Semantics<RootDatabase>, | ||
138 | position: FilePosition, | ||
139 | search_scope: Option<SearchScope>, | ||
140 | tests: &mut FxHashSet<Runnable>, | ||
141 | ) { | ||
142 | if let Some(refs) = references::find_all_refs(&sema, position, search_scope) { | ||
143 | for (file_id, refs) in refs.references { | ||
144 | let file = sema.parse(file_id); | ||
145 | let file = file.syntax(); | ||
146 | let functions = refs.iter().filter_map(|(range, _)| { | ||
147 | let token = file.token_at_offset(range.start()).next()?; | ||
148 | let token = sema.descend_into_macros(token); | ||
149 | let syntax = token.parent(); | ||
150 | syntax.ancestors().find_map(ast::Fn::cast) | ||
151 | }); | ||
152 | |||
153 | for fn_def in functions { | ||
154 | if let Some(runnable) = as_test_runnable(&sema, &fn_def) { | ||
155 | // direct test | ||
156 | tests.insert(runnable); | ||
157 | } else if let Some(module) = parent_test_module(&sema, &fn_def) { | ||
158 | // indirect test | ||
159 | find_related_tests_in_module(sema, &fn_def, &module, tests); | ||
160 | } | ||
161 | } | ||
162 | } | ||
163 | } | ||
164 | } | ||
165 | |||
166 | fn find_related_tests_in_module( | ||
167 | sema: &Semantics<RootDatabase>, | ||
168 | fn_def: &ast::Fn, | ||
169 | parent_module: &hir::Module, | ||
170 | tests: &mut FxHashSet<Runnable>, | ||
171 | ) { | ||
172 | if let Some(fn_name) = fn_def.name() { | ||
173 | let mod_source = parent_module.definition_source(sema.db); | ||
174 | let range = match mod_source.value { | ||
175 | hir::ModuleSource::Module(m) => m.syntax().text_range(), | ||
176 | hir::ModuleSource::BlockExpr(b) => b.syntax().text_range(), | ||
177 | hir::ModuleSource::SourceFile(f) => f.syntax().text_range(), | ||
178 | }; | ||
179 | |||
180 | let file_id = mod_source.file_id.original_file(sema.db); | ||
181 | let mod_scope = SearchScope::file_part(file_id, range); | ||
182 | let fn_pos = FilePosition { file_id, offset: fn_name.syntax().text_range().start() }; | ||
183 | find_related_tests(sema, fn_pos, Some(mod_scope), tests) | ||
184 | } | ||
185 | } | ||
186 | |||
187 | fn as_test_runnable(sema: &Semantics<RootDatabase>, fn_def: &ast::Fn) -> Option<Runnable> { | ||
188 | if test_related_attribute(&fn_def).is_some() { | ||
189 | let function = sema.to_def(fn_def)?; | ||
190 | runnable_fn(sema, function) | ||
191 | } else { | ||
192 | None | ||
193 | } | ||
194 | } | ||
195 | |||
196 | fn parent_test_module(sema: &Semantics<RootDatabase>, fn_def: &ast::Fn) -> Option<hir::Module> { | ||
197 | fn_def.syntax().ancestors().find_map(|node| { | ||
198 | let module = ast::Module::cast(node)?; | ||
199 | let module = sema.to_def(&module)?; | ||
200 | |||
201 | if has_test_function_or_multiple_test_submodules(sema, &module) { | ||
202 | Some(module) | ||
203 | } else { | ||
204 | None | ||
205 | } | ||
206 | }) | ||
207 | } | ||
208 | |||
109 | fn runnables_mod(sema: &Semantics<RootDatabase>, acc: &mut Vec<Runnable>, module: hir::Module) { | 209 | fn runnables_mod(sema: &Semantics<RootDatabase>, acc: &mut Vec<Runnable>, module: hir::Module) { |
110 | acc.extend(module.declarations(sema.db).into_iter().filter_map(|def| { | 210 | acc.extend(module.declarations(sema.db).into_iter().filter_map(|def| { |
111 | let runnable = match def { | 211 | let runnable = match def { |
@@ -255,7 +355,7 @@ fn module_def_doctest(sema: &Semantics<RootDatabase>, def: hir::ModuleDef) -> Op | |||
255 | Some(res) | 355 | Some(res) |
256 | } | 356 | } |
257 | 357 | ||
258 | #[derive(Debug, Copy, Clone)] | 358 | #[derive(Debug, Copy, Clone, Hash, PartialEq, Eq)] |
259 | pub struct TestAttr { | 359 | pub struct TestAttr { |
260 | pub ignore: bool, | 360 | pub ignore: bool, |
261 | } | 361 | } |
@@ -349,6 +449,12 @@ mod tests { | |||
349 | ); | 449 | ); |
350 | } | 450 | } |
351 | 451 | ||
452 | fn check_tests(ra_fixture: &str, expect: Expect) { | ||
453 | let (analysis, position) = fixture::position(ra_fixture); | ||
454 | let tests = analysis.related_tests(position, None).unwrap(); | ||
455 | expect.assert_debug_eq(&tests); | ||
456 | } | ||
457 | |||
352 | #[test] | 458 | #[test] |
353 | fn test_runnables() { | 459 | fn test_runnables() { |
354 | check( | 460 | check( |
@@ -1074,4 +1180,224 @@ mod tests { | |||
1074 | "#]], | 1180 | "#]], |
1075 | ); | 1181 | ); |
1076 | } | 1182 | } |
1183 | |||
1184 | #[test] | ||
1185 | fn find_no_tests() { | ||
1186 | check_tests( | ||
1187 | r#" | ||
1188 | //- /lib.rs | ||
1189 | fn foo$0() { }; | ||
1190 | "#, | ||
1191 | expect![[r#" | ||
1192 | [] | ||
1193 | "#]], | ||
1194 | ); | ||
1195 | } | ||
1196 | |||
1197 | #[test] | ||
1198 | fn find_direct_fn_test() { | ||
1199 | check_tests( | ||
1200 | r#" | ||
1201 | //- /lib.rs | ||
1202 | fn foo$0() { }; | ||
1203 | |||
1204 | mod tests { | ||
1205 | #[test] | ||
1206 | fn foo_test() { | ||
1207 | super::foo() | ||
1208 | } | ||
1209 | } | ||
1210 | "#, | ||
1211 | expect![[r#" | ||
1212 | [ | ||
1213 | Runnable { | ||
1214 | nav: NavigationTarget { | ||
1215 | file_id: FileId( | ||
1216 | 0, | ||
1217 | ), | ||
1218 | full_range: 31..85, | ||
1219 | focus_range: 46..54, | ||
1220 | name: "foo_test", | ||
1221 | kind: Function, | ||
1222 | }, | ||
1223 | kind: Test { | ||
1224 | test_id: Path( | ||
1225 | "tests::foo_test", | ||
1226 | ), | ||
1227 | attr: TestAttr { | ||
1228 | ignore: false, | ||
1229 | }, | ||
1230 | }, | ||
1231 | cfg: None, | ||
1232 | }, | ||
1233 | ] | ||
1234 | "#]], | ||
1235 | ); | ||
1236 | } | ||
1237 | |||
1238 | #[test] | ||
1239 | fn find_direct_struct_test() { | ||
1240 | check_tests( | ||
1241 | r#" | ||
1242 | //- /lib.rs | ||
1243 | struct Fo$0o; | ||
1244 | fn foo(arg: &Foo) { }; | ||
1245 | |||
1246 | mod tests { | ||
1247 | use super::*; | ||
1248 | |||
1249 | #[test] | ||
1250 | fn foo_test() { | ||
1251 | foo(Foo); | ||
1252 | } | ||
1253 | } | ||
1254 | "#, | ||
1255 | expect![[r#" | ||
1256 | [ | ||
1257 | Runnable { | ||
1258 | nav: NavigationTarget { | ||
1259 | file_id: FileId( | ||
1260 | 0, | ||
1261 | ), | ||
1262 | full_range: 71..122, | ||
1263 | focus_range: 86..94, | ||
1264 | name: "foo_test", | ||
1265 | kind: Function, | ||
1266 | }, | ||
1267 | kind: Test { | ||
1268 | test_id: Path( | ||
1269 | "tests::foo_test", | ||
1270 | ), | ||
1271 | attr: TestAttr { | ||
1272 | ignore: false, | ||
1273 | }, | ||
1274 | }, | ||
1275 | cfg: None, | ||
1276 | }, | ||
1277 | ] | ||
1278 | "#]], | ||
1279 | ); | ||
1280 | } | ||
1281 | |||
1282 | #[test] | ||
1283 | fn find_indirect_fn_test() { | ||
1284 | check_tests( | ||
1285 | r#" | ||
1286 | //- /lib.rs | ||
1287 | fn foo$0() { }; | ||
1288 | |||
1289 | mod tests { | ||
1290 | use super::foo; | ||
1291 | |||
1292 | fn check1() { | ||
1293 | check2() | ||
1294 | } | ||
1295 | |||
1296 | fn check2() { | ||
1297 | foo() | ||
1298 | } | ||
1299 | |||
1300 | #[test] | ||
1301 | fn foo_test() { | ||
1302 | check1() | ||
1303 | } | ||
1304 | } | ||
1305 | "#, | ||
1306 | expect![[r#" | ||
1307 | [ | ||
1308 | Runnable { | ||
1309 | nav: NavigationTarget { | ||
1310 | file_id: FileId( | ||
1311 | 0, | ||
1312 | ), | ||
1313 | full_range: 133..183, | ||
1314 | focus_range: 148..156, | ||
1315 | name: "foo_test", | ||
1316 | kind: Function, | ||
1317 | }, | ||
1318 | kind: Test { | ||
1319 | test_id: Path( | ||
1320 | "tests::foo_test", | ||
1321 | ), | ||
1322 | attr: TestAttr { | ||
1323 | ignore: false, | ||
1324 | }, | ||
1325 | }, | ||
1326 | cfg: None, | ||
1327 | }, | ||
1328 | ] | ||
1329 | "#]], | ||
1330 | ); | ||
1331 | } | ||
1332 | |||
1333 | #[test] | ||
1334 | fn tests_are_unique() { | ||
1335 | check_tests( | ||
1336 | r#" | ||
1337 | //- /lib.rs | ||
1338 | fn foo$0() { }; | ||
1339 | |||
1340 | mod tests { | ||
1341 | use super::foo; | ||
1342 | |||
1343 | #[test] | ||
1344 | fn foo_test() { | ||
1345 | foo(); | ||
1346 | foo(); | ||
1347 | } | ||
1348 | |||
1349 | #[test] | ||
1350 | fn foo2_test() { | ||
1351 | foo(); | ||
1352 | foo(); | ||
1353 | } | ||
1354 | |||
1355 | } | ||
1356 | "#, | ||
1357 | expect![[r#" | ||
1358 | [ | ||
1359 | Runnable { | ||
1360 | nav: NavigationTarget { | ||
1361 | file_id: FileId( | ||
1362 | 0, | ||
1363 | ), | ||
1364 | full_range: 52..115, | ||
1365 | focus_range: 67..75, | ||
1366 | name: "foo_test", | ||
1367 | kind: Function, | ||
1368 | }, | ||
1369 | kind: Test { | ||
1370 | test_id: Path( | ||
1371 | "tests::foo_test", | ||
1372 | ), | ||
1373 | attr: TestAttr { | ||
1374 | ignore: false, | ||
1375 | }, | ||
1376 | }, | ||
1377 | cfg: None, | ||
1378 | }, | ||
1379 | Runnable { | ||
1380 | nav: NavigationTarget { | ||
1381 | file_id: FileId( | ||
1382 | 0, | ||
1383 | ), | ||
1384 | full_range: 121..185, | ||
1385 | focus_range: 136..145, | ||
1386 | name: "foo2_test", | ||
1387 | kind: Function, | ||
1388 | }, | ||
1389 | kind: Test { | ||
1390 | test_id: Path( | ||
1391 | "tests::foo2_test", | ||
1392 | ), | ||
1393 | attr: TestAttr { | ||
1394 | ignore: false, | ||
1395 | }, | ||
1396 | }, | ||
1397 | cfg: None, | ||
1398 | }, | ||
1399 | ] | ||
1400 | "#]], | ||
1401 | ); | ||
1402 | } | ||
1077 | } | 1403 | } |