]> git.lizzy.rs Git - rust.git/blob - crates/hir_ty/src/diagnostics/decl_check.rs
Move incorrect case diagnostic things into their module
[rust.git] / crates / hir_ty / src / diagnostics / decl_check.rs
1 //! Provides validators for names of 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 std::fmt;
16
17 use base_db::CrateId;
18 use hir_def::{
19     adt::VariantData,
20     expr::{Pat, PatId},
21     src::HasSource,
22     AdtId, AttrDefId, ConstId, EnumId, FunctionId, Lookup, ModuleDefId, StaticId, StructId,
23 };
24 use hir_expand::{
25     name::{AsName, Name},
26     HirFileId,
27 };
28 use stdx::{always, never};
29 use syntax::{
30     ast::{self, HasName},
31     AstNode, AstPtr,
32 };
33
34 use crate::db::HirDatabase;
35
36 use self::case_conv::{to_camel_case, to_lower_snake_case, to_upper_snake_case};
37
38 mod allow {
39     pub(super) const BAD_STYLE: &str = "bad_style";
40     pub(super) const NONSTANDARD_STYLE: &str = "nonstandard_style";
41     pub(super) const NON_SNAKE_CASE: &str = "non_snake_case";
42     pub(super) const NON_UPPER_CASE_GLOBAL: &str = "non_upper_case_globals";
43     pub(super) const NON_CAMEL_CASE_TYPES: &str = "non_camel_case_types";
44 }
45
46 pub fn incorrect_case(
47     db: &dyn HirDatabase,
48     krate: CrateId,
49     owner: ModuleDefId,
50 ) -> Vec<IncorrectCase> {
51     let _p = profile::span("validate_module_item");
52     let mut validator = DeclValidator::new(db, krate);
53     validator.validate_item(owner);
54     validator.sink
55 }
56
57 #[derive(Debug)]
58 pub enum CaseType {
59     // `some_var`
60     LowerSnakeCase,
61     // `SOME_CONST`
62     UpperSnakeCase,
63     // `SomeStruct`
64     UpperCamelCase,
65 }
66
67 impl fmt::Display for CaseType {
68     fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
69         let repr = match self {
70             CaseType::LowerSnakeCase => "snake_case",
71             CaseType::UpperSnakeCase => "UPPER_SNAKE_CASE",
72             CaseType::UpperCamelCase => "CamelCase",
73         };
74
75         write!(f, "{}", repr)
76     }
77 }
78
79 #[derive(Debug)]
80 pub enum IdentType {
81     Constant,
82     Enum,
83     Field,
84     Function,
85     Parameter,
86     StaticVariable,
87     Structure,
88     Variable,
89     Variant,
90 }
91
92 impl fmt::Display for IdentType {
93     fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
94         let repr = match self {
95             IdentType::Constant => "Constant",
96             IdentType::Enum => "Enum",
97             IdentType::Field => "Field",
98             IdentType::Function => "Function",
99             IdentType::Parameter => "Parameter",
100             IdentType::StaticVariable => "Static variable",
101             IdentType::Structure => "Structure",
102             IdentType::Variable => "Variable",
103             IdentType::Variant => "Variant",
104         };
105
106         write!(f, "{}", repr)
107     }
108 }
109
110 #[derive(Debug)]
111 pub struct IncorrectCase {
112     pub file: HirFileId,
113     pub ident: AstPtr<ast::Name>,
114     pub expected_case: CaseType,
115     pub ident_type: IdentType,
116     pub ident_text: String,
117     pub suggested_text: String,
118 }
119
120 pub(super) struct DeclValidator<'a> {
121     db: &'a dyn HirDatabase,
122     krate: CrateId,
123     pub(super) sink: Vec<IncorrectCase>,
124 }
125
126 #[derive(Debug)]
127 struct Replacement {
128     current_name: Name,
129     suggested_text: String,
130     expected_case: CaseType,
131 }
132
133 impl<'a> DeclValidator<'a> {
134     pub(super) fn new(db: &'a dyn HirDatabase, krate: CrateId) -> DeclValidator<'a> {
135         DeclValidator { db, krate, sink: Vec::new() }
136     }
137
138     pub(super) fn validate_item(&mut self, item: ModuleDefId) {
139         match item {
140             ModuleDefId::FunctionId(func) => self.validate_func(func),
141             ModuleDefId::AdtId(adt) => self.validate_adt(adt),
142             ModuleDefId::ConstId(const_id) => self.validate_const(const_id),
143             ModuleDefId::StaticId(static_id) => self.validate_static(static_id),
144             _ => (),
145         }
146     }
147
148     fn validate_adt(&mut self, adt: AdtId) {
149         match adt {
150             AdtId::StructId(struct_id) => self.validate_struct(struct_id),
151             AdtId::EnumId(enum_id) => self.validate_enum(enum_id),
152             AdtId::UnionId(_) => {
153                 // FIXME: Unions aren't yet supported by this validator.
154             }
155         }
156     }
157
158     /// Checks whether not following the convention is allowed for this item.
159     fn allowed(&self, id: AttrDefId, allow_name: &str, recursing: bool) -> bool {
160         let is_allowed = |def_id| {
161             let attrs = self.db.attrs(def_id);
162             // don't bug the user about directly no_mangle annotated stuff, they can't do anything about it
163             (!recursing && attrs.by_key("no_mangle").exists())
164                 || attrs.by_key("allow").tt_values().any(|tt| {
165                     let allows = tt.to_string();
166                     allows.contains(allow_name)
167                         || allows.contains(allow::BAD_STYLE)
168                         || allows.contains(allow::NONSTANDARD_STYLE)
169                 })
170         };
171
172         is_allowed(id)
173             // go upwards one step or give up
174             || match id {
175                 AttrDefId::ModuleId(m) => m.containing_module(self.db.upcast()).map(|v| v.into()),
176                 AttrDefId::FunctionId(f) => Some(f.lookup(self.db.upcast()).container.into()),
177                 AttrDefId::StaticId(sid) => Some(sid.lookup(self.db.upcast()).container.into()),
178                 AttrDefId::ConstId(cid) => Some(cid.lookup(self.db.upcast()).container.into()),
179                 AttrDefId::TraitId(tid) => Some(tid.lookup(self.db.upcast()).container.into()),
180                 AttrDefId::ImplId(iid) => Some(iid.lookup(self.db.upcast()).container.into()),
181                 // These warnings should not explore macro definitions at all
182                 AttrDefId::MacroDefId(_) => None,
183                 // Will never occur under an enum/struct/union/type alias
184                 AttrDefId::AdtId(_) => None,
185                 AttrDefId::FieldId(_) => None,
186                 AttrDefId::EnumVariantId(_) => None,
187                 AttrDefId::TypeAliasId(_) => None,
188                 AttrDefId::GenericParamId(_) => None,
189             }
190             .map(|mid| self.allowed(mid, allow_name, true))
191             .unwrap_or(false)
192     }
193
194     fn validate_func(&mut self, func: FunctionId) {
195         let data = self.db.function_data(func);
196         if data.is_in_extern_block() {
197             cov_mark::hit!(extern_func_incorrect_case_ignored);
198             return;
199         }
200
201         let body = self.db.body(func.into());
202
203         // Recursively validate inner scope items, such as static variables and constants.
204         for (_, block_def_map) in body.blocks(self.db.upcast()) {
205             for (_, module) in block_def_map.modules() {
206                 for def_id in module.scope.declarations() {
207                     let mut validator = DeclValidator::new(self.db, self.krate);
208                     validator.validate_item(def_id);
209                 }
210             }
211         }
212
213         // Check whether non-snake case identifiers are allowed for this function.
214         if self.allowed(func.into(), allow::NON_SNAKE_CASE, false) {
215             return;
216         }
217
218         // Check the function name.
219         let function_name = data.name.to_string();
220         let fn_name_replacement = to_lower_snake_case(&function_name).map(|new_name| Replacement {
221             current_name: data.name.clone(),
222             suggested_text: new_name,
223             expected_case: CaseType::LowerSnakeCase,
224         });
225
226         // Check the patterns inside the function body.
227         // This includes function parameters.
228         let pats_replacements = body
229             .pats
230             .iter()
231             .filter_map(|(id, pat)| match pat {
232                 Pat::Bind { name, .. } => Some((id, name)),
233                 _ => None,
234             })
235             .filter_map(|(id, bind_name)| {
236                 Some((
237                     id,
238                     Replacement {
239                         current_name: bind_name.clone(),
240                         suggested_text: to_lower_snake_case(&bind_name.to_string())?,
241                         expected_case: CaseType::LowerSnakeCase,
242                     },
243                 ))
244             })
245             .collect();
246
247         // If there is at least one element to spawn a warning on, go to the source map and generate a warning.
248         if let Some(fn_name_replacement) = fn_name_replacement {
249             self.create_incorrect_case_diagnostic_for_func(func, fn_name_replacement);
250         }
251
252         self.create_incorrect_case_diagnostic_for_variables(func, pats_replacements);
253     }
254
255     /// Given the information about incorrect names in the function declaration, looks up into the source code
256     /// for exact locations and adds diagnostics into the sink.
257     fn create_incorrect_case_diagnostic_for_func(
258         &mut self,
259         func: FunctionId,
260         fn_name_replacement: Replacement,
261     ) {
262         let fn_loc = func.lookup(self.db.upcast());
263         let fn_src = fn_loc.source(self.db.upcast());
264
265         // Diagnostic for function name.
266         let ast_ptr = match fn_src.value.name() {
267             Some(name) => name,
268             None => {
269                 never!(
270                     "Replacement ({:?}) was generated for a function without a name: {:?}",
271                     fn_name_replacement,
272                     fn_src
273                 );
274                 return;
275             }
276         };
277
278         let diagnostic = IncorrectCase {
279             file: fn_src.file_id,
280             ident_type: IdentType::Function,
281             ident: AstPtr::new(&ast_ptr),
282             expected_case: fn_name_replacement.expected_case,
283             ident_text: fn_name_replacement.current_name.to_string(),
284             suggested_text: fn_name_replacement.suggested_text,
285         };
286
287         self.sink.push(diagnostic);
288     }
289
290     /// Given the information about incorrect variable names, looks up into the source code
291     /// for exact locations and adds diagnostics into the sink.
292     fn create_incorrect_case_diagnostic_for_variables(
293         &mut self,
294         func: FunctionId,
295         pats_replacements: Vec<(PatId, Replacement)>,
296     ) {
297         // XXX: only look at source_map if we do have missing fields
298         if pats_replacements.is_empty() {
299             return;
300         }
301
302         let (_, source_map) = self.db.body_with_source_map(func.into());
303
304         for (id, replacement) in pats_replacements {
305             if let Ok(source_ptr) = source_map.pat_syntax(id) {
306                 if let Some(expr) = source_ptr.value.as_ref().left() {
307                     let root = source_ptr.file_syntax(self.db.upcast());
308                     if let ast::Pat::IdentPat(ident_pat) = expr.to_node(&root) {
309                         let parent = match ident_pat.syntax().parent() {
310                             Some(parent) => parent,
311                             None => continue,
312                         };
313                         let name_ast = match ident_pat.name() {
314                             Some(name_ast) => name_ast,
315                             None => continue,
316                         };
317
318                         let is_param = ast::Param::can_cast(parent.kind());
319
320                         // We have to check that it's either `let var = ...` or `var @ Variant(_)` statement,
321                         // because e.g. match arms are patterns as well.
322                         // In other words, we check that it's a named variable binding.
323                         let is_binding = ast::LetStmt::can_cast(parent.kind())
324                             || (ast::MatchArm::can_cast(parent.kind())
325                                 && ident_pat.at_token().is_some());
326                         if !(is_param || is_binding) {
327                             // This pattern is not an actual variable declaration, e.g. `Some(val) => {..}` match arm.
328                             continue;
329                         }
330
331                         let ident_type =
332                             if is_param { IdentType::Parameter } else { IdentType::Variable };
333
334                         let diagnostic = IncorrectCase {
335                             file: source_ptr.file_id,
336                             ident_type,
337                             ident: AstPtr::new(&name_ast),
338                             expected_case: replacement.expected_case,
339                             ident_text: replacement.current_name.to_string(),
340                             suggested_text: replacement.suggested_text,
341                         };
342
343                         self.sink.push(diagnostic);
344                     }
345                 }
346             }
347         }
348     }
349
350     fn validate_struct(&mut self, struct_id: StructId) {
351         let data = self.db.struct_data(struct_id);
352
353         let non_camel_case_allowed =
354             self.allowed(struct_id.into(), allow::NON_CAMEL_CASE_TYPES, false);
355         let non_snake_case_allowed = self.allowed(struct_id.into(), allow::NON_SNAKE_CASE, false);
356
357         // Check the structure name.
358         let struct_name = data.name.to_string();
359         let struct_name_replacement = if !non_camel_case_allowed {
360             to_camel_case(&struct_name).map(|new_name| Replacement {
361                 current_name: data.name.clone(),
362                 suggested_text: new_name,
363                 expected_case: CaseType::UpperCamelCase,
364             })
365         } else {
366             None
367         };
368
369         // Check the field names.
370         let mut struct_fields_replacements = Vec::new();
371
372         if !non_snake_case_allowed {
373             if let VariantData::Record(fields) = data.variant_data.as_ref() {
374                 for (_, field) in fields.iter() {
375                     let field_name = field.name.to_string();
376                     if let Some(new_name) = to_lower_snake_case(&field_name) {
377                         let replacement = Replacement {
378                             current_name: field.name.clone(),
379                             suggested_text: new_name,
380                             expected_case: CaseType::LowerSnakeCase,
381                         };
382                         struct_fields_replacements.push(replacement);
383                     }
384                 }
385             }
386         }
387
388         // If there is at least one element to spawn a warning on, go to the source map and generate a warning.
389         self.create_incorrect_case_diagnostic_for_struct(
390             struct_id,
391             struct_name_replacement,
392             struct_fields_replacements,
393         );
394     }
395
396     /// Given the information about incorrect names in the struct declaration, looks up into the source code
397     /// for exact locations and adds diagnostics into the sink.
398     fn create_incorrect_case_diagnostic_for_struct(
399         &mut self,
400         struct_id: StructId,
401         struct_name_replacement: Option<Replacement>,
402         struct_fields_replacements: Vec<Replacement>,
403     ) {
404         // XXX: Only look at sources if we do have incorrect names.
405         if struct_name_replacement.is_none() && struct_fields_replacements.is_empty() {
406             return;
407         }
408
409         let struct_loc = struct_id.lookup(self.db.upcast());
410         let struct_src = struct_loc.source(self.db.upcast());
411
412         if let Some(replacement) = struct_name_replacement {
413             let ast_ptr = match struct_src.value.name() {
414                 Some(name) => name,
415                 None => {
416                     never!(
417                         "Replacement ({:?}) was generated for a structure without a name: {:?}",
418                         replacement,
419                         struct_src
420                     );
421                     return;
422                 }
423             };
424
425             let diagnostic = IncorrectCase {
426                 file: struct_src.file_id,
427                 ident_type: IdentType::Structure,
428                 ident: AstPtr::new(&ast_ptr),
429                 expected_case: replacement.expected_case,
430                 ident_text: replacement.current_name.to_string(),
431                 suggested_text: replacement.suggested_text,
432             };
433
434             self.sink.push(diagnostic);
435         }
436
437         let struct_fields_list = match struct_src.value.field_list() {
438             Some(ast::FieldList::RecordFieldList(fields)) => fields,
439             _ => {
440                 always!(
441                     struct_fields_replacements.is_empty(),
442                     "Replacements ({:?}) were generated for a structure fields which had no fields list: {:?}",
443                     struct_fields_replacements,
444                     struct_src
445                 );
446                 return;
447             }
448         };
449         let mut struct_fields_iter = struct_fields_list.fields();
450         for field_to_rename in struct_fields_replacements {
451             // We assume that parameters in replacement are in the same order as in the
452             // actual params list, but just some of them (ones that named correctly) are skipped.
453             let ast_ptr = loop {
454                 match struct_fields_iter.next().and_then(|field| field.name()) {
455                     Some(field_name) => {
456                         if field_name.as_name() == field_to_rename.current_name {
457                             break field_name;
458                         }
459                     }
460                     None => {
461                         never!(
462                             "Replacement ({:?}) was generated for a structure field which was not found: {:?}",
463                             field_to_rename, struct_src
464                         );
465                         return;
466                     }
467                 }
468             };
469
470             let diagnostic = IncorrectCase {
471                 file: struct_src.file_id,
472                 ident_type: IdentType::Field,
473                 ident: AstPtr::new(&ast_ptr),
474                 expected_case: field_to_rename.expected_case,
475                 ident_text: field_to_rename.current_name.to_string(),
476                 suggested_text: field_to_rename.suggested_text,
477             };
478
479             self.sink.push(diagnostic);
480         }
481     }
482
483     fn validate_enum(&mut self, enum_id: EnumId) {
484         let data = self.db.enum_data(enum_id);
485
486         // Check whether non-camel case names are allowed for this enum.
487         if self.allowed(enum_id.into(), allow::NON_CAMEL_CASE_TYPES, false) {
488             return;
489         }
490
491         // Check the enum name.
492         let enum_name = data.name.to_string();
493         let enum_name_replacement = to_camel_case(&enum_name).map(|new_name| Replacement {
494             current_name: data.name.clone(),
495             suggested_text: new_name,
496             expected_case: CaseType::UpperCamelCase,
497         });
498
499         // Check the field names.
500         let enum_fields_replacements = data
501             .variants
502             .iter()
503             .filter_map(|(_, variant)| {
504                 Some(Replacement {
505                     current_name: variant.name.clone(),
506                     suggested_text: to_camel_case(&variant.name.to_string())?,
507                     expected_case: CaseType::UpperCamelCase,
508                 })
509             })
510             .collect();
511
512         // If there is at least one element to spawn a warning on, go to the source map and generate a warning.
513         self.create_incorrect_case_diagnostic_for_enum(
514             enum_id,
515             enum_name_replacement,
516             enum_fields_replacements,
517         )
518     }
519
520     /// Given the information about incorrect names in the struct declaration, looks up into the source code
521     /// for exact locations and adds diagnostics into the sink.
522     fn create_incorrect_case_diagnostic_for_enum(
523         &mut self,
524         enum_id: EnumId,
525         enum_name_replacement: Option<Replacement>,
526         enum_variants_replacements: Vec<Replacement>,
527     ) {
528         // XXX: only look at sources if we do have incorrect names
529         if enum_name_replacement.is_none() && enum_variants_replacements.is_empty() {
530             return;
531         }
532
533         let enum_loc = enum_id.lookup(self.db.upcast());
534         let enum_src = enum_loc.source(self.db.upcast());
535
536         if let Some(replacement) = enum_name_replacement {
537             let ast_ptr = match enum_src.value.name() {
538                 Some(name) => name,
539                 None => {
540                     never!(
541                         "Replacement ({:?}) was generated for a enum without a name: {:?}",
542                         replacement,
543                         enum_src
544                     );
545                     return;
546                 }
547             };
548
549             let diagnostic = IncorrectCase {
550                 file: enum_src.file_id,
551                 ident_type: IdentType::Enum,
552                 ident: AstPtr::new(&ast_ptr),
553                 expected_case: replacement.expected_case,
554                 ident_text: replacement.current_name.to_string(),
555                 suggested_text: replacement.suggested_text,
556             };
557
558             self.sink.push(diagnostic);
559         }
560
561         let enum_variants_list = match enum_src.value.variant_list() {
562             Some(variants) => variants,
563             _ => {
564                 always!(
565                     enum_variants_replacements.is_empty(),
566                     "Replacements ({:?}) were generated for a enum variants which had no fields list: {:?}",
567                     enum_variants_replacements,
568                     enum_src
569                 );
570                 return;
571             }
572         };
573         let mut enum_variants_iter = enum_variants_list.variants();
574         for variant_to_rename in enum_variants_replacements {
575             // We assume that parameters in replacement are in the same order as in the
576             // actual params list, but just some of them (ones that named correctly) are skipped.
577             let ast_ptr = loop {
578                 match enum_variants_iter.next().and_then(|v| v.name()) {
579                     Some(variant_name) => {
580                         if variant_name.as_name() == variant_to_rename.current_name {
581                             break variant_name;
582                         }
583                     }
584                     None => {
585                         never!(
586                             "Replacement ({:?}) was generated for a enum variant which was not found: {:?}",
587                             variant_to_rename, enum_src
588                         );
589                         return;
590                     }
591                 }
592             };
593
594             let diagnostic = IncorrectCase {
595                 file: enum_src.file_id,
596                 ident_type: IdentType::Variant,
597                 ident: AstPtr::new(&ast_ptr),
598                 expected_case: variant_to_rename.expected_case,
599                 ident_text: variant_to_rename.current_name.to_string(),
600                 suggested_text: variant_to_rename.suggested_text,
601             };
602
603             self.sink.push(diagnostic);
604         }
605     }
606
607     fn validate_const(&mut self, const_id: ConstId) {
608         let data = self.db.const_data(const_id);
609
610         if self.allowed(const_id.into(), allow::NON_UPPER_CASE_GLOBAL, false) {
611             return;
612         }
613
614         let name = match &data.name {
615             Some(name) => name,
616             None => return,
617         };
618
619         let const_name = name.to_string();
620         let replacement = if let Some(new_name) = to_upper_snake_case(&const_name) {
621             Replacement {
622                 current_name: name.clone(),
623                 suggested_text: new_name,
624                 expected_case: CaseType::UpperSnakeCase,
625             }
626         } else {
627             // Nothing to do here.
628             return;
629         };
630
631         let const_loc = const_id.lookup(self.db.upcast());
632         let const_src = const_loc.source(self.db.upcast());
633
634         let ast_ptr = match const_src.value.name() {
635             Some(name) => name,
636             None => return,
637         };
638
639         let diagnostic = IncorrectCase {
640             file: const_src.file_id,
641             ident_type: IdentType::Constant,
642             ident: AstPtr::new(&ast_ptr),
643             expected_case: replacement.expected_case,
644             ident_text: replacement.current_name.to_string(),
645             suggested_text: replacement.suggested_text,
646         };
647
648         self.sink.push(diagnostic);
649     }
650
651     fn validate_static(&mut self, static_id: StaticId) {
652         let data = self.db.static_data(static_id);
653         if data.is_extern {
654             cov_mark::hit!(extern_static_incorrect_case_ignored);
655             return;
656         }
657
658         if self.allowed(static_id.into(), allow::NON_UPPER_CASE_GLOBAL, false) {
659             return;
660         }
661
662         let name = &data.name;
663
664         let static_name = name.to_string();
665         let replacement = if let Some(new_name) = to_upper_snake_case(&static_name) {
666             Replacement {
667                 current_name: name.clone(),
668                 suggested_text: new_name,
669                 expected_case: CaseType::UpperSnakeCase,
670             }
671         } else {
672             // Nothing to do here.
673             return;
674         };
675
676         let static_loc = static_id.lookup(self.db.upcast());
677         let static_src = static_loc.source(self.db.upcast());
678
679         let ast_ptr = match static_src.value.name() {
680             Some(name) => name,
681             None => return,
682         };
683
684         let diagnostic = IncorrectCase {
685             file: static_src.file_id,
686             ident_type: IdentType::StaticVariable,
687             ident: AstPtr::new(&ast_ptr),
688             expected_case: replacement.expected_case,
689             ident_text: replacement.current_name.to_string(),
690             suggested_text: replacement.suggested_text,
691         };
692
693         self.sink.push(diagnostic);
694     }
695 }