]> git.lizzy.rs Git - rust.git/blob - crates/ide/src/runnables.rs
dd59d9e70e48ec6dcf0a359cc3758adee0880889
[rust.git] / crates / ide / src / runnables.rs
1 use std::fmt;
2
3 use cfg::CfgExpr;
4 use hir::{AsAssocItem, Attrs, HirFileId, InFile, Semantics};
5 use ide_db::RootDatabase;
6 use itertools::Itertools;
7 use syntax::{
8     ast::{self, AstNode, AttrsOwner, DocCommentsOwner, ModuleItemOwner, NameOwner},
9     match_ast, SyntaxNode,
10 };
11
12 use crate::{display::ToNav, FileId, NavigationTarget};
13
14 #[derive(Debug, Clone)]
15 pub struct Runnable {
16     pub nav: NavigationTarget,
17     pub kind: RunnableKind,
18     pub cfg_exprs: Vec<CfgExpr>,
19 }
20
21 #[derive(Debug, Clone)]
22 pub enum TestId {
23     Name(String),
24     Path(String),
25 }
26
27 impl fmt::Display for TestId {
28     fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
29         match self {
30             TestId::Name(name) => write!(f, "{}", name),
31             TestId::Path(path) => write!(f, "{}", path),
32         }
33     }
34 }
35
36 #[derive(Debug, Clone)]
37 pub enum RunnableKind {
38     Test { test_id: TestId, attr: TestAttr },
39     TestMod { path: String },
40     Bench { test_id: TestId },
41     DocTest { test_id: TestId },
42     Bin,
43 }
44
45 #[derive(Debug, Eq, PartialEq)]
46 pub struct RunnableAction {
47     pub run_title: &'static str,
48     pub debugee: bool,
49 }
50
51 const TEST: RunnableAction = RunnableAction { run_title: "▶\u{fe0e} Run Test", debugee: true };
52 const DOCTEST: RunnableAction =
53     RunnableAction { run_title: "▶\u{fe0e} Run Doctest", debugee: false };
54 const BENCH: RunnableAction = RunnableAction { run_title: "▶\u{fe0e} Run Bench", debugee: true };
55 const BIN: RunnableAction = RunnableAction { run_title: "▶\u{fe0e} Run", debugee: true };
56
57 impl Runnable {
58     // test package::module::testname
59     pub fn label(&self, target: Option<String>) -> String {
60         match &self.kind {
61             RunnableKind::Test { test_id, .. } => format!("test {}", test_id),
62             RunnableKind::TestMod { path } => format!("test-mod {}", path),
63             RunnableKind::Bench { test_id } => format!("bench {}", test_id),
64             RunnableKind::DocTest { test_id, .. } => format!("doctest {}", test_id),
65             RunnableKind::Bin => {
66                 target.map_or_else(|| "run binary".to_string(), |t| format!("run {}", t))
67             }
68         }
69     }
70
71     pub fn action(&self) -> &'static RunnableAction {
72         match &self.kind {
73             RunnableKind::Test { .. } | RunnableKind::TestMod { .. } => &TEST,
74             RunnableKind::DocTest { .. } => &DOCTEST,
75             RunnableKind::Bench { .. } => &BENCH,
76             RunnableKind::Bin => &BIN,
77         }
78     }
79 }
80
81 // Feature: Run
82 //
83 // Shows a popup suggesting to run a test/benchmark/binary **at the current cursor
84 // location**. Super useful for repeatedly running just a single test. Do bind this
85 // to a shortcut!
86 //
87 // |===
88 // | Editor  | Action Name
89 //
90 // | VS Code | **Rust Analyzer: Run**
91 // |===
92 pub(crate) fn runnables(db: &RootDatabase, file_id: FileId) -> Vec<Runnable> {
93     let sema = Semantics::new(db);
94     let source_file = sema.parse(file_id);
95     source_file.syntax().descendants().filter_map(|i| runnable(&sema, i, file_id)).collect()
96 }
97
98 pub(crate) fn runnable(
99     sema: &Semantics<RootDatabase>,
100     item: SyntaxNode,
101     file_id: FileId,
102 ) -> Option<Runnable> {
103     match_ast! {
104         match item {
105             ast::Fn(it) => runnable_fn(sema, it, file_id),
106             ast::Module(it) => runnable_mod(sema, it, file_id),
107             _ => None,
108         }
109     }
110 }
111
112 fn runnable_fn(
113     sema: &Semantics<RootDatabase>,
114     fn_def: ast::Fn,
115     file_id: FileId,
116 ) -> Option<Runnable> {
117     let name_string = fn_def.name()?.text().to_string();
118
119     let kind = if name_string == "main" {
120         RunnableKind::Bin
121     } else {
122         let test_id = match sema.to_def(&fn_def).map(|def| def.module(sema.db)) {
123             Some(module) => {
124                 let def = sema.to_def(&fn_def)?;
125                 let impl_trait_name = def.as_assoc_item(sema.db).and_then(|assoc_item| {
126                     match assoc_item.container(sema.db) {
127                         hir::AssocItemContainer::Trait(trait_item) => {
128                             Some(trait_item.name(sema.db).to_string())
129                         }
130                         hir::AssocItemContainer::ImplDef(impl_def) => impl_def
131                             .target_ty(sema.db)
132                             .as_adt()
133                             .map(|adt| adt.name(sema.db).to_string()),
134                     }
135                 });
136
137                 let path_iter = module
138                     .path_to_root(sema.db)
139                     .into_iter()
140                     .rev()
141                     .filter_map(|it| it.name(sema.db))
142                     .map(|name| name.to_string());
143
144                 let path = if let Some(impl_trait_name) = impl_trait_name {
145                     path_iter
146                         .chain(std::iter::once(impl_trait_name))
147                         .chain(std::iter::once(name_string))
148                         .join("::")
149                 } else {
150                     path_iter.chain(std::iter::once(name_string)).join("::")
151                 };
152
153                 TestId::Path(path)
154             }
155             None => TestId::Name(name_string),
156         };
157
158         if has_test_related_attribute(&fn_def) {
159             let attr = TestAttr::from_fn(&fn_def);
160             RunnableKind::Test { test_id, attr }
161         } else if fn_def.has_atom_attr("bench") {
162             RunnableKind::Bench { test_id }
163         } else if has_runnable_doc_test(&fn_def) {
164             RunnableKind::DocTest { test_id }
165         } else {
166             return None;
167         }
168     };
169
170     let attrs = Attrs::from_attrs_owner(sema.db, InFile::new(HirFileId::from(file_id), &fn_def));
171     let cfg_exprs = attrs.cfg().collect();
172
173     let nav = if let RunnableKind::DocTest { .. } = kind {
174         NavigationTarget::from_doc_commented(
175             sema.db,
176             InFile::new(file_id.into(), &fn_def),
177             InFile::new(file_id.into(), &fn_def),
178         )
179     } else {
180         NavigationTarget::from_named(sema.db, InFile::new(file_id.into(), &fn_def))
181     };
182     Some(Runnable { nav, kind, cfg_exprs })
183 }
184
185 #[derive(Debug, Copy, Clone)]
186 pub struct TestAttr {
187     pub ignore: bool,
188 }
189
190 impl TestAttr {
191     fn from_fn(fn_def: &ast::Fn) -> TestAttr {
192         let ignore = fn_def
193             .attrs()
194             .filter_map(|attr| attr.simple_name())
195             .any(|attribute_text| attribute_text == "ignore");
196         TestAttr { ignore }
197     }
198 }
199
200 /// This is a method with a heuristics to support test methods annotated with custom test annotations, such as
201 /// `#[test_case(...)]`, `#[tokio::test]` and similar.
202 /// Also a regular `#[test]` annotation is supported.
203 ///
204 /// It may produce false positives, for example, `#[wasm_bindgen_test]` requires a different command to run the test,
205 /// but it's better than not to have the runnables for the tests at all.
206 fn has_test_related_attribute(fn_def: &ast::Fn) -> bool {
207     fn_def
208         .attrs()
209         .filter_map(|attr| attr.path())
210         .map(|path| path.syntax().to_string().to_lowercase())
211         .any(|attribute_text| attribute_text.contains("test"))
212 }
213
214 fn has_runnable_doc_test(fn_def: &ast::Fn) -> bool {
215     fn_def.doc_comment_text().map_or(false, |comments_text| {
216         comments_text.contains("```")
217             && !comments_text.contains("```ignore")
218             && !comments_text.contains("```no_run")
219             && !comments_text.contains("```compile_fail")
220     })
221 }
222
223 fn runnable_mod(
224     sema: &Semantics<RootDatabase>,
225     module: ast::Module,
226     file_id: FileId,
227 ) -> Option<Runnable> {
228     if !has_test_function_or_multiple_test_submodules(&module) {
229         return None;
230     }
231     let module_def = sema.to_def(&module)?;
232
233     let path = module_def
234         .path_to_root(sema.db)
235         .into_iter()
236         .rev()
237         .filter_map(|it| it.name(sema.db))
238         .join("::");
239
240     let attrs = Attrs::from_attrs_owner(sema.db, InFile::new(HirFileId::from(file_id), &module));
241     let cfg_exprs = attrs.cfg().collect();
242     let nav = module_def.to_nav(sema.db);
243     Some(Runnable { nav, kind: RunnableKind::TestMod { path }, cfg_exprs })
244 }
245
246 // We could create runnables for modules with number_of_test_submodules > 0,
247 // but that bloats the runnables for no real benefit, since all tests can be run by the submodule already
248 fn has_test_function_or_multiple_test_submodules(module: &ast::Module) -> bool {
249     if let Some(item_list) = module.item_list() {
250         let mut number_of_test_submodules = 0;
251
252         for item in item_list.items() {
253             match item {
254                 ast::Item::Fn(f) => {
255                     if has_test_related_attribute(&f) {
256                         return true;
257                     }
258                 }
259                 ast::Item::Module(submodule) => {
260                     if has_test_function_or_multiple_test_submodules(&submodule) {
261                         number_of_test_submodules += 1;
262                     }
263                 }
264                 _ => (),
265             }
266         }
267
268         number_of_test_submodules > 1
269     } else {
270         false
271     }
272 }
273
274 #[cfg(test)]
275 mod tests {
276     use expect_test::{expect, Expect};
277
278     use crate::mock_analysis::analysis_and_position;
279
280     use super::{RunnableAction, BENCH, BIN, DOCTEST, TEST};
281
282     fn check(
283         ra_fixture: &str,
284         // FIXME: fold this into `expect` as well
285         actions: &[&RunnableAction],
286         expect: Expect,
287     ) {
288         let (analysis, position) = analysis_and_position(ra_fixture);
289         let runnables = analysis.runnables(position.file_id).unwrap();
290         expect.assert_debug_eq(&runnables);
291         assert_eq!(
292             actions,
293             runnables.into_iter().map(|it| it.action()).collect::<Vec<_>>().as_slice()
294         );
295     }
296
297     #[test]
298     fn test_runnables() {
299         check(
300             r#"
301 //- /lib.rs
302 <|>
303 fn main() {}
304
305 #[test]
306 fn test_foo() {}
307
308 #[test]
309 #[ignore]
310 fn test_foo() {}
311
312 #[bench]
313 fn bench() {}
314 "#,
315             &[&BIN, &TEST, &TEST, &BENCH],
316             expect![[r#"
317                 [
318                     Runnable {
319                         nav: NavigationTarget {
320                             file_id: FileId(
321                                 1,
322                             ),
323                             full_range: 1..13,
324                             focus_range: Some(
325                                 4..8,
326                             ),
327                             name: "main",
328                             kind: FN,
329                             container_name: None,
330                             description: None,
331                             docs: None,
332                         },
333                         kind: Bin,
334                         cfg_exprs: [],
335                     },
336                     Runnable {
337                         nav: NavigationTarget {
338                             file_id: FileId(
339                                 1,
340                             ),
341                             full_range: 15..39,
342                             focus_range: Some(
343                                 26..34,
344                             ),
345                             name: "test_foo",
346                             kind: FN,
347                             container_name: None,
348                             description: None,
349                             docs: None,
350                         },
351                         kind: Test {
352                             test_id: Path(
353                                 "test_foo",
354                             ),
355                             attr: TestAttr {
356                                 ignore: false,
357                             },
358                         },
359                         cfg_exprs: [],
360                     },
361                     Runnable {
362                         nav: NavigationTarget {
363                             file_id: FileId(
364                                 1,
365                             ),
366                             full_range: 41..75,
367                             focus_range: Some(
368                                 62..70,
369                             ),
370                             name: "test_foo",
371                             kind: FN,
372                             container_name: None,
373                             description: None,
374                             docs: None,
375                         },
376                         kind: Test {
377                             test_id: Path(
378                                 "test_foo",
379                             ),
380                             attr: TestAttr {
381                                 ignore: true,
382                             },
383                         },
384                         cfg_exprs: [],
385                     },
386                     Runnable {
387                         nav: NavigationTarget {
388                             file_id: FileId(
389                                 1,
390                             ),
391                             full_range: 77..99,
392                             focus_range: Some(
393                                 89..94,
394                             ),
395                             name: "bench",
396                             kind: FN,
397                             container_name: None,
398                             description: None,
399                             docs: None,
400                         },
401                         kind: Bench {
402                             test_id: Path(
403                                 "bench",
404                             ),
405                         },
406                         cfg_exprs: [],
407                     },
408                 ]
409             "#]],
410         );
411     }
412
413     #[test]
414     fn test_runnables_doc_test() {
415         check(
416             r#"
417 //- /lib.rs
418 <|>
419 fn main() {}
420
421 /// ```
422 /// let x = 5;
423 /// ```
424 fn foo() {}
425
426 /// ```no_run
427 /// let z = 55;
428 /// ```
429 fn should_have_no_runnable() {}
430
431 /// ```ignore
432 /// let z = 55;
433 /// ```
434 fn should_have_no_runnable_2() {}
435
436 /// ```compile_fail
437 /// let z = 55;
438 /// ```
439 fn should_have_no_runnable_3() {}
440 "#,
441             &[&BIN, &DOCTEST],
442             expect![[r#"
443                 [
444                     Runnable {
445                         nav: NavigationTarget {
446                             file_id: FileId(
447                                 1,
448                             ),
449                             full_range: 1..13,
450                             focus_range: Some(
451                                 4..8,
452                             ),
453                             name: "main",
454                             kind: FN,
455                             container_name: None,
456                             description: None,
457                             docs: None,
458                         },
459                         kind: Bin,
460                         cfg_exprs: [],
461                     },
462                     Runnable {
463                         nav: NavigationTarget {
464                             file_id: FileId(
465                                 1,
466                             ),
467                             full_range: 15..57,
468                             focus_range: None,
469                             name: "foo",
470                             kind: FN,
471                             container_name: None,
472                             description: None,
473                             docs: None,
474                         },
475                         kind: DocTest {
476                             test_id: Path(
477                                 "foo",
478                             ),
479                         },
480                         cfg_exprs: [],
481                     },
482                 ]
483             "#]],
484         );
485     }
486
487     #[test]
488     fn test_runnables_doc_test_in_impl() {
489         check(
490             r#"
491 //- /lib.rs
492 <|>
493 fn main() {}
494
495 struct Data;
496 impl Data {
497     /// ```
498     /// let x = 5;
499     /// ```
500     fn foo() {}
501 }
502 "#,
503             &[&BIN, &DOCTEST],
504             expect![[r#"
505                 [
506                     Runnable {
507                         nav: NavigationTarget {
508                             file_id: FileId(
509                                 1,
510                             ),
511                             full_range: 1..13,
512                             focus_range: Some(
513                                 4..8,
514                             ),
515                             name: "main",
516                             kind: FN,
517                             container_name: None,
518                             description: None,
519                             docs: None,
520                         },
521                         kind: Bin,
522                         cfg_exprs: [],
523                     },
524                     Runnable {
525                         nav: NavigationTarget {
526                             file_id: FileId(
527                                 1,
528                             ),
529                             full_range: 44..98,
530                             focus_range: None,
531                             name: "foo",
532                             kind: FN,
533                             container_name: None,
534                             description: None,
535                             docs: None,
536                         },
537                         kind: DocTest {
538                             test_id: Path(
539                                 "Data::foo",
540                             ),
541                         },
542                         cfg_exprs: [],
543                     },
544                 ]
545             "#]],
546         );
547     }
548
549     #[test]
550     fn test_runnables_module() {
551         check(
552             r#"
553 //- /lib.rs
554 <|>
555 mod test_mod {
556     #[test]
557     fn test_foo1() {}
558 }
559 "#,
560             &[&TEST, &TEST],
561             expect![[r#"
562                 [
563                     Runnable {
564                         nav: NavigationTarget {
565                             file_id: FileId(
566                                 1,
567                             ),
568                             full_range: 1..51,
569                             focus_range: Some(
570                                 5..13,
571                             ),
572                             name: "test_mod",
573                             kind: MODULE,
574                             container_name: None,
575                             description: None,
576                             docs: None,
577                         },
578                         kind: TestMod {
579                             path: "test_mod",
580                         },
581                         cfg_exprs: [],
582                     },
583                     Runnable {
584                         nav: NavigationTarget {
585                             file_id: FileId(
586                                 1,
587                             ),
588                             full_range: 20..49,
589                             focus_range: Some(
590                                 35..44,
591                             ),
592                             name: "test_foo1",
593                             kind: FN,
594                             container_name: None,
595                             description: None,
596                             docs: None,
597                         },
598                         kind: Test {
599                             test_id: Path(
600                                 "test_mod::test_foo1",
601                             ),
602                             attr: TestAttr {
603                                 ignore: false,
604                             },
605                         },
606                         cfg_exprs: [],
607                     },
608                 ]
609             "#]],
610         );
611     }
612
613     #[test]
614     fn only_modules_with_test_functions_or_more_than_one_test_submodule_have_runners() {
615         check(
616             r#"
617 //- /lib.rs
618 <|>
619 mod root_tests {
620     mod nested_tests_0 {
621         mod nested_tests_1 {
622             #[test]
623             fn nested_test_11() {}
624
625             #[test]
626             fn nested_test_12() {}
627         }
628
629         mod nested_tests_2 {
630             #[test]
631             fn nested_test_2() {}
632         }
633
634         mod nested_tests_3 {}
635     }
636
637     mod nested_tests_4 {}
638 }
639 "#,
640             &[&TEST, &TEST, &TEST, &TEST, &TEST, &TEST],
641             expect![[r#"
642                 [
643                     Runnable {
644                         nav: NavigationTarget {
645                             file_id: FileId(
646                                 1,
647                             ),
648                             full_range: 22..323,
649                             focus_range: Some(
650                                 26..40,
651                             ),
652                             name: "nested_tests_0",
653                             kind: MODULE,
654                             container_name: None,
655                             description: None,
656                             docs: None,
657                         },
658                         kind: TestMod {
659                             path: "root_tests::nested_tests_0",
660                         },
661                         cfg_exprs: [],
662                     },
663                     Runnable {
664                         nav: NavigationTarget {
665                             file_id: FileId(
666                                 1,
667                             ),
668                             full_range: 51..192,
669                             focus_range: Some(
670                                 55..69,
671                             ),
672                             name: "nested_tests_1",
673                             kind: MODULE,
674                             container_name: None,
675                             description: None,
676                             docs: None,
677                         },
678                         kind: TestMod {
679                             path: "root_tests::nested_tests_0::nested_tests_1",
680                         },
681                         cfg_exprs: [],
682                     },
683                     Runnable {
684                         nav: NavigationTarget {
685                             file_id: FileId(
686                                 1,
687                             ),
688                             full_range: 84..126,
689                             focus_range: Some(
690                                 107..121,
691                             ),
692                             name: "nested_test_11",
693                             kind: FN,
694                             container_name: None,
695                             description: None,
696                             docs: None,
697                         },
698                         kind: Test {
699                             test_id: Path(
700                                 "root_tests::nested_tests_0::nested_tests_1::nested_test_11",
701                             ),
702                             attr: TestAttr {
703                                 ignore: false,
704                             },
705                         },
706                         cfg_exprs: [],
707                     },
708                     Runnable {
709                         nav: NavigationTarget {
710                             file_id: FileId(
711                                 1,
712                             ),
713                             full_range: 140..182,
714                             focus_range: Some(
715                                 163..177,
716                             ),
717                             name: "nested_test_12",
718                             kind: FN,
719                             container_name: None,
720                             description: None,
721                             docs: None,
722                         },
723                         kind: Test {
724                             test_id: Path(
725                                 "root_tests::nested_tests_0::nested_tests_1::nested_test_12",
726                             ),
727                             attr: TestAttr {
728                                 ignore: false,
729                             },
730                         },
731                         cfg_exprs: [],
732                     },
733                     Runnable {
734                         nav: NavigationTarget {
735                             file_id: FileId(
736                                 1,
737                             ),
738                             full_range: 202..286,
739                             focus_range: Some(
740                                 206..220,
741                             ),
742                             name: "nested_tests_2",
743                             kind: MODULE,
744                             container_name: None,
745                             description: None,
746                             docs: None,
747                         },
748                         kind: TestMod {
749                             path: "root_tests::nested_tests_0::nested_tests_2",
750                         },
751                         cfg_exprs: [],
752                     },
753                     Runnable {
754                         nav: NavigationTarget {
755                             file_id: FileId(
756                                 1,
757                             ),
758                             full_range: 235..276,
759                             focus_range: Some(
760                                 258..271,
761                             ),
762                             name: "nested_test_2",
763                             kind: FN,
764                             container_name: None,
765                             description: None,
766                             docs: None,
767                         },
768                         kind: Test {
769                             test_id: Path(
770                                 "root_tests::nested_tests_0::nested_tests_2::nested_test_2",
771                             ),
772                             attr: TestAttr {
773                                 ignore: false,
774                             },
775                         },
776                         cfg_exprs: [],
777                     },
778                 ]
779             "#]],
780         );
781     }
782
783     #[test]
784     fn test_runnables_with_feature() {
785         check(
786             r#"
787 //- /lib.rs crate:foo cfg:feature=foo
788 <|>
789 #[test]
790 #[cfg(feature = "foo")]
791 fn test_foo1() {}
792 "#,
793             &[&TEST],
794             expect![[r#"
795                 [
796                     Runnable {
797                         nav: NavigationTarget {
798                             file_id: FileId(
799                                 1,
800                             ),
801                             full_range: 1..50,
802                             focus_range: Some(
803                                 36..45,
804                             ),
805                             name: "test_foo1",
806                             kind: FN,
807                             container_name: None,
808                             description: None,
809                             docs: None,
810                         },
811                         kind: Test {
812                             test_id: Path(
813                                 "test_foo1",
814                             ),
815                             attr: TestAttr {
816                                 ignore: false,
817                             },
818                         },
819                         cfg_exprs: [
820                             KeyValue {
821                                 key: "feature",
822                                 value: "foo",
823                             },
824                         ],
825                     },
826                 ]
827             "#]],
828         );
829     }
830
831     #[test]
832     fn test_runnables_with_features() {
833         check(
834             r#"
835 //- /lib.rs crate:foo cfg:feature=foo,feature=bar
836 <|>
837 #[test]
838 #[cfg(all(feature = "foo", feature = "bar"))]
839 fn test_foo1() {}
840 "#,
841             &[&TEST],
842             expect![[r#"
843                 [
844                     Runnable {
845                         nav: NavigationTarget {
846                             file_id: FileId(
847                                 1,
848                             ),
849                             full_range: 1..72,
850                             focus_range: Some(
851                                 58..67,
852                             ),
853                             name: "test_foo1",
854                             kind: FN,
855                             container_name: None,
856                             description: None,
857                             docs: None,
858                         },
859                         kind: Test {
860                             test_id: Path(
861                                 "test_foo1",
862                             ),
863                             attr: TestAttr {
864                                 ignore: false,
865                             },
866                         },
867                         cfg_exprs: [
868                             All(
869                                 [
870                                     KeyValue {
871                                         key: "feature",
872                                         value: "foo",
873                                     },
874                                     KeyValue {
875                                         key: "feature",
876                                         value: "bar",
877                                     },
878                                 ],
879                             ),
880                         ],
881                     },
882                 ]
883             "#]],
884         );
885     }
886
887     #[test]
888     fn test_runnables_no_test_function_in_module() {
889         check(
890             r#"
891 //- /lib.rs
892 <|>
893 mod test_mod {
894     fn foo1() {}
895 }
896 "#,
897             &[],
898             expect![[r#"
899                 []
900             "#]],
901         );
902     }
903 }