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, 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 LowerSnakeCase,
50 UpperSnakeCase,
52 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 }
152 }
153 }
154
155 fn validate_module(&mut self, module_id: ModuleId) {
156 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 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 if !self.is_trait_impl_container(container) {
200 let data = self.db.function_signature(func);
201
202 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 self.validate_func_body(func);
221 }
222
223 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 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 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 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 self.validate_struct_fields(struct_id);
305 }
306
307 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 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 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 self.create_incorrect_case_diagnostic_for_item_name(
385 enum_id,
386 &data.name,
387 CaseType::UpperCamelCase,
388 IdentType::Enum,
389 );
390
391 self.validate_enum_variants(enum_id)
393 }
394
395 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 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 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 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 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 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 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}