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