aboutsummaryrefslogtreecommitdiff
path: root/crates/ra_ide/src/diagnostics.rs
diff options
context:
space:
mode:
Diffstat (limited to 'crates/ra_ide/src/diagnostics.rs')
-rw-r--r--crates/ra_ide/src/diagnostics.rs652
1 files changed, 652 insertions, 0 deletions
diff --git a/crates/ra_ide/src/diagnostics.rs b/crates/ra_ide/src/diagnostics.rs
new file mode 100644
index 000000000..cc1ccab4b
--- /dev/null
+++ b/crates/ra_ide/src/diagnostics.rs
@@ -0,0 +1,652 @@
1//! FIXME: write short doc here
2
3use std::cell::RefCell;
4
5use hir::diagnostics::{AstDiagnostic, Diagnostic as _, DiagnosticSink};
6use itertools::Itertools;
7use ra_db::{RelativePath, SourceDatabase, SourceDatabaseExt};
8use ra_prof::profile;
9use ra_syntax::{
10 algo,
11 ast::{self, make, AstNode},
12 Location, SyntaxNode, TextRange, T,
13};
14use ra_text_edit::{TextEdit, TextEditBuilder};
15
16use crate::{db::RootDatabase, Diagnostic, FileId, FileSystemEdit, SourceChange, SourceFileEdit};
17
18#[derive(Debug, Copy, Clone)]
19pub enum Severity {
20 Error,
21 WeakWarning,
22}
23
24pub(crate) fn diagnostics(db: &RootDatabase, file_id: FileId) -> Vec<Diagnostic> {
25 let _p = profile("diagnostics");
26 let parse = db.parse(file_id);
27 let mut res = Vec::new();
28
29 res.extend(parse.errors().iter().map(|err| Diagnostic {
30 range: location_to_range(err.location()),
31 message: format!("Syntax Error: {}", err),
32 severity: Severity::Error,
33 fix: None,
34 }));
35
36 for node in parse.tree().syntax().descendants() {
37 check_unnecessary_braces_in_use_statement(&mut res, file_id, &node);
38 check_struct_shorthand_initialization(&mut res, file_id, &node);
39 }
40 let res = RefCell::new(res);
41 let mut sink = DiagnosticSink::new(|d| {
42 res.borrow_mut().push(Diagnostic {
43 message: d.message(),
44 range: d.highlight_range(),
45 severity: Severity::Error,
46 fix: None,
47 })
48 })
49 .on::<hir::diagnostics::UnresolvedModule, _>(|d| {
50 let original_file = d.source().file_id.original_file(db);
51 let source_root = db.file_source_root(original_file);
52 let path = db
53 .file_relative_path(original_file)
54 .parent()
55 .unwrap_or_else(|| RelativePath::new(""))
56 .join(&d.candidate);
57 let create_file = FileSystemEdit::CreateFile { source_root, path };
58 let fix = SourceChange::file_system_edit("create module", create_file);
59 res.borrow_mut().push(Diagnostic {
60 range: d.highlight_range(),
61 message: d.message(),
62 severity: Severity::Error,
63 fix: Some(fix),
64 })
65 })
66 .on::<hir::diagnostics::MissingFields, _>(|d| {
67 let mut field_list = d.ast(db);
68 for f in d.missed_fields.iter() {
69 let field = make::record_field(make::name_ref(&f.to_string()), Some(make::expr_unit()));
70 field_list = field_list.append_field(&field);
71 }
72
73 let mut builder = TextEditBuilder::default();
74 algo::diff(&d.ast(db).syntax(), &field_list.syntax()).into_text_edit(&mut builder);
75
76 let fix =
77 SourceChange::source_file_edit_from("fill struct fields", file_id, builder.finish());
78 res.borrow_mut().push(Diagnostic {
79 range: d.highlight_range(),
80 message: d.message(),
81 severity: Severity::Error,
82 fix: Some(fix),
83 })
84 })
85 .on::<hir::diagnostics::MissingOkInTailExpr, _>(|d| {
86 let node = d.ast(db);
87 let replacement = format!("Ok({})", node.syntax());
88 let edit = TextEdit::replace(node.syntax().text_range(), replacement);
89 let fix = SourceChange::source_file_edit_from("wrap with ok", file_id, edit);
90 res.borrow_mut().push(Diagnostic {
91 range: d.highlight_range(),
92 message: d.message(),
93 severity: Severity::Error,
94 fix: Some(fix),
95 })
96 });
97 let source_file = db.parse(file_id).tree();
98 let src =
99 hir::Source { file_id: file_id.into(), value: hir::ModuleSource::SourceFile(source_file) };
100 if let Some(m) = hir::Module::from_definition(db, src) {
101 m.diagnostics(db, &mut sink);
102 };
103 drop(sink);
104 res.into_inner()
105}
106fn location_to_range(location: Location) -> TextRange {
107 match location {
108 Location::Offset(offset) => TextRange::offset_len(offset, 1.into()),
109 Location::Range(range) => range,
110 }
111}
112
113fn check_unnecessary_braces_in_use_statement(
114 acc: &mut Vec<Diagnostic>,
115 file_id: FileId,
116 node: &SyntaxNode,
117) -> Option<()> {
118 let use_tree_list = ast::UseTreeList::cast(node.clone())?;
119 if let Some((single_use_tree,)) = use_tree_list.use_trees().collect_tuple() {
120 let range = use_tree_list.syntax().text_range();
121 let edit =
122 text_edit_for_remove_unnecessary_braces_with_self_in_use_statement(&single_use_tree)
123 .unwrap_or_else(|| {
124 let to_replace = single_use_tree.syntax().text().to_string();
125 let mut edit_builder = TextEditBuilder::default();
126 edit_builder.delete(range);
127 edit_builder.insert(range.start(), to_replace);
128 edit_builder.finish()
129 });
130
131 acc.push(Diagnostic {
132 range,
133 message: "Unnecessary braces in use statement".to_string(),
134 severity: Severity::WeakWarning,
135 fix: Some(SourceChange::source_file_edit(
136 "Remove unnecessary braces",
137 SourceFileEdit { file_id, edit },
138 )),
139 });
140 }
141
142 Some(())
143}
144
145fn text_edit_for_remove_unnecessary_braces_with_self_in_use_statement(
146 single_use_tree: &ast::UseTree,
147) -> Option<TextEdit> {
148 let use_tree_list_node = single_use_tree.syntax().parent()?;
149 if single_use_tree.path()?.segment()?.syntax().first_child_or_token()?.kind() == T![self] {
150 let start = use_tree_list_node.prev_sibling_or_token()?.text_range().start();
151 let end = use_tree_list_node.text_range().end();
152 let range = TextRange::from_to(start, end);
153 return Some(TextEdit::delete(range));
154 }
155 None
156}
157
158fn check_struct_shorthand_initialization(
159 acc: &mut Vec<Diagnostic>,
160 file_id: FileId,
161 node: &SyntaxNode,
162) -> Option<()> {
163 let record_lit = ast::RecordLit::cast(node.clone())?;
164 let record_field_list = record_lit.record_field_list()?;
165 for record_field in record_field_list.fields() {
166 if let (Some(name_ref), Some(expr)) = (record_field.name_ref(), record_field.expr()) {
167 let field_name = name_ref.syntax().text().to_string();
168 let field_expr = expr.syntax().text().to_string();
169 if field_name == field_expr {
170 let mut edit_builder = TextEditBuilder::default();
171 edit_builder.delete(record_field.syntax().text_range());
172 edit_builder.insert(record_field.syntax().text_range().start(), field_name);
173 let edit = edit_builder.finish();
174
175 acc.push(Diagnostic {
176 range: record_field.syntax().text_range(),
177 message: "Shorthand struct initialization".to_string(),
178 severity: Severity::WeakWarning,
179 fix: Some(SourceChange::source_file_edit(
180 "use struct shorthand initialization",
181 SourceFileEdit { file_id, edit },
182 )),
183 });
184 }
185 }
186 }
187 Some(())
188}
189
190#[cfg(test)]
191mod tests {
192 use insta::assert_debug_snapshot;
193 use join_to_string::join;
194 use ra_syntax::SourceFile;
195 use test_utils::assert_eq_text;
196
197 use crate::mock_analysis::{analysis_and_position, single_file};
198
199 use super::*;
200
201 type DiagnosticChecker = fn(&mut Vec<Diagnostic>, FileId, &SyntaxNode) -> Option<()>;
202
203 fn check_not_applicable(code: &str, func: DiagnosticChecker) {
204 let parse = SourceFile::parse(code);
205 let mut diagnostics = Vec::new();
206 for node in parse.tree().syntax().descendants() {
207 func(&mut diagnostics, FileId(0), &node);
208 }
209 assert!(diagnostics.is_empty());
210 }
211
212 fn check_apply(before: &str, after: &str, func: DiagnosticChecker) {
213 let parse = SourceFile::parse(before);
214 let mut diagnostics = Vec::new();
215 for node in parse.tree().syntax().descendants() {
216 func(&mut diagnostics, FileId(0), &node);
217 }
218 let diagnostic =
219 diagnostics.pop().unwrap_or_else(|| panic!("no diagnostics for:\n{}\n", before));
220 let mut fix = diagnostic.fix.unwrap();
221 let edit = fix.source_file_edits.pop().unwrap().edit;
222 let actual = edit.apply(&before);
223 assert_eq_text!(after, &actual);
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 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_apply_diagnostic_fix_from_position(fixture: &str, after: &str) {
232 let (analysis, file_position) = analysis_and_position(fixture);
233 let diagnostic = analysis.diagnostics(file_position.file_id).unwrap().pop().unwrap();
234 let mut fix = diagnostic.fix.unwrap();
235 let edit = fix.source_file_edits.pop().unwrap().edit;
236 let target_file_contents = analysis.file_text(file_position.file_id).unwrap();
237 let actual = edit.apply(&target_file_contents);
238
239 // Strip indent and empty lines from `after`, to match the behaviour of
240 // `parse_fixture` called from `analysis_and_position`.
241 let margin = fixture
242 .lines()
243 .filter(|it| it.trim_start().starts_with("//-"))
244 .map(|it| it.len() - it.trim_start().len())
245 .next()
246 .expect("empty fixture");
247 let after = join(after.lines().filter_map(|line| {
248 if line.len() > margin {
249 Some(&line[margin..])
250 } else {
251 None
252 }
253 }))
254 .separator("\n")
255 .suffix("\n")
256 .to_string();
257
258 assert_eq_text!(&after, &actual);
259 assert!(
260 diagnostic.range.start() <= file_position.offset
261 && diagnostic.range.end() >= file_position.offset,
262 "diagnostic range {} does not touch cursor position {}",
263 diagnostic.range,
264 file_position.offset
265 );
266 }
267
268 fn check_apply_diagnostic_fix(before: &str, after: &str) {
269 let (analysis, file_id) = single_file(before);
270 let diagnostic = analysis.diagnostics(file_id).unwrap().pop().unwrap();
271 let mut fix = diagnostic.fix.unwrap();
272 let edit = fix.source_file_edits.pop().unwrap().edit;
273 let actual = edit.apply(&before);
274 assert_eq_text!(after, &actual);
275 }
276
277 /// Takes a multi-file input fixture with annotated cursor position and checks that no diagnostics
278 /// apply to the file containing the cursor.
279 fn check_no_diagnostic_for_target_file(fixture: &str) {
280 let (analysis, file_position) = analysis_and_position(fixture);
281 let diagnostics = analysis.diagnostics(file_position.file_id).unwrap();
282 assert_eq!(diagnostics.len(), 0);
283 }
284
285 fn check_no_diagnostic(content: &str) {
286 let (analysis, file_id) = single_file(content);
287 let diagnostics = analysis.diagnostics(file_id).unwrap();
288 assert_eq!(diagnostics.len(), 0);
289 }
290
291 #[test]
292 fn test_wrap_return_type() {
293 let before = r#"
294 //- /main.rs
295 use std::{string::String, result::Result::{self, Ok, Err}};
296
297 fn div(x: i32, y: i32) -> Result<i32, String> {
298 if y == 0 {
299 return Err("div by zero".into());
300 }
301 x / y<|>
302 }
303
304 //- /std/lib.rs
305 pub mod string {
306 pub struct String { }
307 }
308 pub mod result {
309 pub enum Result<T, E> { Ok(T), Err(E) }
310 }
311 "#;
312 let after = r#"
313 use std::{string::String, result::Result::{self, Ok, Err}};
314
315 fn div(x: i32, y: i32) -> Result<i32, String> {
316 if y == 0 {
317 return Err("div by zero".into());
318 }
319 Ok(x / y)
320 }
321 "#;
322 check_apply_diagnostic_fix_from_position(before, after);
323 }
324
325 #[test]
326 fn test_wrap_return_type_handles_generic_functions() {
327 let before = r#"
328 //- /main.rs
329 use std::result::Result::{self, Ok, Err};
330
331 fn div<T>(x: T) -> Result<T, i32> {
332 if x == 0 {
333 return Err(7);
334 }
335 <|>x
336 }
337
338 //- /std/lib.rs
339 pub mod result {
340 pub enum Result<T, E> { Ok(T), Err(E) }
341 }
342 "#;
343 let after = r#"
344 use std::result::Result::{self, Ok, Err};
345
346 fn div<T>(x: T) -> Result<T, i32> {
347 if x == 0 {
348 return Err(7);
349 }
350 Ok(x)
351 }
352 "#;
353 check_apply_diagnostic_fix_from_position(before, after);
354 }
355
356 #[test]
357 fn test_wrap_return_type_handles_type_aliases() {
358 let before = r#"
359 //- /main.rs
360 use std::{string::String, result::Result::{self, Ok, Err}};
361
362 type MyResult<T> = Result<T, String>;
363
364 fn div(x: i32, y: i32) -> MyResult<i32> {
365 if y == 0 {
366 return Err("div by zero".into());
367 }
368 x <|>/ y
369 }
370
371 //- /std/lib.rs
372 pub mod string {
373 pub struct String { }
374 }
375 pub mod result {
376 pub enum Result<T, E> { Ok(T), Err(E) }
377 }
378 "#;
379 let after = r#"
380 use std::{string::String, result::Result::{self, Ok, Err}};
381
382 type MyResult<T> = Result<T, String>;
383 fn div(x: i32, y: i32) -> MyResult<i32> {
384 if y == 0 {
385 return Err("div by zero".into());
386 }
387 Ok(x / y)
388 }
389 "#;
390 check_apply_diagnostic_fix_from_position(before, after);
391 }
392
393 #[test]
394 fn test_wrap_return_type_not_applicable_when_expr_type_does_not_match_ok_type() {
395 let content = r#"
396 //- /main.rs
397 use std::{string::String, result::Result::{self, Ok, Err}};
398
399 fn foo() -> Result<String, i32> {
400 0<|>
401 }
402
403 //- /std/lib.rs
404 pub mod string {
405 pub struct String { }
406 }
407 pub mod result {
408 pub enum Result<T, E> { Ok(T), Err(E) }
409 }
410 "#;
411 check_no_diagnostic_for_target_file(content);
412 }
413
414 #[test]
415 fn test_wrap_return_type_not_applicable_when_return_type_is_not_result() {
416 let content = r#"
417 //- /main.rs
418 use std::{string::String, result::Result::{self, Ok, Err}};
419
420 enum SomeOtherEnum {
421 Ok(i32),
422 Err(String),
423 }
424
425 fn foo() -> SomeOtherEnum {
426 0<|>
427 }
428
429 //- /std/lib.rs
430 pub mod string {
431 pub struct String { }
432 }
433 pub mod result {
434 pub enum Result<T, E> { Ok(T), Err(E) }
435 }
436 "#;
437 check_no_diagnostic_for_target_file(content);
438 }
439
440 #[test]
441 fn test_fill_struct_fields_empty() {
442 let before = r"
443 struct TestStruct {
444 one: i32,
445 two: i64,
446 }
447
448 fn test_fn() {
449 let s = TestStruct{};
450 }
451 ";
452 let after = r"
453 struct TestStruct {
454 one: i32,
455 two: i64,
456 }
457
458 fn test_fn() {
459 let s = TestStruct{ one: (), two: ()};
460 }
461 ";
462 check_apply_diagnostic_fix(before, after);
463 }
464
465 #[test]
466 fn test_fill_struct_fields_partial() {
467 let before = r"
468 struct TestStruct {
469 one: i32,
470 two: i64,
471 }
472
473 fn test_fn() {
474 let s = TestStruct{ two: 2 };
475 }
476 ";
477 let after = r"
478 struct TestStruct {
479 one: i32,
480 two: i64,
481 }
482
483 fn test_fn() {
484 let s = TestStruct{ two: 2, one: () };
485 }
486 ";
487 check_apply_diagnostic_fix(before, after);
488 }
489
490 #[test]
491 fn test_fill_struct_fields_no_diagnostic() {
492 let content = r"
493 struct TestStruct {
494 one: i32,
495 two: i64,
496 }
497
498 fn test_fn() {
499 let one = 1;
500 let s = TestStruct{ one, two: 2 };
501 }
502 ";
503
504 check_no_diagnostic(content);
505 }
506
507 #[test]
508 fn test_fill_struct_fields_no_diagnostic_on_spread() {
509 let content = r"
510 struct TestStruct {
511 one: i32,
512 two: i64,
513 }
514
515 fn test_fn() {
516 let one = 1;
517 let s = TestStruct{ ..a };
518 }
519 ";
520
521 check_no_diagnostic(content);
522 }
523
524 #[test]
525 fn test_unresolved_module_diagnostic() {
526 let (analysis, file_id) = single_file("mod foo;");
527 let diagnostics = analysis.diagnostics(file_id).unwrap();
528 assert_debug_snapshot!(diagnostics, @r###"
529 [
530 Diagnostic {
531 message: "unresolved module",
532 range: [0; 8),
533 fix: Some(
534 SourceChange {
535 label: "create module",
536 source_file_edits: [],
537 file_system_edits: [
538 CreateFile {
539 source_root: SourceRootId(
540 0,
541 ),
542 path: "foo.rs",
543 },
544 ],
545 cursor_position: None,
546 },
547 ),
548 severity: Error,
549 },
550 ]
551 "###);
552 }
553
554 #[test]
555 fn test_check_unnecessary_braces_in_use_statement() {
556 check_not_applicable(
557 "
558 use a;
559 use a::{c, d::e};
560 ",
561 check_unnecessary_braces_in_use_statement,
562 );
563 check_apply("use {b};", "use b;", check_unnecessary_braces_in_use_statement);
564 check_apply("use a::{c};", "use a::c;", check_unnecessary_braces_in_use_statement);
565 check_apply("use a::{self};", "use a;", check_unnecessary_braces_in_use_statement);
566 check_apply(
567 "use a::{c, d::{e}};",
568 "use a::{c, d::e};",
569 check_unnecessary_braces_in_use_statement,
570 );
571 }
572
573 #[test]
574 fn test_check_struct_shorthand_initialization() {
575 check_not_applicable(
576 r#"
577 struct A {
578 a: &'static str
579 }
580
581 fn main() {
582 A {
583 a: "hello"
584 }
585 }
586 "#,
587 check_struct_shorthand_initialization,
588 );
589
590 check_apply(
591 r#"
592struct A {
593 a: &'static str
594}
595
596fn main() {
597 let a = "haha";
598 A {
599 a: a
600 }
601}
602 "#,
603 r#"
604struct A {
605 a: &'static str
606}
607
608fn main() {
609 let a = "haha";
610 A {
611 a
612 }
613}
614 "#,
615 check_struct_shorthand_initialization,
616 );
617
618 check_apply(
619 r#"
620struct A {
621 a: &'static str,
622 b: &'static str
623}
624
625fn main() {
626 let a = "haha";
627 let b = "bb";
628 A {
629 a: a,
630 b
631 }
632}
633 "#,
634 r#"
635struct A {
636 a: &'static str,
637 b: &'static str
638}
639
640fn main() {
641 let a = "haha";
642 let b = "bb";
643 A {
644 a,
645 b
646 }
647}
648 "#,
649 check_struct_shorthand_initialization,
650 );
651 }
652}