aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--crates/assists/src/handlers/generate_function.rs132
-rw-r--r--crates/assists/src/tests/generated.rs4
-rw-r--r--crates/hir/src/code_model.rs24
-rw-r--r--crates/hir/src/diagnostics.rs3
-rw-r--r--crates/hir_def/src/item_scope.rs6
-rw-r--r--crates/hir_ty/src/diagnostics.rs85
-rw-r--r--crates/hir_ty/src/diagnostics/decl_check.rs833
-rw-r--r--crates/hir_ty/src/diagnostics/decl_check/case_conv.rs194
-rw-r--r--crates/hir_ty/src/diagnostics/unsafe_check.rs6
-rw-r--r--crates/ide/src/diagnostics.rs141
-rw-r--r--crates/ide/src/diagnostics/fixes.rs24
-rw-r--r--crates/ide/src/references.rs2
-rw-r--r--crates/ide/src/references/rename.rs7
-rw-r--r--crates/stdx/src/lib.rs18
-rw-r--r--crates/syntax/src/ast/make.rs12
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
81struct FunctionTemplate { 81struct 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 {
90impl FunctionTemplate { 90impl 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
352fn bar() { 354fn 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
379fn bar() { 381fn 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
403fn bar() { 405fn bar() ${0:-> ()} {
404 ${0:todo!()} 406 todo!()
405} 407}
406 408
407fn foo2() {} 409fn 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
455fn bar(baz: Baz) { 457fn 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
488fn bar(baz: Baz) { 490fn 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
509fn bar(arg: &str) { 511fn 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
530fn bar(arg: char) { 532fn 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
551fn bar(arg: i32) { 553fn 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
572fn bar(arg: u8) { 574fn 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
597fn bar(x: u8) { 599fn 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
620fn bar(worble: ()) { 622fn 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
649fn bar(foo: impl Foo) { 651fn 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
676fn bar(baz: &Baz) { 678fn 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
705fn bar(baz: Baz::Bof) { 707fn 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
728fn bar<T>(t: T) { 730fn 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
759fn bar(arg: fn() -> Baz) { 761fn 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
784fn bar(closure: impl Fn(i64) -> i64) { 786fn 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
805fn bar(baz: ()) { 807fn 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
830fn bar(baz_1: Baz, baz_2: Baz) { 832fn 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
855fn bar(baz_1: Baz, baz_2: Baz, arg_1: &str, arg_2: &str) { 857fn 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"
874mod bar { 876mod 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
914fn baz(foo: foo::Foo) { 916fn baz(foo: foo::Foo) ${0:-> ()} {
915 ${0:todo!()} 917 todo!()
916} 918}
917", 919",
918 ) 920 )
@@ -935,8 +937,8 @@ fn foo() {
935mod bar { 937mod 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"
964mod bar { 966mod 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
995pub(crate) fn bar() { 997pub(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
457fn bar(arg: &str, baz: Baz) { 457fn 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
260pub use hir_def::{ 279pub 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 @@
2pub use hir_def::diagnostics::UnresolvedModule; 2pub use hir_def::diagnostics::UnresolvedModule;
3pub use hir_expand::diagnostics::{Diagnostic, DiagnosticSink, DiagnosticSinkBuilder}; 3pub use hir_expand::diagnostics::{Diagnostic, DiagnosticSink, DiagnosticSinkBuilder};
4pub use hir_ty::diagnostics::{ 4pub 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 @@
2mod expr; 2mod expr;
3mod match_check; 3mod match_check;
4mod unsafe_check; 4mod unsafe_check;
5mod decl_check;
5 6
6use std::any::Any; 7use std::{any::Any, fmt};
7 8
8use hir_def::DefWithBodyId; 9use hir_def::{DefWithBodyId, ModuleDefId};
9use hir_expand::diagnostics::{Diagnostic, DiagnosticCode, DiagnosticSink}; 10use hir_expand::diagnostics::{Diagnostic, DiagnosticCode, DiagnosticSink};
10use hir_expand::{name::Name, HirFileId, InFile}; 11use hir_expand::{name::Name, HirFileId, InFile};
11use stdx::format_to; 12use stdx::format_to;
@@ -15,6 +16,16 @@ use crate::db::HirDatabase;
15 16
16pub use crate::diagnostics::expr::{record_literal_missing_fields, record_pattern_missing_fields}; 17pub use crate::diagnostics::expr::{record_literal_missing_fields, record_pattern_missing_fields};
17 18
19pub 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
18pub fn validate_body(db: &dyn HirDatabase, owner: DefWithBodyId, sink: &mut DiagnosticSink<'_>) { 29pub 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)]
246pub enum CaseType {
247 // `some_var`
248 LowerSnakeCase,
249 // `SOME_CONST`
250 UpperSnakeCase,
251 // `SomeStruct`
252 UpperCamelCase,
253}
254
255impl 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)]
268pub 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
277impl 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)]
235mod tests { 306mod 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
13mod case_conv;
14
15use hir_def::{
16 adt::VariantData,
17 expr::{Pat, PatId},
18 src::HasSource,
19 AdtId, ConstId, EnumId, FunctionId, Lookup, ModuleDefId, StaticId, StructId,
20};
21use hir_expand::{
22 diagnostics::DiagnosticSink,
23 name::{AsName, Name},
24};
25use syntax::{
26 ast::{self, NameOwner},
27 AstNode, AstPtr,
28};
29
30use crate::{
31 db::HirDatabase,
32 diagnostics::{decl_check::case_conv::*, CaseType, IncorrectCase},
33};
34
35pub(super) struct DeclValidator<'a, 'b: 'a> {
36 owner: ModuleDefId,
37 sink: &'a mut DiagnosticSink<'b>,
38}
39
40#[derive(Debug)]
41struct Replacement {
42 current_name: Name,
43 suggested_text: String,
44 expected_case: CaseType,
45}
46
47impl<'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(), &param_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
653fn 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
661fn 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)]
670mod tests {
671 use crate::diagnostics::tests::check_diagnostics;
672
673 #[test]
674 fn incorrect_function_name() {
675 check_diagnostics(
676 r#"
677fn 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#"
687fn foo(SomeParam: u8) {}
688 // ^^^^^^^^^ Argument `SomeParam` should have snake_case name, e.g. `some_param`
689
690fn 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#"
700fn 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#"
714struct 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#"
724struct 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#"
734enum 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#"
744enum 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#"
754const some_weird_const: u8 = 10;
755 // ^^^^^^^^^^^^^^^^ Constant `some_weird_const` should have UPPER_SNAKE_CASE name, e.g. `SOME_WEIRD_CONST`
756
757fn 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#"
770static some_weird_const: u8 = 10;
771 // ^^^^^^^^^^^^^^^^ Static variable `some_weird_const` should have UPPER_SNAKE_CASE name, e.g. `SOME_WEIRD_CONST`
772
773fn 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#"
785struct someStruct;
786 // ^^^^^^^^^^ Structure `someStruct` should have CamelCase name, e.g. `SomeStruct`
787
788impl 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#"
805enum Option { Some, None }
806
807fn 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#"
821enum Option { Some, None }
822
823fn 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)]
5enum DetectedCase {
6 LowerCamelCase,
7 UpperCamelCase,
8 LowerSnakeCase,
9 UpperSnakeCase,
10 Unknown,
11}
12
13fn 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.
51pub 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.
105pub 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.
129pub 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)]
151mod 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
193static mut static_mut: Ty = Ty { a: 0 }; 193static mut STATIC_MUT: Ty = Ty { a: 0 };
194 194
195fn main() { 195fn 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
136fn 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
133fn check_unnecessary_braces_in_use_statement( 145fn 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#"
839pub struct test_struct<|> { one: i32 }
840
841pub fn some_fn(val: test_struct) -> test_struct {
842 test_struct { one: val.one + 1 }
843}
844"#,
845 r#"
846pub struct TestStruct { one: i32 }
847
848pub fn some_fn(val: TestStruct) -> TestStruct {
849 TestStruct { one: val.one + 1 }
850}
851"#,
852 );
853
854 check_fixes(
855 r#"
856pub fn some_fn(NonSnakeCase<|>: u8) -> u8 {
857 NonSnakeCase
858}
859"#,
860 r#"
861pub fn some_fn(non_snake_case: u8) -> u8 {
862 non_snake_case
863}
864"#,
865 );
866
867 check_fixes(
868 r#"
869pub fn SomeFn<|>(val: u8) -> u8 {
870 if val != 0 { SomeFn(val - 1) } else { val }
871}
872"#,
873 r#"
874pub 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#"
882fn some_fn() {
883 let whatAWeird_Formatting<|> = 10;
884 another_func(whatAWeird_Formatting);
885}
886"#,
887 r#"
888fn 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#"
900fn 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#"
911pub struct TestStruct;
912
913impl TestStruct {
914 pub fn SomeFn<|>() -> TestStruct {
915 TestStruct
916 }
917}
918"#,
919 r#"
920pub struct TestStruct;
921
922impl 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 @@
3use base_db::FileId; 3use base_db::FileId;
4use hir::{ 4use 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};
9use ide_db::{ 12use ide_db::{
@@ -17,7 +20,7 @@ use syntax::{
17}; 20};
18use text_edit::TextEdit; 21use text_edit::TextEdit;
19 22
20use crate::diagnostics::Fix; 23use 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
105impl 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
102fn missing_record_expr_field_fix( 122fn 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
12mod rename; 12pub(crate) mod rename;
13 13
14use hir::Semantics; 14use hir::Semantics;
15use ide_db::{ 15use 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
48pub(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
31pub fn to_lower_snake_case(s: &str) -> String { 31fn 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
49pub fn to_lower_snake_case(s: &str) -> String {
50 to_snake_case(s, char::to_ascii_lowercase)
51}
52
53pub fn to_upper_snake_case(s: &str) -> String {
54 to_snake_case(s, char::to_ascii_uppercase)
55}
56
45pub fn replace(buf: &mut String, from: char, to: &str) { 57pub 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
323pub fn ret_type(ty: ast::Type) -> ast::RetType {
324 ast_from_text(&format!("fn f() -> {} {{ }}", ty))
325}
326
323pub fn param_list(pats: impl IntoIterator<Item = ast::Param>) -> ast::ParamList { 327pub 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
363fn ast_from_text<N: AstNode>(text: &str) -> N { 373fn ast_from_text<N: AstNode>(text: &str) -> N {