aboutsummaryrefslogtreecommitdiff
path: root/crates/ide/src/diagnostics.rs
diff options
context:
space:
mode:
Diffstat (limited to 'crates/ide/src/diagnostics.rs')
-rw-r--r--crates/ide/src/diagnostics.rs678
1 files changed, 678 insertions, 0 deletions
diff --git a/crates/ide/src/diagnostics.rs b/crates/ide/src/diagnostics.rs
new file mode 100644
index 000000000..a3ec98178
--- /dev/null
+++ b/crates/ide/src/diagnostics.rs
@@ -0,0 +1,678 @@
1//! Collects diagnostics & fixits for a single file.
2//!
3//! The tricky bit here is that diagnostics are produced by hir in terms of
4//! macro-expanded files, but we need to present them to the users in terms of
5//! original files. So we need to map the ranges.
6
7use std::cell::RefCell;
8
9use base_db::SourceDatabase;
10use hir::{diagnostics::DiagnosticSinkBuilder, Semantics};
11use ide_db::RootDatabase;
12use itertools::Itertools;
13use syntax::{
14 ast::{self, AstNode},
15 SyntaxNode, TextRange, T,
16};
17use text_edit::TextEdit;
18
19use crate::{Diagnostic, FileId, Fix, SourceFileEdit};
20
21mod diagnostics_with_fix;
22use diagnostics_with_fix::DiagnosticWithFix;
23
24#[derive(Debug, Copy, Clone)]
25pub enum Severity {
26 Error,
27 WeakWarning,
28}
29
30pub(crate) fn diagnostics(
31 db: &RootDatabase,
32 file_id: FileId,
33 enable_experimental: bool,
34) -> Vec<Diagnostic> {
35 let _p = profile::span("diagnostics");
36 let sema = Semantics::new(db);
37 let parse = db.parse(file_id);
38 let mut res = Vec::new();
39
40 // [#34344] Only take first 128 errors to prevent slowing down editor/ide, the number 128 is chosen arbitrarily.
41 res.extend(parse.errors().iter().take(128).map(|err| Diagnostic {
42 range: err.range(),
43 message: format!("Syntax Error: {}", err),
44 severity: Severity::Error,
45 fix: None,
46 }));
47
48 for node in parse.tree().syntax().descendants() {
49 check_unnecessary_braces_in_use_statement(&mut res, file_id, &node);
50 check_struct_shorthand_initialization(&mut res, file_id, &node);
51 }
52 let res = RefCell::new(res);
53 let mut sink = DiagnosticSinkBuilder::new()
54 .on::<hir::diagnostics::UnresolvedModule, _>(|d| {
55 res.borrow_mut().push(diagnostic_with_fix(d, &sema));
56 })
57 .on::<hir::diagnostics::MissingFields, _>(|d| {
58 res.borrow_mut().push(diagnostic_with_fix(d, &sema));
59 })
60 .on::<hir::diagnostics::MissingOkInTailExpr, _>(|d| {
61 res.borrow_mut().push(diagnostic_with_fix(d, &sema));
62 })
63 .on::<hir::diagnostics::NoSuchField, _>(|d| {
64 res.borrow_mut().push(diagnostic_with_fix(d, &sema));
65 })
66 // Only collect experimental diagnostics when they're enabled.
67 .filter(|diag| !diag.is_experimental() || enable_experimental)
68 // Diagnostics not handled above get no fix and default treatment.
69 .build(|d| {
70 res.borrow_mut().push(Diagnostic {
71 message: d.message(),
72 range: sema.diagnostics_display_range(d).range,
73 severity: Severity::Error,
74 fix: None,
75 })
76 });
77
78 if let Some(m) = sema.to_module_def(file_id) {
79 m.diagnostics(db, &mut sink);
80 };
81 drop(sink);
82 res.into_inner()
83}
84
85fn diagnostic_with_fix<D: DiagnosticWithFix>(d: &D, sema: &Semantics<RootDatabase>) -> Diagnostic {
86 Diagnostic {
87 range: sema.diagnostics_display_range(d).range,
88 message: d.message(),
89 severity: Severity::Error,
90 fix: d.fix(&sema),
91 }
92}
93
94fn check_unnecessary_braces_in_use_statement(
95 acc: &mut Vec<Diagnostic>,
96 file_id: FileId,
97 node: &SyntaxNode,
98) -> Option<()> {
99 let use_tree_list = ast::UseTreeList::cast(node.clone())?;
100 if let Some((single_use_tree,)) = use_tree_list.use_trees().collect_tuple() {
101 let use_range = use_tree_list.syntax().text_range();
102 let edit =
103 text_edit_for_remove_unnecessary_braces_with_self_in_use_statement(&single_use_tree)
104 .unwrap_or_else(|| {
105 let to_replace = single_use_tree.syntax().text().to_string();
106 let mut edit_builder = TextEdit::builder();
107 edit_builder.delete(use_range);
108 edit_builder.insert(use_range.start(), to_replace);
109 edit_builder.finish()
110 });
111
112 acc.push(Diagnostic {
113 range: use_range,
114 message: "Unnecessary braces in use statement".to_string(),
115 severity: Severity::WeakWarning,
116 fix: Some(Fix::new(
117 "Remove unnecessary braces",
118 SourceFileEdit { file_id, edit }.into(),
119 use_range,
120 )),
121 });
122 }
123
124 Some(())
125}
126
127fn text_edit_for_remove_unnecessary_braces_with_self_in_use_statement(
128 single_use_tree: &ast::UseTree,
129) -> Option<TextEdit> {
130 let use_tree_list_node = single_use_tree.syntax().parent()?;
131 if single_use_tree.path()?.segment()?.syntax().first_child_or_token()?.kind() == T![self] {
132 let start = use_tree_list_node.prev_sibling_or_token()?.text_range().start();
133 let end = use_tree_list_node.text_range().end();
134 return Some(TextEdit::delete(TextRange::new(start, end)));
135 }
136 None
137}
138
139fn check_struct_shorthand_initialization(
140 acc: &mut Vec<Diagnostic>,
141 file_id: FileId,
142 node: &SyntaxNode,
143) -> Option<()> {
144 let record_lit = ast::RecordExpr::cast(node.clone())?;
145 let record_field_list = record_lit.record_expr_field_list()?;
146 for record_field in record_field_list.fields() {
147 if let (Some(name_ref), Some(expr)) = (record_field.name_ref(), record_field.expr()) {
148 let field_name = name_ref.syntax().text().to_string();
149 let field_expr = expr.syntax().text().to_string();
150 let field_name_is_tup_index = name_ref.as_tuple_field().is_some();
151 if field_name == field_expr && !field_name_is_tup_index {
152 let mut edit_builder = TextEdit::builder();
153 edit_builder.delete(record_field.syntax().text_range());
154 edit_builder.insert(record_field.syntax().text_range().start(), field_name);
155 let edit = edit_builder.finish();
156
157 let field_range = record_field.syntax().text_range();
158 acc.push(Diagnostic {
159 range: field_range,
160 message: "Shorthand struct initialization".to_string(),
161 severity: Severity::WeakWarning,
162 fix: Some(Fix::new(
163 "Use struct shorthand initialization",
164 SourceFileEdit { file_id, edit }.into(),
165 field_range,
166 )),
167 });
168 }
169 }
170 }
171 Some(())
172}
173
174#[cfg(test)]
175mod tests {
176 use stdx::trim_indent;
177 use test_utils::assert_eq_text;
178
179 use crate::mock_analysis::{analysis_and_position, single_file, MockAnalysis};
180 use expect::{expect, Expect};
181
182 /// Takes a multi-file input fixture with annotated cursor positions,
183 /// and checks that:
184 /// * a diagnostic is produced
185 /// * this diagnostic fix trigger range touches the input cursor position
186 /// * that the contents of the file containing the cursor match `after` after the diagnostic fix is applied
187 fn check_fix(ra_fixture_before: &str, ra_fixture_after: &str) {
188 let after = trim_indent(ra_fixture_after);
189
190 let (analysis, file_position) = analysis_and_position(ra_fixture_before);
191 let diagnostic = analysis.diagnostics(file_position.file_id, true).unwrap().pop().unwrap();
192 let mut fix = diagnostic.fix.unwrap();
193 let edit = fix.source_change.source_file_edits.pop().unwrap().edit;
194 let target_file_contents = analysis.file_text(file_position.file_id).unwrap();
195 let actual = {
196 let mut actual = target_file_contents.to_string();
197 edit.apply(&mut actual);
198 actual
199 };
200
201 assert_eq_text!(&after, &actual);
202 assert!(
203 fix.fix_trigger_range.start() <= file_position.offset
204 && fix.fix_trigger_range.end() >= file_position.offset,
205 "diagnostic fix range {:?} does not touch cursor position {:?}",
206 fix.fix_trigger_range,
207 file_position.offset
208 );
209 }
210
211 /// Checks that a diagnostic applies to the file containing the `<|>` cursor marker
212 /// which has a fix that can apply to other files.
213 fn check_apply_diagnostic_fix_in_other_file(ra_fixture_before: &str, ra_fixture_after: &str) {
214 let ra_fixture_after = &trim_indent(ra_fixture_after);
215 let (analysis, file_pos) = analysis_and_position(ra_fixture_before);
216 let current_file_id = file_pos.file_id;
217 let diagnostic = analysis.diagnostics(current_file_id, true).unwrap().pop().unwrap();
218 let mut fix = diagnostic.fix.unwrap();
219 let edit = fix.source_change.source_file_edits.pop().unwrap();
220 let changed_file_id = edit.file_id;
221 let before = analysis.file_text(changed_file_id).unwrap();
222 let actual = {
223 let mut actual = before.to_string();
224 edit.edit.apply(&mut actual);
225 actual
226 };
227 assert_eq_text!(ra_fixture_after, &actual);
228 }
229
230 /// Takes a multi-file input fixture with annotated cursor position and checks that no diagnostics
231 /// apply to the file containing the cursor.
232 fn check_no_diagnostics(ra_fixture: &str) {
233 let mock = MockAnalysis::with_files(ra_fixture);
234 let files = mock.files().map(|(it, _)| it).collect::<Vec<_>>();
235 let analysis = mock.analysis();
236 let diagnostics = files
237 .into_iter()
238 .flat_map(|file_id| analysis.diagnostics(file_id, true).unwrap())
239 .collect::<Vec<_>>();
240 assert_eq!(diagnostics.len(), 0, "unexpected diagnostics:\n{:#?}", diagnostics);
241 }
242
243 fn check_expect(ra_fixture: &str, expect: Expect) {
244 let (analysis, file_id) = single_file(ra_fixture);
245 let diagnostics = analysis.diagnostics(file_id, true).unwrap();
246 expect.assert_debug_eq(&diagnostics)
247 }
248
249 #[test]
250 fn test_wrap_return_type() {
251 check_fix(
252 r#"
253//- /main.rs
254use core::result::Result::{self, Ok, Err};
255
256fn div(x: i32, y: i32) -> Result<i32, ()> {
257 if y == 0 {
258 return Err(());
259 }
260 x / y<|>
261}
262//- /core/lib.rs
263pub mod result {
264 pub enum Result<T, E> { Ok(T), Err(E) }
265}
266"#,
267 r#"
268use core::result::Result::{self, Ok, Err};
269
270fn div(x: i32, y: i32) -> Result<i32, ()> {
271 if y == 0 {
272 return Err(());
273 }
274 Ok(x / y)
275}
276"#,
277 );
278 }
279
280 #[test]
281 fn test_wrap_return_type_handles_generic_functions() {
282 check_fix(
283 r#"
284//- /main.rs
285use core::result::Result::{self, Ok, Err};
286
287fn div<T>(x: T) -> Result<T, i32> {
288 if x == 0 {
289 return Err(7);
290 }
291 <|>x
292}
293//- /core/lib.rs
294pub mod result {
295 pub enum Result<T, E> { Ok(T), Err(E) }
296}
297"#,
298 r#"
299use core::result::Result::{self, Ok, Err};
300
301fn div<T>(x: T) -> Result<T, i32> {
302 if x == 0 {
303 return Err(7);
304 }
305 Ok(x)
306}
307"#,
308 );
309 }
310
311 #[test]
312 fn test_wrap_return_type_handles_type_aliases() {
313 check_fix(
314 r#"
315//- /main.rs
316use core::result::Result::{self, Ok, Err};
317
318type MyResult<T> = Result<T, ()>;
319
320fn div(x: i32, y: i32) -> MyResult<i32> {
321 if y == 0 {
322 return Err(());
323 }
324 x <|>/ y
325}
326//- /core/lib.rs
327pub mod result {
328 pub enum Result<T, E> { Ok(T), Err(E) }
329}
330"#,
331 r#"
332use core::result::Result::{self, Ok, Err};
333
334type MyResult<T> = Result<T, ()>;
335
336fn div(x: i32, y: i32) -> MyResult<i32> {
337 if y == 0 {
338 return Err(());
339 }
340 Ok(x / y)
341}
342"#,
343 );
344 }
345
346 #[test]
347 fn test_wrap_return_type_not_applicable_when_expr_type_does_not_match_ok_type() {
348 check_no_diagnostics(
349 r#"
350//- /main.rs
351use core::result::Result::{self, Ok, Err};
352
353fn foo() -> Result<(), i32> { 0 }
354
355//- /core/lib.rs
356pub mod result {
357 pub enum Result<T, E> { Ok(T), Err(E) }
358}
359"#,
360 );
361 }
362
363 #[test]
364 fn test_wrap_return_type_not_applicable_when_return_type_is_not_result() {
365 check_no_diagnostics(
366 r#"
367//- /main.rs
368use core::result::Result::{self, Ok, Err};
369
370enum SomeOtherEnum { Ok(i32), Err(String) }
371
372fn foo() -> SomeOtherEnum { 0 }
373
374//- /core/lib.rs
375pub mod result {
376 pub enum Result<T, E> { Ok(T), Err(E) }
377}
378"#,
379 );
380 }
381
382 #[test]
383 fn test_fill_struct_fields_empty() {
384 check_fix(
385 r#"
386struct TestStruct { one: i32, two: i64 }
387
388fn test_fn() {
389 let s = TestStruct {<|>};
390}
391"#,
392 r#"
393struct TestStruct { one: i32, two: i64 }
394
395fn test_fn() {
396 let s = TestStruct { one: (), two: ()};
397}
398"#,
399 );
400 }
401
402 #[test]
403 fn test_fill_struct_fields_self() {
404 check_fix(
405 r#"
406struct TestStruct { one: i32 }
407
408impl TestStruct {
409 fn test_fn() { let s = Self {<|>}; }
410}
411"#,
412 r#"
413struct TestStruct { one: i32 }
414
415impl TestStruct {
416 fn test_fn() { let s = Self { one: ()}; }
417}
418"#,
419 );
420 }
421
422 #[test]
423 fn test_fill_struct_fields_enum() {
424 check_fix(
425 r#"
426enum Expr {
427 Bin { lhs: Box<Expr>, rhs: Box<Expr> }
428}
429
430impl Expr {
431 fn new_bin(lhs: Box<Expr>, rhs: Box<Expr>) -> Expr {
432 Expr::Bin {<|> }
433 }
434}
435"#,
436 r#"
437enum Expr {
438 Bin { lhs: Box<Expr>, rhs: Box<Expr> }
439}
440
441impl Expr {
442 fn new_bin(lhs: Box<Expr>, rhs: Box<Expr>) -> Expr {
443 Expr::Bin { lhs: (), rhs: () }
444 }
445}
446"#,
447 );
448 }
449
450 #[test]
451 fn test_fill_struct_fields_partial() {
452 check_fix(
453 r#"
454struct TestStruct { one: i32, two: i64 }
455
456fn test_fn() {
457 let s = TestStruct{ two: 2<|> };
458}
459"#,
460 r"
461struct TestStruct { one: i32, two: i64 }
462
463fn test_fn() {
464 let s = TestStruct{ two: 2, one: () };
465}
466",
467 );
468 }
469
470 #[test]
471 fn test_fill_struct_fields_no_diagnostic() {
472 check_no_diagnostics(
473 r"
474 struct TestStruct { one: i32, two: i64 }
475
476 fn test_fn() {
477 let one = 1;
478 let s = TestStruct{ one, two: 2 };
479 }
480 ",
481 );
482 }
483
484 #[test]
485 fn test_fill_struct_fields_no_diagnostic_on_spread() {
486 check_no_diagnostics(
487 r"
488 struct TestStruct { one: i32, two: i64 }
489
490 fn test_fn() {
491 let one = 1;
492 let s = TestStruct{ ..a };
493 }
494 ",
495 );
496 }
497
498 #[test]
499 fn test_unresolved_module_diagnostic() {
500 check_expect(
501 r#"mod foo;"#,
502 expect![[r#"
503 [
504 Diagnostic {
505 message: "unresolved module",
506 range: 0..8,
507 severity: Error,
508 fix: Some(
509 Fix {
510 label: "Create module",
511 source_change: SourceChange {
512 source_file_edits: [],
513 file_system_edits: [
514 CreateFile {
515 anchor: FileId(
516 1,
517 ),
518 dst: "foo.rs",
519 },
520 ],
521 is_snippet: false,
522 },
523 fix_trigger_range: 0..8,
524 },
525 ),
526 },
527 ]
528 "#]],
529 );
530 }
531
532 #[test]
533 fn range_mapping_out_of_macros() {
534 // FIXME: this is very wrong, but somewhat tricky to fix.
535 check_fix(
536 r#"
537fn some() {}
538fn items() {}
539fn here() {}
540
541macro_rules! id { ($($tt:tt)*) => { $($tt)*}; }
542
543fn main() {
544 let _x = id![Foo { a: <|>42 }];
545}
546
547pub struct Foo { pub a: i32, pub b: i32 }
548"#,
549 r#"
550fn {a:42, b: ()} {}
551fn items() {}
552fn here() {}
553
554macro_rules! id { ($($tt:tt)*) => { $($tt)*}; }
555
556fn main() {
557 let _x = id![Foo { a: 42 }];
558}
559
560pub struct Foo { pub a: i32, pub b: i32 }
561"#,
562 );
563 }
564
565 #[test]
566 fn test_check_unnecessary_braces_in_use_statement() {
567 check_no_diagnostics(
568 r#"
569use a;
570use a::{c, d::e};
571"#,
572 );
573 check_fix(r#"use {<|>b};"#, r#"use b;"#);
574 check_fix(r#"use {b<|>};"#, r#"use b;"#);
575 check_fix(r#"use a::{c<|>};"#, r#"use a::c;"#);
576 check_fix(r#"use a::{self<|>};"#, r#"use a;"#);
577 check_fix(r#"use a::{c, d::{e<|>}};"#, r#"use a::{c, d::e};"#);
578 }
579
580 #[test]
581 fn test_check_struct_shorthand_initialization() {
582 check_no_diagnostics(
583 r#"
584struct A { a: &'static str }
585fn main() { A { a: "hello" } }
586"#,
587 );
588 check_no_diagnostics(
589 r#"
590struct A(usize);
591fn main() { A { 0: 0 } }
592"#,
593 );
594
595 check_fix(
596 r#"
597struct A { a: &'static str }
598fn main() {
599 let a = "haha";
600 A { a<|>: a }
601}
602"#,
603 r#"
604struct A { a: &'static str }
605fn main() {
606 let a = "haha";
607 A { a }
608}
609"#,
610 );
611
612 check_fix(
613 r#"
614struct A { a: &'static str, b: &'static str }
615fn main() {
616 let a = "haha";
617 let b = "bb";
618 A { a<|>: a, b }
619}
620"#,
621 r#"
622struct A { a: &'static str, b: &'static str }
623fn main() {
624 let a = "haha";
625 let b = "bb";
626 A { a, b }
627}
628"#,
629 );
630 }
631
632 #[test]
633 fn test_add_field_from_usage() {
634 check_fix(
635 r"
636fn main() {
637 Foo { bar: 3, baz<|>: false};
638}
639struct Foo {
640 bar: i32
641}
642",
643 r"
644fn main() {
645 Foo { bar: 3, baz: false};
646}
647struct Foo {
648 bar: i32,
649 baz: bool
650}
651",
652 )
653 }
654
655 #[test]
656 fn test_add_field_in_other_file_from_usage() {
657 check_apply_diagnostic_fix_in_other_file(
658 r"
659 //- /main.rs
660 mod foo;
661
662 fn main() {
663 <|>foo::Foo { bar: 3, baz: false};
664 }
665 //- /foo.rs
666 struct Foo {
667 bar: i32
668 }
669 ",
670 r"
671 struct Foo {
672 bar: i32,
673 pub(crate) baz: bool
674 }
675 ",
676 )
677 }
678}