1mod 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, UnionId,
21 attrs::AttrFlags,
22 expr_store::Body,
23 hir::Pat,
24 item_tree::FieldsShape,
25 signatures::{
26 ConstSignature, EnumSignature, FunctionSignature, StaticFlags, StaticSignature,
27 StructSignature, TraitSignature, TypeAliasSignature, UnionSignature,
28 },
29 src::HasSource,
30};
31use hir_expand::{
32 HirFileId,
33 name::{AsName, Name},
34};
35use rustc_abi::ExternAbi;
36use stdx::{always, never};
37use syntax::{
38 AstNode, AstPtr, ToSmolStr,
39 ast::{self, HasName},
40 utils::is_raw_identifier,
41};
42
43use crate::db::HirDatabase;
44
45use self::case_conv::{to_camel_case, to_lower_snake_case, to_upper_snake_case};
46
47pub fn incorrect_case(db: &dyn HirDatabase, owner: ModuleDefId) -> Vec<IncorrectCase> {
48 let _p = tracing::info_span!("incorrect_case").entered();
49 let mut validator = DeclValidator::new(db);
50 validator.validate_item(owner);
51 validator.sink
52}
53
54#[derive(Debug)]
55pub enum CaseType {
56 LowerSnakeCase,
58 UpperSnakeCase,
60 UpperCamelCase,
62}
63
64impl fmt::Display for CaseType {
65 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
66 let repr = match self {
67 CaseType::LowerSnakeCase => "snake_case",
68 CaseType::UpperSnakeCase => "UPPER_SNAKE_CASE",
69 CaseType::UpperCamelCase => "UpperCamelCase",
70 };
71
72 repr.fmt(f)
73 }
74}
75
76#[derive(Debug)]
77pub enum IdentType {
78 Constant,
79 Enum,
80 Field,
81 Function,
82 Module,
83 Parameter,
84 StaticVariable,
85 Structure,
86 Trait,
87 TypeAlias,
88 Union,
89 Variable,
90 Variant,
91}
92
93impl fmt::Display for IdentType {
94 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
95 let repr = match self {
96 IdentType::Constant => "Constant",
97 IdentType::Enum => "Enum",
98 IdentType::Field => "Field",
99 IdentType::Function => "Function",
100 IdentType::Module => "Module",
101 IdentType::Parameter => "Parameter",
102 IdentType::StaticVariable => "Static variable",
103 IdentType::Structure => "Structure",
104 IdentType::Trait => "Trait",
105 IdentType::TypeAlias => "Type alias",
106 IdentType::Union => "Union",
107 IdentType::Variable => "Variable",
108 IdentType::Variant => "Variant",
109 };
110
111 repr.fmt(f)
112 }
113}
114
115#[derive(Debug)]
116pub struct IncorrectCase {
117 pub file: HirFileId,
118 pub ident: AstPtr<ast::Name>,
119 pub expected_case: CaseType,
120 pub ident_type: IdentType,
121 pub ident_text: String,
122 pub suggested_text: String,
123}
124
125pub(super) struct DeclValidator<'a> {
126 db: &'a dyn HirDatabase,
127 pub(super) sink: Vec<IncorrectCase>,
128}
129
130#[derive(Debug)]
131struct Replacement {
132 current_name: Name,
133 suggested_text: String,
134 expected_case: CaseType,
135}
136
137impl<'a> DeclValidator<'a> {
138 pub(super) fn new(db: &'a dyn HirDatabase) -> DeclValidator<'a> {
139 DeclValidator { db, sink: Vec::new() }
140 }
141
142 pub(super) fn validate_item(&mut self, item: ModuleDefId) {
143 match item {
144 ModuleDefId::ModuleId(module_id) => self.validate_module(module_id),
145 ModuleDefId::TraitId(trait_id) => self.validate_trait(trait_id),
146 ModuleDefId::FunctionId(func) => self.validate_func(func),
147 ModuleDefId::AdtId(adt) => self.validate_adt(adt),
148 ModuleDefId::ConstId(const_id) => self.validate_const(const_id),
149 ModuleDefId::StaticId(static_id) => self.validate_static(static_id),
150 ModuleDefId::TypeAliasId(type_alias_id) => self.validate_type_alias(type_alias_id),
151 _ => (),
152 }
153 }
154
155 fn validate_adt(&mut self, adt: AdtId) {
156 match adt {
157 AdtId::StructId(struct_id) => self.validate_struct(struct_id),
158 AdtId::EnumId(enum_id) => self.validate_enum(enum_id),
159 AdtId::UnionId(union_id) => self.validate_union(union_id),
160 }
161 }
162
163 fn validate_module(&mut self, module_id: ModuleId) {
164 let Some(module_name) = module_id.name(self.db) else { return };
166 let Some(module_name_replacement) =
167 to_lower_snake_case(module_name.as_str()).map(|new_name| Replacement {
168 current_name: module_name,
169 suggested_text: new_name,
170 expected_case: CaseType::LowerSnakeCase,
171 })
172 else {
173 return;
174 };
175 let module_data = &module_id.def_map(self.db)[module_id];
176 let Some(module_src) = module_data.declaration_source(self.db) else {
177 return;
178 };
179 self.create_incorrect_case_diagnostic_for_ast_node(
180 module_name_replacement,
181 module_src.file_id,
182 &module_src.value,
183 IdentType::Module,
184 );
185 }
186
187 fn validate_trait(&mut self, trait_id: TraitId) {
188 let data = TraitSignature::of(self.db, trait_id);
190 self.create_incorrect_case_diagnostic_for_item_name(
191 trait_id,
192 &data.name,
193 CaseType::UpperCamelCase,
194 IdentType::Trait,
195 );
196 }
197
198 fn validate_func(&mut self, func: FunctionId) {
199 let container = func.lookup(self.db).container;
200 if matches!(container, ItemContainerId::ExternBlockId(_)) {
201 cov_mark::hit!(extern_func_incorrect_case_ignored);
202 return;
203 }
204
205 if !self.is_trait_impl_container(container) {
208 let data = FunctionSignature::of(self.db, func);
209
210 let no_mangle = AttrFlags::query(self.db, func.into()).contains(AttrFlags::NO_MANGLE);
213 if no_mangle && data.abi != ExternAbi::Rust {
214 cov_mark::hit!(extern_func_no_mangle_ignored);
215 } else {
216 self.create_incorrect_case_diagnostic_for_item_name(
217 func,
218 &data.name,
219 CaseType::LowerSnakeCase,
220 IdentType::Function,
221 );
222 }
223 } else {
224 cov_mark::hit!(trait_impl_assoc_func_name_incorrect_case_ignored);
225 }
226
227 self.validate_func_body(func);
229 }
230
231 fn validate_func_body(&mut self, func: FunctionId) {
234 let body = Body::of(self.db, func.into());
235 let edition = self.edition(func);
236 let mut pats_replacements = body
237 .pats()
238 .filter_map(|(pat_id, pat)| match pat {
239 Pat::Bind { id, .. } => {
240 let bind_name = &body[*id].name;
241 let mut suggested_text = to_lower_snake_case(bind_name.as_str())?;
242 if is_raw_identifier(&suggested_text, edition) {
243 suggested_text.insert_str(0, "r#");
244 }
245 let replacement = Replacement {
246 current_name: bind_name.clone(),
247 suggested_text,
248 expected_case: CaseType::LowerSnakeCase,
249 };
250 Some((pat_id, replacement))
251 }
252 _ => None,
253 })
254 .peekable();
255
256 if pats_replacements.peek().is_none() {
258 return;
259 }
260
261 let source_map = &Body::with_source_map(self.db, func.into()).1;
262 for (id, replacement) in pats_replacements {
263 let Ok(source_ptr) = source_map.pat_syntax(id) else {
264 continue;
265 };
266 let Some(ptr) = source_ptr.value.cast::<ast::IdentPat>() else {
267 continue;
268 };
269 let root = source_ptr.file_syntax(self.db);
270 let ident_pat = ptr.to_node(&root);
271 let Some(parent) = ident_pat.syntax().parent() else {
272 continue;
273 };
274
275 let is_shorthand = ast::RecordPatField::cast(parent.clone())
276 .map(|parent| parent.name_ref().is_none())
277 .unwrap_or_default();
278 if is_shorthand {
279 continue;
282 }
283
284 let is_param = ast::Param::can_cast(parent.kind());
285 let ident_type = if is_param { IdentType::Parameter } else { IdentType::Variable };
286
287 self.create_incorrect_case_diagnostic_for_ast_node(
288 replacement,
289 source_ptr.file_id,
290 &ident_pat,
291 ident_type,
292 );
293 }
294 }
295
296 fn edition(&self, id: impl HasModule) -> span::Edition {
297 let krate = id.krate(self.db);
298 krate.data(self.db).edition
299 }
300
301 fn validate_struct(&mut self, struct_id: StructId) {
302 let data = StructSignature::of(self.db, struct_id);
304
305 let has_repr_c = data.repr(self.db, struct_id).is_some_and(|repr| repr.c());
308 if !has_repr_c {
309 self.create_incorrect_case_diagnostic_for_item_name(
310 struct_id,
311 &data.name,
312 CaseType::UpperCamelCase,
313 IdentType::Structure,
314 );
315 }
316
317 self.validate_struct_fields(struct_id);
319 }
320
321 fn validate_struct_fields(&mut self, struct_id: StructId) {
323 let data = struct_id.fields(self.db);
324 if data.shape != FieldsShape::Record {
325 return;
326 };
327 let edition = self.edition(struct_id);
328 let mut struct_fields_replacements = data
329 .fields()
330 .iter()
331 .filter_map(|(_, field)| {
332 to_lower_snake_case(&field.name.display_no_db(edition).to_smolstr()).map(
333 |new_name| Replacement {
334 current_name: field.name.clone(),
335 suggested_text: new_name,
336 expected_case: CaseType::LowerSnakeCase,
337 },
338 )
339 })
340 .peekable();
341
342 if struct_fields_replacements.peek().is_none() {
344 return;
345 }
346
347 let struct_loc = struct_id.lookup(self.db);
348 let struct_src = struct_loc.source(self.db);
349
350 let Some(ast::FieldList::RecordFieldList(struct_fields_list)) =
351 struct_src.value.field_list()
352 else {
353 always!(
354 struct_fields_replacements.peek().is_none(),
355 "Replacements ({:?}) were generated for a structure fields \
356 which had no fields list: {:?}",
357 struct_fields_replacements.collect::<Vec<_>>(),
358 struct_src
359 );
360 return;
361 };
362 let mut struct_fields_iter = struct_fields_list.fields();
363 for field_replacement in struct_fields_replacements {
364 let field = loop {
367 if let Some(field) = struct_fields_iter.next() {
368 let Some(field_name) = field.name() else {
369 continue;
370 };
371 if field_name.as_name() == field_replacement.current_name {
372 break field;
373 }
374 } else {
375 never!(
376 "Replacement ({:?}) was generated for a structure field \
377 which was not found: {:?}",
378 field_replacement,
379 struct_src
380 );
381 return;
382 }
383 };
384
385 self.create_incorrect_case_diagnostic_for_ast_node(
386 field_replacement,
387 struct_src.file_id,
388 &field,
389 IdentType::Field,
390 );
391 }
392 }
393
394 fn validate_union(&mut self, union_id: UnionId) {
395 let data = UnionSignature::of(self.db, union_id);
397
398 let has_repr_c = AttrFlags::repr(self.db, union_id.into()).is_some_and(|repr| repr.c());
401 if !has_repr_c {
402 self.create_incorrect_case_diagnostic_for_item_name(
403 union_id,
404 &data.name,
405 CaseType::UpperCamelCase,
406 IdentType::Union,
407 );
408 }
409
410 self.validate_union_fields(union_id);
412 }
413
414 fn validate_union_fields(&mut self, union_id: UnionId) {
416 let data = union_id.fields(self.db);
417 let edition = self.edition(union_id);
418 let mut union_fields_replacements = data
419 .fields()
420 .iter()
421 .filter_map(|(_, field)| {
422 to_lower_snake_case(&field.name.display_no_db(edition).to_smolstr()).map(
423 |new_name| Replacement {
424 current_name: field.name.clone(),
425 suggested_text: new_name,
426 expected_case: CaseType::LowerSnakeCase,
427 },
428 )
429 })
430 .peekable();
431
432 if union_fields_replacements.peek().is_none() {
434 return;
435 }
436
437 let union_loc = union_id.lookup(self.db);
438 let union_src = union_loc.source(self.db);
439
440 let Some(union_fields_list) = union_src.value.record_field_list() else {
441 always!(
442 union_fields_replacements.peek().is_none(),
443 "Replacements ({:?}) were generated for a union fields \
444 which had no fields list: {:?}",
445 union_fields_replacements.collect::<Vec<_>>(),
446 union_src
447 );
448 return;
449 };
450 let mut union_fields_iter = union_fields_list.fields();
451 for field_replacement in union_fields_replacements {
452 let field = loop {
455 if let Some(field) = union_fields_iter.next() {
456 let Some(field_name) = field.name() else {
457 continue;
458 };
459 if field_name.as_name() == field_replacement.current_name {
460 break field;
461 }
462 } else {
463 never!(
464 "Replacement ({:?}) was generated for a union field \
465 which was not found: {:?}",
466 field_replacement,
467 union_src
468 );
469 return;
470 }
471 };
472
473 self.create_incorrect_case_diagnostic_for_ast_node(
474 field_replacement,
475 union_src.file_id,
476 &field,
477 IdentType::Field,
478 );
479 }
480 }
481
482 fn validate_enum(&mut self, enum_id: EnumId) {
483 let data = EnumSignature::of(self.db, enum_id);
485
486 let has_repr_c = data.repr(self.db, enum_id).is_some_and(|repr| repr.c());
489 if !has_repr_c {
490 self.create_incorrect_case_diagnostic_for_item_name(
491 enum_id,
492 &data.name,
493 CaseType::UpperCamelCase,
494 IdentType::Enum,
495 );
496 }
497
498 self.validate_enum_variants(enum_id)
500 }
501
502 fn validate_enum_variants(&mut self, enum_id: EnumId) {
504 let data = enum_id.enum_variants(self.db);
505
506 for (variant_id, _) in data.variants.values() {
507 self.validate_enum_variant_fields(*variant_id);
508 }
509
510 let mut enum_variants_replacements = data
511 .variants
512 .keys()
513 .filter_map(|name| {
514 to_camel_case(name.as_str()).map(|new_name| Replacement {
515 current_name: name.clone(),
516 suggested_text: new_name,
517 expected_case: CaseType::UpperCamelCase,
518 })
519 })
520 .peekable();
521
522 if enum_variants_replacements.peek().is_none() {
524 return;
525 }
526
527 let enum_loc = enum_id.lookup(self.db);
528 let enum_src = enum_loc.source(self.db);
529
530 let Some(enum_variants_list) = enum_src.value.variant_list() else {
531 always!(
532 enum_variants_replacements.peek().is_none(),
533 "Replacements ({:?}) were generated for enum variants \
534 which had no fields list: {:?}",
535 enum_variants_replacements,
536 enum_src
537 );
538 return;
539 };
540 let mut enum_variants_iter = enum_variants_list.variants();
541 for variant_replacement in enum_variants_replacements {
542 let variant = loop {
545 if let Some(variant) = enum_variants_iter.next() {
546 let Some(variant_name) = variant.name() else {
547 continue;
548 };
549 if variant_name.as_name() == variant_replacement.current_name {
550 break variant;
551 }
552 } else {
553 never!(
554 "Replacement ({:?}) was generated for an enum variant \
555 which was not found: {:?}",
556 variant_replacement,
557 enum_src
558 );
559 return;
560 }
561 };
562
563 self.create_incorrect_case_diagnostic_for_ast_node(
564 variant_replacement,
565 enum_src.file_id,
566 &variant,
567 IdentType::Variant,
568 );
569 }
570 }
571
572 fn validate_enum_variant_fields(&mut self, variant_id: EnumVariantId) {
574 let variant_data = variant_id.fields(self.db);
575 if variant_data.shape != FieldsShape::Record {
576 return;
577 };
578 let edition = self.edition(variant_id);
579 let mut variant_field_replacements = variant_data
580 .fields()
581 .iter()
582 .filter_map(|(_, field)| {
583 to_lower_snake_case(&field.name.display_no_db(edition).to_smolstr()).map(
584 |new_name| Replacement {
585 current_name: field.name.clone(),
586 suggested_text: new_name,
587 expected_case: CaseType::LowerSnakeCase,
588 },
589 )
590 })
591 .peekable();
592
593 if variant_field_replacements.peek().is_none() {
595 return;
596 }
597
598 let variant_loc = variant_id.lookup(self.db);
599 let variant_src = variant_loc.source(self.db);
600
601 let Some(ast::FieldList::RecordFieldList(variant_fields_list)) =
602 variant_src.value.field_list()
603 else {
604 always!(
605 variant_field_replacements.peek().is_none(),
606 "Replacements ({:?}) were generated for an enum variant \
607 which had no fields list: {:?}",
608 variant_field_replacements.collect::<Vec<_>>(),
609 variant_src
610 );
611 return;
612 };
613 let mut variant_variants_iter = variant_fields_list.fields();
614 for field_replacement in variant_field_replacements {
615 let field = loop {
618 if let Some(field) = variant_variants_iter.next() {
619 let Some(field_name) = field.name() else {
620 continue;
621 };
622 if field_name.as_name() == field_replacement.current_name {
623 break field;
624 }
625 } else {
626 never!(
627 "Replacement ({:?}) was generated for an enum variant field \
628 which was not found: {:?}",
629 field_replacement,
630 variant_src
631 );
632 return;
633 }
634 };
635
636 self.create_incorrect_case_diagnostic_for_ast_node(
637 field_replacement,
638 variant_src.file_id,
639 &field,
640 IdentType::Field,
641 );
642 }
643 }
644
645 fn validate_const(&mut self, const_id: ConstId) {
646 let container = const_id.lookup(self.db).container;
647 if self.is_trait_impl_container(container) {
648 cov_mark::hit!(trait_impl_assoc_const_incorrect_case_ignored);
649 return;
650 }
651
652 let data = ConstSignature::of(self.db, const_id);
653 let Some(name) = &data.name else {
654 return;
655 };
656 self.create_incorrect_case_diagnostic_for_item_name(
657 const_id,
658 name,
659 CaseType::UpperSnakeCase,
660 IdentType::Constant,
661 );
662 }
663
664 fn validate_static(&mut self, static_id: StaticId) {
665 let data = StaticSignature::of(self.db, static_id);
666 if data.flags.contains(StaticFlags::EXTERN) {
667 cov_mark::hit!(extern_static_incorrect_case_ignored);
668 return;
669 }
670 if AttrFlags::query(self.db, static_id.into()).contains(AttrFlags::NO_MANGLE) {
671 cov_mark::hit!(no_mangle_static_incorrect_case_ignored);
672 return;
673 }
674
675 self.create_incorrect_case_diagnostic_for_item_name(
676 static_id,
677 &data.name,
678 CaseType::UpperSnakeCase,
679 IdentType::StaticVariable,
680 );
681 }
682
683 fn validate_type_alias(&mut self, type_alias_id: TypeAliasId) {
684 let container = type_alias_id.lookup(self.db).container;
685 if self.is_trait_impl_container(container) {
686 cov_mark::hit!(trait_impl_assoc_type_incorrect_case_ignored);
687 return;
688 }
689
690 let data = TypeAliasSignature::of(self.db, type_alias_id);
692 self.create_incorrect_case_diagnostic_for_item_name(
693 type_alias_id,
694 &data.name,
695 CaseType::UpperCamelCase,
696 IdentType::TypeAlias,
697 );
698 }
699
700 fn create_incorrect_case_diagnostic_for_item_name<N, S, L>(
701 &mut self,
702 item_id: L,
703 name: &Name,
704 expected_case: CaseType,
705 ident_type: IdentType,
706 ) where
707 N: AstNode + HasName + fmt::Debug,
708 S: HasSource<Value = N>,
709 L: Lookup<Data = S> + HasModule + Copy,
710 {
711 let to_expected_case_type = match expected_case {
712 CaseType::LowerSnakeCase => to_lower_snake_case,
713 CaseType::UpperSnakeCase => to_upper_snake_case,
714 CaseType::UpperCamelCase => to_camel_case,
715 };
716 let edition = self.edition(item_id);
717 let Some(replacement) = to_expected_case_type(name.as_str()).map(|mut new_name| {
718 if is_raw_identifier(&new_name, edition) {
719 new_name.insert_str(0, "r#");
720 }
721 Replacement { current_name: name.clone(), suggested_text: new_name, expected_case }
722 }) else {
723 return;
724 };
725
726 let item_loc = item_id.lookup(self.db);
727 let item_src = item_loc.source(self.db);
728 self.create_incorrect_case_diagnostic_for_ast_node(
729 replacement,
730 item_src.file_id,
731 &item_src.value,
732 ident_type,
733 );
734 }
735
736 fn create_incorrect_case_diagnostic_for_ast_node<T>(
737 &mut self,
738 replacement: Replacement,
739 file_id: HirFileId,
740 node: &T,
741 ident_type: IdentType,
742 ) where
743 T: AstNode + HasName + fmt::Debug,
744 {
745 let Some(name_ast) = node.name() else {
746 never!(
747 "Replacement ({:?}) was generated for a {:?} without a name: {:?}",
748 replacement,
749 ident_type,
750 node
751 );
752 return;
753 };
754
755 let edition = file_id.original_file(self.db).edition(self.db);
756 let diagnostic = IncorrectCase {
757 file: file_id,
758 ident_type,
759 ident: AstPtr::new(&name_ast),
760 expected_case: replacement.expected_case,
761 ident_text: replacement.current_name.display(self.db, edition).to_string(),
762 suggested_text: replacement.suggested_text,
763 };
764
765 self.sink.push(diagnostic);
766 }
767
768 fn is_trait_impl_container(&self, container_id: ItemContainerId) -> bool {
769 if let ItemContainerId::ImplId(impl_id) = container_id
770 && self.db.impl_trait(impl_id).is_some()
771 {
772 return true;
773 }
774 false
775 }
776}