aboutsummaryrefslogtreecommitdiff
path: root/crates/hir_ty/src/diagnostics
diff options
context:
space:
mode:
Diffstat (limited to 'crates/hir_ty/src/diagnostics')
-rw-r--r--crates/hir_ty/src/diagnostics/decl_check.rs833
-rw-r--r--crates/hir_ty/src/diagnostics/decl_check/case_conv.rs194
-rw-r--r--crates/hir_ty/src/diagnostics/unsafe_check.rs6
3 files changed, 1030 insertions, 3 deletions
diff --git a/crates/hir_ty/src/diagnostics/decl_check.rs b/crates/hir_ty/src/diagnostics/decl_check.rs
new file mode 100644
index 000000000..f987636fe
--- /dev/null
+++ b/crates/hir_ty/src/diagnostics/decl_check.rs
@@ -0,0 +1,833 @@
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
13mod case_conv;
14
15use hir_def::{
16 adt::VariantData,
17 expr::{Pat, PatId},
18 src::HasSource,
19 AdtId, ConstId, EnumId, FunctionId, Lookup, ModuleDefId, StaticId, StructId,
20};
21use hir_expand::{
22 diagnostics::DiagnosticSink,
23 name::{AsName, Name},
24};
25use syntax::{
26 ast::{self, NameOwner},
27 AstNode, AstPtr,
28};
29
30use crate::{
31 db::HirDatabase,
32 diagnostics::{decl_check::case_conv::*, CaseType, IncorrectCase},
33};
34
35pub(super) struct DeclValidator<'a, 'b: 'a> {
36 owner: ModuleDefId,
37 sink: &'a mut DiagnosticSink<'b>,
38}
39
40#[derive(Debug)]
41struct Replacement {
42 current_name: Name,
43 suggested_text: String,
44 expected_case: CaseType,
45}
46
47impl<'a, 'b> DeclValidator<'a, 'b> {
48 pub(super) fn new(
49 owner: ModuleDefId,
50 sink: &'a mut DiagnosticSink<'b>,
51 ) -> DeclValidator<'a, 'b> {
52 DeclValidator { owner, sink }
53 }
54
55 pub(super) fn validate_item(&mut self, db: &dyn HirDatabase) {
56 match self.owner {
57 ModuleDefId::FunctionId(func) => self.validate_func(db, func),
58 ModuleDefId::AdtId(adt) => self.validate_adt(db, adt),
59 ModuleDefId::ConstId(const_id) => self.validate_const(db, const_id),
60 ModuleDefId::StaticId(static_id) => self.validate_static(db, static_id),
61 _ => return,
62 }
63 }
64
65 fn validate_adt(&mut self, db: &dyn HirDatabase, adt: AdtId) {
66 match adt {
67 AdtId::StructId(struct_id) => self.validate_struct(db, struct_id),
68 AdtId::EnumId(enum_id) => self.validate_enum(db, enum_id),
69 AdtId::UnionId(_) => {
70 // Unions aren't yet supported by this validator.
71 }
72 }
73 }
74
75 fn validate_func(&mut self, db: &dyn HirDatabase, func: FunctionId) {
76 let data = db.function_data(func);
77 let body = db.body(func.into());
78
79 // 1. Check the function name.
80 let function_name = data.name.to_string();
81 let fn_name_replacement = if let Some(new_name) = to_lower_snake_case(&function_name) {
82 let replacement = Replacement {
83 current_name: data.name.clone(),
84 suggested_text: new_name,
85 expected_case: CaseType::LowerSnakeCase,
86 };
87 Some(replacement)
88 } else {
89 None
90 };
91
92 // 2. Check the param names.
93 let mut fn_param_replacements = Vec::new();
94
95 for pat_id in body.params.iter().cloned() {
96 let pat = &body[pat_id];
97
98 let param_name = match pat {
99 Pat::Bind { name, .. } => name,
100 _ => continue,
101 };
102
103 let name = param_name.to_string();
104 if let Some(new_name) = to_lower_snake_case(&name) {
105 let replacement = Replacement {
106 current_name: param_name.clone(),
107 suggested_text: new_name,
108 expected_case: CaseType::LowerSnakeCase,
109 };
110 fn_param_replacements.push(replacement);
111 }
112 }
113
114 // 3. Check the patterns inside the function body.
115 let mut pats_replacements = Vec::new();
116
117 for (pat_idx, pat) in body.pats.iter() {
118 if body.params.contains(&pat_idx) {
119 // We aren't interested in function parameters, we've processed them above.
120 continue;
121 }
122
123 let bind_name = match pat {
124 Pat::Bind { name, .. } => name,
125 _ => continue,
126 };
127
128 let name = bind_name.to_string();
129 if let Some(new_name) = to_lower_snake_case(&name) {
130 let replacement = Replacement {
131 current_name: bind_name.clone(),
132 suggested_text: new_name,
133 expected_case: CaseType::LowerSnakeCase,
134 };
135 pats_replacements.push((pat_idx, replacement));
136 }
137 }
138
139 // 4. If there is at least one element to spawn a warning on, go to the source map and generate a warning.
140 self.create_incorrect_case_diagnostic_for_func(
141 func,
142 db,
143 fn_name_replacement,
144 fn_param_replacements,
145 );
146 self.create_incorrect_case_diagnostic_for_variables(func, db, pats_replacements);
147
148 // 5. Recursively validate inner scope items, such as static variables and constants.
149 for (item_id, _) in body.item_scope.values() {
150 let mut validator = DeclValidator::new(item_id, self.sink);
151 validator.validate_item(db);
152 }
153 }
154
155 /// Given the information about incorrect names in the function declaration, looks up into the source code
156 /// for exact locations and adds diagnostics into the sink.
157 fn create_incorrect_case_diagnostic_for_func(
158 &mut self,
159 func: FunctionId,
160 db: &dyn HirDatabase,
161 fn_name_replacement: Option<Replacement>,
162 fn_param_replacements: Vec<Replacement>,
163 ) {
164 // XXX: only look at sources if we do have incorrect names
165 if fn_name_replacement.is_none() && fn_param_replacements.is_empty() {
166 return;
167 }
168
169 let fn_loc = func.lookup(db.upcast());
170 let fn_src = fn_loc.source(db.upcast());
171
172 // 1. Diagnostic for function name.
173 if let Some(replacement) = fn_name_replacement {
174 let ast_ptr = match fn_src.value.name() {
175 Some(name) => name,
176 None => {
177 // We don't want rust-analyzer to panic over this, but it is definitely some kind of error in the logic.
178 log::error!(
179 "Replacement ({:?}) was generated for a function without a name: {:?}",
180 replacement,
181 fn_src
182 );
183 return;
184 }
185 };
186
187 let diagnostic = IncorrectCase {
188 file: fn_src.file_id,
189 ident_type: "Function".to_string(),
190 ident: AstPtr::new(&ast_ptr).into(),
191 expected_case: replacement.expected_case,
192 ident_text: replacement.current_name.to_string(),
193 suggested_text: replacement.suggested_text,
194 };
195
196 self.sink.push(diagnostic);
197 }
198
199 // 2. Diagnostics for function params.
200 let fn_params_list = match fn_src.value.param_list() {
201 Some(params) => params,
202 None => {
203 if !fn_param_replacements.is_empty() {
204 log::error!(
205 "Replacements ({:?}) were generated for a function parameters which had no parameters list: {:?}",
206 fn_param_replacements, fn_src
207 );
208 }
209 return;
210 }
211 };
212 let mut fn_params_iter = fn_params_list.params();
213 for param_to_rename in fn_param_replacements {
214 // We assume that parameters in replacement are in the same order as in the
215 // actual params list, but just some of them (ones that named correctly) are skipped.
216 let ast_ptr: ast::Name = loop {
217 match fn_params_iter.next() {
218 Some(element)
219 if pat_equals_to_name(element.pat(), &param_to_rename.current_name) =>
220 {
221 if let ast::Pat::IdentPat(pat) = element.pat().unwrap() {
222 break pat.name().unwrap();
223 } else {
224 // This is critical. If we consider this parameter the expected one,
225 // it **must** have a name.
226 panic!(
227 "Pattern {:?} equals to expected replacement {:?}, but has no name",
228 element, param_to_rename
229 );
230 }
231 }
232 Some(_) => {}
233 None => {
234 log::error!(
235 "Replacement ({:?}) was generated for a function parameter which was not found: {:?}",
236 param_to_rename, fn_src
237 );
238 return;
239 }
240 }
241 };
242
243 let diagnostic = IncorrectCase {
244 file: fn_src.file_id,
245 ident_type: "Argument".to_string(),
246 ident: AstPtr::new(&ast_ptr).into(),
247 expected_case: param_to_rename.expected_case,
248 ident_text: param_to_rename.current_name.to_string(),
249 suggested_text: param_to_rename.suggested_text,
250 };
251
252 self.sink.push(diagnostic);
253 }
254 }
255
256 /// Given the information about incorrect variable names, looks up into the source code
257 /// for exact locations and adds diagnostics into the sink.
258 fn create_incorrect_case_diagnostic_for_variables(
259 &mut self,
260 func: FunctionId,
261 db: &dyn HirDatabase,
262 pats_replacements: Vec<(PatId, Replacement)>,
263 ) {
264 // XXX: only look at source_map if we do have missing fields
265 if pats_replacements.is_empty() {
266 return;
267 }
268
269 let (_, source_map) = db.body_with_source_map(func.into());
270
271 for (id, replacement) in pats_replacements {
272 if let Ok(source_ptr) = source_map.pat_syntax(id) {
273 if let Some(expr) = source_ptr.value.as_ref().left() {
274 let root = source_ptr.file_syntax(db.upcast());
275 if let ast::Pat::IdentPat(ident_pat) = expr.to_node(&root) {
276 let parent = match ident_pat.syntax().parent() {
277 Some(parent) => parent,
278 None => continue,
279 };
280 let name_ast = match ident_pat.name() {
281 Some(name_ast) => name_ast,
282 None => continue,
283 };
284
285 // We have to check that it's either `let var = ...` or `var @ Variant(_)` statement,
286 // because e.g. match arms are patterns as well.
287 // In other words, we check that it's a named variable binding.
288 let is_binding = ast::LetStmt::cast(parent.clone()).is_some()
289 || (ast::MatchArm::cast(parent).is_some()
290 && ident_pat.at_token().is_some());
291 if !is_binding {
292 // This pattern is not an actual variable declaration, e.g. `Some(val) => {..}` match arm.
293 continue;
294 }
295
296 let diagnostic = IncorrectCase {
297 file: source_ptr.file_id,
298 ident_type: "Variable".to_string(),
299 ident: AstPtr::new(&name_ast).into(),
300 expected_case: replacement.expected_case,
301 ident_text: replacement.current_name.to_string(),
302 suggested_text: replacement.suggested_text,
303 };
304
305 self.sink.push(diagnostic);
306 }
307 }
308 }
309 }
310 }
311
312 fn validate_struct(&mut self, db: &dyn HirDatabase, struct_id: StructId) {
313 let data = db.struct_data(struct_id);
314
315 // 1. Check the structure name.
316 let struct_name = data.name.to_string();
317 let struct_name_replacement = if let Some(new_name) = to_camel_case(&struct_name) {
318 let replacement = Replacement {
319 current_name: data.name.clone(),
320 suggested_text: new_name,
321 expected_case: CaseType::UpperCamelCase,
322 };
323 Some(replacement)
324 } else {
325 None
326 };
327
328 // 2. Check the field names.
329 let mut struct_fields_replacements = Vec::new();
330
331 if let VariantData::Record(fields) = data.variant_data.as_ref() {
332 for (_, field) in fields.iter() {
333 let field_name = field.name.to_string();
334 if let Some(new_name) = to_lower_snake_case(&field_name) {
335 let replacement = Replacement {
336 current_name: field.name.clone(),
337 suggested_text: new_name,
338 expected_case: CaseType::LowerSnakeCase,
339 };
340 struct_fields_replacements.push(replacement);
341 }
342 }
343 }
344
345 // 3. If there is at least one element to spawn a warning on, go to the source map and generate a warning.
346 self.create_incorrect_case_diagnostic_for_struct(
347 struct_id,
348 db,
349 struct_name_replacement,
350 struct_fields_replacements,
351 );
352 }
353
354 /// Given the information about incorrect names in the struct declaration, looks up into the source code
355 /// for exact locations and adds diagnostics into the sink.
356 fn create_incorrect_case_diagnostic_for_struct(
357 &mut self,
358 struct_id: StructId,
359 db: &dyn HirDatabase,
360 struct_name_replacement: Option<Replacement>,
361 struct_fields_replacements: Vec<Replacement>,
362 ) {
363 // XXX: only look at sources if we do have incorrect names
364 if struct_name_replacement.is_none() && struct_fields_replacements.is_empty() {
365 return;
366 }
367
368 let struct_loc = struct_id.lookup(db.upcast());
369 let struct_src = struct_loc.source(db.upcast());
370
371 if let Some(replacement) = struct_name_replacement {
372 let ast_ptr = match struct_src.value.name() {
373 Some(name) => name,
374 None => {
375 // We don't want rust-analyzer to panic over this, but it is definitely some kind of error in the logic.
376 log::error!(
377 "Replacement ({:?}) was generated for a structure without a name: {:?}",
378 replacement,
379 struct_src
380 );
381 return;
382 }
383 };
384
385 let diagnostic = IncorrectCase {
386 file: struct_src.file_id,
387 ident_type: "Structure".to_string(),
388 ident: AstPtr::new(&ast_ptr).into(),
389 expected_case: replacement.expected_case,
390 ident_text: replacement.current_name.to_string(),
391 suggested_text: replacement.suggested_text,
392 };
393
394 self.sink.push(diagnostic);
395 }
396
397 let struct_fields_list = match struct_src.value.field_list() {
398 Some(ast::FieldList::RecordFieldList(fields)) => fields,
399 _ => {
400 if !struct_fields_replacements.is_empty() {
401 log::error!(
402 "Replacements ({:?}) were generated for a structure fields which had no fields list: {:?}",
403 struct_fields_replacements, struct_src
404 );
405 }
406 return;
407 }
408 };
409 let mut struct_fields_iter = struct_fields_list.fields();
410 for field_to_rename in struct_fields_replacements {
411 // We assume that parameters in replacement are in the same order as in the
412 // actual params list, but just some of them (ones that named correctly) are skipped.
413 let ast_ptr = loop {
414 match struct_fields_iter.next() {
415 Some(element) if names_equal(element.name(), &field_to_rename.current_name) => {
416 break element.name().unwrap()
417 }
418 Some(_) => {}
419 None => {
420 log::error!(
421 "Replacement ({:?}) was generated for a structure field which was not found: {:?}",
422 field_to_rename, struct_src
423 );
424 return;
425 }
426 }
427 };
428
429 let diagnostic = IncorrectCase {
430 file: struct_src.file_id,
431 ident_type: "Field".to_string(),
432 ident: AstPtr::new(&ast_ptr).into(),
433 expected_case: field_to_rename.expected_case,
434 ident_text: field_to_rename.current_name.to_string(),
435 suggested_text: field_to_rename.suggested_text,
436 };
437
438 self.sink.push(diagnostic);
439 }
440 }
441
442 fn validate_enum(&mut self, db: &dyn HirDatabase, enum_id: EnumId) {
443 let data = db.enum_data(enum_id);
444
445 // 1. Check the enum name.
446 let enum_name = data.name.to_string();
447 let enum_name_replacement = if let Some(new_name) = to_camel_case(&enum_name) {
448 let replacement = Replacement {
449 current_name: data.name.clone(),
450 suggested_text: new_name,
451 expected_case: CaseType::UpperCamelCase,
452 };
453 Some(replacement)
454 } else {
455 None
456 };
457
458 // 2. Check the field names.
459 let mut enum_fields_replacements = Vec::new();
460
461 for (_, variant) in data.variants.iter() {
462 let variant_name = variant.name.to_string();
463 if let Some(new_name) = to_camel_case(&variant_name) {
464 let replacement = Replacement {
465 current_name: variant.name.clone(),
466 suggested_text: new_name,
467 expected_case: CaseType::UpperCamelCase,
468 };
469 enum_fields_replacements.push(replacement);
470 }
471 }
472
473 // 3. If there is at least one element to spawn a warning on, go to the source map and generate a warning.
474 self.create_incorrect_case_diagnostic_for_enum(
475 enum_id,
476 db,
477 enum_name_replacement,
478 enum_fields_replacements,
479 )
480 }
481
482 /// Given the information about incorrect names in the struct declaration, looks up into the source code
483 /// for exact locations and adds diagnostics into the sink.
484 fn create_incorrect_case_diagnostic_for_enum(
485 &mut self,
486 enum_id: EnumId,
487 db: &dyn HirDatabase,
488 enum_name_replacement: Option<Replacement>,
489 enum_variants_replacements: Vec<Replacement>,
490 ) {
491 // XXX: only look at sources if we do have incorrect names
492 if enum_name_replacement.is_none() && enum_variants_replacements.is_empty() {
493 return;
494 }
495
496 let enum_loc = enum_id.lookup(db.upcast());
497 let enum_src = enum_loc.source(db.upcast());
498
499 if let Some(replacement) = enum_name_replacement {
500 let ast_ptr = match enum_src.value.name() {
501 Some(name) => name,
502 None => {
503 // We don't want rust-analyzer to panic over this, but it is definitely some kind of error in the logic.
504 log::error!(
505 "Replacement ({:?}) was generated for a enum without a name: {:?}",
506 replacement,
507 enum_src
508 );
509 return;
510 }
511 };
512
513 let diagnostic = IncorrectCase {
514 file: enum_src.file_id,
515 ident_type: "Enum".to_string(),
516 ident: AstPtr::new(&ast_ptr).into(),
517 expected_case: replacement.expected_case,
518 ident_text: replacement.current_name.to_string(),
519 suggested_text: replacement.suggested_text,
520 };
521
522 self.sink.push(diagnostic);
523 }
524
525 let enum_variants_list = match enum_src.value.variant_list() {
526 Some(variants) => variants,
527 _ => {
528 if !enum_variants_replacements.is_empty() {
529 log::error!(
530 "Replacements ({:?}) were generated for a enum variants which had no fields list: {:?}",
531 enum_variants_replacements, enum_src
532 );
533 }
534 return;
535 }
536 };
537 let mut enum_variants_iter = enum_variants_list.variants();
538 for variant_to_rename in enum_variants_replacements {
539 // We assume that parameters in replacement are in the same order as in the
540 // actual params list, but just some of them (ones that named correctly) are skipped.
541 let ast_ptr = loop {
542 match enum_variants_iter.next() {
543 Some(variant)
544 if names_equal(variant.name(), &variant_to_rename.current_name) =>
545 {
546 break variant.name().unwrap()
547 }
548 Some(_) => {}
549 None => {
550 log::error!(
551 "Replacement ({:?}) was generated for a enum variant which was not found: {:?}",
552 variant_to_rename, enum_src
553 );
554 return;
555 }
556 }
557 };
558
559 let diagnostic = IncorrectCase {
560 file: enum_src.file_id,
561 ident_type: "Variant".to_string(),
562 ident: AstPtr::new(&ast_ptr).into(),
563 expected_case: variant_to_rename.expected_case,
564 ident_text: variant_to_rename.current_name.to_string(),
565 suggested_text: variant_to_rename.suggested_text,
566 };
567
568 self.sink.push(diagnostic);
569 }
570 }
571
572 fn validate_const(&mut self, db: &dyn HirDatabase, const_id: ConstId) {
573 let data = db.const_data(const_id);
574
575 let name = match &data.name {
576 Some(name) => name,
577 None => return,
578 };
579
580 let const_name = name.to_string();
581 let replacement = if let Some(new_name) = to_upper_snake_case(&const_name) {
582 Replacement {
583 current_name: name.clone(),
584 suggested_text: new_name,
585 expected_case: CaseType::UpperSnakeCase,
586 }
587 } else {
588 // Nothing to do here.
589 return;
590 };
591
592 let const_loc = const_id.lookup(db.upcast());
593 let const_src = const_loc.source(db.upcast());
594
595 let ast_ptr = match const_src.value.name() {
596 Some(name) => name,
597 None => return,
598 };
599
600 let diagnostic = IncorrectCase {
601 file: const_src.file_id,
602 ident_type: "Constant".to_string(),
603 ident: AstPtr::new(&ast_ptr).into(),
604 expected_case: replacement.expected_case,
605 ident_text: replacement.current_name.to_string(),
606 suggested_text: replacement.suggested_text,
607 };
608
609 self.sink.push(diagnostic);
610 }
611
612 fn validate_static(&mut self, db: &dyn HirDatabase, static_id: StaticId) {
613 let data = db.static_data(static_id);
614
615 let name = match &data.name {
616 Some(name) => name,
617 None => return,
618 };
619
620 let static_name = name.to_string();
621 let replacement = if let Some(new_name) = to_upper_snake_case(&static_name) {
622 Replacement {
623 current_name: name.clone(),
624 suggested_text: new_name,
625 expected_case: CaseType::UpperSnakeCase,
626 }
627 } else {
628 // Nothing to do here.
629 return;
630 };
631
632 let static_loc = static_id.lookup(db.upcast());
633 let static_src = static_loc.source(db.upcast());
634
635 let ast_ptr = match static_src.value.name() {
636 Some(name) => name,
637 None => return,
638 };
639
640 let diagnostic = IncorrectCase {
641 file: static_src.file_id,
642 ident_type: "Static variable".to_string(),
643 ident: AstPtr::new(&ast_ptr).into(),
644 expected_case: replacement.expected_case,
645 ident_text: replacement.current_name.to_string(),
646 suggested_text: replacement.suggested_text,
647 };
648
649 self.sink.push(diagnostic);
650 }
651}
652
653fn names_equal(left: Option<ast::Name>, right: &Name) -> bool {
654 if let Some(left) = left {
655 &left.as_name() == right
656 } else {
657 false
658 }
659}
660
661fn pat_equals_to_name(pat: Option<ast::Pat>, name: &Name) -> bool {
662 if let Some(ast::Pat::IdentPat(ident)) = pat {
663 ident.to_string() == name.to_string()
664 } else {
665 false
666 }
667}
668
669#[cfg(test)]
670mod tests {
671 use crate::diagnostics::tests::check_diagnostics;
672
673 #[test]
674 fn incorrect_function_name() {
675 check_diagnostics(
676 r#"
677fn NonSnakeCaseName() {}
678// ^^^^^^^^^^^^^^^^ Function `NonSnakeCaseName` should have snake_case name, e.g. `non_snake_case_name`
679"#,
680 );
681 }
682
683 #[test]
684 fn incorrect_function_params() {
685 check_diagnostics(
686 r#"
687fn foo(SomeParam: u8) {}
688 // ^^^^^^^^^ Argument `SomeParam` should have snake_case name, e.g. `some_param`
689
690fn foo2(ok_param: &str, CAPS_PARAM: u8) {}
691 // ^^^^^^^^^^ Argument `CAPS_PARAM` should have snake_case name, e.g. `caps_param`
692"#,
693 );
694 }
695
696 #[test]
697 fn incorrect_variable_names() {
698 check_diagnostics(
699 r#"
700fn foo() {
701 let SOME_VALUE = 10;
702 // ^^^^^^^^^^ Variable `SOME_VALUE` should have snake_case name, e.g. `some_value`
703 let AnotherValue = 20;
704 // ^^^^^^^^^^^^ Variable `AnotherValue` should have snake_case name, e.g. `another_value`
705}
706"#,
707 );
708 }
709
710 #[test]
711 fn incorrect_struct_name() {
712 check_diagnostics(
713 r#"
714struct non_camel_case_name {}
715 // ^^^^^^^^^^^^^^^^^^^ Structure `non_camel_case_name` should have CamelCase name, e.g. `NonCamelCaseName`
716"#,
717 );
718 }
719
720 #[test]
721 fn incorrect_struct_field() {
722 check_diagnostics(
723 r#"
724struct SomeStruct { SomeField: u8 }
725 // ^^^^^^^^^ Field `SomeField` should have snake_case name, e.g. `some_field`
726"#,
727 );
728 }
729
730 #[test]
731 fn incorrect_enum_name() {
732 check_diagnostics(
733 r#"
734enum some_enum { Val(u8) }
735 // ^^^^^^^^^ Enum `some_enum` should have CamelCase name, e.g. `SomeEnum`
736"#,
737 );
738 }
739
740 #[test]
741 fn incorrect_enum_variant_name() {
742 check_diagnostics(
743 r#"
744enum SomeEnum { SOME_VARIANT(u8) }
745 // ^^^^^^^^^^^^ Variant `SOME_VARIANT` should have CamelCase name, e.g. `SomeVariant`
746"#,
747 );
748 }
749
750 #[test]
751 fn incorrect_const_name() {
752 check_diagnostics(
753 r#"
754const some_weird_const: u8 = 10;
755 // ^^^^^^^^^^^^^^^^ Constant `some_weird_const` should have UPPER_SNAKE_CASE name, e.g. `SOME_WEIRD_CONST`
756
757fn func() {
758 const someConstInFunc: &str = "hi there";
759 // ^^^^^^^^^^^^^^^ Constant `someConstInFunc` should have UPPER_SNAKE_CASE name, e.g. `SOME_CONST_IN_FUNC`
760
761}
762"#,
763 );
764 }
765
766 #[test]
767 fn incorrect_static_name() {
768 check_diagnostics(
769 r#"
770static some_weird_const: u8 = 10;
771 // ^^^^^^^^^^^^^^^^ Static variable `some_weird_const` should have UPPER_SNAKE_CASE name, e.g. `SOME_WEIRD_CONST`
772
773fn func() {
774 static someConstInFunc: &str = "hi there";
775 // ^^^^^^^^^^^^^^^ Static variable `someConstInFunc` should have UPPER_SNAKE_CASE name, e.g. `SOME_CONST_IN_FUNC`
776}
777"#,
778 );
779 }
780
781 #[test]
782 fn fn_inside_impl_struct() {
783 check_diagnostics(
784 r#"
785struct someStruct;
786 // ^^^^^^^^^^ Structure `someStruct` should have CamelCase name, e.g. `SomeStruct`
787
788impl someStruct {
789 fn SomeFunc(&self) {
790 // ^^^^^^^^ Function `SomeFunc` should have snake_case name, e.g. `some_func`
791 static someConstInFunc: &str = "hi there";
792 // ^^^^^^^^^^^^^^^ Static variable `someConstInFunc` should have UPPER_SNAKE_CASE name, e.g. `SOME_CONST_IN_FUNC`
793 let WHY_VAR_IS_CAPS = 10;
794 // ^^^^^^^^^^^^^^^ Variable `WHY_VAR_IS_CAPS` should have snake_case name, e.g. `why_var_is_caps`
795 }
796}
797"#,
798 );
799 }
800
801 #[test]
802 fn no_diagnostic_for_enum_varinats() {
803 check_diagnostics(
804 r#"
805enum Option { Some, None }
806
807fn main() {
808 match Option::None {
809 None => (),
810 Some => (),
811 }
812}
813"#,
814 );
815 }
816
817 #[test]
818 fn non_let_bind() {
819 check_diagnostics(
820 r#"
821enum Option { Some, None }
822
823fn main() {
824 match Option::None {
825 SOME_VAR @ None => (),
826 // ^^^^^^^^ Variable `SOME_VAR` should have snake_case name, e.g. `some_var`
827 Some => (),
828 }
829}
830"#,
831 );
832 }
833}
diff --git a/crates/hir_ty/src/diagnostics/decl_check/case_conv.rs b/crates/hir_ty/src/diagnostics/decl_check/case_conv.rs
new file mode 100644
index 000000000..3800f2a6b
--- /dev/null
+++ b/crates/hir_ty/src/diagnostics/decl_check/case_conv.rs
@@ -0,0 +1,194 @@
1//! Functions for string case manipulation, such as detecting the identifier case,
2//! and converting it into appropriate form.
3
4#[derive(Debug)]
5enum DetectedCase {
6 LowerCamelCase,
7 UpperCamelCase,
8 LowerSnakeCase,
9 UpperSnakeCase,
10 Unknown,
11}
12
13fn detect_case(ident: &str) -> DetectedCase {
14 let trimmed_ident = ident.trim_matches('_');
15 let first_lowercase = trimmed_ident.starts_with(|chr: char| chr.is_ascii_lowercase());
16 let mut has_lowercase = first_lowercase;
17 let mut has_uppercase = false;
18 let mut has_underscore = false;
19
20 for chr in trimmed_ident.chars() {
21 if chr == '_' {
22 has_underscore = true;
23 } else if chr.is_ascii_uppercase() {
24 has_uppercase = true;
25 } else if chr.is_ascii_lowercase() {
26 has_lowercase = true;
27 }
28 }
29
30 if has_uppercase {
31 if !has_lowercase {
32 DetectedCase::UpperSnakeCase
33 } else if !has_underscore {
34 if first_lowercase {
35 DetectedCase::LowerCamelCase
36 } else {
37 DetectedCase::UpperCamelCase
38 }
39 } else {
40 // It has uppercase, it has lowercase, it has underscore.
41 // No assumptions here
42 DetectedCase::Unknown
43 }
44 } else {
45 DetectedCase::LowerSnakeCase
46 }
47}
48
49/// Converts an identifier to an UpperCamelCase form.
50/// Returns `None` if the string is already is UpperCamelCase.
51pub fn to_camel_case(ident: &str) -> Option<String> {
52 let detected_case = detect_case(ident);
53
54 match detected_case {
55 DetectedCase::UpperCamelCase => return None,
56 DetectedCase::LowerCamelCase => {
57 let mut first_capitalized = false;
58 let output = ident
59 .chars()
60 .map(|chr| {
61 if !first_capitalized && chr.is_ascii_lowercase() {
62 first_capitalized = true;
63 chr.to_ascii_uppercase()
64 } else {
65 chr
66 }
67 })
68 .collect();
69 return Some(output);
70 }
71 _ => {}
72 }
73
74 let mut output = String::with_capacity(ident.len());
75
76 let mut capital_added = false;
77 for chr in ident.chars() {
78 if chr.is_alphabetic() {
79 if !capital_added {
80 output.push(chr.to_ascii_uppercase());
81 capital_added = true;
82 } else {
83 output.push(chr.to_ascii_lowercase());
84 }
85 } else if chr == '_' {
86 // Skip this character and make the next one capital.
87 capital_added = false;
88 } else {
89 // Put the characted as-is.
90 output.push(chr);
91 }
92 }
93
94 if output == ident {
95 // While we didn't detect the correct case at the beginning, there
96 // may be special cases: e.g. `A` is both valid CamelCase and UPPER_SNAKE_CASE.
97 None
98 } else {
99 Some(output)
100 }
101}
102
103/// Converts an identifier to a lower_snake_case form.
104/// Returns `None` if the string is already in lower_snake_case.
105pub fn to_lower_snake_case(ident: &str) -> Option<String> {
106 // First, assume that it's UPPER_SNAKE_CASE.
107 match detect_case(ident) {
108 DetectedCase::LowerSnakeCase => return None,
109 DetectedCase::UpperSnakeCase => {
110 return Some(ident.chars().map(|chr| chr.to_ascii_lowercase()).collect())
111 }
112 _ => {}
113 }
114
115 // Otherwise, assume that it's CamelCase.
116 let lower_snake_case = stdx::to_lower_snake_case(ident);
117
118 if lower_snake_case == ident {
119 // While we didn't detect the correct case at the beginning, there
120 // may be special cases: e.g. `a` is both valid camelCase and snake_case.
121 None
122 } else {
123 Some(lower_snake_case)
124 }
125}
126
127/// Converts an identifier to an UPPER_SNAKE_CASE form.
128/// Returns `None` if the string is already is UPPER_SNAKE_CASE.
129pub fn to_upper_snake_case(ident: &str) -> Option<String> {
130 match detect_case(ident) {
131 DetectedCase::UpperSnakeCase => return None,
132 DetectedCase::LowerSnakeCase => {
133 return Some(ident.chars().map(|chr| chr.to_ascii_uppercase()).collect())
134 }
135 _ => {}
136 }
137
138 // Normalize the string from whatever form it's in currently, and then just make it uppercase.
139 let upper_snake_case = stdx::to_upper_snake_case(ident);
140
141 if upper_snake_case == ident {
142 // While we didn't detect the correct case at the beginning, there
143 // may be special cases: e.g. `A` is both valid CamelCase and UPPER_SNAKE_CASE.
144 None
145 } else {
146 Some(upper_snake_case)
147 }
148}
149
150#[cfg(test)]
151mod tests {
152 use super::*;
153 use expect_test::{expect, Expect};
154
155 fn check<F: Fn(&str) -> Option<String>>(fun: F, input: &str, expect: Expect) {
156 // `None` is translated to empty string, meaning that there is nothing to fix.
157 let output = fun(input).unwrap_or_default();
158
159 expect.assert_eq(&output);
160 }
161
162 #[test]
163 fn test_to_lower_snake_case() {
164 check(to_lower_snake_case, "lower_snake_case", expect![[""]]);
165 check(to_lower_snake_case, "UPPER_SNAKE_CASE", expect![["upper_snake_case"]]);
166 check(to_lower_snake_case, "Weird_Case", expect![["weird_case"]]);
167 check(to_lower_snake_case, "CamelCase", expect![["camel_case"]]);
168 check(to_lower_snake_case, "lowerCamelCase", expect![["lower_camel_case"]]);
169 check(to_lower_snake_case, "a", expect![[""]]);
170 }
171
172 #[test]
173 fn test_to_camel_case() {
174 check(to_camel_case, "CamelCase", expect![[""]]);
175 check(to_camel_case, "CamelCase_", expect![[""]]);
176 check(to_camel_case, "_CamelCase", expect![[""]]);
177 check(to_camel_case, "lowerCamelCase", expect![["LowerCamelCase"]]);
178 check(to_camel_case, "lower_snake_case", expect![["LowerSnakeCase"]]);
179 check(to_camel_case, "UPPER_SNAKE_CASE", expect![["UpperSnakeCase"]]);
180 check(to_camel_case, "Weird_Case", expect![["WeirdCase"]]);
181 check(to_camel_case, "name", expect![["Name"]]);
182 check(to_camel_case, "A", expect![[""]]);
183 }
184
185 #[test]
186 fn test_to_upper_snake_case() {
187 check(to_upper_snake_case, "UPPER_SNAKE_CASE", expect![[""]]);
188 check(to_upper_snake_case, "lower_snake_case", expect![["LOWER_SNAKE_CASE"]]);
189 check(to_upper_snake_case, "Weird_Case", expect![["WEIRD_CASE"]]);
190 check(to_upper_snake_case, "CamelCase", expect![["CAMEL_CASE"]]);
191 check(to_upper_snake_case, "lowerCamelCase", expect![["LOWER_CAMEL_CASE"]]);
192 check(to_upper_snake_case, "A", expect![[""]]);
193 }
194}
diff --git a/crates/hir_ty/src/diagnostics/unsafe_check.rs b/crates/hir_ty/src/diagnostics/unsafe_check.rs
index 61ffbf5d1..21a121aad 100644
--- a/crates/hir_ty/src/diagnostics/unsafe_check.rs
+++ b/crates/hir_ty/src/diagnostics/unsafe_check.rs
@@ -190,13 +190,13 @@ struct Ty {
190 a: u8, 190 a: u8,
191} 191}
192 192
193static mut static_mut: Ty = Ty { a: 0 }; 193static mut STATIC_MUT: Ty = Ty { a: 0 };
194 194
195fn main() { 195fn main() {
196 let x = static_mut.a; 196 let x = STATIC_MUT.a;
197 //^^^^^^^^^^ This operation is unsafe and requires an unsafe function or block 197 //^^^^^^^^^^ This operation is unsafe and requires an unsafe function or block
198 unsafe { 198 unsafe {
199 let x = static_mut.a; 199 let x = STATIC_MUT.a;
200 } 200 }
201} 201}
202"#, 202"#,