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