hir_ty/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//! - modules (e.g. `mod foo { ... }` or `mod foo;`)
13
14mod case_conv;
15
16use std::fmt;
17
18use hir_def::{
19    AdtId, ConstId, EnumId, EnumVariantId, FunctionId, HasModule, ItemContainerId, Lookup,
20    ModuleDefId, ModuleId, StaticId, StructId, TraitId, TypeAliasId, db::DefDatabase, hir::Pat,
21    item_tree::FieldsShape, signatures::StaticFlags, src::HasSource,
22};
23use hir_expand::{
24    HirFileId,
25    name::{AsName, Name},
26};
27use intern::sym;
28use stdx::{always, never};
29use syntax::{
30    AstNode, AstPtr, ToSmolStr,
31    ast::{self, HasName},
32    utils::is_raw_identifier,
33};
34
35use crate::db::HirDatabase;
36
37use self::case_conv::{to_camel_case, to_lower_snake_case, to_upper_snake_case};
38
39pub fn incorrect_case(db: &dyn HirDatabase, owner: ModuleDefId) -> Vec<IncorrectCase> {
40    let _p = tracing::info_span!("incorrect_case").entered();
41    let mut validator = DeclValidator::new(db);
42    validator.validate_item(owner);
43    validator.sink
44}
45
46#[derive(Debug)]
47pub enum CaseType {
48    /// `some_var`
49    LowerSnakeCase,
50    /// `SOME_CONST`
51    UpperSnakeCase,
52    /// `SomeStruct`
53    UpperCamelCase,
54}
55
56impl fmt::Display for CaseType {
57    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
58        let repr = match self {
59            CaseType::LowerSnakeCase => "snake_case",
60            CaseType::UpperSnakeCase => "UPPER_SNAKE_CASE",
61            CaseType::UpperCamelCase => "UpperCamelCase",
62        };
63
64        repr.fmt(f)
65    }
66}
67
68#[derive(Debug)]
69pub enum IdentType {
70    Constant,
71    Enum,
72    Field,
73    Function,
74    Module,
75    Parameter,
76    StaticVariable,
77    Structure,
78    Trait,
79    TypeAlias,
80    Variable,
81    Variant,
82}
83
84impl fmt::Display for IdentType {
85    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
86        let repr = match self {
87            IdentType::Constant => "Constant",
88            IdentType::Enum => "Enum",
89            IdentType::Field => "Field",
90            IdentType::Function => "Function",
91            IdentType::Module => "Module",
92            IdentType::Parameter => "Parameter",
93            IdentType::StaticVariable => "Static variable",
94            IdentType::Structure => "Structure",
95            IdentType::Trait => "Trait",
96            IdentType::TypeAlias => "Type alias",
97            IdentType::Variable => "Variable",
98            IdentType::Variant => "Variant",
99        };
100
101        repr.fmt(f)
102    }
103}
104
105#[derive(Debug)]
106pub struct IncorrectCase {
107    pub file: HirFileId,
108    pub ident: AstPtr<ast::Name>,
109    pub expected_case: CaseType,
110    pub ident_type: IdentType,
111    pub ident_text: String,
112    pub suggested_text: String,
113}
114
115pub(super) struct DeclValidator<'a> {
116    db: &'a dyn HirDatabase,
117    pub(super) sink: Vec<IncorrectCase>,
118}
119
120#[derive(Debug)]
121struct Replacement {
122    current_name: Name,
123    suggested_text: String,
124    expected_case: CaseType,
125}
126
127impl<'a> DeclValidator<'a> {
128    pub(super) fn new(db: &'a dyn HirDatabase) -> DeclValidator<'a> {
129        DeclValidator { db, sink: Vec::new() }
130    }
131
132    pub(super) fn validate_item(&mut self, item: ModuleDefId) {
133        match item {
134            ModuleDefId::ModuleId(module_id) => self.validate_module(module_id),
135            ModuleDefId::TraitId(trait_id) => self.validate_trait(trait_id),
136            ModuleDefId::FunctionId(func) => self.validate_func(func),
137            ModuleDefId::AdtId(adt) => self.validate_adt(adt),
138            ModuleDefId::ConstId(const_id) => self.validate_const(const_id),
139            ModuleDefId::StaticId(static_id) => self.validate_static(static_id),
140            ModuleDefId::TypeAliasId(type_alias_id) => self.validate_type_alias(type_alias_id),
141            _ => (),
142        }
143    }
144
145    fn validate_adt(&mut self, adt: AdtId) {
146        match adt {
147            AdtId::StructId(struct_id) => self.validate_struct(struct_id),
148            AdtId::EnumId(enum_id) => self.validate_enum(enum_id),
149            AdtId::UnionId(_) => {
150                // FIXME: Unions aren't yet supported by this validator.
151            }
152        }
153    }
154
155    fn validate_module(&mut self, module_id: ModuleId) {
156        // Check the module name.
157        let Some(module_name) = module_id.name(self.db) else { return };
158        let Some(module_name_replacement) =
159            to_lower_snake_case(module_name.as_str()).map(|new_name| Replacement {
160                current_name: module_name,
161                suggested_text: new_name,
162                expected_case: CaseType::LowerSnakeCase,
163            })
164        else {
165            return;
166        };
167        let module_data = &module_id.def_map(self.db)[module_id.local_id];
168        let Some(module_src) = module_data.declaration_source(self.db) else {
169            return;
170        };
171        self.create_incorrect_case_diagnostic_for_ast_node(
172            module_name_replacement,
173            module_src.file_id,
174            &module_src.value,
175            IdentType::Module,
176        );
177    }
178
179    fn validate_trait(&mut self, trait_id: TraitId) {
180        // Check the trait name.
181        let data = self.db.trait_signature(trait_id);
182        self.create_incorrect_case_diagnostic_for_item_name(
183            trait_id,
184            &data.name,
185            CaseType::UpperCamelCase,
186            IdentType::Trait,
187        );
188    }
189
190    fn validate_func(&mut self, func: FunctionId) {
191        let container = func.lookup(self.db).container;
192        if matches!(container, ItemContainerId::ExternBlockId(_)) {
193            cov_mark::hit!(extern_func_incorrect_case_ignored);
194            return;
195        }
196
197        // Check the function name.
198        // Skipped if function is an associated item of a trait implementation.
199        if !self.is_trait_impl_container(container) {
200            let data = self.db.function_signature(func);
201
202            // Don't run the lint on extern "[not Rust]" fn items with the
203            // #[no_mangle] attribute.
204            let no_mangle = self.db.attrs(func.into()).by_key(sym::no_mangle).exists();
205            if no_mangle && data.abi.as_ref().is_some_and(|abi| *abi != sym::Rust) {
206                cov_mark::hit!(extern_func_no_mangle_ignored);
207            } else {
208                self.create_incorrect_case_diagnostic_for_item_name(
209                    func,
210                    &data.name,
211                    CaseType::LowerSnakeCase,
212                    IdentType::Function,
213                );
214            }
215        } else {
216            cov_mark::hit!(trait_impl_assoc_func_name_incorrect_case_ignored);
217        }
218
219        // Check the patterns inside the function body.
220        self.validate_func_body(func);
221    }
222
223    /// Check incorrect names for patterns inside the function body.
224    /// This includes function parameters except for trait implementation associated functions.
225    fn validate_func_body(&mut self, func: FunctionId) {
226        let body = self.db.body(func.into());
227        let edition = self.edition(func);
228        let mut pats_replacements = body
229            .pats()
230            .filter_map(|(pat_id, pat)| match pat {
231                Pat::Bind { id, .. } => {
232                    let bind_name = &body[*id].name;
233                    let mut suggested_text = to_lower_snake_case(bind_name.as_str())?;
234                    if is_raw_identifier(&suggested_text, edition) {
235                        suggested_text.insert_str(0, "r#");
236                    }
237                    let replacement = Replacement {
238                        current_name: bind_name.clone(),
239                        suggested_text,
240                        expected_case: CaseType::LowerSnakeCase,
241                    };
242                    Some((pat_id, replacement))
243                }
244                _ => None,
245            })
246            .peekable();
247
248        // XXX: only look at source_map if we do have missing fields
249        if pats_replacements.peek().is_none() {
250            return;
251        }
252
253        let source_map = self.db.body_with_source_map(func.into()).1;
254        for (id, replacement) in pats_replacements {
255            let Ok(source_ptr) = source_map.pat_syntax(id) else {
256                continue;
257            };
258            let Some(ptr) = source_ptr.value.cast::<ast::IdentPat>() else {
259                continue;
260            };
261            let root = source_ptr.file_syntax(self.db);
262            let ident_pat = ptr.to_node(&root);
263            let Some(parent) = ident_pat.syntax().parent() else {
264                continue;
265            };
266
267            let is_shorthand = ast::RecordPatField::cast(parent.clone())
268                .map(|parent| parent.name_ref().is_none())
269                .unwrap_or_default();
270            if is_shorthand {
271                // We don't check shorthand field patterns, such as 'field' in `Thing { field }`,
272                // since the shorthand isn't the declaration.
273                continue;
274            }
275
276            let is_param = ast::Param::can_cast(parent.kind());
277            let ident_type = if is_param { IdentType::Parameter } else { IdentType::Variable };
278
279            self.create_incorrect_case_diagnostic_for_ast_node(
280                replacement,
281                source_ptr.file_id,
282                &ident_pat,
283                ident_type,
284            );
285        }
286    }
287
288    fn edition(&self, id: impl HasModule) -> span::Edition {
289        let krate = id.krate(self.db);
290        krate.data(self.db).edition
291    }
292
293    fn validate_struct(&mut self, struct_id: StructId) {
294        // Check the structure name.
295        let data = self.db.struct_signature(struct_id);
296        self.create_incorrect_case_diagnostic_for_item_name(
297            struct_id,
298            &data.name,
299            CaseType::UpperCamelCase,
300            IdentType::Structure,
301        );
302
303        // Check the field names.
304        self.validate_struct_fields(struct_id);
305    }
306
307    /// Check incorrect names for struct fields.
308    fn validate_struct_fields(&mut self, struct_id: StructId) {
309        let data = struct_id.fields(self.db);
310        if data.shape != FieldsShape::Record {
311            return;
312        };
313        let edition = self.edition(struct_id);
314        let mut struct_fields_replacements = data
315            .fields()
316            .iter()
317            .filter_map(|(_, field)| {
318                to_lower_snake_case(&field.name.display_no_db(edition).to_smolstr()).map(
319                    |new_name| Replacement {
320                        current_name: field.name.clone(),
321                        suggested_text: new_name,
322                        expected_case: CaseType::LowerSnakeCase,
323                    },
324                )
325            })
326            .peekable();
327
328        // XXX: Only look at sources if we do have incorrect names.
329        if struct_fields_replacements.peek().is_none() {
330            return;
331        }
332
333        let struct_loc = struct_id.lookup(self.db);
334        let struct_src = struct_loc.source(self.db);
335
336        let Some(ast::FieldList::RecordFieldList(struct_fields_list)) =
337            struct_src.value.field_list()
338        else {
339            always!(
340                struct_fields_replacements.peek().is_none(),
341                "Replacements ({:?}) were generated for a structure fields \
342                which had no fields list: {:?}",
343                struct_fields_replacements.collect::<Vec<_>>(),
344                struct_src
345            );
346            return;
347        };
348        let mut struct_fields_iter = struct_fields_list.fields();
349        for field_replacement in struct_fields_replacements {
350            // We assume that parameters in replacement are in the same order as in the
351            // actual params list, but just some of them (ones that named correctly) are skipped.
352            let field = loop {
353                if let Some(field) = struct_fields_iter.next() {
354                    let Some(field_name) = field.name() else {
355                        continue;
356                    };
357                    if field_name.as_name() == field_replacement.current_name {
358                        break field;
359                    }
360                } else {
361                    never!(
362                        "Replacement ({:?}) was generated for a structure field \
363                        which was not found: {:?}",
364                        field_replacement,
365                        struct_src
366                    );
367                    return;
368                }
369            };
370
371            self.create_incorrect_case_diagnostic_for_ast_node(
372                field_replacement,
373                struct_src.file_id,
374                &field,
375                IdentType::Field,
376            );
377        }
378    }
379
380    fn validate_enum(&mut self, enum_id: EnumId) {
381        let data = self.db.enum_signature(enum_id);
382
383        // Check the enum name.
384        self.create_incorrect_case_diagnostic_for_item_name(
385            enum_id,
386            &data.name,
387            CaseType::UpperCamelCase,
388            IdentType::Enum,
389        );
390
391        // Check the variant names.
392        self.validate_enum_variants(enum_id)
393    }
394
395    /// Check incorrect names for enum variants.
396    fn validate_enum_variants(&mut self, enum_id: EnumId) {
397        let data = enum_id.enum_variants(self.db);
398
399        for (variant_id, _, _) in data.variants.iter() {
400            self.validate_enum_variant_fields(*variant_id);
401        }
402
403        let edition = self.edition(enum_id);
404        let mut enum_variants_replacements = data
405            .variants
406            .iter()
407            .filter_map(|(_, name, _)| {
408                to_camel_case(&name.display_no_db(edition).to_smolstr()).map(|new_name| {
409                    Replacement {
410                        current_name: name.clone(),
411                        suggested_text: new_name,
412                        expected_case: CaseType::UpperCamelCase,
413                    }
414                })
415            })
416            .peekable();
417
418        // XXX: only look at sources if we do have incorrect names
419        if enum_variants_replacements.peek().is_none() {
420            return;
421        }
422
423        let enum_loc = enum_id.lookup(self.db);
424        let enum_src = enum_loc.source(self.db);
425
426        let Some(enum_variants_list) = enum_src.value.variant_list() else {
427            always!(
428                enum_variants_replacements.peek().is_none(),
429                "Replacements ({:?}) were generated for enum variants \
430                which had no fields list: {:?}",
431                enum_variants_replacements,
432                enum_src
433            );
434            return;
435        };
436        let mut enum_variants_iter = enum_variants_list.variants();
437        for variant_replacement in enum_variants_replacements {
438            // We assume that parameters in replacement are in the same order as in the
439            // actual params list, but just some of them (ones that named correctly) are skipped.
440            let variant = loop {
441                if let Some(variant) = enum_variants_iter.next() {
442                    let Some(variant_name) = variant.name() else {
443                        continue;
444                    };
445                    if variant_name.as_name() == variant_replacement.current_name {
446                        break variant;
447                    }
448                } else {
449                    never!(
450                        "Replacement ({:?}) was generated for an enum variant \
451                        which was not found: {:?}",
452                        variant_replacement,
453                        enum_src
454                    );
455                    return;
456                }
457            };
458
459            self.create_incorrect_case_diagnostic_for_ast_node(
460                variant_replacement,
461                enum_src.file_id,
462                &variant,
463                IdentType::Variant,
464            );
465        }
466    }
467
468    /// Check incorrect names for fields of enum variant.
469    fn validate_enum_variant_fields(&mut self, variant_id: EnumVariantId) {
470        let variant_data = variant_id.fields(self.db);
471        if variant_data.shape != FieldsShape::Record {
472            return;
473        };
474        let edition = self.edition(variant_id);
475        let mut variant_field_replacements = variant_data
476            .fields()
477            .iter()
478            .filter_map(|(_, field)| {
479                to_lower_snake_case(&field.name.display_no_db(edition).to_smolstr()).map(
480                    |new_name| Replacement {
481                        current_name: field.name.clone(),
482                        suggested_text: new_name,
483                        expected_case: CaseType::LowerSnakeCase,
484                    },
485                )
486            })
487            .peekable();
488
489        // XXX: only look at sources if we do have incorrect names
490        if variant_field_replacements.peek().is_none() {
491            return;
492        }
493
494        let variant_loc = variant_id.lookup(self.db);
495        let variant_src = variant_loc.source(self.db);
496
497        let Some(ast::FieldList::RecordFieldList(variant_fields_list)) =
498            variant_src.value.field_list()
499        else {
500            always!(
501                variant_field_replacements.peek().is_none(),
502                "Replacements ({:?}) were generated for an enum variant \
503                which had no fields list: {:?}",
504                variant_field_replacements.collect::<Vec<_>>(),
505                variant_src
506            );
507            return;
508        };
509        let mut variant_variants_iter = variant_fields_list.fields();
510        for field_replacement in variant_field_replacements {
511            // We assume that parameters in replacement are in the same order as in the
512            // actual params list, but just some of them (ones that named correctly) are skipped.
513            let field = loop {
514                if let Some(field) = variant_variants_iter.next() {
515                    let Some(field_name) = field.name() else {
516                        continue;
517                    };
518                    if field_name.as_name() == field_replacement.current_name {
519                        break field;
520                    }
521                } else {
522                    never!(
523                        "Replacement ({:?}) was generated for an enum variant field \
524                        which was not found: {:?}",
525                        field_replacement,
526                        variant_src
527                    );
528                    return;
529                }
530            };
531
532            self.create_incorrect_case_diagnostic_for_ast_node(
533                field_replacement,
534                variant_src.file_id,
535                &field,
536                IdentType::Field,
537            );
538        }
539    }
540
541    fn validate_const(&mut self, const_id: ConstId) {
542        let container = const_id.lookup(self.db).container;
543        if self.is_trait_impl_container(container) {
544            cov_mark::hit!(trait_impl_assoc_const_incorrect_case_ignored);
545            return;
546        }
547
548        let data = self.db.const_signature(const_id);
549        let Some(name) = &data.name else {
550            return;
551        };
552        self.create_incorrect_case_diagnostic_for_item_name(
553            const_id,
554            name,
555            CaseType::UpperSnakeCase,
556            IdentType::Constant,
557        );
558    }
559
560    fn validate_static(&mut self, static_id: StaticId) {
561        let data = self.db.static_signature(static_id);
562        if data.flags.contains(StaticFlags::EXTERN) {
563            cov_mark::hit!(extern_static_incorrect_case_ignored);
564            return;
565        }
566
567        self.create_incorrect_case_diagnostic_for_item_name(
568            static_id,
569            &data.name,
570            CaseType::UpperSnakeCase,
571            IdentType::StaticVariable,
572        );
573    }
574
575    fn validate_type_alias(&mut self, type_alias_id: TypeAliasId) {
576        let container = type_alias_id.lookup(self.db).container;
577        if self.is_trait_impl_container(container) {
578            cov_mark::hit!(trait_impl_assoc_type_incorrect_case_ignored);
579            return;
580        }
581
582        // Check the type alias name.
583        let data = self.db.type_alias_signature(type_alias_id);
584        self.create_incorrect_case_diagnostic_for_item_name(
585            type_alias_id,
586            &data.name,
587            CaseType::UpperCamelCase,
588            IdentType::TypeAlias,
589        );
590    }
591
592    fn create_incorrect_case_diagnostic_for_item_name<N, S, L>(
593        &mut self,
594        item_id: L,
595        name: &Name,
596        expected_case: CaseType,
597        ident_type: IdentType,
598    ) where
599        N: AstNode + HasName + fmt::Debug,
600        S: HasSource<Value = N>,
601        L: Lookup<Data = S, Database = dyn DefDatabase> + HasModule + Copy,
602    {
603        let to_expected_case_type = match expected_case {
604            CaseType::LowerSnakeCase => to_lower_snake_case,
605            CaseType::UpperSnakeCase => to_upper_snake_case,
606            CaseType::UpperCamelCase => to_camel_case,
607        };
608        let edition = self.edition(item_id);
609        let Some(replacement) =
610            to_expected_case_type(&name.display(self.db, edition).to_smolstr()).map(|new_name| {
611                Replacement { current_name: name.clone(), suggested_text: new_name, expected_case }
612            })
613        else {
614            return;
615        };
616
617        let item_loc = item_id.lookup(self.db);
618        let item_src = item_loc.source(self.db);
619        self.create_incorrect_case_diagnostic_for_ast_node(
620            replacement,
621            item_src.file_id,
622            &item_src.value,
623            ident_type,
624        );
625    }
626
627    fn create_incorrect_case_diagnostic_for_ast_node<T>(
628        &mut self,
629        replacement: Replacement,
630        file_id: HirFileId,
631        node: &T,
632        ident_type: IdentType,
633    ) where
634        T: AstNode + HasName + fmt::Debug,
635    {
636        let Some(name_ast) = node.name() else {
637            never!(
638                "Replacement ({:?}) was generated for a {:?} without a name: {:?}",
639                replacement,
640                ident_type,
641                node
642            );
643            return;
644        };
645
646        let edition = file_id.original_file(self.db).edition(self.db);
647        let diagnostic = IncorrectCase {
648            file: file_id,
649            ident_type,
650            ident: AstPtr::new(&name_ast),
651            expected_case: replacement.expected_case,
652            ident_text: replacement.current_name.display(self.db, edition).to_string(),
653            suggested_text: replacement.suggested_text,
654        };
655
656        self.sink.push(diagnostic);
657    }
658
659    fn is_trait_impl_container(&self, container_id: ItemContainerId) -> bool {
660        if let ItemContainerId::ImplId(impl_id) = container_id
661            && self.db.impl_trait(impl_id).is_some()
662        {
663            return true;
664        }
665        false
666    }
667}