1use super::{
2 MaintainabilityImpact, OrganizationAntiPattern, OrganizationDetector, PrimitiveUsageContext,
3};
4use crate::common::{SourceLocation, capitalize_first};
5use std::collections::HashMap;
6use syn::{self, visit::Visit};
7
8pub struct PrimitiveObsessionDetector {
9 track_string_identifiers: bool,
10 track_numeric_measurements: bool,
11 min_occurrences: usize,
12}
13
14impl Default for PrimitiveObsessionDetector {
15 fn default() -> Self {
16 Self {
17 track_string_identifiers: true,
18 track_numeric_measurements: true,
19 min_occurrences: 3,
20 }
21 }
22}
23
24impl PrimitiveObsessionDetector {
25 pub fn new() -> Self {
26 Self::default()
27 }
28
29 fn analyze_primitive_usage(&self, type_usage: &TypeUsage) -> Option<PrimitiveUsageContext> {
30 let name_lower = type_usage.context.to_lowercase();
31 let type_name = &type_usage.type_name;
32
33 match () {
35 _ if self.track_string_identifiers
36 && type_name == "String"
37 && Self::contains_any(&name_lower, &["id", "key", "code"]) =>
38 {
39 Some(PrimitiveUsageContext::Identifier)
40 }
41 _ if self.track_numeric_measurements
42 && Self::is_numeric_type(type_name)
43 && Self::contains_any(
44 &name_lower,
45 &[
46 "distance",
47 "weight",
48 "height",
49 "temperature",
50 "price",
51 "amount",
52 ],
53 ) =>
54 {
55 Some(PrimitiveUsageContext::Measurement)
56 }
57 _ if type_name == "bool"
58 && Self::contains_any(&name_lower, &["status", "state", "flag"]) =>
59 {
60 Some(PrimitiveUsageContext::Status)
61 }
62 _ if Self::is_category_type(type_name)
63 && Self::contains_any(&name_lower, &["type", "category", "kind", "mode"]) =>
64 {
65 Some(PrimitiveUsageContext::Category)
66 }
67 _ => None,
68 }
69 }
70
71 fn contains_any(text: &str, patterns: &[&str]) -> bool {
73 patterns.iter().any(|pattern| text.contains(pattern))
74 }
75
76 fn is_numeric_type(type_name: &str) -> bool {
77 matches!(type_name, "f64" | "f32" | "i32" | "u32")
78 }
79
80 fn is_category_type(type_name: &str) -> bool {
81 matches!(type_name, "String" | "i32")
82 }
83
84 fn suggest_domain_type(&self, primitive_type: &str, context: &PrimitiveUsageContext) -> String {
85 match context {
86 PrimitiveUsageContext::Identifier => match primitive_type {
87 "String" => "Id<T>".to_string(),
88 _ => format!("{}Id", capitalize_first(primitive_type)),
89 },
90 PrimitiveUsageContext::Measurement => "Measurement<Unit>".to_string(),
91 PrimitiveUsageContext::Status => "StatusEnum".to_string(),
92 PrimitiveUsageContext::Category => "CategoryEnum".to_string(),
93 PrimitiveUsageContext::BusinessRule => {
94 format!("{}Rule", capitalize_first(primitive_type))
95 }
96 }
97 }
98
99 fn group_similar_usages(
100 &self,
101 usages: &[TypeUsage],
102 ) -> HashMap<(String, PrimitiveUsageContext), Vec<TypeUsage>> {
103 let mut groups = HashMap::new();
104
105 for usage in usages {
106 if let Some(context) = self.analyze_primitive_usage(usage) {
107 let key = (usage.type_name.clone(), context);
108 groups
109 .entry(key)
110 .or_insert_with(Vec::new)
111 .push(usage.clone());
112 }
113 }
114
115 groups
116 }
117}
118
119impl OrganizationDetector for PrimitiveObsessionDetector {
120 fn detect_anti_patterns(&self, file: &syn::File) -> Vec<OrganizationAntiPattern> {
121 let mut patterns = Vec::new();
122 let mut visitor = TypeUsageVisitor::new();
123 visitor.visit_file(file);
124
125 let grouped = self.group_similar_usages(&visitor.type_usages);
126
127 for ((primitive_type, usage_context), usages) in grouped {
128 if usages.len() >= self.min_occurrences {
129 patterns.push(OrganizationAntiPattern::PrimitiveObsession {
130 primitive_type: primitive_type.clone(),
131 usage_context: usage_context.clone(),
132 occurrence_count: usages.len(),
133 suggested_domain_type: self
134 .suggest_domain_type(&primitive_type, &usage_context),
135 locations: vec![SourceLocation::default()], });
137 }
138 }
139
140 patterns
141 }
142
143 fn detector_name(&self) -> &'static str {
144 "PrimitiveObsessionDetector"
145 }
146
147 fn estimate_maintainability_impact(
148 &self,
149 pattern: &OrganizationAntiPattern,
150 ) -> MaintainabilityImpact {
151 match pattern {
152 OrganizationAntiPattern::PrimitiveObsession {
153 occurrence_count,
154 usage_context,
155 ..
156 } => match usage_context {
157 PrimitiveUsageContext::Identifier | PrimitiveUsageContext::BusinessRule => {
158 if *occurrence_count > 10 {
159 MaintainabilityImpact::High
160 } else if *occurrence_count > 5 {
161 MaintainabilityImpact::Medium
162 } else {
163 MaintainabilityImpact::Low
164 }
165 }
166 _ => {
167 if *occurrence_count > 15 {
168 MaintainabilityImpact::Medium
169 } else {
170 MaintainabilityImpact::Low
171 }
172 }
173 },
174 _ => MaintainabilityImpact::Low,
175 }
176 }
177}
178
179#[derive(Clone)]
180struct TypeUsage {
181 type_name: String,
182 context: String, }
184
185struct TypeUsageVisitor {
186 type_usages: Vec<TypeUsage>,
187}
188
189impl TypeUsageVisitor {
190 fn new() -> Self {
191 Self {
192 type_usages: Vec::new(),
193 }
194 }
195
196 #[allow(clippy::only_used_in_recursion)]
197 fn extract_type_name(&self, ty: &syn::Type) -> String {
198 match ty {
199 syn::Type::Path(type_path) => type_path
200 .path
201 .segments
202 .last()
203 .map(|seg| seg.ident.to_string())
204 .unwrap_or_else(|| "Unknown".to_string()),
205 syn::Type::Reference(type_ref) => self.extract_type_name(&type_ref.elem),
206 _ => "Unknown".to_string(),
207 }
208 }
209}
210
211impl<'ast> Visit<'ast> for TypeUsageVisitor {
212 fn visit_field(&mut self, node: &'ast syn::Field) {
213 if let Some(ident) = &node.ident {
214 let type_name = self.extract_type_name(&node.ty);
215
216 if is_primitive_type(&type_name) {
218 self.type_usages.push(TypeUsage {
219 type_name,
220 context: ident.to_string(),
221 });
222 }
223 }
224
225 syn::visit::visit_field(self, node);
226 }
227
228 fn visit_fn_arg(&mut self, node: &'ast syn::FnArg) {
229 if let syn::FnArg::Typed(pat_type) = node
230 && let syn::Pat::Ident(pat_ident) = &*pat_type.pat
231 {
232 let type_name = self.extract_type_name(&pat_type.ty);
233
234 if is_primitive_type(&type_name) {
236 self.type_usages.push(TypeUsage {
237 type_name,
238 context: pat_ident.ident.to_string(),
239 });
240 }
241 }
242
243 syn::visit::visit_fn_arg(self, node);
244 }
245
246 fn visit_local(&mut self, node: &'ast syn::Local) {
247 if let syn::Pat::Ident(pat_ident) = &node.pat
248 && let Some(init) = &node.init
249 {
250 let type_name = self.infer_type_from_expr(&init.expr);
252
253 if is_primitive_type(&type_name) {
254 self.type_usages.push(TypeUsage {
255 type_name,
256 context: pat_ident.ident.to_string(),
257 });
258 }
259 }
260
261 syn::visit::visit_local(self, node);
262 }
263}
264
265impl TypeUsageVisitor {
266 fn infer_type_from_expr(&self, expr: &syn::Expr) -> String {
267 match expr {
268 syn::Expr::Lit(expr_lit) => Self::infer_type_from_literal(&expr_lit.lit),
269 syn::Expr::Call(expr_call) => Self::extract_function_name(expr_call),
270 _ => "Unknown".to_string(),
271 }
272 }
273
274 fn infer_type_from_literal(lit: &syn::Lit) -> String {
275 match lit {
276 syn::Lit::Str(_) => "String",
277 syn::Lit::Int(_) => "i32",
278 syn::Lit::Float(_) => "f64",
279 syn::Lit::Bool(_) => "bool",
280 _ => "Unknown",
281 }
282 .to_string()
283 }
284
285 fn extract_function_name(expr_call: &syn::ExprCall) -> String {
286 if let syn::Expr::Path(path) = &*expr_call.func {
287 path.path
288 .segments
289 .last()
290 .map(|seg| seg.ident.to_string())
291 .unwrap_or_else(|| "Unknown".to_string())
292 } else {
293 "Unknown".to_string()
294 }
295 }
296}
297
298fn is_primitive_type(type_name: &str) -> bool {
299 matches!(
300 type_name,
301 "bool"
302 | "char"
303 | "str"
304 | "String"
305 | "i8"
306 | "i16"
307 | "i32"
308 | "i64"
309 | "i128"
310 | "isize"
311 | "u8"
312 | "u16"
313 | "u32"
314 | "u64"
315 | "u128"
316 | "usize"
317 | "f32"
318 | "f64"
319 )
320}