aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--crates/ide/src/diagnostics.rs531
-rw-r--r--crates/ide/src/diagnostics/fixes.rs245
-rw-r--r--crates/ide/src/diagnostics/fixes/change_case.rs155
-rw-r--r--crates/ide/src/diagnostics/fixes/create_field.rs157
-rw-r--r--crates/ide/src/diagnostics/fixes/remove_semicolon.rs31
-rw-r--r--crates/ide/src/diagnostics/fixes/replace_with_find_map.rs42
-rw-r--r--crates/ide/src/diagnostics/fixes/unresolved_module.rs87
-rw-r--r--crates/ide/src/diagnostics/fixes/wrap_tail_expr.rs211
-rw-r--r--xtask/src/tidy.rs5
9 files changed, 695 insertions, 769 deletions
diff --git a/crates/ide/src/diagnostics.rs b/crates/ide/src/diagnostics.rs
index d5fba6740..4172f6cae 100644
--- a/crates/ide/src/diagnostics.rs
+++ b/crates/ide/src/diagnostics.rs
@@ -375,7 +375,7 @@ mod tests {
375 assert_eq!(diagnostics.len(), 0, "unexpected diagnostics:\n{:#?}", diagnostics); 375 assert_eq!(diagnostics.len(), 0, "unexpected diagnostics:\n{:#?}", diagnostics);
376 } 376 }
377 377
378 fn check_expect(ra_fixture: &str, expect: Expect) { 378 pub(crate) fn check_expect(ra_fixture: &str, expect: Expect) {
379 let (analysis, file_id) = fixture::file(ra_fixture); 379 let (analysis, file_id) = fixture::file(ra_fixture);
380 let diagnostics = analysis 380 let diagnostics = analysis
381 .diagnostics(&DiagnosticsConfig::default(), AssistResolveStrategy::All, file_id) 381 .diagnostics(&DiagnosticsConfig::default(), AssistResolveStrategy::All, file_id)
@@ -384,374 +384,6 @@ mod tests {
384 } 384 }
385 385
386 #[test] 386 #[test]
387 fn test_wrap_return_type_option() {
388 check_fix(
389 r#"
390//- /main.rs crate:main deps:core
391use core::option::Option::{self, Some, None};
392
393fn div(x: i32, y: i32) -> Option<i32> {
394 if y == 0 {
395 return None;
396 }
397 x / y$0
398}
399//- /core/lib.rs crate:core
400pub mod result {
401 pub enum Result<T, E> { Ok(T), Err(E) }
402}
403pub mod option {
404 pub enum Option<T> { Some(T), None }
405}
406"#,
407 r#"
408use core::option::Option::{self, Some, None};
409
410fn div(x: i32, y: i32) -> Option<i32> {
411 if y == 0 {
412 return None;
413 }
414 Some(x / y)
415}
416"#,
417 );
418 }
419
420 #[test]
421 fn test_wrap_return_type() {
422 check_fix(
423 r#"
424//- /main.rs crate:main deps:core
425use core::result::Result::{self, Ok, Err};
426
427fn div(x: i32, y: i32) -> Result<i32, ()> {
428 if y == 0 {
429 return Err(());
430 }
431 x / y$0
432}
433//- /core/lib.rs crate:core
434pub mod result {
435 pub enum Result<T, E> { Ok(T), Err(E) }
436}
437pub mod option {
438 pub enum Option<T> { Some(T), None }
439}
440"#,
441 r#"
442use core::result::Result::{self, Ok, Err};
443
444fn div(x: i32, y: i32) -> Result<i32, ()> {
445 if y == 0 {
446 return Err(());
447 }
448 Ok(x / y)
449}
450"#,
451 );
452 }
453
454 #[test]
455 fn test_wrap_return_type_handles_generic_functions() {
456 check_fix(
457 r#"
458//- /main.rs crate:main deps:core
459use core::result::Result::{self, Ok, Err};
460
461fn div<T>(x: T) -> Result<T, i32> {
462 if x == 0 {
463 return Err(7);
464 }
465 $0x
466}
467//- /core/lib.rs crate:core
468pub mod result {
469 pub enum Result<T, E> { Ok(T), Err(E) }
470}
471pub mod option {
472 pub enum Option<T> { Some(T), None }
473}
474"#,
475 r#"
476use core::result::Result::{self, Ok, Err};
477
478fn div<T>(x: T) -> Result<T, i32> {
479 if x == 0 {
480 return Err(7);
481 }
482 Ok(x)
483}
484"#,
485 );
486 }
487
488 #[test]
489 fn test_wrap_return_type_handles_type_aliases() {
490 check_fix(
491 r#"
492//- /main.rs crate:main deps:core
493use core::result::Result::{self, Ok, Err};
494
495type MyResult<T> = Result<T, ()>;
496
497fn div(x: i32, y: i32) -> MyResult<i32> {
498 if y == 0 {
499 return Err(());
500 }
501 x $0/ y
502}
503//- /core/lib.rs crate:core
504pub mod result {
505 pub enum Result<T, E> { Ok(T), Err(E) }
506}
507pub mod option {
508 pub enum Option<T> { Some(T), None }
509}
510"#,
511 r#"
512use core::result::Result::{self, Ok, Err};
513
514type MyResult<T> = Result<T, ()>;
515
516fn div(x: i32, y: i32) -> MyResult<i32> {
517 if y == 0 {
518 return Err(());
519 }
520 Ok(x / y)
521}
522"#,
523 );
524 }
525
526 #[test]
527 fn test_wrap_return_type_not_applicable_when_expr_type_does_not_match_ok_type() {
528 check_no_diagnostics(
529 r#"
530//- /main.rs crate:main deps:core
531use core::result::Result::{self, Ok, Err};
532
533fn foo() -> Result<(), i32> { 0 }
534
535//- /core/lib.rs crate:core
536pub mod result {
537 pub enum Result<T, E> { Ok(T), Err(E) }
538}
539pub mod option {
540 pub enum Option<T> { Some(T), None }
541}
542"#,
543 );
544 }
545
546 #[test]
547 fn test_wrap_return_type_not_applicable_when_return_type_is_not_result_or_option() {
548 check_no_diagnostics(
549 r#"
550//- /main.rs crate:main deps:core
551use core::result::Result::{self, Ok, Err};
552
553enum SomeOtherEnum { Ok(i32), Err(String) }
554
555fn foo() -> SomeOtherEnum { 0 }
556
557//- /core/lib.rs crate:core
558pub mod result {
559 pub enum Result<T, E> { Ok(T), Err(E) }
560}
561pub mod option {
562 pub enum Option<T> { Some(T), None }
563}
564"#,
565 );
566 }
567
568 #[test]
569 fn test_fill_struct_fields_empty() {
570 check_fix(
571 r#"
572struct TestStruct { one: i32, two: i64 }
573
574fn test_fn() {
575 let s = TestStruct {$0};
576}
577"#,
578 r#"
579struct TestStruct { one: i32, two: i64 }
580
581fn test_fn() {
582 let s = TestStruct { one: (), two: () };
583}
584"#,
585 );
586 }
587
588 #[test]
589 fn test_fill_struct_fields_self() {
590 check_fix(
591 r#"
592struct TestStruct { one: i32 }
593
594impl TestStruct {
595 fn test_fn() { let s = Self {$0}; }
596}
597"#,
598 r#"
599struct TestStruct { one: i32 }
600
601impl TestStruct {
602 fn test_fn() { let s = Self { one: () }; }
603}
604"#,
605 );
606 }
607
608 #[test]
609 fn test_fill_struct_fields_enum() {
610 check_fix(
611 r#"
612enum Expr {
613 Bin { lhs: Box<Expr>, rhs: Box<Expr> }
614}
615
616impl Expr {
617 fn new_bin(lhs: Box<Expr>, rhs: Box<Expr>) -> Expr {
618 Expr::Bin {$0 }
619 }
620}
621"#,
622 r#"
623enum Expr {
624 Bin { lhs: Box<Expr>, rhs: Box<Expr> }
625}
626
627impl Expr {
628 fn new_bin(lhs: Box<Expr>, rhs: Box<Expr>) -> Expr {
629 Expr::Bin { lhs: (), rhs: () }
630 }
631}
632"#,
633 );
634 }
635
636 #[test]
637 fn test_fill_struct_fields_partial() {
638 check_fix(
639 r#"
640struct TestStruct { one: i32, two: i64 }
641
642fn test_fn() {
643 let s = TestStruct{ two: 2$0 };
644}
645"#,
646 r"
647struct TestStruct { one: i32, two: i64 }
648
649fn test_fn() {
650 let s = TestStruct{ two: 2, one: () };
651}
652",
653 );
654 }
655
656 #[test]
657 fn test_fill_struct_fields_raw_ident() {
658 check_fix(
659 r#"
660struct TestStruct { r#type: u8 }
661
662fn test_fn() {
663 TestStruct { $0 };
664}
665"#,
666 r"
667struct TestStruct { r#type: u8 }
668
669fn test_fn() {
670 TestStruct { r#type: () };
671}
672",
673 );
674 }
675
676 #[test]
677 fn test_fill_struct_fields_no_diagnostic() {
678 check_no_diagnostics(
679 r"
680 struct TestStruct { one: i32, two: i64 }
681
682 fn test_fn() {
683 let one = 1;
684 let s = TestStruct{ one, two: 2 };
685 }
686 ",
687 );
688 }
689
690 #[test]
691 fn test_fill_struct_fields_no_diagnostic_on_spread() {
692 check_no_diagnostics(
693 r"
694 struct TestStruct { one: i32, two: i64 }
695
696 fn test_fn() {
697 let one = 1;
698 let s = TestStruct{ ..a };
699 }
700 ",
701 );
702 }
703
704 #[test]
705 fn test_unresolved_module_diagnostic() {
706 check_expect(
707 r#"mod foo;"#,
708 expect![[r#"
709 [
710 Diagnostic {
711 message: "unresolved module",
712 range: 0..8,
713 severity: Error,
714 fix: Some(
715 Assist {
716 id: AssistId(
717 "create_module",
718 QuickFix,
719 ),
720 label: "Create module",
721 group: None,
722 target: 0..8,
723 source_change: Some(
724 SourceChange {
725 source_file_edits: {},
726 file_system_edits: [
727 CreateFile {
728 dst: AnchoredPathBuf {
729 anchor: FileId(
730 0,
731 ),
732 path: "foo.rs",
733 },
734 initial_contents: "",
735 },
736 ],
737 is_snippet: false,
738 },
739 ),
740 },
741 ),
742 unused: false,
743 code: Some(
744 DiagnosticCode(
745 "unresolved-module",
746 ),
747 ),
748 },
749 ]
750 "#]],
751 );
752 }
753
754 #[test]
755 fn test_unresolved_macro_range() { 387 fn test_unresolved_macro_range() {
756 check_expect( 388 check_expect(
757 r#"foo::bar!(92);"#, 389 r#"foo::bar!(92);"#,
@@ -891,53 +523,6 @@ mod a {
891 } 523 }
892 524
893 #[test] 525 #[test]
894 fn test_add_field_from_usage() {
895 check_fix(
896 r"
897fn main() {
898 Foo { bar: 3, baz$0: false};
899}
900struct Foo {
901 bar: i32
902}
903",
904 r"
905fn main() {
906 Foo { bar: 3, baz: false};
907}
908struct Foo {
909 bar: i32,
910 baz: bool
911}
912",
913 )
914 }
915
916 #[test]
917 fn test_add_field_in_other_file_from_usage() {
918 check_fix(
919 r#"
920//- /main.rs
921mod foo;
922
923fn main() {
924 foo::Foo { bar: 3, $0baz: false};
925}
926//- /foo.rs
927struct Foo {
928 bar: i32
929}
930"#,
931 r#"
932struct Foo {
933 bar: i32,
934 pub(crate) baz: bool
935}
936"#,
937 )
938 }
939
940 #[test]
941 fn test_disabled_diagnostics() { 526 fn test_disabled_diagnostics() {
942 let mut config = DiagnosticsConfig::default(); 527 let mut config = DiagnosticsConfig::default();
943 config.disabled.insert("unresolved-module".into()); 528 config.disabled.insert("unresolved-module".into());
@@ -955,120 +540,6 @@ struct Foo {
955 } 540 }
956 541
957 #[test] 542 #[test]
958 fn test_rename_incorrect_case() {
959 check_fix(
960 r#"
961pub struct test_struct$0 { one: i32 }
962
963pub fn some_fn(val: test_struct) -> test_struct {
964 test_struct { one: val.one + 1 }
965}
966"#,
967 r#"
968pub struct TestStruct { one: i32 }
969
970pub fn some_fn(val: TestStruct) -> TestStruct {
971 TestStruct { one: val.one + 1 }
972}
973"#,
974 );
975
976 check_fix(
977 r#"
978pub fn some_fn(NonSnakeCase$0: u8) -> u8 {
979 NonSnakeCase
980}
981"#,
982 r#"
983pub fn some_fn(non_snake_case: u8) -> u8 {
984 non_snake_case
985}
986"#,
987 );
988
989 check_fix(
990 r#"
991pub fn SomeFn$0(val: u8) -> u8 {
992 if val != 0 { SomeFn(val - 1) } else { val }
993}
994"#,
995 r#"
996pub fn some_fn(val: u8) -> u8 {
997 if val != 0 { some_fn(val - 1) } else { val }
998}
999"#,
1000 );
1001
1002 check_fix(
1003 r#"
1004fn some_fn() {
1005 let whatAWeird_Formatting$0 = 10;
1006 another_func(whatAWeird_Formatting);
1007}
1008"#,
1009 r#"
1010fn some_fn() {
1011 let what_a_weird_formatting = 10;
1012 another_func(what_a_weird_formatting);
1013}
1014"#,
1015 );
1016 }
1017
1018 #[test]
1019 fn test_uppercase_const_no_diagnostics() {
1020 check_no_diagnostics(
1021 r#"
1022fn foo() {
1023 const ANOTHER_ITEM$0: &str = "some_item";
1024}
1025"#,
1026 );
1027 }
1028
1029 #[test]
1030 fn test_rename_incorrect_case_struct_method() {
1031 check_fix(
1032 r#"
1033pub struct TestStruct;
1034
1035impl TestStruct {
1036 pub fn SomeFn$0() -> TestStruct {
1037 TestStruct
1038 }
1039}
1040"#,
1041 r#"
1042pub struct TestStruct;
1043
1044impl TestStruct {
1045 pub fn some_fn() -> TestStruct {
1046 TestStruct
1047 }
1048}
1049"#,
1050 );
1051 }
1052
1053 #[test]
1054 fn test_single_incorrect_case_diagnostic_in_function_name_issue_6970() {
1055 let input = r#"fn FOO$0() {}"#;
1056 let expected = r#"fn foo() {}"#;
1057
1058 let (analysis, file_position) = fixture::position(input);
1059 let diagnostics = analysis
1060 .diagnostics(
1061 &DiagnosticsConfig::default(),
1062 AssistResolveStrategy::All,
1063 file_position.file_id,
1064 )
1065 .unwrap();
1066 assert_eq!(diagnostics.len(), 1);
1067
1068 check_fix(input, expected);
1069 }
1070
1071 #[test]
1072 fn unlinked_file_prepend_first_item() { 543 fn unlinked_file_prepend_first_item() {
1073 cov_mark::check!(unlinked_file_prepend_before_first_item); 544 cov_mark::check!(unlinked_file_prepend_before_first_item);
1074 check_fix( 545 check_fix(
diff --git a/crates/ide/src/diagnostics/fixes.rs b/crates/ide/src/diagnostics/fixes.rs
index 5330449f9..92b3f5a2d 100644
--- a/crates/ide/src/diagnostics/fixes.rs
+++ b/crates/ide/src/diagnostics/fixes.rs
@@ -1,32 +1,18 @@
1//! Provides a way to attach fixes to the diagnostics. 1//! Provides a way to attach fixes to the diagnostics.
2//! The same module also has all curret custom fixes for the diagnostics implemented. 2//! The same module also has all curret custom fixes for the diagnostics implemented.
3mod change_case;
4mod create_field;
3mod fill_missing_fields; 5mod fill_missing_fields;
6mod remove_semicolon;
7mod replace_with_find_map;
8mod unresolved_module;
9mod wrap_tail_expr;
4 10
5use hir::{ 11use hir::{diagnostics::Diagnostic, Semantics};
6 db::AstDatabase,
7 diagnostics::{
8 Diagnostic, IncorrectCase, MissingOkOrSomeInTailExpr, NoSuchField, RemoveThisSemicolon,
9 ReplaceFilterMapNextWithFindMap, UnresolvedModule,
10 },
11 HasSource, HirDisplay, InFile, Semantics, VariantDef,
12};
13use ide_assists::AssistResolveStrategy; 12use ide_assists::AssistResolveStrategy;
14use ide_db::{ 13use ide_db::RootDatabase;
15 base_db::{AnchoredPathBuf, FileId},
16 source_change::{FileSystemEdit, SourceChange},
17 RootDatabase,
18};
19use syntax::{
20 ast::{self, edit::IndentLevel, make, ArgListOwner},
21 AstNode, TextRange,
22};
23use text_edit::TextEdit;
24 14
25use crate::{ 15use crate::Assist;
26 diagnostics::{fix, unresolved_fix},
27 references::rename::rename_with_semantics,
28 Assist, FilePosition,
29};
30 16
31/// A [Diagnostic] that potentially has a fix available. 17/// A [Diagnostic] that potentially has a fix available.
32/// 18///
@@ -43,216 +29,3 @@ pub(crate) trait DiagnosticWithFix: Diagnostic {
43 _resolve: &AssistResolveStrategy, 29 _resolve: &AssistResolveStrategy,
44 ) -> Option<Assist>; 30 ) -> Option<Assist>;
45} 31}
46
47impl DiagnosticWithFix for UnresolvedModule {
48 fn fix(
49 &self,
50 sema: &Semantics<RootDatabase>,
51 _resolve: &AssistResolveStrategy,
52 ) -> Option<Assist> {
53 let root = sema.db.parse_or_expand(self.file)?;
54 let unresolved_module = self.decl.to_node(&root);
55 Some(fix(
56 "create_module",
57 "Create module",
58 FileSystemEdit::CreateFile {
59 dst: AnchoredPathBuf {
60 anchor: self.file.original_file(sema.db),
61 path: self.candidate.clone(),
62 },
63 initial_contents: "".to_string(),
64 }
65 .into(),
66 unresolved_module.syntax().text_range(),
67 ))
68 }
69}
70
71impl DiagnosticWithFix for NoSuchField {
72 fn fix(
73 &self,
74 sema: &Semantics<RootDatabase>,
75 _resolve: &AssistResolveStrategy,
76 ) -> Option<Assist> {
77 let root = sema.db.parse_or_expand(self.file)?;
78 missing_record_expr_field_fix(
79 &sema,
80 self.file.original_file(sema.db),
81 &self.field.to_node(&root),
82 )
83 }
84}
85
86impl DiagnosticWithFix for MissingOkOrSomeInTailExpr {
87 fn fix(
88 &self,
89 sema: &Semantics<RootDatabase>,
90 _resolve: &AssistResolveStrategy,
91 ) -> Option<Assist> {
92 let root = sema.db.parse_or_expand(self.file)?;
93 let tail_expr = self.expr.to_node(&root);
94 let tail_expr_range = tail_expr.syntax().text_range();
95 let replacement = format!("{}({})", self.required, tail_expr.syntax());
96 let edit = TextEdit::replace(tail_expr_range, replacement);
97 let source_change = SourceChange::from_text_edit(self.file.original_file(sema.db), edit);
98 let name = if self.required == "Ok" { "Wrap with Ok" } else { "Wrap with Some" };
99 Some(fix("wrap_tail_expr", name, source_change, tail_expr_range))
100 }
101}
102
103impl DiagnosticWithFix for RemoveThisSemicolon {
104 fn fix(
105 &self,
106 sema: &Semantics<RootDatabase>,
107 _resolve: &AssistResolveStrategy,
108 ) -> Option<Assist> {
109 let root = sema.db.parse_or_expand(self.file)?;
110
111 let semicolon = self
112 .expr
113 .to_node(&root)
114 .syntax()
115 .parent()
116 .and_then(ast::ExprStmt::cast)
117 .and_then(|expr| expr.semicolon_token())?
118 .text_range();
119
120 let edit = TextEdit::delete(semicolon);
121 let source_change = SourceChange::from_text_edit(self.file.original_file(sema.db), edit);
122
123 Some(fix("remove_semicolon", "Remove this semicolon", source_change, semicolon))
124 }
125}
126
127impl DiagnosticWithFix for IncorrectCase {
128 fn fix(
129 &self,
130 sema: &Semantics<RootDatabase>,
131 resolve: &AssistResolveStrategy,
132 ) -> Option<Assist> {
133 let root = sema.db.parse_or_expand(self.file)?;
134 let name_node = self.ident.to_node(&root);
135
136 let name_node = InFile::new(self.file, name_node.syntax());
137 let frange = name_node.original_file_range(sema.db);
138 let file_position = FilePosition { file_id: frange.file_id, offset: frange.range.start() };
139
140 let label = format!("Rename to {}", self.suggested_text);
141 let mut res = unresolved_fix("change_case", &label, frange.range);
142 if resolve.should_resolve(&res.id) {
143 let source_change = rename_with_semantics(sema, file_position, &self.suggested_text);
144 res.source_change = Some(source_change.ok().unwrap_or_default());
145 }
146
147 Some(res)
148 }
149}
150
151impl DiagnosticWithFix for ReplaceFilterMapNextWithFindMap {
152 fn fix(
153 &self,
154 sema: &Semantics<RootDatabase>,
155 _resolve: &AssistResolveStrategy,
156 ) -> Option<Assist> {
157 let root = sema.db.parse_or_expand(self.file)?;
158 let next_expr = self.next_expr.to_node(&root);
159 let next_call = ast::MethodCallExpr::cast(next_expr.syntax().clone())?;
160
161 let filter_map_call = ast::MethodCallExpr::cast(next_call.receiver()?.syntax().clone())?;
162 let filter_map_name_range = filter_map_call.name_ref()?.ident_token()?.text_range();
163 let filter_map_args = filter_map_call.arg_list()?;
164
165 let range_to_replace =
166 TextRange::new(filter_map_name_range.start(), next_expr.syntax().text_range().end());
167 let replacement = format!("find_map{}", filter_map_args.syntax().text());
168 let trigger_range = next_expr.syntax().text_range();
169
170 let edit = TextEdit::replace(range_to_replace, replacement);
171
172 let source_change = SourceChange::from_text_edit(self.file.original_file(sema.db), edit);
173
174 Some(fix(
175 "replace_with_find_map",
176 "Replace filter_map(..).next() with find_map()",
177 source_change,
178 trigger_range,
179 ))
180 }
181}
182
183fn missing_record_expr_field_fix(
184 sema: &Semantics<RootDatabase>,
185 usage_file_id: FileId,
186 record_expr_field: &ast::RecordExprField,
187) -> Option<Assist> {
188 let record_lit = ast::RecordExpr::cast(record_expr_field.syntax().parent()?.parent()?)?;
189 let def_id = sema.resolve_variant(record_lit)?;
190 let module;
191 let def_file_id;
192 let record_fields = match def_id {
193 VariantDef::Struct(s) => {
194 module = s.module(sema.db);
195 let source = s.source(sema.db)?;
196 def_file_id = source.file_id;
197 let fields = source.value.field_list()?;
198 record_field_list(fields)?
199 }
200 VariantDef::Union(u) => {
201 module = u.module(sema.db);
202 let source = u.source(sema.db)?;
203 def_file_id = source.file_id;
204 source.value.record_field_list()?
205 }
206 VariantDef::Variant(e) => {
207 module = e.module(sema.db);
208 let source = e.source(sema.db)?;
209 def_file_id = source.file_id;
210 let fields = source.value.field_list()?;
211 record_field_list(fields)?
212 }
213 };
214 let def_file_id = def_file_id.original_file(sema.db);
215
216 let new_field_type = sema.type_of_expr(&record_expr_field.expr()?)?;
217 if new_field_type.is_unknown() {
218 return None;
219 }
220 let new_field = make::record_field(
221 None,
222 make::name(&record_expr_field.field_name()?.text()),
223 make::ty(&new_field_type.display_source_code(sema.db, module.into()).ok()?),
224 );
225
226 let last_field = record_fields.fields().last()?;
227 let last_field_syntax = last_field.syntax();
228 let indent = IndentLevel::from_node(last_field_syntax);
229
230 let mut new_field = new_field.to_string();
231 if usage_file_id != def_file_id {
232 new_field = format!("pub(crate) {}", new_field);
233 }
234 new_field = format!("\n{}{}", indent, new_field);
235
236 let needs_comma = !last_field_syntax.to_string().ends_with(',');
237 if needs_comma {
238 new_field = format!(",{}", new_field);
239 }
240
241 let source_change = SourceChange::from_text_edit(
242 def_file_id,
243 TextEdit::insert(last_field_syntax.text_range().end(), new_field),
244 );
245 return Some(fix(
246 "create_field",
247 "Create field",
248 source_change,
249 record_expr_field.syntax().text_range(),
250 ));
251
252 fn record_field_list(field_def_list: ast::FieldList) -> Option<ast::RecordFieldList> {
253 match field_def_list {
254 ast::FieldList::RecordFieldList(it) => Some(it),
255 ast::FieldList::TupleFieldList(_) => None,
256 }
257 }
258}
diff --git a/crates/ide/src/diagnostics/fixes/change_case.rs b/crates/ide/src/diagnostics/fixes/change_case.rs
new file mode 100644
index 000000000..80aca58a1
--- /dev/null
+++ b/crates/ide/src/diagnostics/fixes/change_case.rs
@@ -0,0 +1,155 @@
1use hir::{db::AstDatabase, diagnostics::IncorrectCase, InFile, Semantics};
2use ide_assists::{Assist, AssistResolveStrategy};
3use ide_db::{base_db::FilePosition, RootDatabase};
4use syntax::AstNode;
5
6use crate::{
7 diagnostics::{unresolved_fix, DiagnosticWithFix},
8 references::rename::rename_with_semantics,
9};
10
11impl DiagnosticWithFix for IncorrectCase {
12 fn fix(
13 &self,
14 sema: &Semantics<RootDatabase>,
15 resolve: &AssistResolveStrategy,
16 ) -> Option<Assist> {
17 let root = sema.db.parse_or_expand(self.file)?;
18 let name_node = self.ident.to_node(&root);
19
20 let name_node = InFile::new(self.file, name_node.syntax());
21 let frange = name_node.original_file_range(sema.db);
22 let file_position = FilePosition { file_id: frange.file_id, offset: frange.range.start() };
23
24 let label = format!("Rename to {}", self.suggested_text);
25 let mut res = unresolved_fix("change_case", &label, frange.range);
26 if resolve.should_resolve(&res.id) {
27 let source_change = rename_with_semantics(sema, file_position, &self.suggested_text);
28 res.source_change = Some(source_change.ok().unwrap_or_default());
29 }
30
31 Some(res)
32 }
33}
34
35#[cfg(test)]
36mod change_case {
37 use crate::{
38 diagnostics::tests::{check_fix, check_no_diagnostics},
39 fixture, AssistResolveStrategy, DiagnosticsConfig,
40 };
41
42 #[test]
43 fn test_rename_incorrect_case() {
44 check_fix(
45 r#"
46pub struct test_struct$0 { one: i32 }
47
48pub fn some_fn(val: test_struct) -> test_struct {
49 test_struct { one: val.one + 1 }
50}
51"#,
52 r#"
53pub struct TestStruct { one: i32 }
54
55pub fn some_fn(val: TestStruct) -> TestStruct {
56 TestStruct { one: val.one + 1 }
57}
58"#,
59 );
60
61 check_fix(
62 r#"
63pub fn some_fn(NonSnakeCase$0: u8) -> u8 {
64 NonSnakeCase
65}
66"#,
67 r#"
68pub fn some_fn(non_snake_case: u8) -> u8 {
69 non_snake_case
70}
71"#,
72 );
73
74 check_fix(
75 r#"
76pub fn SomeFn$0(val: u8) -> u8 {
77 if val != 0 { SomeFn(val - 1) } else { val }
78}
79"#,
80 r#"
81pub fn some_fn(val: u8) -> u8 {
82 if val != 0 { some_fn(val - 1) } else { val }
83}
84"#,
85 );
86
87 check_fix(
88 r#"
89fn some_fn() {
90 let whatAWeird_Formatting$0 = 10;
91 another_func(whatAWeird_Formatting);
92}
93"#,
94 r#"
95fn some_fn() {
96 let what_a_weird_formatting = 10;
97 another_func(what_a_weird_formatting);
98}
99"#,
100 );
101 }
102
103 #[test]
104 fn test_uppercase_const_no_diagnostics() {
105 check_no_diagnostics(
106 r#"
107fn foo() {
108 const ANOTHER_ITEM$0: &str = "some_item";
109}
110"#,
111 );
112 }
113
114 #[test]
115 fn test_rename_incorrect_case_struct_method() {
116 check_fix(
117 r#"
118pub struct TestStruct;
119
120impl TestStruct {
121 pub fn SomeFn$0() -> TestStruct {
122 TestStruct
123 }
124}
125"#,
126 r#"
127pub struct TestStruct;
128
129impl TestStruct {
130 pub fn some_fn() -> TestStruct {
131 TestStruct
132 }
133}
134"#,
135 );
136 }
137
138 #[test]
139 fn test_single_incorrect_case_diagnostic_in_function_name_issue_6970() {
140 let input = r#"fn FOO$0() {}"#;
141 let expected = r#"fn foo() {}"#;
142
143 let (analysis, file_position) = fixture::position(input);
144 let diagnostics = analysis
145 .diagnostics(
146 &DiagnosticsConfig::default(),
147 AssistResolveStrategy::All,
148 file_position.file_id,
149 )
150 .unwrap();
151 assert_eq!(diagnostics.len(), 1);
152
153 check_fix(input, expected);
154 }
155}
diff --git a/crates/ide/src/diagnostics/fixes/create_field.rs b/crates/ide/src/diagnostics/fixes/create_field.rs
new file mode 100644
index 000000000..24e0fda52
--- /dev/null
+++ b/crates/ide/src/diagnostics/fixes/create_field.rs
@@ -0,0 +1,157 @@
1use hir::{db::AstDatabase, diagnostics::NoSuchField, HasSource, HirDisplay, Semantics};
2use ide_db::{base_db::FileId, source_change::SourceChange, RootDatabase};
3use syntax::{
4 ast::{self, edit::IndentLevel, make},
5 AstNode,
6};
7use text_edit::TextEdit;
8
9use crate::{
10 diagnostics::{fix, DiagnosticWithFix},
11 Assist, AssistResolveStrategy,
12};
13
14impl DiagnosticWithFix for NoSuchField {
15 fn fix(
16 &self,
17 sema: &Semantics<RootDatabase>,
18 _resolve: &AssistResolveStrategy,
19 ) -> Option<Assist> {
20 let root = sema.db.parse_or_expand(self.file)?;
21 missing_record_expr_field_fix(
22 &sema,
23 self.file.original_file(sema.db),
24 &self.field.to_node(&root),
25 )
26 }
27}
28
29fn missing_record_expr_field_fix(
30 sema: &Semantics<RootDatabase>,
31 usage_file_id: FileId,
32 record_expr_field: &ast::RecordExprField,
33) -> Option<Assist> {
34 let record_lit = ast::RecordExpr::cast(record_expr_field.syntax().parent()?.parent()?)?;
35 let def_id = sema.resolve_variant(record_lit)?;
36 let module;
37 let def_file_id;
38 let record_fields = match def_id {
39 hir::VariantDef::Struct(s) => {
40 module = s.module(sema.db);
41 let source = s.source(sema.db)?;
42 def_file_id = source.file_id;
43 let fields = source.value.field_list()?;
44 record_field_list(fields)?
45 }
46 hir::VariantDef::Union(u) => {
47 module = u.module(sema.db);
48 let source = u.source(sema.db)?;
49 def_file_id = source.file_id;
50 source.value.record_field_list()?
51 }
52 hir::VariantDef::Variant(e) => {
53 module = e.module(sema.db);
54 let source = e.source(sema.db)?;
55 def_file_id = source.file_id;
56 let fields = source.value.field_list()?;
57 record_field_list(fields)?
58 }
59 };
60 let def_file_id = def_file_id.original_file(sema.db);
61
62 let new_field_type = sema.type_of_expr(&record_expr_field.expr()?)?;
63 if new_field_type.is_unknown() {
64 return None;
65 }
66 let new_field = make::record_field(
67 None,
68 make::name(&record_expr_field.field_name()?.text()),
69 make::ty(&new_field_type.display_source_code(sema.db, module.into()).ok()?),
70 );
71
72 let last_field = record_fields.fields().last()?;
73 let last_field_syntax = last_field.syntax();
74 let indent = IndentLevel::from_node(last_field_syntax);
75
76 let mut new_field = new_field.to_string();
77 if usage_file_id != def_file_id {
78 new_field = format!("pub(crate) {}", new_field);
79 }
80 new_field = format!("\n{}{}", indent, new_field);
81
82 let needs_comma = !last_field_syntax.to_string().ends_with(',');
83 if needs_comma {
84 new_field = format!(",{}", new_field);
85 }
86
87 let source_change = SourceChange::from_text_edit(
88 def_file_id,
89 TextEdit::insert(last_field_syntax.text_range().end(), new_field),
90 );
91
92 return Some(fix(
93 "create_field",
94 "Create field",
95 source_change,
96 record_expr_field.syntax().text_range(),
97 ));
98
99 fn record_field_list(field_def_list: ast::FieldList) -> Option<ast::RecordFieldList> {
100 match field_def_list {
101 ast::FieldList::RecordFieldList(it) => Some(it),
102 ast::FieldList::TupleFieldList(_) => None,
103 }
104 }
105}
106
107#[cfg(test)]
108mod tests {
109 use crate::diagnostics::tests::check_fix;
110
111 #[test]
112 fn test_add_field_from_usage() {
113 check_fix(
114 r"
115fn main() {
116 Foo { bar: 3, baz$0: false};
117}
118struct Foo {
119 bar: i32
120}
121",
122 r"
123fn main() {
124 Foo { bar: 3, baz: false};
125}
126struct Foo {
127 bar: i32,
128 baz: bool
129}
130",
131 )
132 }
133
134 #[test]
135 fn test_add_field_in_other_file_from_usage() {
136 check_fix(
137 r#"
138//- /main.rs
139mod foo;
140
141fn main() {
142 foo::Foo { bar: 3, $0baz: false};
143}
144//- /foo.rs
145struct Foo {
146 bar: i32
147}
148"#,
149 r#"
150struct Foo {
151 bar: i32,
152 pub(crate) baz: bool
153}
154"#,
155 )
156 }
157}
diff --git a/crates/ide/src/diagnostics/fixes/remove_semicolon.rs b/crates/ide/src/diagnostics/fixes/remove_semicolon.rs
new file mode 100644
index 000000000..058002c69
--- /dev/null
+++ b/crates/ide/src/diagnostics/fixes/remove_semicolon.rs
@@ -0,0 +1,31 @@
1use hir::{db::AstDatabase, diagnostics::RemoveThisSemicolon, Semantics};
2use ide_assists::{Assist, AssistResolveStrategy};
3use ide_db::{source_change::SourceChange, RootDatabase};
4use syntax::{ast, AstNode};
5use text_edit::TextEdit;
6
7use crate::diagnostics::{fix, DiagnosticWithFix};
8
9impl DiagnosticWithFix for RemoveThisSemicolon {
10 fn fix(
11 &self,
12 sema: &Semantics<RootDatabase>,
13 _resolve: &AssistResolveStrategy,
14 ) -> Option<Assist> {
15 let root = sema.db.parse_or_expand(self.file)?;
16
17 let semicolon = self
18 .expr
19 .to_node(&root)
20 .syntax()
21 .parent()
22 .and_then(ast::ExprStmt::cast)
23 .and_then(|expr| expr.semicolon_token())?
24 .text_range();
25
26 let edit = TextEdit::delete(semicolon);
27 let source_change = SourceChange::from_text_edit(self.file.original_file(sema.db), edit);
28
29 Some(fix("remove_semicolon", "Remove this semicolon", source_change, semicolon))
30 }
31}
diff --git a/crates/ide/src/diagnostics/fixes/replace_with_find_map.rs b/crates/ide/src/diagnostics/fixes/replace_with_find_map.rs
new file mode 100644
index 000000000..5ddfd2064
--- /dev/null
+++ b/crates/ide/src/diagnostics/fixes/replace_with_find_map.rs
@@ -0,0 +1,42 @@
1use hir::{db::AstDatabase, diagnostics::ReplaceFilterMapNextWithFindMap, Semantics};
2use ide_assists::{Assist, AssistResolveStrategy};
3use ide_db::{source_change::SourceChange, RootDatabase};
4use syntax::{
5 ast::{self, ArgListOwner},
6 AstNode, TextRange,
7};
8use text_edit::TextEdit;
9
10use crate::diagnostics::{fix, DiagnosticWithFix};
11
12impl DiagnosticWithFix for ReplaceFilterMapNextWithFindMap {
13 fn fix(
14 &self,
15 sema: &Semantics<RootDatabase>,
16 _resolve: &AssistResolveStrategy,
17 ) -> Option<Assist> {
18 let root = sema.db.parse_or_expand(self.file)?;
19 let next_expr = self.next_expr.to_node(&root);
20 let next_call = ast::MethodCallExpr::cast(next_expr.syntax().clone())?;
21
22 let filter_map_call = ast::MethodCallExpr::cast(next_call.receiver()?.syntax().clone())?;
23 let filter_map_name_range = filter_map_call.name_ref()?.ident_token()?.text_range();
24 let filter_map_args = filter_map_call.arg_list()?;
25
26 let range_to_replace =
27 TextRange::new(filter_map_name_range.start(), next_expr.syntax().text_range().end());
28 let replacement = format!("find_map{}", filter_map_args.syntax().text());
29 let trigger_range = next_expr.syntax().text_range();
30
31 let edit = TextEdit::replace(range_to_replace, replacement);
32
33 let source_change = SourceChange::from_text_edit(self.file.original_file(sema.db), edit);
34
35 Some(fix(
36 "replace_with_find_map",
37 "Replace filter_map(..).next() with find_map()",
38 source_change,
39 trigger_range,
40 ))
41 }
42}
diff --git a/crates/ide/src/diagnostics/fixes/unresolved_module.rs b/crates/ide/src/diagnostics/fixes/unresolved_module.rs
new file mode 100644
index 000000000..81244b293
--- /dev/null
+++ b/crates/ide/src/diagnostics/fixes/unresolved_module.rs
@@ -0,0 +1,87 @@
1use hir::{db::AstDatabase, diagnostics::UnresolvedModule, Semantics};
2use ide_assists::{Assist, AssistResolveStrategy};
3use ide_db::{base_db::AnchoredPathBuf, source_change::FileSystemEdit, RootDatabase};
4use syntax::AstNode;
5
6use crate::diagnostics::{fix, DiagnosticWithFix};
7
8impl DiagnosticWithFix for UnresolvedModule {
9 fn fix(
10 &self,
11 sema: &Semantics<RootDatabase>,
12 _resolve: &AssistResolveStrategy,
13 ) -> Option<Assist> {
14 let root = sema.db.parse_or_expand(self.file)?;
15 let unresolved_module = self.decl.to_node(&root);
16 Some(fix(
17 "create_module",
18 "Create module",
19 FileSystemEdit::CreateFile {
20 dst: AnchoredPathBuf {
21 anchor: self.file.original_file(sema.db),
22 path: self.candidate.clone(),
23 },
24 initial_contents: "".to_string(),
25 }
26 .into(),
27 unresolved_module.syntax().text_range(),
28 ))
29 }
30}
31
32#[cfg(test)]
33mod tests {
34 use expect_test::expect;
35
36 use crate::diagnostics::tests::check_expect;
37
38 #[test]
39 fn test_unresolved_module_diagnostic() {
40 check_expect(
41 r#"mod foo;"#,
42 expect![[r#"
43 [
44 Diagnostic {
45 message: "unresolved module",
46 range: 0..8,
47 severity: Error,
48 fix: Some(
49 Assist {
50 id: AssistId(
51 "create_module",
52 QuickFix,
53 ),
54 label: "Create module",
55 group: None,
56 target: 0..8,
57 source_change: Some(
58 SourceChange {
59 source_file_edits: {},
60 file_system_edits: [
61 CreateFile {
62 dst: AnchoredPathBuf {
63 anchor: FileId(
64 0,
65 ),
66 path: "foo.rs",
67 },
68 initial_contents: "",
69 },
70 ],
71 is_snippet: false,
72 },
73 ),
74 },
75 ),
76 unused: false,
77 code: Some(
78 DiagnosticCode(
79 "unresolved-module",
80 ),
81 ),
82 },
83 ]
84 "#]],
85 );
86 }
87}
diff --git a/crates/ide/src/diagnostics/fixes/wrap_tail_expr.rs b/crates/ide/src/diagnostics/fixes/wrap_tail_expr.rs
new file mode 100644
index 000000000..66676064a
--- /dev/null
+++ b/crates/ide/src/diagnostics/fixes/wrap_tail_expr.rs
@@ -0,0 +1,211 @@
1use hir::{db::AstDatabase, diagnostics::MissingOkOrSomeInTailExpr, Semantics};
2use ide_assists::{Assist, AssistResolveStrategy};
3use ide_db::{source_change::SourceChange, RootDatabase};
4use syntax::AstNode;
5use text_edit::TextEdit;
6
7use crate::diagnostics::{fix, DiagnosticWithFix};
8
9impl DiagnosticWithFix for MissingOkOrSomeInTailExpr {
10 fn fix(
11 &self,
12 sema: &Semantics<RootDatabase>,
13 _resolve: &AssistResolveStrategy,
14 ) -> Option<Assist> {
15 let root = sema.db.parse_or_expand(self.file)?;
16 let tail_expr = self.expr.to_node(&root);
17 let tail_expr_range = tail_expr.syntax().text_range();
18 let replacement = format!("{}({})", self.required, tail_expr.syntax());
19 let edit = TextEdit::replace(tail_expr_range, replacement);
20 let source_change = SourceChange::from_text_edit(self.file.original_file(sema.db), edit);
21 let name = if self.required == "Ok" { "Wrap with Ok" } else { "Wrap with Some" };
22 Some(fix("wrap_tail_expr", name, source_change, tail_expr_range))
23 }
24}
25
26#[cfg(test)]
27mod tests {
28 use crate::diagnostics::tests::{check_fix, check_no_diagnostics};
29
30 #[test]
31 fn test_wrap_return_type_option() {
32 check_fix(
33 r#"
34//- /main.rs crate:main deps:core
35use core::option::Option::{self, Some, None};
36
37fn div(x: i32, y: i32) -> Option<i32> {
38 if y == 0 {
39 return None;
40 }
41 x / y$0
42}
43//- /core/lib.rs crate:core
44pub mod result {
45 pub enum Result<T, E> { Ok(T), Err(E) }
46}
47pub mod option {
48 pub enum Option<T> { Some(T), None }
49}
50"#,
51 r#"
52use core::option::Option::{self, Some, None};
53
54fn div(x: i32, y: i32) -> Option<i32> {
55 if y == 0 {
56 return None;
57 }
58 Some(x / y)
59}
60"#,
61 );
62 }
63
64 #[test]
65 fn test_wrap_return_type() {
66 check_fix(
67 r#"
68//- /main.rs crate:main deps:core
69use core::result::Result::{self, Ok, Err};
70
71fn div(x: i32, y: i32) -> Result<i32, ()> {
72 if y == 0 {
73 return Err(());
74 }
75 x / y$0
76}
77//- /core/lib.rs crate:core
78pub mod result {
79 pub enum Result<T, E> { Ok(T), Err(E) }
80}
81pub mod option {
82 pub enum Option<T> { Some(T), None }
83}
84"#,
85 r#"
86use core::result::Result::{self, Ok, Err};
87
88fn div(x: i32, y: i32) -> Result<i32, ()> {
89 if y == 0 {
90 return Err(());
91 }
92 Ok(x / y)
93}
94"#,
95 );
96 }
97
98 #[test]
99 fn test_wrap_return_type_handles_generic_functions() {
100 check_fix(
101 r#"
102//- /main.rs crate:main deps:core
103use core::result::Result::{self, Ok, Err};
104
105fn div<T>(x: T) -> Result<T, i32> {
106 if x == 0 {
107 return Err(7);
108 }
109 $0x
110}
111//- /core/lib.rs crate:core
112pub mod result {
113 pub enum Result<T, E> { Ok(T), Err(E) }
114}
115pub mod option {
116 pub enum Option<T> { Some(T), None }
117}
118"#,
119 r#"
120use core::result::Result::{self, Ok, Err};
121
122fn div<T>(x: T) -> Result<T, i32> {
123 if x == 0 {
124 return Err(7);
125 }
126 Ok(x)
127}
128"#,
129 );
130 }
131
132 #[test]
133 fn test_wrap_return_type_handles_type_aliases() {
134 check_fix(
135 r#"
136//- /main.rs crate:main deps:core
137use core::result::Result::{self, Ok, Err};
138
139type MyResult<T> = Result<T, ()>;
140
141fn div(x: i32, y: i32) -> MyResult<i32> {
142 if y == 0 {
143 return Err(());
144 }
145 x $0/ y
146}
147//- /core/lib.rs crate:core
148pub mod result {
149 pub enum Result<T, E> { Ok(T), Err(E) }
150}
151pub mod option {
152 pub enum Option<T> { Some(T), None }
153}
154"#,
155 r#"
156use core::result::Result::{self, Ok, Err};
157
158type MyResult<T> = Result<T, ()>;
159
160fn div(x: i32, y: i32) -> MyResult<i32> {
161 if y == 0 {
162 return Err(());
163 }
164 Ok(x / y)
165}
166"#,
167 );
168 }
169
170 #[test]
171 fn test_wrap_return_type_not_applicable_when_expr_type_does_not_match_ok_type() {
172 check_no_diagnostics(
173 r#"
174//- /main.rs crate:main deps:core
175use core::result::Result::{self, Ok, Err};
176
177fn foo() -> Result<(), i32> { 0 }
178
179//- /core/lib.rs crate:core
180pub mod result {
181 pub enum Result<T, E> { Ok(T), Err(E) }
182}
183pub mod option {
184 pub enum Option<T> { Some(T), None }
185}
186"#,
187 );
188 }
189
190 #[test]
191 fn test_wrap_return_type_not_applicable_when_return_type_is_not_result_or_option() {
192 check_no_diagnostics(
193 r#"
194//- /main.rs crate:main deps:core
195use core::result::Result::{self, Ok, Err};
196
197enum SomeOtherEnum { Ok(i32), Err(String) }
198
199fn foo() -> SomeOtherEnum { 0 }
200
201//- /core/lib.rs crate:core
202pub mod result {
203 pub enum Result<T, E> { Ok(T), Err(E) }
204}
205pub mod option {
206 pub enum Option<T> { Some(T), None }
207}
208"#,
209 );
210 }
211}
diff --git a/xtask/src/tidy.rs b/xtask/src/tidy.rs
index c3c785eff..6c55823eb 100644
--- a/xtask/src/tidy.rs
+++ b/xtask/src/tidy.rs
@@ -347,9 +347,8 @@ struct TidyDocs {
347 347
348impl TidyDocs { 348impl TidyDocs {
349 fn visit(&mut self, path: &Path, text: &str) { 349 fn visit(&mut self, path: &Path, text: &str) {
350 // Test hopefully don't really need comments, and for assists we already 350 // Tests and diagnostic fixes don't need module level comments.
351 // have special comments which are source of doc tests and user docs. 351 if is_exclude_dir(path, &["tests", "test_data", "fixes"]) {
352 if is_exclude_dir(path, &["tests", "test_data"]) {
353 return; 352 return;
354 } 353 }
355 354