diff options
-rw-r--r-- | crates/assists/src/handlers/generate_function.rs | 132 | ||||
-rw-r--r-- | crates/assists/src/tests/generated.rs | 4 | ||||
-rw-r--r-- | crates/hir/src/code_model.rs | 24 | ||||
-rw-r--r-- | crates/hir/src/diagnostics.rs | 3 | ||||
-rw-r--r-- | crates/hir_def/src/item_scope.rs | 6 | ||||
-rw-r--r-- | crates/hir_ty/src/diagnostics.rs | 85 | ||||
-rw-r--r-- | crates/hir_ty/src/diagnostics/decl_check.rs | 833 | ||||
-rw-r--r-- | crates/hir_ty/src/diagnostics/decl_check/case_conv.rs | 194 | ||||
-rw-r--r-- | crates/hir_ty/src/diagnostics/unsafe_check.rs | 6 | ||||
-rw-r--r-- | crates/ide/src/diagnostics.rs | 141 | ||||
-rw-r--r-- | crates/ide/src/diagnostics/fixes.rs | 24 | ||||
-rw-r--r-- | crates/ide/src/references.rs | 2 | ||||
-rw-r--r-- | crates/ide/src/references/rename.rs | 7 | ||||
-rw-r--r-- | crates/stdx/src/lib.rs | 18 | ||||
-rw-r--r-- | crates/syntax/src/ast/make.rs | 12 |
15 files changed, 1407 insertions, 84 deletions
diff --git a/crates/assists/src/handlers/generate_function.rs b/crates/assists/src/handlers/generate_function.rs index b38d64058..d23f4293b 100644 --- a/crates/assists/src/handlers/generate_function.rs +++ b/crates/assists/src/handlers/generate_function.rs | |||
@@ -36,8 +36,8 @@ use crate::{ | |||
36 | // bar("", baz()); | 36 | // bar("", baz()); |
37 | // } | 37 | // } |
38 | // | 38 | // |
39 | // fn bar(arg: &str, baz: Baz) { | 39 | // fn bar(arg: &str, baz: Baz) ${0:-> ()} { |
40 | // ${0:todo!()} | 40 | // todo!() |
41 | // } | 41 | // } |
42 | // | 42 | // |
43 | // ``` | 43 | // ``` |
@@ -80,9 +80,9 @@ pub(crate) fn generate_function(acc: &mut Assists, ctx: &AssistContext) -> Optio | |||
80 | 80 | ||
81 | struct FunctionTemplate { | 81 | struct FunctionTemplate { |
82 | insert_offset: TextSize, | 82 | insert_offset: TextSize, |
83 | placeholder_expr: ast::MacroCall, | ||
84 | leading_ws: String, | 83 | leading_ws: String, |
85 | fn_def: ast::Fn, | 84 | fn_def: ast::Fn, |
85 | ret_type: ast::RetType, | ||
86 | trailing_ws: String, | 86 | trailing_ws: String, |
87 | file: FileId, | 87 | file: FileId, |
88 | } | 88 | } |
@@ -90,11 +90,9 @@ struct FunctionTemplate { | |||
90 | impl FunctionTemplate { | 90 | impl FunctionTemplate { |
91 | fn to_string(&self, cap: Option<SnippetCap>) -> String { | 91 | fn to_string(&self, cap: Option<SnippetCap>) -> String { |
92 | let f = match cap { | 92 | let f = match cap { |
93 | Some(cap) => render_snippet( | 93 | Some(cap) => { |
94 | cap, | 94 | render_snippet(cap, self.fn_def.syntax(), Cursor::Replace(self.ret_type.syntax())) |
95 | self.fn_def.syntax(), | 95 | } |
96 | Cursor::Replace(self.placeholder_expr.syntax()), | ||
97 | ), | ||
98 | None => self.fn_def.to_string(), | 96 | None => self.fn_def.to_string(), |
99 | }; | 97 | }; |
100 | format!("{}{}{}", self.leading_ws, f, self.trailing_ws) | 98 | format!("{}{}{}", self.leading_ws, f, self.trailing_ws) |
@@ -141,8 +139,14 @@ impl FunctionBuilder { | |||
141 | let placeholder_expr = make::expr_todo(); | 139 | let placeholder_expr = make::expr_todo(); |
142 | let fn_body = make::block_expr(vec![], Some(placeholder_expr)); | 140 | let fn_body = make::block_expr(vec![], Some(placeholder_expr)); |
143 | let visibility = if self.needs_pub { Some(make::visibility_pub_crate()) } else { None }; | 141 | let visibility = if self.needs_pub { Some(make::visibility_pub_crate()) } else { None }; |
144 | let mut fn_def = | 142 | let mut fn_def = make::fn_( |
145 | make::fn_(visibility, self.fn_name, self.type_params, self.params, fn_body); | 143 | visibility, |
144 | self.fn_name, | ||
145 | self.type_params, | ||
146 | self.params, | ||
147 | fn_body, | ||
148 | Some(make::ret_type(make::ty("()"))), | ||
149 | ); | ||
146 | let leading_ws; | 150 | let leading_ws; |
147 | let trailing_ws; | 151 | let trailing_ws; |
148 | 152 | ||
@@ -163,12 +167,10 @@ impl FunctionBuilder { | |||
163 | } | 167 | } |
164 | }; | 168 | }; |
165 | 169 | ||
166 | let placeholder_expr = | ||
167 | fn_def.syntax().descendants().find_map(ast::MacroCall::cast).unwrap(); | ||
168 | FunctionTemplate { | 170 | FunctionTemplate { |
169 | insert_offset, | 171 | insert_offset, |
170 | placeholder_expr, | ||
171 | leading_ws, | 172 | leading_ws, |
173 | ret_type: fn_def.ret_type().unwrap(), | ||
172 | fn_def, | 174 | fn_def, |
173 | trailing_ws, | 175 | trailing_ws, |
174 | file: self.file, | 176 | file: self.file, |
@@ -349,8 +351,8 @@ fn foo() { | |||
349 | bar(); | 351 | bar(); |
350 | } | 352 | } |
351 | 353 | ||
352 | fn bar() { | 354 | fn bar() ${0:-> ()} { |
353 | ${0:todo!()} | 355 | todo!() |
354 | } | 356 | } |
355 | ", | 357 | ", |
356 | ) | 358 | ) |
@@ -376,8 +378,8 @@ impl Foo { | |||
376 | } | 378 | } |
377 | } | 379 | } |
378 | 380 | ||
379 | fn bar() { | 381 | fn bar() ${0:-> ()} { |
380 | ${0:todo!()} | 382 | todo!() |
381 | } | 383 | } |
382 | ", | 384 | ", |
383 | ) | 385 | ) |
@@ -400,8 +402,8 @@ fn foo1() { | |||
400 | bar(); | 402 | bar(); |
401 | } | 403 | } |
402 | 404 | ||
403 | fn bar() { | 405 | fn bar() ${0:-> ()} { |
404 | ${0:todo!()} | 406 | todo!() |
405 | } | 407 | } |
406 | 408 | ||
407 | fn foo2() {} | 409 | fn foo2() {} |
@@ -426,8 +428,8 @@ mod baz { | |||
426 | bar(); | 428 | bar(); |
427 | } | 429 | } |
428 | 430 | ||
429 | fn bar() { | 431 | fn bar() ${0:-> ()} { |
430 | ${0:todo!()} | 432 | todo!() |
431 | } | 433 | } |
432 | } | 434 | } |
433 | ", | 435 | ", |
@@ -452,8 +454,8 @@ fn foo() { | |||
452 | bar(baz()); | 454 | bar(baz()); |
453 | } | 455 | } |
454 | 456 | ||
455 | fn bar(baz: Baz) { | 457 | fn bar(baz: Baz) ${0:-> ()} { |
456 | ${0:todo!()} | 458 | todo!() |
457 | } | 459 | } |
458 | ", | 460 | ", |
459 | ); | 461 | ); |
@@ -485,8 +487,8 @@ impl Baz { | |||
485 | } | 487 | } |
486 | } | 488 | } |
487 | 489 | ||
488 | fn bar(baz: Baz) { | 490 | fn bar(baz: Baz) ${0:-> ()} { |
489 | ${0:todo!()} | 491 | todo!() |
490 | } | 492 | } |
491 | ", | 493 | ", |
492 | ) | 494 | ) |
@@ -506,8 +508,8 @@ fn foo() { | |||
506 | bar("bar") | 508 | bar("bar") |
507 | } | 509 | } |
508 | 510 | ||
509 | fn bar(arg: &str) { | 511 | fn bar(arg: &str) ${0:-> ()} { |
510 | ${0:todo!()} | 512 | todo!() |
511 | } | 513 | } |
512 | "#, | 514 | "#, |
513 | ) | 515 | ) |
@@ -527,8 +529,8 @@ fn foo() { | |||
527 | bar('x') | 529 | bar('x') |
528 | } | 530 | } |
529 | 531 | ||
530 | fn bar(arg: char) { | 532 | fn bar(arg: char) ${0:-> ()} { |
531 | ${0:todo!()} | 533 | todo!() |
532 | } | 534 | } |
533 | "#, | 535 | "#, |
534 | ) | 536 | ) |
@@ -548,8 +550,8 @@ fn foo() { | |||
548 | bar(42) | 550 | bar(42) |
549 | } | 551 | } |
550 | 552 | ||
551 | fn bar(arg: i32) { | 553 | fn bar(arg: i32) ${0:-> ()} { |
552 | ${0:todo!()} | 554 | todo!() |
553 | } | 555 | } |
554 | ", | 556 | ", |
555 | ) | 557 | ) |
@@ -569,8 +571,8 @@ fn foo() { | |||
569 | bar(42 as u8) | 571 | bar(42 as u8) |
570 | } | 572 | } |
571 | 573 | ||
572 | fn bar(arg: u8) { | 574 | fn bar(arg: u8) ${0:-> ()} { |
573 | ${0:todo!()} | 575 | todo!() |
574 | } | 576 | } |
575 | ", | 577 | ", |
576 | ) | 578 | ) |
@@ -594,8 +596,8 @@ fn foo() { | |||
594 | bar(x as u8) | 596 | bar(x as u8) |
595 | } | 597 | } |
596 | 598 | ||
597 | fn bar(x: u8) { | 599 | fn bar(x: u8) ${0:-> ()} { |
598 | ${0:todo!()} | 600 | todo!() |
599 | } | 601 | } |
600 | ", | 602 | ", |
601 | ) | 603 | ) |
@@ -617,8 +619,8 @@ fn foo() { | |||
617 | bar(worble) | 619 | bar(worble) |
618 | } | 620 | } |
619 | 621 | ||
620 | fn bar(worble: ()) { | 622 | fn bar(worble: ()) ${0:-> ()} { |
621 | ${0:todo!()} | 623 | todo!() |
622 | } | 624 | } |
623 | ", | 625 | ", |
624 | ) | 626 | ) |
@@ -646,8 +648,8 @@ fn baz() { | |||
646 | bar(foo()) | 648 | bar(foo()) |
647 | } | 649 | } |
648 | 650 | ||
649 | fn bar(foo: impl Foo) { | 651 | fn bar(foo: impl Foo) ${0:-> ()} { |
650 | ${0:todo!()} | 652 | todo!() |
651 | } | 653 | } |
652 | ", | 654 | ", |
653 | ) | 655 | ) |
@@ -673,8 +675,8 @@ fn foo() { | |||
673 | bar(&baz()) | 675 | bar(&baz()) |
674 | } | 676 | } |
675 | 677 | ||
676 | fn bar(baz: &Baz) { | 678 | fn bar(baz: &Baz) ${0:-> ()} { |
677 | ${0:todo!()} | 679 | todo!() |
678 | } | 680 | } |
679 | ", | 681 | ", |
680 | ) | 682 | ) |
@@ -702,8 +704,8 @@ fn foo() { | |||
702 | bar(Baz::baz()) | 704 | bar(Baz::baz()) |
703 | } | 705 | } |
704 | 706 | ||
705 | fn bar(baz: Baz::Bof) { | 707 | fn bar(baz: Baz::Bof) ${0:-> ()} { |
706 | ${0:todo!()} | 708 | todo!() |
707 | } | 709 | } |
708 | ", | 710 | ", |
709 | ) | 711 | ) |
@@ -725,8 +727,8 @@ fn foo<T>(t: T) { | |||
725 | bar(t) | 727 | bar(t) |
726 | } | 728 | } |
727 | 729 | ||
728 | fn bar<T>(t: T) { | 730 | fn bar<T>(t: T) ${0:-> ()} { |
729 | ${0:todo!()} | 731 | todo!() |
730 | } | 732 | } |
731 | ", | 733 | ", |
732 | ) | 734 | ) |
@@ -756,8 +758,8 @@ fn foo() { | |||
756 | bar(Baz::new); | 758 | bar(Baz::new); |
757 | } | 759 | } |
758 | 760 | ||
759 | fn bar(arg: fn() -> Baz) { | 761 | fn bar(arg: fn() -> Baz) ${0:-> ()} { |
760 | ${0:todo!()} | 762 | todo!() |
761 | } | 763 | } |
762 | ", | 764 | ", |
763 | ) | 765 | ) |
@@ -781,8 +783,8 @@ fn foo() { | |||
781 | bar(closure) | 783 | bar(closure) |
782 | } | 784 | } |
783 | 785 | ||
784 | fn bar(closure: impl Fn(i64) -> i64) { | 786 | fn bar(closure: impl Fn(i64) -> i64) ${0:-> ()} { |
785 | ${0:todo!()} | 787 | todo!() |
786 | } | 788 | } |
787 | ", | 789 | ", |
788 | ) | 790 | ) |
@@ -802,8 +804,8 @@ fn foo() { | |||
802 | bar(baz) | 804 | bar(baz) |
803 | } | 805 | } |
804 | 806 | ||
805 | fn bar(baz: ()) { | 807 | fn bar(baz: ()) ${0:-> ()} { |
806 | ${0:todo!()} | 808 | todo!() |
807 | } | 809 | } |
808 | ", | 810 | ", |
809 | ) | 811 | ) |
@@ -827,8 +829,8 @@ fn foo() { | |||
827 | bar(baz(), baz()) | 829 | bar(baz(), baz()) |
828 | } | 830 | } |
829 | 831 | ||
830 | fn bar(baz_1: Baz, baz_2: Baz) { | 832 | fn bar(baz_1: Baz, baz_2: Baz) ${0:-> ()} { |
831 | ${0:todo!()} | 833 | todo!() |
832 | } | 834 | } |
833 | ", | 835 | ", |
834 | ) | 836 | ) |
@@ -852,8 +854,8 @@ fn foo() { | |||
852 | bar(baz(), baz(), "foo", "bar") | 854 | bar(baz(), baz(), "foo", "bar") |
853 | } | 855 | } |
854 | 856 | ||
855 | fn bar(baz_1: Baz, baz_2: Baz, arg_1: &str, arg_2: &str) { | 857 | fn bar(baz_1: Baz, baz_2: Baz, arg_1: &str, arg_2: &str) ${0:-> ()} { |
856 | ${0:todo!()} | 858 | todo!() |
857 | } | 859 | } |
858 | "#, | 860 | "#, |
859 | ) | 861 | ) |
@@ -872,8 +874,8 @@ fn foo() { | |||
872 | ", | 874 | ", |
873 | r" | 875 | r" |
874 | mod bar { | 876 | mod bar { |
875 | pub(crate) fn my_fn() { | 877 | pub(crate) fn my_fn() ${0:-> ()} { |
876 | ${0:todo!()} | 878 | todo!() |
877 | } | 879 | } |
878 | } | 880 | } |
879 | 881 | ||
@@ -911,8 +913,8 @@ fn bar() { | |||
911 | baz(foo) | 913 | baz(foo) |
912 | } | 914 | } |
913 | 915 | ||
914 | fn baz(foo: foo::Foo) { | 916 | fn baz(foo: foo::Foo) ${0:-> ()} { |
915 | ${0:todo!()} | 917 | todo!() |
916 | } | 918 | } |
917 | ", | 919 | ", |
918 | ) | 920 | ) |
@@ -935,8 +937,8 @@ fn foo() { | |||
935 | mod bar { | 937 | mod bar { |
936 | fn something_else() {} | 938 | fn something_else() {} |
937 | 939 | ||
938 | pub(crate) fn my_fn() { | 940 | pub(crate) fn my_fn() ${0:-> ()} { |
939 | ${0:todo!()} | 941 | todo!() |
940 | } | 942 | } |
941 | } | 943 | } |
942 | 944 | ||
@@ -963,8 +965,8 @@ fn foo() { | |||
963 | r" | 965 | r" |
964 | mod bar { | 966 | mod bar { |
965 | mod baz { | 967 | mod baz { |
966 | pub(crate) fn my_fn() { | 968 | pub(crate) fn my_fn() ${0:-> ()} { |
967 | ${0:todo!()} | 969 | todo!() |
968 | } | 970 | } |
969 | } | 971 | } |
970 | } | 972 | } |
@@ -992,8 +994,8 @@ fn main() { | |||
992 | r" | 994 | r" |
993 | 995 | ||
994 | 996 | ||
995 | pub(crate) fn bar() { | 997 | pub(crate) fn bar() ${0:-> ()} { |
996 | ${0:todo!()} | 998 | todo!() |
997 | }", | 999 | }", |
998 | ) | 1000 | ) |
999 | } | 1001 | } |
diff --git a/crates/assists/src/tests/generated.rs b/crates/assists/src/tests/generated.rs index 7f6e98a54..41f536574 100644 --- a/crates/assists/src/tests/generated.rs +++ b/crates/assists/src/tests/generated.rs | |||
@@ -454,8 +454,8 @@ fn foo() { | |||
454 | bar("", baz()); | 454 | bar("", baz()); |
455 | } | 455 | } |
456 | 456 | ||
457 | fn bar(arg: &str, baz: Baz) { | 457 | fn bar(arg: &str, baz: Baz) ${0:-> ()} { |
458 | ${0:todo!()} | 458 | todo!() |
459 | } | 459 | } |
460 | 460 | ||
461 | "#####, | 461 | "#####, |
diff --git a/crates/hir/src/code_model.rs b/crates/hir/src/code_model.rs index a101d724e..b65be4fe1 100644 --- a/crates/hir/src/code_model.rs +++ b/crates/hir/src/code_model.rs | |||
@@ -255,6 +255,25 @@ impl ModuleDef { | |||
255 | ModuleDef::BuiltinType(it) => Some(it.as_name()), | 255 | ModuleDef::BuiltinType(it) => Some(it.as_name()), |
256 | } | 256 | } |
257 | } | 257 | } |
258 | |||
259 | pub fn diagnostics(self, db: &dyn HirDatabase, sink: &mut DiagnosticSink) { | ||
260 | let id = match self { | ||
261 | ModuleDef::Adt(it) => match it { | ||
262 | Adt::Struct(it) => it.id.into(), | ||
263 | Adt::Enum(it) => it.id.into(), | ||
264 | Adt::Union(it) => it.id.into(), | ||
265 | }, | ||
266 | ModuleDef::Trait(it) => it.id.into(), | ||
267 | ModuleDef::Function(it) => it.id.into(), | ||
268 | ModuleDef::TypeAlias(it) => it.id.into(), | ||
269 | ModuleDef::Module(it) => it.id.into(), | ||
270 | ModuleDef::Const(it) => it.id.into(), | ||
271 | ModuleDef::Static(it) => it.id.into(), | ||
272 | _ => return, | ||
273 | }; | ||
274 | |||
275 | hir_ty::diagnostics::validate_module_item(db, id, sink) | ||
276 | } | ||
258 | } | 277 | } |
259 | 278 | ||
260 | pub use hir_def::{ | 279 | pub use hir_def::{ |
@@ -358,6 +377,8 @@ impl Module { | |||
358 | let crate_def_map = db.crate_def_map(self.id.krate); | 377 | let crate_def_map = db.crate_def_map(self.id.krate); |
359 | crate_def_map.add_diagnostics(db.upcast(), self.id.local_id, sink); | 378 | crate_def_map.add_diagnostics(db.upcast(), self.id.local_id, sink); |
360 | for decl in self.declarations(db) { | 379 | for decl in self.declarations(db) { |
380 | decl.diagnostics(db, sink); | ||
381 | |||
361 | match decl { | 382 | match decl { |
362 | crate::ModuleDef::Function(f) => f.diagnostics(db, sink), | 383 | crate::ModuleDef::Function(f) => f.diagnostics(db, sink), |
363 | crate::ModuleDef::Module(m) => { | 384 | crate::ModuleDef::Module(m) => { |
@@ -760,7 +781,8 @@ impl Function { | |||
760 | } | 781 | } |
761 | 782 | ||
762 | pub fn diagnostics(self, db: &dyn HirDatabase, sink: &mut DiagnosticSink) { | 783 | pub fn diagnostics(self, db: &dyn HirDatabase, sink: &mut DiagnosticSink) { |
763 | hir_ty::diagnostics::validate_body(db, self.id.into(), sink) | 784 | hir_ty::diagnostics::validate_module_item(db, self.id.into(), sink); |
785 | hir_ty::diagnostics::validate_body(db, self.id.into(), sink); | ||
764 | } | 786 | } |
765 | 787 | ||
766 | /// Whether this function declaration has a definition. | 788 | /// Whether this function declaration has a definition. |
diff --git a/crates/hir/src/diagnostics.rs b/crates/hir/src/diagnostics.rs index 363164b9b..da2b40849 100644 --- a/crates/hir/src/diagnostics.rs +++ b/crates/hir/src/diagnostics.rs | |||
@@ -2,5 +2,6 @@ | |||
2 | pub use hir_def::diagnostics::UnresolvedModule; | 2 | pub use hir_def::diagnostics::UnresolvedModule; |
3 | pub use hir_expand::diagnostics::{Diagnostic, DiagnosticSink, DiagnosticSinkBuilder}; | 3 | pub use hir_expand::diagnostics::{Diagnostic, DiagnosticSink, DiagnosticSinkBuilder}; |
4 | pub use hir_ty::diagnostics::{ | 4 | pub use hir_ty::diagnostics::{ |
5 | MismatchedArgCount, MissingFields, MissingMatchArms, MissingOkInTailExpr, NoSuchField, | 5 | IncorrectCase, MismatchedArgCount, MissingFields, MissingMatchArms, MissingOkInTailExpr, |
6 | NoSuchField, | ||
6 | }; | 7 | }; |
diff --git a/crates/hir_def/src/item_scope.rs b/crates/hir_def/src/item_scope.rs index 12c24e1ca..a8b3fe844 100644 --- a/crates/hir_def/src/item_scope.rs +++ b/crates/hir_def/src/item_scope.rs | |||
@@ -95,6 +95,12 @@ impl ItemScope { | |||
95 | self.impls.iter().copied() | 95 | self.impls.iter().copied() |
96 | } | 96 | } |
97 | 97 | ||
98 | pub fn values( | ||
99 | &self, | ||
100 | ) -> impl Iterator<Item = (ModuleDefId, Visibility)> + ExactSizeIterator + '_ { | ||
101 | self.values.values().copied() | ||
102 | } | ||
103 | |||
98 | pub fn visibility_of(&self, def: ModuleDefId) -> Option<Visibility> { | 104 | pub fn visibility_of(&self, def: ModuleDefId) -> Option<Visibility> { |
99 | self.name_of(ItemInNs::Types(def)) | 105 | self.name_of(ItemInNs::Types(def)) |
100 | .or_else(|| self.name_of(ItemInNs::Values(def))) | 106 | .or_else(|| self.name_of(ItemInNs::Values(def))) |
diff --git a/crates/hir_ty/src/diagnostics.rs b/crates/hir_ty/src/diagnostics.rs index 9ba005fab..dfe98571e 100644 --- a/crates/hir_ty/src/diagnostics.rs +++ b/crates/hir_ty/src/diagnostics.rs | |||
@@ -2,10 +2,11 @@ | |||
2 | mod expr; | 2 | mod expr; |
3 | mod match_check; | 3 | mod match_check; |
4 | mod unsafe_check; | 4 | mod unsafe_check; |
5 | mod decl_check; | ||
5 | 6 | ||
6 | use std::any::Any; | 7 | use std::{any::Any, fmt}; |
7 | 8 | ||
8 | use hir_def::DefWithBodyId; | 9 | use hir_def::{DefWithBodyId, ModuleDefId}; |
9 | use hir_expand::diagnostics::{Diagnostic, DiagnosticCode, DiagnosticSink}; | 10 | use hir_expand::diagnostics::{Diagnostic, DiagnosticCode, DiagnosticSink}; |
10 | use hir_expand::{name::Name, HirFileId, InFile}; | 11 | use hir_expand::{name::Name, HirFileId, InFile}; |
11 | use stdx::format_to; | 12 | use stdx::format_to; |
@@ -15,6 +16,16 @@ use crate::db::HirDatabase; | |||
15 | 16 | ||
16 | pub use crate::diagnostics::expr::{record_literal_missing_fields, record_pattern_missing_fields}; | 17 | pub use crate::diagnostics::expr::{record_literal_missing_fields, record_pattern_missing_fields}; |
17 | 18 | ||
19 | pub fn validate_module_item( | ||
20 | db: &dyn HirDatabase, | ||
21 | owner: ModuleDefId, | ||
22 | sink: &mut DiagnosticSink<'_>, | ||
23 | ) { | ||
24 | let _p = profile::span("validate_module_item"); | ||
25 | let mut validator = decl_check::DeclValidator::new(owner, sink); | ||
26 | validator.validate_item(db); | ||
27 | } | ||
28 | |||
18 | pub fn validate_body(db: &dyn HirDatabase, owner: DefWithBodyId, sink: &mut DiagnosticSink<'_>) { | 29 | pub fn validate_body(db: &dyn HirDatabase, owner: DefWithBodyId, sink: &mut DiagnosticSink<'_>) { |
19 | let _p = profile::span("validate_body"); | 30 | let _p = profile::span("validate_body"); |
20 | let infer = db.infer(owner); | 31 | let infer = db.infer(owner); |
@@ -231,6 +242,66 @@ impl Diagnostic for MismatchedArgCount { | |||
231 | } | 242 | } |
232 | } | 243 | } |
233 | 244 | ||
245 | #[derive(Debug)] | ||
246 | pub enum CaseType { | ||
247 | // `some_var` | ||
248 | LowerSnakeCase, | ||
249 | // `SOME_CONST` | ||
250 | UpperSnakeCase, | ||
251 | // `SomeStruct` | ||
252 | UpperCamelCase, | ||
253 | } | ||
254 | |||
255 | impl fmt::Display for CaseType { | ||
256 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { | ||
257 | let repr = match self { | ||
258 | CaseType::LowerSnakeCase => "snake_case", | ||
259 | CaseType::UpperSnakeCase => "UPPER_SNAKE_CASE", | ||
260 | CaseType::UpperCamelCase => "CamelCase", | ||
261 | }; | ||
262 | |||
263 | write!(f, "{}", repr) | ||
264 | } | ||
265 | } | ||
266 | |||
267 | #[derive(Debug)] | ||
268 | pub struct IncorrectCase { | ||
269 | pub file: HirFileId, | ||
270 | pub ident: AstPtr<ast::Name>, | ||
271 | pub expected_case: CaseType, | ||
272 | pub ident_type: String, | ||
273 | pub ident_text: String, | ||
274 | pub suggested_text: String, | ||
275 | } | ||
276 | |||
277 | impl Diagnostic for IncorrectCase { | ||
278 | fn code(&self) -> DiagnosticCode { | ||
279 | DiagnosticCode("incorrect-ident-case") | ||
280 | } | ||
281 | |||
282 | fn message(&self) -> String { | ||
283 | format!( | ||
284 | "{} `{}` should have {} name, e.g. `{}`", | ||
285 | self.ident_type, | ||
286 | self.ident_text, | ||
287 | self.expected_case.to_string(), | ||
288 | self.suggested_text | ||
289 | ) | ||
290 | } | ||
291 | |||
292 | fn display_source(&self) -> InFile<SyntaxNodePtr> { | ||
293 | InFile::new(self.file, self.ident.clone().into()) | ||
294 | } | ||
295 | |||
296 | fn as_any(&self) -> &(dyn Any + Send + 'static) { | ||
297 | self | ||
298 | } | ||
299 | |||
300 | fn is_experimental(&self) -> bool { | ||
301 | true | ||
302 | } | ||
303 | } | ||
304 | |||
234 | #[cfg(test)] | 305 | #[cfg(test)] |
235 | mod tests { | 306 | mod tests { |
236 | use base_db::{fixture::WithFixture, FileId, SourceDatabase, SourceDatabaseExt}; | 307 | use base_db::{fixture::WithFixture, FileId, SourceDatabase, SourceDatabaseExt}; |
@@ -242,7 +313,10 @@ mod tests { | |||
242 | use rustc_hash::FxHashMap; | 313 | use rustc_hash::FxHashMap; |
243 | use syntax::{TextRange, TextSize}; | 314 | use syntax::{TextRange, TextSize}; |
244 | 315 | ||
245 | use crate::{diagnostics::validate_body, test_db::TestDB}; | 316 | use crate::{ |
317 | diagnostics::{validate_body, validate_module_item}, | ||
318 | test_db::TestDB, | ||
319 | }; | ||
246 | 320 | ||
247 | impl TestDB { | 321 | impl TestDB { |
248 | fn diagnostics<F: FnMut(&dyn Diagnostic)>(&self, mut cb: F) { | 322 | fn diagnostics<F: FnMut(&dyn Diagnostic)>(&self, mut cb: F) { |
@@ -253,6 +327,9 @@ mod tests { | |||
253 | let mut fns = Vec::new(); | 327 | let mut fns = Vec::new(); |
254 | for (module_id, _) in crate_def_map.modules.iter() { | 328 | for (module_id, _) in crate_def_map.modules.iter() { |
255 | for decl in crate_def_map[module_id].scope.declarations() { | 329 | for decl in crate_def_map[module_id].scope.declarations() { |
330 | let mut sink = DiagnosticSinkBuilder::new().build(&mut cb); | ||
331 | validate_module_item(self, decl, &mut sink); | ||
332 | |||
256 | if let ModuleDefId::FunctionId(f) = decl { | 333 | if let ModuleDefId::FunctionId(f) = decl { |
257 | fns.push(f) | 334 | fns.push(f) |
258 | } | 335 | } |
@@ -262,6 +339,8 @@ mod tests { | |||
262 | let impl_data = self.impl_data(impl_id); | 339 | let impl_data = self.impl_data(impl_id); |
263 | for item in impl_data.items.iter() { | 340 | for item in impl_data.items.iter() { |
264 | if let AssocItemId::FunctionId(f) = item { | 341 | if let AssocItemId::FunctionId(f) = item { |
342 | let mut sink = DiagnosticSinkBuilder::new().build(&mut cb); | ||
343 | validate_module_item(self, ModuleDefId::FunctionId(*f), &mut sink); | ||
265 | fns.push(*f) | 344 | fns.push(*f) |
266 | } | 345 | } |
267 | } | 346 | } |
diff --git a/crates/hir_ty/src/diagnostics/decl_check.rs b/crates/hir_ty/src/diagnostics/decl_check.rs new file mode 100644 index 000000000..f987636fe --- /dev/null +++ b/crates/hir_ty/src/diagnostics/decl_check.rs | |||
@@ -0,0 +1,833 @@ | |||
1 | //! Provides validators for the item declarations. | ||
2 | //! | ||
3 | //! This includes the following items: | ||
4 | //! | ||
5 | //! - variable bindings (e.g. `let x = foo();`) | ||
6 | //! - struct fields (e.g. `struct Foo { field: u8 }`) | ||
7 | //! - enum variants (e.g. `enum Foo { Variant { field: u8 } }`) | ||
8 | //! - function/method arguments (e.g. `fn foo(arg: u8)`) | ||
9 | //! - constants (e.g. `const FOO: u8 = 10;`) | ||
10 | //! - static items (e.g. `static FOO: u8 = 10;`) | ||
11 | //! - match arm bindings (e.g. `foo @ Some(_)`) | ||
12 | |||
13 | mod case_conv; | ||
14 | |||
15 | use hir_def::{ | ||
16 | adt::VariantData, | ||
17 | expr::{Pat, PatId}, | ||
18 | src::HasSource, | ||
19 | AdtId, ConstId, EnumId, FunctionId, Lookup, ModuleDefId, StaticId, StructId, | ||
20 | }; | ||
21 | use hir_expand::{ | ||
22 | diagnostics::DiagnosticSink, | ||
23 | name::{AsName, Name}, | ||
24 | }; | ||
25 | use syntax::{ | ||
26 | ast::{self, NameOwner}, | ||
27 | AstNode, AstPtr, | ||
28 | }; | ||
29 | |||
30 | use crate::{ | ||
31 | db::HirDatabase, | ||
32 | diagnostics::{decl_check::case_conv::*, CaseType, IncorrectCase}, | ||
33 | }; | ||
34 | |||
35 | pub(super) struct DeclValidator<'a, 'b: 'a> { | ||
36 | owner: ModuleDefId, | ||
37 | sink: &'a mut DiagnosticSink<'b>, | ||
38 | } | ||
39 | |||
40 | #[derive(Debug)] | ||
41 | struct Replacement { | ||
42 | current_name: Name, | ||
43 | suggested_text: String, | ||
44 | expected_case: CaseType, | ||
45 | } | ||
46 | |||
47 | impl<'a, 'b> DeclValidator<'a, 'b> { | ||
48 | pub(super) fn new( | ||
49 | owner: ModuleDefId, | ||
50 | sink: &'a mut DiagnosticSink<'b>, | ||
51 | ) -> DeclValidator<'a, 'b> { | ||
52 | DeclValidator { owner, sink } | ||
53 | } | ||
54 | |||
55 | pub(super) fn validate_item(&mut self, db: &dyn HirDatabase) { | ||
56 | match self.owner { | ||
57 | ModuleDefId::FunctionId(func) => self.validate_func(db, func), | ||
58 | ModuleDefId::AdtId(adt) => self.validate_adt(db, adt), | ||
59 | ModuleDefId::ConstId(const_id) => self.validate_const(db, const_id), | ||
60 | ModuleDefId::StaticId(static_id) => self.validate_static(db, static_id), | ||
61 | _ => return, | ||
62 | } | ||
63 | } | ||
64 | |||
65 | fn validate_adt(&mut self, db: &dyn HirDatabase, adt: AdtId) { | ||
66 | match adt { | ||
67 | AdtId::StructId(struct_id) => self.validate_struct(db, struct_id), | ||
68 | AdtId::EnumId(enum_id) => self.validate_enum(db, enum_id), | ||
69 | AdtId::UnionId(_) => { | ||
70 | // Unions aren't yet supported by this validator. | ||
71 | } | ||
72 | } | ||
73 | } | ||
74 | |||
75 | fn validate_func(&mut self, db: &dyn HirDatabase, func: FunctionId) { | ||
76 | let data = db.function_data(func); | ||
77 | let body = db.body(func.into()); | ||
78 | |||
79 | // 1. Check the function name. | ||
80 | let function_name = data.name.to_string(); | ||
81 | let fn_name_replacement = if let Some(new_name) = to_lower_snake_case(&function_name) { | ||
82 | let replacement = Replacement { | ||
83 | current_name: data.name.clone(), | ||
84 | suggested_text: new_name, | ||
85 | expected_case: CaseType::LowerSnakeCase, | ||
86 | }; | ||
87 | Some(replacement) | ||
88 | } else { | ||
89 | None | ||
90 | }; | ||
91 | |||
92 | // 2. Check the param names. | ||
93 | let mut fn_param_replacements = Vec::new(); | ||
94 | |||
95 | for pat_id in body.params.iter().cloned() { | ||
96 | let pat = &body[pat_id]; | ||
97 | |||
98 | let param_name = match pat { | ||
99 | Pat::Bind { name, .. } => name, | ||
100 | _ => continue, | ||
101 | }; | ||
102 | |||
103 | let name = param_name.to_string(); | ||
104 | if let Some(new_name) = to_lower_snake_case(&name) { | ||
105 | let replacement = Replacement { | ||
106 | current_name: param_name.clone(), | ||
107 | suggested_text: new_name, | ||
108 | expected_case: CaseType::LowerSnakeCase, | ||
109 | }; | ||
110 | fn_param_replacements.push(replacement); | ||
111 | } | ||
112 | } | ||
113 | |||
114 | // 3. Check the patterns inside the function body. | ||
115 | let mut pats_replacements = Vec::new(); | ||
116 | |||
117 | for (pat_idx, pat) in body.pats.iter() { | ||
118 | if body.params.contains(&pat_idx) { | ||
119 | // We aren't interested in function parameters, we've processed them above. | ||
120 | continue; | ||
121 | } | ||
122 | |||
123 | let bind_name = match pat { | ||
124 | Pat::Bind { name, .. } => name, | ||
125 | _ => continue, | ||
126 | }; | ||
127 | |||
128 | let name = bind_name.to_string(); | ||
129 | if let Some(new_name) = to_lower_snake_case(&name) { | ||
130 | let replacement = Replacement { | ||
131 | current_name: bind_name.clone(), | ||
132 | suggested_text: new_name, | ||
133 | expected_case: CaseType::LowerSnakeCase, | ||
134 | }; | ||
135 | pats_replacements.push((pat_idx, replacement)); | ||
136 | } | ||
137 | } | ||
138 | |||
139 | // 4. If there is at least one element to spawn a warning on, go to the source map and generate a warning. | ||
140 | self.create_incorrect_case_diagnostic_for_func( | ||
141 | func, | ||
142 | db, | ||
143 | fn_name_replacement, | ||
144 | fn_param_replacements, | ||
145 | ); | ||
146 | self.create_incorrect_case_diagnostic_for_variables(func, db, pats_replacements); | ||
147 | |||
148 | // 5. Recursively validate inner scope items, such as static variables and constants. | ||
149 | for (item_id, _) in body.item_scope.values() { | ||
150 | let mut validator = DeclValidator::new(item_id, self.sink); | ||
151 | validator.validate_item(db); | ||
152 | } | ||
153 | } | ||
154 | |||
155 | /// Given the information about incorrect names in the function declaration, looks up into the source code | ||
156 | /// for exact locations and adds diagnostics into the sink. | ||
157 | fn create_incorrect_case_diagnostic_for_func( | ||
158 | &mut self, | ||
159 | func: FunctionId, | ||
160 | db: &dyn HirDatabase, | ||
161 | fn_name_replacement: Option<Replacement>, | ||
162 | fn_param_replacements: Vec<Replacement>, | ||
163 | ) { | ||
164 | // XXX: only look at sources if we do have incorrect names | ||
165 | if fn_name_replacement.is_none() && fn_param_replacements.is_empty() { | ||
166 | return; | ||
167 | } | ||
168 | |||
169 | let fn_loc = func.lookup(db.upcast()); | ||
170 | let fn_src = fn_loc.source(db.upcast()); | ||
171 | |||
172 | // 1. Diagnostic for function name. | ||
173 | if let Some(replacement) = fn_name_replacement { | ||
174 | let ast_ptr = match fn_src.value.name() { | ||
175 | Some(name) => name, | ||
176 | None => { | ||
177 | // We don't want rust-analyzer to panic over this, but it is definitely some kind of error in the logic. | ||
178 | log::error!( | ||
179 | "Replacement ({:?}) was generated for a function without a name: {:?}", | ||
180 | replacement, | ||
181 | fn_src | ||
182 | ); | ||
183 | return; | ||
184 | } | ||
185 | }; | ||
186 | |||
187 | let diagnostic = IncorrectCase { | ||
188 | file: fn_src.file_id, | ||
189 | ident_type: "Function".to_string(), | ||
190 | ident: AstPtr::new(&ast_ptr).into(), | ||
191 | expected_case: replacement.expected_case, | ||
192 | ident_text: replacement.current_name.to_string(), | ||
193 | suggested_text: replacement.suggested_text, | ||
194 | }; | ||
195 | |||
196 | self.sink.push(diagnostic); | ||
197 | } | ||
198 | |||
199 | // 2. Diagnostics for function params. | ||
200 | let fn_params_list = match fn_src.value.param_list() { | ||
201 | Some(params) => params, | ||
202 | None => { | ||
203 | if !fn_param_replacements.is_empty() { | ||
204 | log::error!( | ||
205 | "Replacements ({:?}) were generated for a function parameters which had no parameters list: {:?}", | ||
206 | fn_param_replacements, fn_src | ||
207 | ); | ||
208 | } | ||
209 | return; | ||
210 | } | ||
211 | }; | ||
212 | let mut fn_params_iter = fn_params_list.params(); | ||
213 | for param_to_rename in fn_param_replacements { | ||
214 | // We assume that parameters in replacement are in the same order as in the | ||
215 | // actual params list, but just some of them (ones that named correctly) are skipped. | ||
216 | let ast_ptr: ast::Name = loop { | ||
217 | match fn_params_iter.next() { | ||
218 | Some(element) | ||
219 | if pat_equals_to_name(element.pat(), ¶m_to_rename.current_name) => | ||
220 | { | ||
221 | if let ast::Pat::IdentPat(pat) = element.pat().unwrap() { | ||
222 | break pat.name().unwrap(); | ||
223 | } else { | ||
224 | // This is critical. If we consider this parameter the expected one, | ||
225 | // it **must** have a name. | ||
226 | panic!( | ||
227 | "Pattern {:?} equals to expected replacement {:?}, but has no name", | ||
228 | element, param_to_rename | ||
229 | ); | ||
230 | } | ||
231 | } | ||
232 | Some(_) => {} | ||
233 | None => { | ||
234 | log::error!( | ||
235 | "Replacement ({:?}) was generated for a function parameter which was not found: {:?}", | ||
236 | param_to_rename, fn_src | ||
237 | ); | ||
238 | return; | ||
239 | } | ||
240 | } | ||
241 | }; | ||
242 | |||
243 | let diagnostic = IncorrectCase { | ||
244 | file: fn_src.file_id, | ||
245 | ident_type: "Argument".to_string(), | ||
246 | ident: AstPtr::new(&ast_ptr).into(), | ||
247 | expected_case: param_to_rename.expected_case, | ||
248 | ident_text: param_to_rename.current_name.to_string(), | ||
249 | suggested_text: param_to_rename.suggested_text, | ||
250 | }; | ||
251 | |||
252 | self.sink.push(diagnostic); | ||
253 | } | ||
254 | } | ||
255 | |||
256 | /// Given the information about incorrect variable names, looks up into the source code | ||
257 | /// for exact locations and adds diagnostics into the sink. | ||
258 | fn create_incorrect_case_diagnostic_for_variables( | ||
259 | &mut self, | ||
260 | func: FunctionId, | ||
261 | db: &dyn HirDatabase, | ||
262 | pats_replacements: Vec<(PatId, Replacement)>, | ||
263 | ) { | ||
264 | // XXX: only look at source_map if we do have missing fields | ||
265 | if pats_replacements.is_empty() { | ||
266 | return; | ||
267 | } | ||
268 | |||
269 | let (_, source_map) = db.body_with_source_map(func.into()); | ||
270 | |||
271 | for (id, replacement) in pats_replacements { | ||
272 | if let Ok(source_ptr) = source_map.pat_syntax(id) { | ||
273 | if let Some(expr) = source_ptr.value.as_ref().left() { | ||
274 | let root = source_ptr.file_syntax(db.upcast()); | ||
275 | if let ast::Pat::IdentPat(ident_pat) = expr.to_node(&root) { | ||
276 | let parent = match ident_pat.syntax().parent() { | ||
277 | Some(parent) => parent, | ||
278 | None => continue, | ||
279 | }; | ||
280 | let name_ast = match ident_pat.name() { | ||
281 | Some(name_ast) => name_ast, | ||
282 | None => continue, | ||
283 | }; | ||
284 | |||
285 | // We have to check that it's either `let var = ...` or `var @ Variant(_)` statement, | ||
286 | // because e.g. match arms are patterns as well. | ||
287 | // In other words, we check that it's a named variable binding. | ||
288 | let is_binding = ast::LetStmt::cast(parent.clone()).is_some() | ||
289 | || (ast::MatchArm::cast(parent).is_some() | ||
290 | && ident_pat.at_token().is_some()); | ||
291 | if !is_binding { | ||
292 | // This pattern is not an actual variable declaration, e.g. `Some(val) => {..}` match arm. | ||
293 | continue; | ||
294 | } | ||
295 | |||
296 | let diagnostic = IncorrectCase { | ||
297 | file: source_ptr.file_id, | ||
298 | ident_type: "Variable".to_string(), | ||
299 | ident: AstPtr::new(&name_ast).into(), | ||
300 | expected_case: replacement.expected_case, | ||
301 | ident_text: replacement.current_name.to_string(), | ||
302 | suggested_text: replacement.suggested_text, | ||
303 | }; | ||
304 | |||
305 | self.sink.push(diagnostic); | ||
306 | } | ||
307 | } | ||
308 | } | ||
309 | } | ||
310 | } | ||
311 | |||
312 | fn validate_struct(&mut self, db: &dyn HirDatabase, struct_id: StructId) { | ||
313 | let data = db.struct_data(struct_id); | ||
314 | |||
315 | // 1. Check the structure name. | ||
316 | let struct_name = data.name.to_string(); | ||
317 | let struct_name_replacement = if let Some(new_name) = to_camel_case(&struct_name) { | ||
318 | let replacement = Replacement { | ||
319 | current_name: data.name.clone(), | ||
320 | suggested_text: new_name, | ||
321 | expected_case: CaseType::UpperCamelCase, | ||
322 | }; | ||
323 | Some(replacement) | ||
324 | } else { | ||
325 | None | ||
326 | }; | ||
327 | |||
328 | // 2. Check the field names. | ||
329 | let mut struct_fields_replacements = Vec::new(); | ||
330 | |||
331 | if let VariantData::Record(fields) = data.variant_data.as_ref() { | ||
332 | for (_, field) in fields.iter() { | ||
333 | let field_name = field.name.to_string(); | ||
334 | if let Some(new_name) = to_lower_snake_case(&field_name) { | ||
335 | let replacement = Replacement { | ||
336 | current_name: field.name.clone(), | ||
337 | suggested_text: new_name, | ||
338 | expected_case: CaseType::LowerSnakeCase, | ||
339 | }; | ||
340 | struct_fields_replacements.push(replacement); | ||
341 | } | ||
342 | } | ||
343 | } | ||
344 | |||
345 | // 3. If there is at least one element to spawn a warning on, go to the source map and generate a warning. | ||
346 | self.create_incorrect_case_diagnostic_for_struct( | ||
347 | struct_id, | ||
348 | db, | ||
349 | struct_name_replacement, | ||
350 | struct_fields_replacements, | ||
351 | ); | ||
352 | } | ||
353 | |||
354 | /// Given the information about incorrect names in the struct declaration, looks up into the source code | ||
355 | /// for exact locations and adds diagnostics into the sink. | ||
356 | fn create_incorrect_case_diagnostic_for_struct( | ||
357 | &mut self, | ||
358 | struct_id: StructId, | ||
359 | db: &dyn HirDatabase, | ||
360 | struct_name_replacement: Option<Replacement>, | ||
361 | struct_fields_replacements: Vec<Replacement>, | ||
362 | ) { | ||
363 | // XXX: only look at sources if we do have incorrect names | ||
364 | if struct_name_replacement.is_none() && struct_fields_replacements.is_empty() { | ||
365 | return; | ||
366 | } | ||
367 | |||
368 | let struct_loc = struct_id.lookup(db.upcast()); | ||
369 | let struct_src = struct_loc.source(db.upcast()); | ||
370 | |||
371 | if let Some(replacement) = struct_name_replacement { | ||
372 | let ast_ptr = match struct_src.value.name() { | ||
373 | Some(name) => name, | ||
374 | None => { | ||
375 | // We don't want rust-analyzer to panic over this, but it is definitely some kind of error in the logic. | ||
376 | log::error!( | ||
377 | "Replacement ({:?}) was generated for a structure without a name: {:?}", | ||
378 | replacement, | ||
379 | struct_src | ||
380 | ); | ||
381 | return; | ||
382 | } | ||
383 | }; | ||
384 | |||
385 | let diagnostic = IncorrectCase { | ||
386 | file: struct_src.file_id, | ||
387 | ident_type: "Structure".to_string(), | ||
388 | ident: AstPtr::new(&ast_ptr).into(), | ||
389 | expected_case: replacement.expected_case, | ||
390 | ident_text: replacement.current_name.to_string(), | ||
391 | suggested_text: replacement.suggested_text, | ||
392 | }; | ||
393 | |||
394 | self.sink.push(diagnostic); | ||
395 | } | ||
396 | |||
397 | let struct_fields_list = match struct_src.value.field_list() { | ||
398 | Some(ast::FieldList::RecordFieldList(fields)) => fields, | ||
399 | _ => { | ||
400 | if !struct_fields_replacements.is_empty() { | ||
401 | log::error!( | ||
402 | "Replacements ({:?}) were generated for a structure fields which had no fields list: {:?}", | ||
403 | struct_fields_replacements, struct_src | ||
404 | ); | ||
405 | } | ||
406 | return; | ||
407 | } | ||
408 | }; | ||
409 | let mut struct_fields_iter = struct_fields_list.fields(); | ||
410 | for field_to_rename in struct_fields_replacements { | ||
411 | // We assume that parameters in replacement are in the same order as in the | ||
412 | // actual params list, but just some of them (ones that named correctly) are skipped. | ||
413 | let ast_ptr = loop { | ||
414 | match struct_fields_iter.next() { | ||
415 | Some(element) if names_equal(element.name(), &field_to_rename.current_name) => { | ||
416 | break element.name().unwrap() | ||
417 | } | ||
418 | Some(_) => {} | ||
419 | None => { | ||
420 | log::error!( | ||
421 | "Replacement ({:?}) was generated for a structure field which was not found: {:?}", | ||
422 | field_to_rename, struct_src | ||
423 | ); | ||
424 | return; | ||
425 | } | ||
426 | } | ||
427 | }; | ||
428 | |||
429 | let diagnostic = IncorrectCase { | ||
430 | file: struct_src.file_id, | ||
431 | ident_type: "Field".to_string(), | ||
432 | ident: AstPtr::new(&ast_ptr).into(), | ||
433 | expected_case: field_to_rename.expected_case, | ||
434 | ident_text: field_to_rename.current_name.to_string(), | ||
435 | suggested_text: field_to_rename.suggested_text, | ||
436 | }; | ||
437 | |||
438 | self.sink.push(diagnostic); | ||
439 | } | ||
440 | } | ||
441 | |||
442 | fn validate_enum(&mut self, db: &dyn HirDatabase, enum_id: EnumId) { | ||
443 | let data = db.enum_data(enum_id); | ||
444 | |||
445 | // 1. Check the enum name. | ||
446 | let enum_name = data.name.to_string(); | ||
447 | let enum_name_replacement = if let Some(new_name) = to_camel_case(&enum_name) { | ||
448 | let replacement = Replacement { | ||
449 | current_name: data.name.clone(), | ||
450 | suggested_text: new_name, | ||
451 | expected_case: CaseType::UpperCamelCase, | ||
452 | }; | ||
453 | Some(replacement) | ||
454 | } else { | ||
455 | None | ||
456 | }; | ||
457 | |||
458 | // 2. Check the field names. | ||
459 | let mut enum_fields_replacements = Vec::new(); | ||
460 | |||
461 | for (_, variant) in data.variants.iter() { | ||
462 | let variant_name = variant.name.to_string(); | ||
463 | if let Some(new_name) = to_camel_case(&variant_name) { | ||
464 | let replacement = Replacement { | ||
465 | current_name: variant.name.clone(), | ||
466 | suggested_text: new_name, | ||
467 | expected_case: CaseType::UpperCamelCase, | ||
468 | }; | ||
469 | enum_fields_replacements.push(replacement); | ||
470 | } | ||
471 | } | ||
472 | |||
473 | // 3. If there is at least one element to spawn a warning on, go to the source map and generate a warning. | ||
474 | self.create_incorrect_case_diagnostic_for_enum( | ||
475 | enum_id, | ||
476 | db, | ||
477 | enum_name_replacement, | ||
478 | enum_fields_replacements, | ||
479 | ) | ||
480 | } | ||
481 | |||
482 | /// Given the information about incorrect names in the struct declaration, looks up into the source code | ||
483 | /// for exact locations and adds diagnostics into the sink. | ||
484 | fn create_incorrect_case_diagnostic_for_enum( | ||
485 | &mut self, | ||
486 | enum_id: EnumId, | ||
487 | db: &dyn HirDatabase, | ||
488 | enum_name_replacement: Option<Replacement>, | ||
489 | enum_variants_replacements: Vec<Replacement>, | ||
490 | ) { | ||
491 | // XXX: only look at sources if we do have incorrect names | ||
492 | if enum_name_replacement.is_none() && enum_variants_replacements.is_empty() { | ||
493 | return; | ||
494 | } | ||
495 | |||
496 | let enum_loc = enum_id.lookup(db.upcast()); | ||
497 | let enum_src = enum_loc.source(db.upcast()); | ||
498 | |||
499 | if let Some(replacement) = enum_name_replacement { | ||
500 | let ast_ptr = match enum_src.value.name() { | ||
501 | Some(name) => name, | ||
502 | None => { | ||
503 | // We don't want rust-analyzer to panic over this, but it is definitely some kind of error in the logic. | ||
504 | log::error!( | ||
505 | "Replacement ({:?}) was generated for a enum without a name: {:?}", | ||
506 | replacement, | ||
507 | enum_src | ||
508 | ); | ||
509 | return; | ||
510 | } | ||
511 | }; | ||
512 | |||
513 | let diagnostic = IncorrectCase { | ||
514 | file: enum_src.file_id, | ||
515 | ident_type: "Enum".to_string(), | ||
516 | ident: AstPtr::new(&ast_ptr).into(), | ||
517 | expected_case: replacement.expected_case, | ||
518 | ident_text: replacement.current_name.to_string(), | ||
519 | suggested_text: replacement.suggested_text, | ||
520 | }; | ||
521 | |||
522 | self.sink.push(diagnostic); | ||
523 | } | ||
524 | |||
525 | let enum_variants_list = match enum_src.value.variant_list() { | ||
526 | Some(variants) => variants, | ||
527 | _ => { | ||
528 | if !enum_variants_replacements.is_empty() { | ||
529 | log::error!( | ||
530 | "Replacements ({:?}) were generated for a enum variants which had no fields list: {:?}", | ||
531 | enum_variants_replacements, enum_src | ||
532 | ); | ||
533 | } | ||
534 | return; | ||
535 | } | ||
536 | }; | ||
537 | let mut enum_variants_iter = enum_variants_list.variants(); | ||
538 | for variant_to_rename in enum_variants_replacements { | ||
539 | // We assume that parameters in replacement are in the same order as in the | ||
540 | // actual params list, but just some of them (ones that named correctly) are skipped. | ||
541 | let ast_ptr = loop { | ||
542 | match enum_variants_iter.next() { | ||
543 | Some(variant) | ||
544 | if names_equal(variant.name(), &variant_to_rename.current_name) => | ||
545 | { | ||
546 | break variant.name().unwrap() | ||
547 | } | ||
548 | Some(_) => {} | ||
549 | None => { | ||
550 | log::error!( | ||
551 | "Replacement ({:?}) was generated for a enum variant which was not found: {:?}", | ||
552 | variant_to_rename, enum_src | ||
553 | ); | ||
554 | return; | ||
555 | } | ||
556 | } | ||
557 | }; | ||
558 | |||
559 | let diagnostic = IncorrectCase { | ||
560 | file: enum_src.file_id, | ||
561 | ident_type: "Variant".to_string(), | ||
562 | ident: AstPtr::new(&ast_ptr).into(), | ||
563 | expected_case: variant_to_rename.expected_case, | ||
564 | ident_text: variant_to_rename.current_name.to_string(), | ||
565 | suggested_text: variant_to_rename.suggested_text, | ||
566 | }; | ||
567 | |||
568 | self.sink.push(diagnostic); | ||
569 | } | ||
570 | } | ||
571 | |||
572 | fn validate_const(&mut self, db: &dyn HirDatabase, const_id: ConstId) { | ||
573 | let data = db.const_data(const_id); | ||
574 | |||
575 | let name = match &data.name { | ||
576 | Some(name) => name, | ||
577 | None => return, | ||
578 | }; | ||
579 | |||
580 | let const_name = name.to_string(); | ||
581 | let replacement = if let Some(new_name) = to_upper_snake_case(&const_name) { | ||
582 | Replacement { | ||
583 | current_name: name.clone(), | ||
584 | suggested_text: new_name, | ||
585 | expected_case: CaseType::UpperSnakeCase, | ||
586 | } | ||
587 | } else { | ||
588 | // Nothing to do here. | ||
589 | return; | ||
590 | }; | ||
591 | |||
592 | let const_loc = const_id.lookup(db.upcast()); | ||
593 | let const_src = const_loc.source(db.upcast()); | ||
594 | |||
595 | let ast_ptr = match const_src.value.name() { | ||
596 | Some(name) => name, | ||
597 | None => return, | ||
598 | }; | ||
599 | |||
600 | let diagnostic = IncorrectCase { | ||
601 | file: const_src.file_id, | ||
602 | ident_type: "Constant".to_string(), | ||
603 | ident: AstPtr::new(&ast_ptr).into(), | ||
604 | expected_case: replacement.expected_case, | ||
605 | ident_text: replacement.current_name.to_string(), | ||
606 | suggested_text: replacement.suggested_text, | ||
607 | }; | ||
608 | |||
609 | self.sink.push(diagnostic); | ||
610 | } | ||
611 | |||
612 | fn validate_static(&mut self, db: &dyn HirDatabase, static_id: StaticId) { | ||
613 | let data = db.static_data(static_id); | ||
614 | |||
615 | let name = match &data.name { | ||
616 | Some(name) => name, | ||
617 | None => return, | ||
618 | }; | ||
619 | |||
620 | let static_name = name.to_string(); | ||
621 | let replacement = if let Some(new_name) = to_upper_snake_case(&static_name) { | ||
622 | Replacement { | ||
623 | current_name: name.clone(), | ||
624 | suggested_text: new_name, | ||
625 | expected_case: CaseType::UpperSnakeCase, | ||
626 | } | ||
627 | } else { | ||
628 | // Nothing to do here. | ||
629 | return; | ||
630 | }; | ||
631 | |||
632 | let static_loc = static_id.lookup(db.upcast()); | ||
633 | let static_src = static_loc.source(db.upcast()); | ||
634 | |||
635 | let ast_ptr = match static_src.value.name() { | ||
636 | Some(name) => name, | ||
637 | None => return, | ||
638 | }; | ||
639 | |||
640 | let diagnostic = IncorrectCase { | ||
641 | file: static_src.file_id, | ||
642 | ident_type: "Static variable".to_string(), | ||
643 | ident: AstPtr::new(&ast_ptr).into(), | ||
644 | expected_case: replacement.expected_case, | ||
645 | ident_text: replacement.current_name.to_string(), | ||
646 | suggested_text: replacement.suggested_text, | ||
647 | }; | ||
648 | |||
649 | self.sink.push(diagnostic); | ||
650 | } | ||
651 | } | ||
652 | |||
653 | fn names_equal(left: Option<ast::Name>, right: &Name) -> bool { | ||
654 | if let Some(left) = left { | ||
655 | &left.as_name() == right | ||
656 | } else { | ||
657 | false | ||
658 | } | ||
659 | } | ||
660 | |||
661 | fn pat_equals_to_name(pat: Option<ast::Pat>, name: &Name) -> bool { | ||
662 | if let Some(ast::Pat::IdentPat(ident)) = pat { | ||
663 | ident.to_string() == name.to_string() | ||
664 | } else { | ||
665 | false | ||
666 | } | ||
667 | } | ||
668 | |||
669 | #[cfg(test)] | ||
670 | mod tests { | ||
671 | use crate::diagnostics::tests::check_diagnostics; | ||
672 | |||
673 | #[test] | ||
674 | fn incorrect_function_name() { | ||
675 | check_diagnostics( | ||
676 | r#" | ||
677 | fn NonSnakeCaseName() {} | ||
678 | // ^^^^^^^^^^^^^^^^ Function `NonSnakeCaseName` should have snake_case name, e.g. `non_snake_case_name` | ||
679 | "#, | ||
680 | ); | ||
681 | } | ||
682 | |||
683 | #[test] | ||
684 | fn incorrect_function_params() { | ||
685 | check_diagnostics( | ||
686 | r#" | ||
687 | fn foo(SomeParam: u8) {} | ||
688 | // ^^^^^^^^^ Argument `SomeParam` should have snake_case name, e.g. `some_param` | ||
689 | |||
690 | fn foo2(ok_param: &str, CAPS_PARAM: u8) {} | ||
691 | // ^^^^^^^^^^ Argument `CAPS_PARAM` should have snake_case name, e.g. `caps_param` | ||
692 | "#, | ||
693 | ); | ||
694 | } | ||
695 | |||
696 | #[test] | ||
697 | fn incorrect_variable_names() { | ||
698 | check_diagnostics( | ||
699 | r#" | ||
700 | fn foo() { | ||
701 | let SOME_VALUE = 10; | ||
702 | // ^^^^^^^^^^ Variable `SOME_VALUE` should have snake_case name, e.g. `some_value` | ||
703 | let AnotherValue = 20; | ||
704 | // ^^^^^^^^^^^^ Variable `AnotherValue` should have snake_case name, e.g. `another_value` | ||
705 | } | ||
706 | "#, | ||
707 | ); | ||
708 | } | ||
709 | |||
710 | #[test] | ||
711 | fn incorrect_struct_name() { | ||
712 | check_diagnostics( | ||
713 | r#" | ||
714 | struct non_camel_case_name {} | ||
715 | // ^^^^^^^^^^^^^^^^^^^ Structure `non_camel_case_name` should have CamelCase name, e.g. `NonCamelCaseName` | ||
716 | "#, | ||
717 | ); | ||
718 | } | ||
719 | |||
720 | #[test] | ||
721 | fn incorrect_struct_field() { | ||
722 | check_diagnostics( | ||
723 | r#" | ||
724 | struct SomeStruct { SomeField: u8 } | ||
725 | // ^^^^^^^^^ Field `SomeField` should have snake_case name, e.g. `some_field` | ||
726 | "#, | ||
727 | ); | ||
728 | } | ||
729 | |||
730 | #[test] | ||
731 | fn incorrect_enum_name() { | ||
732 | check_diagnostics( | ||
733 | r#" | ||
734 | enum some_enum { Val(u8) } | ||
735 | // ^^^^^^^^^ Enum `some_enum` should have CamelCase name, e.g. `SomeEnum` | ||
736 | "#, | ||
737 | ); | ||
738 | } | ||
739 | |||
740 | #[test] | ||
741 | fn incorrect_enum_variant_name() { | ||
742 | check_diagnostics( | ||
743 | r#" | ||
744 | enum SomeEnum { SOME_VARIANT(u8) } | ||
745 | // ^^^^^^^^^^^^ Variant `SOME_VARIANT` should have CamelCase name, e.g. `SomeVariant` | ||
746 | "#, | ||
747 | ); | ||
748 | } | ||
749 | |||
750 | #[test] | ||
751 | fn incorrect_const_name() { | ||
752 | check_diagnostics( | ||
753 | r#" | ||
754 | const some_weird_const: u8 = 10; | ||
755 | // ^^^^^^^^^^^^^^^^ Constant `some_weird_const` should have UPPER_SNAKE_CASE name, e.g. `SOME_WEIRD_CONST` | ||
756 | |||
757 | fn func() { | ||
758 | const someConstInFunc: &str = "hi there"; | ||
759 | // ^^^^^^^^^^^^^^^ Constant `someConstInFunc` should have UPPER_SNAKE_CASE name, e.g. `SOME_CONST_IN_FUNC` | ||
760 | |||
761 | } | ||
762 | "#, | ||
763 | ); | ||
764 | } | ||
765 | |||
766 | #[test] | ||
767 | fn incorrect_static_name() { | ||
768 | check_diagnostics( | ||
769 | r#" | ||
770 | static some_weird_const: u8 = 10; | ||
771 | // ^^^^^^^^^^^^^^^^ Static variable `some_weird_const` should have UPPER_SNAKE_CASE name, e.g. `SOME_WEIRD_CONST` | ||
772 | |||
773 | fn func() { | ||
774 | static someConstInFunc: &str = "hi there"; | ||
775 | // ^^^^^^^^^^^^^^^ Static variable `someConstInFunc` should have UPPER_SNAKE_CASE name, e.g. `SOME_CONST_IN_FUNC` | ||
776 | } | ||
777 | "#, | ||
778 | ); | ||
779 | } | ||
780 | |||
781 | #[test] | ||
782 | fn fn_inside_impl_struct() { | ||
783 | check_diagnostics( | ||
784 | r#" | ||
785 | struct someStruct; | ||
786 | // ^^^^^^^^^^ Structure `someStruct` should have CamelCase name, e.g. `SomeStruct` | ||
787 | |||
788 | impl someStruct { | ||
789 | fn SomeFunc(&self) { | ||
790 | // ^^^^^^^^ Function `SomeFunc` should have snake_case name, e.g. `some_func` | ||
791 | static someConstInFunc: &str = "hi there"; | ||
792 | // ^^^^^^^^^^^^^^^ Static variable `someConstInFunc` should have UPPER_SNAKE_CASE name, e.g. `SOME_CONST_IN_FUNC` | ||
793 | let WHY_VAR_IS_CAPS = 10; | ||
794 | // ^^^^^^^^^^^^^^^ Variable `WHY_VAR_IS_CAPS` should have snake_case name, e.g. `why_var_is_caps` | ||
795 | } | ||
796 | } | ||
797 | "#, | ||
798 | ); | ||
799 | } | ||
800 | |||
801 | #[test] | ||
802 | fn no_diagnostic_for_enum_varinats() { | ||
803 | check_diagnostics( | ||
804 | r#" | ||
805 | enum Option { Some, None } | ||
806 | |||
807 | fn main() { | ||
808 | match Option::None { | ||
809 | None => (), | ||
810 | Some => (), | ||
811 | } | ||
812 | } | ||
813 | "#, | ||
814 | ); | ||
815 | } | ||
816 | |||
817 | #[test] | ||
818 | fn non_let_bind() { | ||
819 | check_diagnostics( | ||
820 | r#" | ||
821 | enum Option { Some, None } | ||
822 | |||
823 | fn main() { | ||
824 | match Option::None { | ||
825 | SOME_VAR @ None => (), | ||
826 | // ^^^^^^^^ Variable `SOME_VAR` should have snake_case name, e.g. `some_var` | ||
827 | Some => (), | ||
828 | } | ||
829 | } | ||
830 | "#, | ||
831 | ); | ||
832 | } | ||
833 | } | ||
diff --git a/crates/hir_ty/src/diagnostics/decl_check/case_conv.rs b/crates/hir_ty/src/diagnostics/decl_check/case_conv.rs new file mode 100644 index 000000000..3800f2a6b --- /dev/null +++ b/crates/hir_ty/src/diagnostics/decl_check/case_conv.rs | |||
@@ -0,0 +1,194 @@ | |||
1 | //! Functions for string case manipulation, such as detecting the identifier case, | ||
2 | //! and converting it into appropriate form. | ||
3 | |||
4 | #[derive(Debug)] | ||
5 | enum DetectedCase { | ||
6 | LowerCamelCase, | ||
7 | UpperCamelCase, | ||
8 | LowerSnakeCase, | ||
9 | UpperSnakeCase, | ||
10 | Unknown, | ||
11 | } | ||
12 | |||
13 | fn detect_case(ident: &str) -> DetectedCase { | ||
14 | let trimmed_ident = ident.trim_matches('_'); | ||
15 | let first_lowercase = trimmed_ident.starts_with(|chr: char| chr.is_ascii_lowercase()); | ||
16 | let mut has_lowercase = first_lowercase; | ||
17 | let mut has_uppercase = false; | ||
18 | let mut has_underscore = false; | ||
19 | |||
20 | for chr in trimmed_ident.chars() { | ||
21 | if chr == '_' { | ||
22 | has_underscore = true; | ||
23 | } else if chr.is_ascii_uppercase() { | ||
24 | has_uppercase = true; | ||
25 | } else if chr.is_ascii_lowercase() { | ||
26 | has_lowercase = true; | ||
27 | } | ||
28 | } | ||
29 | |||
30 | if has_uppercase { | ||
31 | if !has_lowercase { | ||
32 | DetectedCase::UpperSnakeCase | ||
33 | } else if !has_underscore { | ||
34 | if first_lowercase { | ||
35 | DetectedCase::LowerCamelCase | ||
36 | } else { | ||
37 | DetectedCase::UpperCamelCase | ||
38 | } | ||
39 | } else { | ||
40 | // It has uppercase, it has lowercase, it has underscore. | ||
41 | // No assumptions here | ||
42 | DetectedCase::Unknown | ||
43 | } | ||
44 | } else { | ||
45 | DetectedCase::LowerSnakeCase | ||
46 | } | ||
47 | } | ||
48 | |||
49 | /// Converts an identifier to an UpperCamelCase form. | ||
50 | /// Returns `None` if the string is already is UpperCamelCase. | ||
51 | pub fn to_camel_case(ident: &str) -> Option<String> { | ||
52 | let detected_case = detect_case(ident); | ||
53 | |||
54 | match detected_case { | ||
55 | DetectedCase::UpperCamelCase => return None, | ||
56 | DetectedCase::LowerCamelCase => { | ||
57 | let mut first_capitalized = false; | ||
58 | let output = ident | ||
59 | .chars() | ||
60 | .map(|chr| { | ||
61 | if !first_capitalized && chr.is_ascii_lowercase() { | ||
62 | first_capitalized = true; | ||
63 | chr.to_ascii_uppercase() | ||
64 | } else { | ||
65 | chr | ||
66 | } | ||
67 | }) | ||
68 | .collect(); | ||
69 | return Some(output); | ||
70 | } | ||
71 | _ => {} | ||
72 | } | ||
73 | |||
74 | let mut output = String::with_capacity(ident.len()); | ||
75 | |||
76 | let mut capital_added = false; | ||
77 | for chr in ident.chars() { | ||
78 | if chr.is_alphabetic() { | ||
79 | if !capital_added { | ||
80 | output.push(chr.to_ascii_uppercase()); | ||
81 | capital_added = true; | ||
82 | } else { | ||
83 | output.push(chr.to_ascii_lowercase()); | ||
84 | } | ||
85 | } else if chr == '_' { | ||
86 | // Skip this character and make the next one capital. | ||
87 | capital_added = false; | ||
88 | } else { | ||
89 | // Put the characted as-is. | ||
90 | output.push(chr); | ||
91 | } | ||
92 | } | ||
93 | |||
94 | if output == ident { | ||
95 | // While we didn't detect the correct case at the beginning, there | ||
96 | // may be special cases: e.g. `A` is both valid CamelCase and UPPER_SNAKE_CASE. | ||
97 | None | ||
98 | } else { | ||
99 | Some(output) | ||
100 | } | ||
101 | } | ||
102 | |||
103 | /// Converts an identifier to a lower_snake_case form. | ||
104 | /// Returns `None` if the string is already in lower_snake_case. | ||
105 | pub fn to_lower_snake_case(ident: &str) -> Option<String> { | ||
106 | // First, assume that it's UPPER_SNAKE_CASE. | ||
107 | match detect_case(ident) { | ||
108 | DetectedCase::LowerSnakeCase => return None, | ||
109 | DetectedCase::UpperSnakeCase => { | ||
110 | return Some(ident.chars().map(|chr| chr.to_ascii_lowercase()).collect()) | ||
111 | } | ||
112 | _ => {} | ||
113 | } | ||
114 | |||
115 | // Otherwise, assume that it's CamelCase. | ||
116 | let lower_snake_case = stdx::to_lower_snake_case(ident); | ||
117 | |||
118 | if lower_snake_case == ident { | ||
119 | // While we didn't detect the correct case at the beginning, there | ||
120 | // may be special cases: e.g. `a` is both valid camelCase and snake_case. | ||
121 | None | ||
122 | } else { | ||
123 | Some(lower_snake_case) | ||
124 | } | ||
125 | } | ||
126 | |||
127 | /// Converts an identifier to an UPPER_SNAKE_CASE form. | ||
128 | /// Returns `None` if the string is already is UPPER_SNAKE_CASE. | ||
129 | pub fn to_upper_snake_case(ident: &str) -> Option<String> { | ||
130 | match detect_case(ident) { | ||
131 | DetectedCase::UpperSnakeCase => return None, | ||
132 | DetectedCase::LowerSnakeCase => { | ||
133 | return Some(ident.chars().map(|chr| chr.to_ascii_uppercase()).collect()) | ||
134 | } | ||
135 | _ => {} | ||
136 | } | ||
137 | |||
138 | // Normalize the string from whatever form it's in currently, and then just make it uppercase. | ||
139 | let upper_snake_case = stdx::to_upper_snake_case(ident); | ||
140 | |||
141 | if upper_snake_case == ident { | ||
142 | // While we didn't detect the correct case at the beginning, there | ||
143 | // may be special cases: e.g. `A` is both valid CamelCase and UPPER_SNAKE_CASE. | ||
144 | None | ||
145 | } else { | ||
146 | Some(upper_snake_case) | ||
147 | } | ||
148 | } | ||
149 | |||
150 | #[cfg(test)] | ||
151 | mod tests { | ||
152 | use super::*; | ||
153 | use expect_test::{expect, Expect}; | ||
154 | |||
155 | fn check<F: Fn(&str) -> Option<String>>(fun: F, input: &str, expect: Expect) { | ||
156 | // `None` is translated to empty string, meaning that there is nothing to fix. | ||
157 | let output = fun(input).unwrap_or_default(); | ||
158 | |||
159 | expect.assert_eq(&output); | ||
160 | } | ||
161 | |||
162 | #[test] | ||
163 | fn test_to_lower_snake_case() { | ||
164 | check(to_lower_snake_case, "lower_snake_case", expect![[""]]); | ||
165 | check(to_lower_snake_case, "UPPER_SNAKE_CASE", expect![["upper_snake_case"]]); | ||
166 | check(to_lower_snake_case, "Weird_Case", expect![["weird_case"]]); | ||
167 | check(to_lower_snake_case, "CamelCase", expect![["camel_case"]]); | ||
168 | check(to_lower_snake_case, "lowerCamelCase", expect![["lower_camel_case"]]); | ||
169 | check(to_lower_snake_case, "a", expect![[""]]); | ||
170 | } | ||
171 | |||
172 | #[test] | ||
173 | fn test_to_camel_case() { | ||
174 | check(to_camel_case, "CamelCase", expect![[""]]); | ||
175 | check(to_camel_case, "CamelCase_", expect![[""]]); | ||
176 | check(to_camel_case, "_CamelCase", expect![[""]]); | ||
177 | check(to_camel_case, "lowerCamelCase", expect![["LowerCamelCase"]]); | ||
178 | check(to_camel_case, "lower_snake_case", expect![["LowerSnakeCase"]]); | ||
179 | check(to_camel_case, "UPPER_SNAKE_CASE", expect![["UpperSnakeCase"]]); | ||
180 | check(to_camel_case, "Weird_Case", expect![["WeirdCase"]]); | ||
181 | check(to_camel_case, "name", expect![["Name"]]); | ||
182 | check(to_camel_case, "A", expect![[""]]); | ||
183 | } | ||
184 | |||
185 | #[test] | ||
186 | fn test_to_upper_snake_case() { | ||
187 | check(to_upper_snake_case, "UPPER_SNAKE_CASE", expect![[""]]); | ||
188 | check(to_upper_snake_case, "lower_snake_case", expect![["LOWER_SNAKE_CASE"]]); | ||
189 | check(to_upper_snake_case, "Weird_Case", expect![["WEIRD_CASE"]]); | ||
190 | check(to_upper_snake_case, "CamelCase", expect![["CAMEL_CASE"]]); | ||
191 | check(to_upper_snake_case, "lowerCamelCase", expect![["LOWER_CAMEL_CASE"]]); | ||
192 | check(to_upper_snake_case, "A", expect![[""]]); | ||
193 | } | ||
194 | } | ||
diff --git a/crates/hir_ty/src/diagnostics/unsafe_check.rs b/crates/hir_ty/src/diagnostics/unsafe_check.rs index 61ffbf5d1..21a121aad 100644 --- a/crates/hir_ty/src/diagnostics/unsafe_check.rs +++ b/crates/hir_ty/src/diagnostics/unsafe_check.rs | |||
@@ -190,13 +190,13 @@ struct Ty { | |||
190 | a: u8, | 190 | a: u8, |
191 | } | 191 | } |
192 | 192 | ||
193 | static mut static_mut: Ty = Ty { a: 0 }; | 193 | static mut STATIC_MUT: Ty = Ty { a: 0 }; |
194 | 194 | ||
195 | fn main() { | 195 | fn main() { |
196 | let x = static_mut.a; | 196 | let x = STATIC_MUT.a; |
197 | //^^^^^^^^^^ This operation is unsafe and requires an unsafe function or block | 197 | //^^^^^^^^^^ This operation is unsafe and requires an unsafe function or block |
198 | unsafe { | 198 | unsafe { |
199 | let x = static_mut.a; | 199 | let x = STATIC_MUT.a; |
200 | } | 200 | } |
201 | } | 201 | } |
202 | "#, | 202 | "#, |
diff --git a/crates/ide/src/diagnostics.rs b/crates/ide/src/diagnostics.rs index f5d627b6e..b30cdb6ed 100644 --- a/crates/ide/src/diagnostics.rs +++ b/crates/ide/src/diagnostics.rs | |||
@@ -96,6 +96,9 @@ pub(crate) fn diagnostics( | |||
96 | .on::<hir::diagnostics::NoSuchField, _>(|d| { | 96 | .on::<hir::diagnostics::NoSuchField, _>(|d| { |
97 | res.borrow_mut().push(diagnostic_with_fix(d, &sema)); | 97 | res.borrow_mut().push(diagnostic_with_fix(d, &sema)); |
98 | }) | 98 | }) |
99 | .on::<hir::diagnostics::IncorrectCase, _>(|d| { | ||
100 | res.borrow_mut().push(warning_with_fix(d, &sema)); | ||
101 | }) | ||
99 | // Only collect experimental diagnostics when they're enabled. | 102 | // Only collect experimental diagnostics when they're enabled. |
100 | .filter(|diag| !(diag.is_experimental() && config.disable_experimental)) | 103 | .filter(|diag| !(diag.is_experimental() && config.disable_experimental)) |
101 | .filter(|diag| !config.disabled.contains(diag.code().as_str())); | 104 | .filter(|diag| !config.disabled.contains(diag.code().as_str())); |
@@ -130,6 +133,15 @@ fn diagnostic_with_fix<D: DiagnosticWithFix>(d: &D, sema: &Semantics<RootDatabas | |||
130 | } | 133 | } |
131 | } | 134 | } |
132 | 135 | ||
136 | fn warning_with_fix<D: DiagnosticWithFix>(d: &D, sema: &Semantics<RootDatabase>) -> Diagnostic { | ||
137 | Diagnostic { | ||
138 | range: sema.diagnostics_display_range(d).range, | ||
139 | message: d.message(), | ||
140 | severity: Severity::WeakWarning, | ||
141 | fix: d.fix(&sema), | ||
142 | } | ||
143 | } | ||
144 | |||
133 | fn check_unnecessary_braces_in_use_statement( | 145 | fn check_unnecessary_braces_in_use_statement( |
134 | acc: &mut Vec<Diagnostic>, | 146 | acc: &mut Vec<Diagnostic>, |
135 | file_id: FileId, | 147 | file_id: FileId, |
@@ -245,8 +257,37 @@ mod tests { | |||
245 | 257 | ||
246 | assert_eq_text!(&after, &actual); | 258 | assert_eq_text!(&after, &actual); |
247 | assert!( | 259 | assert!( |
248 | fix.fix_trigger_range.start() <= file_position.offset | 260 | fix.fix_trigger_range.contains_inclusive(file_position.offset), |
249 | && fix.fix_trigger_range.end() >= file_position.offset, | 261 | "diagnostic fix range {:?} does not touch cursor position {:?}", |
262 | fix.fix_trigger_range, | ||
263 | file_position.offset | ||
264 | ); | ||
265 | } | ||
266 | |||
267 | /// Similar to `check_fix`, but applies all the available fixes. | ||
268 | fn check_fixes(ra_fixture_before: &str, ra_fixture_after: &str) { | ||
269 | let after = trim_indent(ra_fixture_after); | ||
270 | |||
271 | let (analysis, file_position) = fixture::position(ra_fixture_before); | ||
272 | let diagnostic = analysis | ||
273 | .diagnostics(&DiagnosticsConfig::default(), file_position.file_id) | ||
274 | .unwrap() | ||
275 | .pop() | ||
276 | .unwrap(); | ||
277 | let fix = diagnostic.fix.unwrap(); | ||
278 | let target_file_contents = analysis.file_text(file_position.file_id).unwrap(); | ||
279 | let actual = { | ||
280 | let mut actual = target_file_contents.to_string(); | ||
281 | // Go from the last one to the first one, so that ranges won't be affected by previous edits. | ||
282 | for edit in fix.source_change.source_file_edits.iter().rev() { | ||
283 | edit.edit.apply(&mut actual); | ||
284 | } | ||
285 | actual | ||
286 | }; | ||
287 | |||
288 | assert_eq_text!(&after, &actual); | ||
289 | assert!( | ||
290 | fix.fix_trigger_range.contains_inclusive(file_position.offset), | ||
250 | "diagnostic fix range {:?} does not touch cursor position {:?}", | 291 | "diagnostic fix range {:?} does not touch cursor position {:?}", |
251 | fix.fix_trigger_range, | 292 | fix.fix_trigger_range, |
252 | file_position.offset | 293 | file_position.offset |
@@ -790,4 +831,100 @@ struct Foo { | |||
790 | let diagnostics = analysis.diagnostics(&DiagnosticsConfig::default(), file_id).unwrap(); | 831 | let diagnostics = analysis.diagnostics(&DiagnosticsConfig::default(), file_id).unwrap(); |
791 | assert!(!diagnostics.is_empty()); | 832 | assert!(!diagnostics.is_empty()); |
792 | } | 833 | } |
834 | |||
835 | #[test] | ||
836 | fn test_rename_incorrect_case() { | ||
837 | check_fixes( | ||
838 | r#" | ||
839 | pub struct test_struct<|> { one: i32 } | ||
840 | |||
841 | pub fn some_fn(val: test_struct) -> test_struct { | ||
842 | test_struct { one: val.one + 1 } | ||
843 | } | ||
844 | "#, | ||
845 | r#" | ||
846 | pub struct TestStruct { one: i32 } | ||
847 | |||
848 | pub fn some_fn(val: TestStruct) -> TestStruct { | ||
849 | TestStruct { one: val.one + 1 } | ||
850 | } | ||
851 | "#, | ||
852 | ); | ||
853 | |||
854 | check_fixes( | ||
855 | r#" | ||
856 | pub fn some_fn(NonSnakeCase<|>: u8) -> u8 { | ||
857 | NonSnakeCase | ||
858 | } | ||
859 | "#, | ||
860 | r#" | ||
861 | pub fn some_fn(non_snake_case: u8) -> u8 { | ||
862 | non_snake_case | ||
863 | } | ||
864 | "#, | ||
865 | ); | ||
866 | |||
867 | check_fixes( | ||
868 | r#" | ||
869 | pub fn SomeFn<|>(val: u8) -> u8 { | ||
870 | if val != 0 { SomeFn(val - 1) } else { val } | ||
871 | } | ||
872 | "#, | ||
873 | r#" | ||
874 | pub fn some_fn(val: u8) -> u8 { | ||
875 | if val != 0 { some_fn(val - 1) } else { val } | ||
876 | } | ||
877 | "#, | ||
878 | ); | ||
879 | |||
880 | check_fixes( | ||
881 | r#" | ||
882 | fn some_fn() { | ||
883 | let whatAWeird_Formatting<|> = 10; | ||
884 | another_func(whatAWeird_Formatting); | ||
885 | } | ||
886 | "#, | ||
887 | r#" | ||
888 | fn some_fn() { | ||
889 | let what_a_weird_formatting = 10; | ||
890 | another_func(what_a_weird_formatting); | ||
891 | } | ||
892 | "#, | ||
893 | ); | ||
894 | } | ||
895 | |||
896 | #[test] | ||
897 | fn test_uppercase_const_no_diagnostics() { | ||
898 | check_no_diagnostics( | ||
899 | r#" | ||
900 | fn foo() { | ||
901 | const ANOTHER_ITEM<|>: &str = "some_item"; | ||
902 | } | ||
903 | "#, | ||
904 | ); | ||
905 | } | ||
906 | |||
907 | #[test] | ||
908 | fn test_rename_incorrect_case_struct_method() { | ||
909 | check_fixes( | ||
910 | r#" | ||
911 | pub struct TestStruct; | ||
912 | |||
913 | impl TestStruct { | ||
914 | pub fn SomeFn<|>() -> TestStruct { | ||
915 | TestStruct | ||
916 | } | ||
917 | } | ||
918 | "#, | ||
919 | r#" | ||
920 | pub struct TestStruct; | ||
921 | |||
922 | impl TestStruct { | ||
923 | pub fn some_fn() -> TestStruct { | ||
924 | TestStruct | ||
925 | } | ||
926 | } | ||
927 | "#, | ||
928 | ); | ||
929 | } | ||
793 | } | 930 | } |
diff --git a/crates/ide/src/diagnostics/fixes.rs b/crates/ide/src/diagnostics/fixes.rs index 68ae1c239..0c75e50b0 100644 --- a/crates/ide/src/diagnostics/fixes.rs +++ b/crates/ide/src/diagnostics/fixes.rs | |||
@@ -3,7 +3,10 @@ | |||
3 | use base_db::FileId; | 3 | use base_db::FileId; |
4 | use hir::{ | 4 | use hir::{ |
5 | db::AstDatabase, | 5 | db::AstDatabase, |
6 | diagnostics::{Diagnostic, MissingFields, MissingOkInTailExpr, NoSuchField, UnresolvedModule}, | 6 | diagnostics::{ |
7 | Diagnostic, IncorrectCase, MissingFields, MissingOkInTailExpr, NoSuchField, | ||
8 | UnresolvedModule, | ||
9 | }, | ||
7 | HasSource, HirDisplay, Semantics, VariantDef, | 10 | HasSource, HirDisplay, Semantics, VariantDef, |
8 | }; | 11 | }; |
9 | use ide_db::{ | 12 | use ide_db::{ |
@@ -17,7 +20,7 @@ use syntax::{ | |||
17 | }; | 20 | }; |
18 | use text_edit::TextEdit; | 21 | use text_edit::TextEdit; |
19 | 22 | ||
20 | use crate::diagnostics::Fix; | 23 | use crate::{diagnostics::Fix, references::rename::rename_with_semantics, FilePosition}; |
21 | 24 | ||
22 | /// A [Diagnostic] that potentially has a fix available. | 25 | /// A [Diagnostic] that potentially has a fix available. |
23 | /// | 26 | /// |
@@ -99,6 +102,23 @@ impl DiagnosticWithFix for MissingOkInTailExpr { | |||
99 | } | 102 | } |
100 | } | 103 | } |
101 | 104 | ||
105 | impl DiagnosticWithFix for IncorrectCase { | ||
106 | fn fix(&self, sema: &Semantics<RootDatabase>) -> Option<Fix> { | ||
107 | let root = sema.db.parse_or_expand(self.file)?; | ||
108 | let name_node = self.ident.to_node(&root); | ||
109 | |||
110 | let file_id = self.file.original_file(sema.db); | ||
111 | let offset = name_node.syntax().text_range().start(); | ||
112 | let file_position = FilePosition { file_id, offset }; | ||
113 | |||
114 | let rename_changes = | ||
115 | rename_with_semantics(sema, file_position, &self.suggested_text).ok()?; | ||
116 | |||
117 | let label = format!("Rename to {}", self.suggested_text); | ||
118 | Some(Fix::new(&label, rename_changes.info, rename_changes.range)) | ||
119 | } | ||
120 | } | ||
121 | |||
102 | fn missing_record_expr_field_fix( | 122 | fn missing_record_expr_field_fix( |
103 | sema: &Semantics<RootDatabase>, | 123 | sema: &Semantics<RootDatabase>, |
104 | usage_file_id: FileId, | 124 | usage_file_id: FileId, |
diff --git a/crates/ide/src/references.rs b/crates/ide/src/references.rs index f65a05ea3..88e2f2db3 100644 --- a/crates/ide/src/references.rs +++ b/crates/ide/src/references.rs | |||
@@ -9,7 +9,7 @@ | |||
9 | //! at the index that the match starts at and its tree parent is | 9 | //! at the index that the match starts at and its tree parent is |
10 | //! resolved to the search element definition, we get a reference. | 10 | //! resolved to the search element definition, we get a reference. |
11 | 11 | ||
12 | mod rename; | 12 | pub(crate) mod rename; |
13 | 13 | ||
14 | use hir::Semantics; | 14 | use hir::Semantics; |
15 | use ide_db::{ | 15 | use ide_db::{ |
diff --git a/crates/ide/src/references/rename.rs b/crates/ide/src/references/rename.rs index f3b5cfc8c..f9a11e43d 100644 --- a/crates/ide/src/references/rename.rs +++ b/crates/ide/src/references/rename.rs | |||
@@ -42,7 +42,14 @@ pub(crate) fn rename( | |||
42 | new_name: &str, | 42 | new_name: &str, |
43 | ) -> Result<RangeInfo<SourceChange>, RenameError> { | 43 | ) -> Result<RangeInfo<SourceChange>, RenameError> { |
44 | let sema = Semantics::new(db); | 44 | let sema = Semantics::new(db); |
45 | rename_with_semantics(&sema, position, new_name) | ||
46 | } | ||
45 | 47 | ||
48 | pub(crate) fn rename_with_semantics( | ||
49 | sema: &Semantics<RootDatabase>, | ||
50 | position: FilePosition, | ||
51 | new_name: &str, | ||
52 | ) -> Result<RangeInfo<SourceChange>, RenameError> { | ||
46 | match lex_single_syntax_kind(new_name) { | 53 | match lex_single_syntax_kind(new_name) { |
47 | Some(res) => match res { | 54 | Some(res) => match res { |
48 | (SyntaxKind::IDENT, _) => (), | 55 | (SyntaxKind::IDENT, _) => (), |
diff --git a/crates/stdx/src/lib.rs b/crates/stdx/src/lib.rs index 011935cad..59d89f47d 100644 --- a/crates/stdx/src/lib.rs +++ b/crates/stdx/src/lib.rs | |||
@@ -28,20 +28,32 @@ pub fn timeit(label: &'static str) -> impl Drop { | |||
28 | Guard { label, start: Instant::now() } | 28 | Guard { label, start: Instant::now() } |
29 | } | 29 | } |
30 | 30 | ||
31 | pub fn to_lower_snake_case(s: &str) -> String { | 31 | fn to_snake_case<F: Fn(&char) -> char>(s: &str, change_case: F) -> String { |
32 | let mut buf = String::with_capacity(s.len()); | 32 | let mut buf = String::with_capacity(s.len()); |
33 | let mut prev = false; | 33 | let mut prev = false; |
34 | for c in s.chars() { | 34 | for c in s.chars() { |
35 | // `&& prev` is required to not insert `_` before the first symbol. | ||
35 | if c.is_ascii_uppercase() && prev { | 36 | if c.is_ascii_uppercase() && prev { |
36 | buf.push('_') | 37 | // This check is required to not translate `Weird_Case` into `weird__case`. |
38 | if !buf.ends_with('_') { | ||
39 | buf.push('_') | ||
40 | } | ||
37 | } | 41 | } |
38 | prev = true; | 42 | prev = true; |
39 | 43 | ||
40 | buf.push(c.to_ascii_lowercase()); | 44 | buf.push(change_case(&c)); |
41 | } | 45 | } |
42 | buf | 46 | buf |
43 | } | 47 | } |
44 | 48 | ||
49 | pub fn to_lower_snake_case(s: &str) -> String { | ||
50 | to_snake_case(s, char::to_ascii_lowercase) | ||
51 | } | ||
52 | |||
53 | pub fn to_upper_snake_case(s: &str) -> String { | ||
54 | to_snake_case(s, char::to_ascii_uppercase) | ||
55 | } | ||
56 | |||
45 | pub fn replace(buf: &mut String, from: char, to: &str) { | 57 | pub fn replace(buf: &mut String, from: char, to: &str) { |
46 | if !buf.contains(from) { | 58 | if !buf.contains(from) { |
47 | return; | 59 | return; |
diff --git a/crates/syntax/src/ast/make.rs b/crates/syntax/src/ast/make.rs index 3a184094c..74dbdfaf7 100644 --- a/crates/syntax/src/ast/make.rs +++ b/crates/syntax/src/ast/make.rs | |||
@@ -320,6 +320,10 @@ pub fn param(name: String, ty: String) -> ast::Param { | |||
320 | ast_from_text(&format!("fn f({}: {}) {{ }}", name, ty)) | 320 | ast_from_text(&format!("fn f({}: {}) {{ }}", name, ty)) |
321 | } | 321 | } |
322 | 322 | ||
323 | pub fn ret_type(ty: ast::Type) -> ast::RetType { | ||
324 | ast_from_text(&format!("fn f() -> {} {{ }}", ty)) | ||
325 | } | ||
326 | |||
323 | pub fn param_list(pats: impl IntoIterator<Item = ast::Param>) -> ast::ParamList { | 327 | pub fn param_list(pats: impl IntoIterator<Item = ast::Param>) -> ast::ParamList { |
324 | let args = pats.into_iter().join(", "); | 328 | let args = pats.into_iter().join(", "); |
325 | ast_from_text(&format!("fn f({}) {{ }}", args)) | 329 | ast_from_text(&format!("fn f({}) {{ }}", args)) |
@@ -350,14 +354,20 @@ pub fn fn_( | |||
350 | type_params: Option<ast::GenericParamList>, | 354 | type_params: Option<ast::GenericParamList>, |
351 | params: ast::ParamList, | 355 | params: ast::ParamList, |
352 | body: ast::BlockExpr, | 356 | body: ast::BlockExpr, |
357 | ret_type: Option<ast::RetType>, | ||
353 | ) -> ast::Fn { | 358 | ) -> ast::Fn { |
354 | let type_params = | 359 | let type_params = |
355 | if let Some(type_params) = type_params { format!("<{}>", type_params) } else { "".into() }; | 360 | if let Some(type_params) = type_params { format!("<{}>", type_params) } else { "".into() }; |
361 | let ret_type = if let Some(ret_type) = ret_type { format!("{} ", ret_type) } else { "".into() }; | ||
356 | let visibility = match visibility { | 362 | let visibility = match visibility { |
357 | None => String::new(), | 363 | None => String::new(), |
358 | Some(it) => format!("{} ", it), | 364 | Some(it) => format!("{} ", it), |
359 | }; | 365 | }; |
360 | ast_from_text(&format!("{}fn {}{}{} {}", visibility, fn_name, type_params, params, body)) | 366 | |
367 | ast_from_text(&format!( | ||
368 | "{}fn {}{}{} {}{}", | ||
369 | visibility, fn_name, type_params, params, ret_type, body | ||
370 | )) | ||
361 | } | 371 | } |
362 | 372 | ||
363 | fn ast_from_text<N: AstNode>(text: &str) -> N { | 373 | fn ast_from_text<N: AstNode>(text: &str) -> N { |