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 edition = self.edition(enum_id);
511 let mut enum_variants_replacements = data
512 .variants
513 .keys()
514 .filter_map(|name| {
515 to_camel_case(&name.display_no_db(edition).to_smolstr()).map(|new_name| {
516 Replacement {
517 current_name: name.clone(),
518 suggested_text: new_name,
519 expected_case: CaseType::UpperCamelCase,
520 }
521 })
522 })
523 .peekable();
524
525 if enum_variants_replacements.peek().is_none() {
527 return;
528 }
529
530 let enum_loc = enum_id.lookup(self.db);
531 let enum_src = enum_loc.source(self.db);
532
533 let Some(enum_variants_list) = enum_src.value.variant_list() else {
534 always!(
535 enum_variants_replacements.peek().is_none(),
536 "Replacements ({:?}) were generated for enum variants \
537 which had no fields list: {:?}",
538 enum_variants_replacements,
539 enum_src
540 );
541 return;
542 };
543 let mut enum_variants_iter = enum_variants_list.variants();
544 for variant_replacement in enum_variants_replacements {
545 let variant = loop {
548 if let Some(variant) = enum_variants_iter.next() {
549 let Some(variant_name) = variant.name() else {
550 continue;
551 };
552 if variant_name.as_name() == variant_replacement.current_name {
553 break variant;
554 }
555 } else {
556 never!(
557 "Replacement ({:?}) was generated for an enum variant \
558 which was not found: {:?}",
559 variant_replacement,
560 enum_src
561 );
562 return;
563 }
564 };
565
566 self.create_incorrect_case_diagnostic_for_ast_node(
567 variant_replacement,
568 enum_src.file_id,
569 &variant,
570 IdentType::Variant,
571 );
572 }
573 }
574
575 fn validate_enum_variant_fields(&mut self, variant_id: EnumVariantId) {
577 let variant_data = variant_id.fields(self.db);
578 if variant_data.shape != FieldsShape::Record {
579 return;
580 };
581 let edition = self.edition(variant_id);
582 let mut variant_field_replacements = variant_data
583 .fields()
584 .iter()
585 .filter_map(|(_, field)| {
586 to_lower_snake_case(&field.name.display_no_db(edition).to_smolstr()).map(
587 |new_name| Replacement {
588 current_name: field.name.clone(),
589 suggested_text: new_name,
590 expected_case: CaseType::LowerSnakeCase,
591 },
592 )
593 })
594 .peekable();
595
596 if variant_field_replacements.peek().is_none() {
598 return;
599 }
600
601 let variant_loc = variant_id.lookup(self.db);
602 let variant_src = variant_loc.source(self.db);
603
604 let Some(ast::FieldList::RecordFieldList(variant_fields_list)) =
605 variant_src.value.field_list()
606 else {
607 always!(
608 variant_field_replacements.peek().is_none(),
609 "Replacements ({:?}) were generated for an enum variant \
610 which had no fields list: {:?}",
611 variant_field_replacements.collect::<Vec<_>>(),
612 variant_src
613 );
614 return;
615 };
616 let mut variant_variants_iter = variant_fields_list.fields();
617 for field_replacement in variant_field_replacements {
618 let field = loop {
621 if let Some(field) = variant_variants_iter.next() {
622 let Some(field_name) = field.name() else {
623 continue;
624 };
625 if field_name.as_name() == field_replacement.current_name {
626 break field;
627 }
628 } else {
629 never!(
630 "Replacement ({:?}) was generated for an enum variant field \
631 which was not found: {:?}",
632 field_replacement,
633 variant_src
634 );
635 return;
636 }
637 };
638
639 self.create_incorrect_case_diagnostic_for_ast_node(
640 field_replacement,
641 variant_src.file_id,
642 &field,
643 IdentType::Field,
644 );
645 }
646 }
647
648 fn validate_const(&mut self, const_id: ConstId) {
649 let container = const_id.lookup(self.db).container;
650 if self.is_trait_impl_container(container) {
651 cov_mark::hit!(trait_impl_assoc_const_incorrect_case_ignored);
652 return;
653 }
654
655 let data = ConstSignature::of(self.db, const_id);
656 let Some(name) = &data.name else {
657 return;
658 };
659 self.create_incorrect_case_diagnostic_for_item_name(
660 const_id,
661 name,
662 CaseType::UpperSnakeCase,
663 IdentType::Constant,
664 );
665 }
666
667 fn validate_static(&mut self, static_id: StaticId) {
668 let data = StaticSignature::of(self.db, static_id);
669 if data.flags.contains(StaticFlags::EXTERN) {
670 cov_mark::hit!(extern_static_incorrect_case_ignored);
671 return;
672 }
673 if AttrFlags::query(self.db, static_id.into()).contains(AttrFlags::NO_MANGLE) {
674 cov_mark::hit!(no_mangle_static_incorrect_case_ignored);
675 return;
676 }
677
678 self.create_incorrect_case_diagnostic_for_item_name(
679 static_id,
680 &data.name,
681 CaseType::UpperSnakeCase,
682 IdentType::StaticVariable,
683 );
684 }
685
686 fn validate_type_alias(&mut self, type_alias_id: TypeAliasId) {
687 let container = type_alias_id.lookup(self.db).container;
688 if self.is_trait_impl_container(container) {
689 cov_mark::hit!(trait_impl_assoc_type_incorrect_case_ignored);
690 return;
691 }
692
693 let data = TypeAliasSignature::of(self.db, type_alias_id);
695 self.create_incorrect_case_diagnostic_for_item_name(
696 type_alias_id,
697 &data.name,
698 CaseType::UpperCamelCase,
699 IdentType::TypeAlias,
700 );
701 }
702
703 fn create_incorrect_case_diagnostic_for_item_name<N, S, L>(
704 &mut self,
705 item_id: L,
706 name: &Name,
707 expected_case: CaseType,
708 ident_type: IdentType,
709 ) where
710 N: AstNode + HasName + fmt::Debug,
711 S: HasSource<Value = N>,
712 L: Lookup<Data = S> + HasModule + Copy,
713 {
714 let to_expected_case_type = match expected_case {
715 CaseType::LowerSnakeCase => to_lower_snake_case,
716 CaseType::UpperSnakeCase => to_upper_snake_case,
717 CaseType::UpperCamelCase => to_camel_case,
718 };
719 let edition = self.edition(item_id);
720 let Some(replacement) =
721 to_expected_case_type(&name.display(self.db, edition).to_smolstr()).map(|new_name| {
722 Replacement { current_name: name.clone(), suggested_text: new_name, expected_case }
723 })
724 else {
725 return;
726 };
727
728 let item_loc = item_id.lookup(self.db);
729 let item_src = item_loc.source(self.db);
730 self.create_incorrect_case_diagnostic_for_ast_node(
731 replacement,
732 item_src.file_id,
733 &item_src.value,
734 ident_type,
735 );
736 }
737
738 fn create_incorrect_case_diagnostic_for_ast_node<T>(
739 &mut self,
740 replacement: Replacement,
741 file_id: HirFileId,
742 node: &T,
743 ident_type: IdentType,
744 ) where
745 T: AstNode + HasName + fmt::Debug,
746 {
747 let Some(name_ast) = node.name() else {
748 never!(
749 "Replacement ({:?}) was generated for a {:?} without a name: {:?}",
750 replacement,
751 ident_type,
752 node
753 );
754 return;
755 };
756
757 let edition = file_id.original_file(self.db).edition(self.db);
758 let diagnostic = IncorrectCase {
759 file: file_id,
760 ident_type,
761 ident: AstPtr::new(&name_ast),
762 expected_case: replacement.expected_case,
763 ident_text: replacement.current_name.display(self.db, edition).to_string(),
764 suggested_text: replacement.suggested_text,
765 };
766
767 self.sink.push(diagnostic);
768 }
769
770 fn is_trait_impl_container(&self, container_id: ItemContainerId) -> bool {
771 if let ItemContainerId::ImplId(impl_id) = container_id
772 && self.db.impl_trait(impl_id).is_some()
773 {
774 return true;
775 }
776 false
777 }
778}