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