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