]> git.lizzy.rs Git - rust.git/blob - crates/ide_assists/src/tests.rs
Merge #11481
[rust.git] / crates / ide_assists / src / tests.rs
1 mod sourcegen;
2 mod generated;
3
4 use expect_test::expect;
5 use hir::{db::DefDatabase, Semantics};
6 use ide_db::{
7     base_db::{fixture::WithFixture, FileId, FileRange, SourceDatabaseExt},
8     helpers::{
9         insert_use::{ImportGranularity, InsertUseConfig},
10         SnippetCap,
11     },
12     source_change::FileSystemEdit,
13     RootDatabase,
14 };
15 use stdx::{format_to, trim_indent};
16 use syntax::TextRange;
17 use test_utils::{assert_eq_text, extract_offset};
18
19 use crate::{
20     assists, handlers::Handler, Assist, AssistConfig, AssistContext, AssistKind,
21     AssistResolveStrategy, Assists, SingleResolve,
22 };
23
24 pub(crate) const TEST_CONFIG: AssistConfig = AssistConfig {
25     snippet_cap: SnippetCap::new(true),
26     allowed: None,
27     insert_use: InsertUseConfig {
28         granularity: ImportGranularity::Crate,
29         prefix_kind: hir::PrefixKind::Plain,
30         enforce_granularity: true,
31         group: true,
32         skip_glob_imports: true,
33     },
34 };
35
36 pub(crate) fn with_single_file(text: &str) -> (RootDatabase, FileId) {
37     RootDatabase::with_single_file(text)
38 }
39
40 #[track_caller]
41 pub(crate) fn check_assist(assist: Handler, ra_fixture_before: &str, ra_fixture_after: &str) {
42     let ra_fixture_after = trim_indent(ra_fixture_after);
43     check(assist, ra_fixture_before, ExpectedResult::After(&ra_fixture_after), None);
44 }
45
46 // There is no way to choose what assist within a group you want to test against,
47 // so this is here to allow you choose.
48 pub(crate) fn check_assist_by_label(
49     assist: Handler,
50     ra_fixture_before: &str,
51     ra_fixture_after: &str,
52     label: &str,
53 ) {
54     let ra_fixture_after = trim_indent(ra_fixture_after);
55     check(assist, ra_fixture_before, ExpectedResult::After(&ra_fixture_after), Some(label));
56 }
57
58 // FIXME: instead of having a separate function here, maybe use
59 // `extract_ranges` and mark the target as `<target> </target>` in the
60 // fixture?
61 #[track_caller]
62 pub(crate) fn check_assist_target(assist: Handler, ra_fixture: &str, target: &str) {
63     check(assist, ra_fixture, ExpectedResult::Target(target), None);
64 }
65
66 #[track_caller]
67 pub(crate) fn check_assist_not_applicable(assist: Handler, ra_fixture: &str) {
68     check(assist, ra_fixture, ExpectedResult::NotApplicable, None);
69 }
70
71 /// Check assist in unresolved state. Useful to check assists for lazy computation.
72 #[track_caller]
73 pub(crate) fn check_assist_unresolved(assist: Handler, ra_fixture: &str) {
74     check(assist, ra_fixture, ExpectedResult::Unresolved, None);
75 }
76
77 #[track_caller]
78 fn check_doc_test(assist_id: &str, before: &str, after: &str) {
79     let after = trim_indent(after);
80     let (db, file_id, selection) = RootDatabase::with_range_or_offset(before);
81     let before = db.file_text(file_id).to_string();
82     let frange = FileRange { file_id, range: selection.into() };
83
84     let assist = assists(&db, &TEST_CONFIG, AssistResolveStrategy::All, frange)
85         .into_iter()
86         .find(|assist| assist.id.0 == assist_id)
87         .unwrap_or_else(|| {
88             panic!(
89                 "\n\nAssist is not applicable: {}\nAvailable assists: {}",
90                 assist_id,
91                 assists(&db, &TEST_CONFIG, AssistResolveStrategy::None, frange)
92                     .into_iter()
93                     .map(|assist| assist.id.0)
94                     .collect::<Vec<_>>()
95                     .join(", ")
96             )
97         });
98
99     let actual = {
100         let source_change =
101             assist.source_change.expect("Assist did not contain any source changes");
102         let mut actual = before;
103         if let Some(source_file_edit) = source_change.get_source_edit(file_id) {
104             source_file_edit.apply(&mut actual);
105         }
106         actual
107     };
108     assert_eq_text!(&after, &actual);
109 }
110
111 enum ExpectedResult<'a> {
112     NotApplicable,
113     Unresolved,
114     After(&'a str),
115     Target(&'a str),
116 }
117
118 #[track_caller]
119 fn check(handler: Handler, before: &str, expected: ExpectedResult, assist_label: Option<&str>) {
120     let (mut db, file_with_caret_id, range_or_offset) = RootDatabase::with_range_or_offset(before);
121     db.set_enable_proc_attr_macros(true);
122     let text_without_caret = db.file_text(file_with_caret_id).to_string();
123
124     let frange = FileRange { file_id: file_with_caret_id, range: range_or_offset.into() };
125
126     let sema = Semantics::new(&db);
127     let config = TEST_CONFIG;
128     let ctx = AssistContext::new(sema, &config, frange);
129     let resolve = match expected {
130         ExpectedResult::Unresolved => AssistResolveStrategy::None,
131         _ => AssistResolveStrategy::All,
132     };
133     let mut acc = Assists::new(&ctx, resolve);
134     handler(&mut acc, &ctx);
135     let mut res = acc.finish();
136
137     let assist = match assist_label {
138         Some(label) => res.into_iter().find(|resolved| resolved.label == label),
139         None => res.pop(),
140     };
141
142     match (assist, expected) {
143         (Some(assist), ExpectedResult::After(after)) => {
144             let source_change =
145                 assist.source_change.expect("Assist did not contain any source changes");
146             let skip_header = source_change.source_file_edits.len() == 1
147                 && source_change.file_system_edits.len() == 0;
148
149             let mut buf = String::new();
150             for (file_id, edit) in source_change.source_file_edits {
151                 let mut text = db.file_text(file_id).as_ref().to_owned();
152                 edit.apply(&mut text);
153                 if !skip_header {
154                     let sr = db.file_source_root(file_id);
155                     let sr = db.source_root(sr);
156                     let path = sr.path_for_file(&file_id).unwrap();
157                     format_to!(buf, "//- {}\n", path)
158                 }
159                 buf.push_str(&text);
160             }
161
162             for file_system_edit in source_change.file_system_edits {
163                 let (dst, contents) = match file_system_edit {
164                     FileSystemEdit::CreateFile { dst, initial_contents } => (dst, initial_contents),
165                     FileSystemEdit::MoveFile { src, dst } => {
166                         (dst, db.file_text(src).as_ref().to_owned())
167                     }
168                 };
169                 let sr = db.file_source_root(dst.anchor);
170                 let sr = db.source_root(sr);
171                 let mut base = sr.path_for_file(&dst.anchor).unwrap().clone();
172                 base.pop();
173                 let created_file_path = base.join(&dst.path).unwrap();
174                 format_to!(buf, "//- {}\n", created_file_path);
175                 buf.push_str(&contents);
176             }
177
178             assert_eq_text!(after, &buf);
179         }
180         (Some(assist), ExpectedResult::Target(target)) => {
181             let range = assist.target;
182             assert_eq_text!(&text_without_caret[range], target);
183         }
184         (Some(assist), ExpectedResult::Unresolved) => assert!(
185             assist.source_change.is_none(),
186             "unresolved assist should not contain source changes"
187         ),
188         (Some(_), ExpectedResult::NotApplicable) => panic!("assist should not be applicable!"),
189         (
190             None,
191             ExpectedResult::After(_) | ExpectedResult::Target(_) | ExpectedResult::Unresolved,
192         ) => {
193             panic!("code action is not applicable")
194         }
195         (None, ExpectedResult::NotApplicable) => (),
196     };
197 }
198
199 fn labels(assists: &[Assist]) -> String {
200     let mut labels = assists
201         .iter()
202         .map(|assist| {
203             let mut label = match &assist.group {
204                 Some(g) => g.0.clone(),
205                 None => assist.label.to_string(),
206             };
207             label.push('\n');
208             label
209         })
210         .collect::<Vec<_>>();
211     labels.dedup();
212     labels.into_iter().collect::<String>()
213 }
214
215 #[test]
216 fn assist_order_field_struct() {
217     let before = "struct Foo { $0bar: u32 }";
218     let (before_cursor_pos, before) = extract_offset(before);
219     let (db, file_id) = with_single_file(&before);
220     let frange = FileRange { file_id, range: TextRange::empty(before_cursor_pos) };
221     let assists = assists(&db, &TEST_CONFIG, AssistResolveStrategy::None, frange);
222     let mut assists = assists.iter();
223
224     assert_eq!(assists.next().expect("expected assist").label, "Change visibility to pub(crate)");
225     assert_eq!(assists.next().expect("expected assist").label, "Generate a getter method");
226     assert_eq!(assists.next().expect("expected assist").label, "Generate a mut getter method");
227     assert_eq!(assists.next().expect("expected assist").label, "Generate a setter method");
228     assert_eq!(assists.next().expect("expected assist").label, "Generate `Deref` impl using `bar`");
229     assert_eq!(assists.next().expect("expected assist").label, "Add `#[derive]`");
230 }
231
232 #[test]
233 fn assist_order_if_expr() {
234     let (db, frange) = RootDatabase::with_range(
235         r#"
236 pub fn test_some_range(a: int) -> bool {
237     if let 2..6 = $05$0 {
238         true
239     } else {
240         false
241     }
242 }
243 "#,
244     );
245
246     let assists = assists(&db, &TEST_CONFIG, AssistResolveStrategy::None, frange);
247     let expected = labels(&assists);
248
249     expect![[r#"
250         Convert integer base
251         Extract into variable
252         Extract into function
253         Replace if let with match
254     "#]]
255     .assert_eq(&expected);
256 }
257
258 #[test]
259 fn assist_filter_works() {
260     let (db, frange) = RootDatabase::with_range(
261         r#"
262 pub fn test_some_range(a: int) -> bool {
263     if let 2..6 = $05$0 {
264         true
265     } else {
266         false
267     }
268 }
269 "#,
270     );
271     {
272         let mut cfg = TEST_CONFIG;
273         cfg.allowed = Some(vec![AssistKind::Refactor]);
274
275         let assists = assists(&db, &cfg, AssistResolveStrategy::None, frange);
276         let expected = labels(&assists);
277
278         expect![[r#"
279             Convert integer base
280             Extract into variable
281             Extract into function
282             Replace if let with match
283         "#]]
284         .assert_eq(&expected);
285     }
286
287     {
288         let mut cfg = TEST_CONFIG;
289         cfg.allowed = Some(vec![AssistKind::RefactorExtract]);
290         let assists = assists(&db, &cfg, AssistResolveStrategy::None, frange);
291         let expected = labels(&assists);
292
293         expect![[r#"
294             Extract into variable
295             Extract into function
296         "#]]
297         .assert_eq(&expected);
298     }
299
300     {
301         let mut cfg = TEST_CONFIG;
302         cfg.allowed = Some(vec![AssistKind::QuickFix]);
303         let assists = assists(&db, &cfg, AssistResolveStrategy::None, frange);
304         let expected = labels(&assists);
305
306         expect![[r#""#]].assert_eq(&expected);
307     }
308 }
309
310 #[test]
311 fn various_resolve_strategies() {
312     let (db, frange) = RootDatabase::with_range(
313         r#"
314 pub fn test_some_range(a: int) -> bool {
315     if let 2..6 = $05$0 {
316         true
317     } else {
318         false
319     }
320 }
321 "#,
322     );
323
324     let mut cfg = TEST_CONFIG;
325     cfg.allowed = Some(vec![AssistKind::RefactorExtract]);
326
327     {
328         let assists = assists(&db, &cfg, AssistResolveStrategy::None, frange);
329         assert_eq!(2, assists.len());
330         let mut assists = assists.into_iter();
331
332         let extract_into_variable_assist = assists.next().unwrap();
333         expect![[r#"
334             Assist {
335                 id: AssistId(
336                     "extract_variable",
337                     RefactorExtract,
338                 ),
339                 label: "Extract into variable",
340                 group: None,
341                 target: 59..60,
342                 source_change: None,
343             }
344         "#]]
345         .assert_debug_eq(&extract_into_variable_assist);
346
347         let extract_into_function_assist = assists.next().unwrap();
348         expect![[r#"
349             Assist {
350                 id: AssistId(
351                     "extract_function",
352                     RefactorExtract,
353                 ),
354                 label: "Extract into function",
355                 group: None,
356                 target: 59..60,
357                 source_change: None,
358             }
359         "#]]
360         .assert_debug_eq(&extract_into_function_assist);
361     }
362
363     {
364         let assists = assists(
365             &db,
366             &cfg,
367             AssistResolveStrategy::Single(SingleResolve {
368                 assist_id: "SOMETHING_MISMATCHING".to_string(),
369                 assist_kind: AssistKind::RefactorExtract,
370             }),
371             frange,
372         );
373         assert_eq!(2, assists.len());
374         let mut assists = assists.into_iter();
375
376         let extract_into_variable_assist = assists.next().unwrap();
377         expect![[r#"
378             Assist {
379                 id: AssistId(
380                     "extract_variable",
381                     RefactorExtract,
382                 ),
383                 label: "Extract into variable",
384                 group: None,
385                 target: 59..60,
386                 source_change: None,
387             }
388         "#]]
389         .assert_debug_eq(&extract_into_variable_assist);
390
391         let extract_into_function_assist = assists.next().unwrap();
392         expect![[r#"
393             Assist {
394                 id: AssistId(
395                     "extract_function",
396                     RefactorExtract,
397                 ),
398                 label: "Extract into function",
399                 group: None,
400                 target: 59..60,
401                 source_change: None,
402             }
403         "#]]
404         .assert_debug_eq(&extract_into_function_assist);
405     }
406
407     {
408         let assists = assists(
409             &db,
410             &cfg,
411             AssistResolveStrategy::Single(SingleResolve {
412                 assist_id: "extract_variable".to_string(),
413                 assist_kind: AssistKind::RefactorExtract,
414             }),
415             frange,
416         );
417         assert_eq!(2, assists.len());
418         let mut assists = assists.into_iter();
419
420         let extract_into_variable_assist = assists.next().unwrap();
421         expect![[r#"
422             Assist {
423                 id: AssistId(
424                     "extract_variable",
425                     RefactorExtract,
426                 ),
427                 label: "Extract into variable",
428                 group: None,
429                 target: 59..60,
430                 source_change: Some(
431                     SourceChange {
432                         source_file_edits: {
433                             FileId(
434                                 0,
435                             ): TextEdit {
436                                 indels: [
437                                     Indel {
438                                         insert: "let $0var_name = 5;\n    ",
439                                         delete: 45..45,
440                                     },
441                                     Indel {
442                                         insert: "var_name",
443                                         delete: 59..60,
444                                     },
445                                 ],
446                             },
447                         },
448                         file_system_edits: [],
449                         is_snippet: true,
450                     },
451                 ),
452             }
453         "#]]
454         .assert_debug_eq(&extract_into_variable_assist);
455
456         let extract_into_function_assist = assists.next().unwrap();
457         expect![[r#"
458             Assist {
459                 id: AssistId(
460                     "extract_function",
461                     RefactorExtract,
462                 ),
463                 label: "Extract into function",
464                 group: None,
465                 target: 59..60,
466                 source_change: None,
467             }
468         "#]]
469         .assert_debug_eq(&extract_into_function_assist);
470     }
471
472     {
473         let assists = assists(&db, &cfg, AssistResolveStrategy::All, frange);
474         assert_eq!(2, assists.len());
475         let mut assists = assists.into_iter();
476
477         let extract_into_variable_assist = assists.next().unwrap();
478         expect![[r#"
479             Assist {
480                 id: AssistId(
481                     "extract_variable",
482                     RefactorExtract,
483                 ),
484                 label: "Extract into variable",
485                 group: None,
486                 target: 59..60,
487                 source_change: Some(
488                     SourceChange {
489                         source_file_edits: {
490                             FileId(
491                                 0,
492                             ): TextEdit {
493                                 indels: [
494                                     Indel {
495                                         insert: "let $0var_name = 5;\n    ",
496                                         delete: 45..45,
497                                     },
498                                     Indel {
499                                         insert: "var_name",
500                                         delete: 59..60,
501                                     },
502                                 ],
503                             },
504                         },
505                         file_system_edits: [],
506                         is_snippet: true,
507                     },
508                 ),
509             }
510         "#]]
511         .assert_debug_eq(&extract_into_variable_assist);
512
513         let extract_into_function_assist = assists.next().unwrap();
514         expect![[r#"
515             Assist {
516                 id: AssistId(
517                     "extract_function",
518                     RefactorExtract,
519                 ),
520                 label: "Extract into function",
521                 group: None,
522                 target: 59..60,
523                 source_change: Some(
524                     SourceChange {
525                         source_file_edits: {
526                             FileId(
527                                 0,
528                             ): TextEdit {
529                                 indels: [
530                                     Indel {
531                                         insert: "fun_name()",
532                                         delete: 59..60,
533                                     },
534                                     Indel {
535                                         insert: "\n\nfn $0fun_name() -> i32 {\n    5\n}",
536                                         delete: 110..110,
537                                     },
538                                 ],
539                             },
540                         },
541                         file_system_edits: [],
542                         is_snippet: true,
543                     },
544                 ),
545             }
546         "#]]
547         .assert_debug_eq(&extract_into_function_assist);
548     }
549 }