1use crate::{AnalysisContext, Finding, Location, Plugin, Severity, SmellCategory};
2
3pub struct NamingAnalyzer {
5 pub min_name_length: usize,
6 pub max_name_length: usize,
7}
8
9impl Default for NamingAnalyzer {
10 fn default() -> Self {
11 Self {
12 min_name_length: 2,
13 max_name_length: 50,
14 }
15 }
16}
17
18impl Plugin for NamingAnalyzer {
19 fn name(&self) -> &str {
20 "naming"
21 }
22
23 fn description(&self) -> &str {
24 "Naming convention violations"
25 }
26
27 fn analyze(&self, ctx: &AnalysisContext) -> Vec<Finding> {
28 let mut findings = Vec::new();
29 self.check_functions(ctx, &mut findings);
30 self.check_classes(ctx, &mut findings);
31 findings
32 }
33}
34
35impl NamingAnalyzer {
36 fn check_functions(&self, ctx: &AnalysisContext, findings: &mut Vec<Finding>) {
37 for f in &ctx.model.functions {
38 let check = NameCheck {
39 name: &f.name,
40 kind: "Function",
41 path: &ctx.file.path,
42 start_line: f.start_line,
43 start_col: f.name_col,
44 end_line: f.start_line,
45 end_col: f.name_end_col,
46 };
47 if let Some(finding) = check_name(&check, self.min_name_length, self.max_name_length) {
48 findings.push(finding);
49 }
50 }
51 }
52
53 fn check_classes(&self, ctx: &AnalysisContext, findings: &mut Vec<Finding>) {
54 for c in &ctx.model.classes {
55 if let Some(f) = check_pascal_case(c, &ctx.file.path) {
56 findings.push(f);
57 }
58 let check = NameCheck {
59 name: &c.name,
60 kind: "Class",
61 path: &ctx.file.path,
62 start_line: c.start_line,
63 start_col: c.name_col,
64 end_line: c.start_line,
65 end_col: c.name_end_col,
66 };
67 if let Some(f) = check_name(&check, self.min_name_length, self.max_name_length) {
68 findings.push(f);
69 }
70 }
71 }
72}
73
74fn check_pascal_case(c: &crate::ClassInfo, path: &std::path::Path) -> Option<Finding> {
76 if c.name.is_empty() || c.name.chars().next().is_some_and(|ch| ch.is_uppercase()) {
77 return None;
78 }
79 Some(Finding {
80 smell_name: "naming_convention".into(),
81 category: SmellCategory::Bloaters,
82 severity: Severity::Hint,
83 location: Location {
84 path: path.to_path_buf(),
85 start_line: c.start_line,
86 start_col: c.name_col,
87 end_line: c.start_line,
88 end_col: c.name_end_col,
89 name: Some(c.name.clone()),
90 },
91 message: format!("Class `{}` should use PascalCase", c.name),
92 suggested_refactorings: vec!["Rename Method".into()],
93 ..Default::default()
94 })
95}
96
97struct NameCheck<'a> {
98 name: &'a str,
99 kind: &'a str,
100 path: &'a std::path::Path,
101 start_line: usize,
102 start_col: usize,
103 end_line: usize,
104 end_col: usize,
105}
106
107fn check_name(check: &NameCheck, min_len: usize, max_len: usize) -> Option<Finding> {
108 let (smell, severity, qualifier, limit) = if check.name.len() < min_len {
109 ("naming_too_short", Severity::Warning, "short", min_len)
110 } else if check.name.len() > max_len {
111 ("naming_too_long", Severity::Hint, "long", max_len)
112 } else {
113 return None;
114 };
115 let bound_label = if qualifier == "short" { "min" } else { "max" };
116 Some(Finding {
117 smell_name: smell.into(),
118 category: SmellCategory::Bloaters,
119 severity,
120 location: Location {
121 path: check.path.to_path_buf(),
122 start_line: check.start_line,
123 start_col: check.start_col,
124 end_line: check.end_line,
125 end_col: check.end_col,
126 name: Some(check.name.to_string()),
127 },
128 message: format!(
129 "{} `{}` name is too {} ({} chars, {}: {})",
130 check.kind,
131 check.name,
132 qualifier,
133 check.name.len(),
134 bound_label,
135 limit
136 ),
137 suggested_refactorings: vec!["Rename Method".into()],
138 ..Default::default()
139 })
140}