Skip to main content

debtmap/organization/
primitive_obsession_detector.rs

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        // Use pattern matching with guards for cleaner logic
34        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    // Helper methods to simplify pattern matching
72    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()], // TODO: Extract actual locations
136                });
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, // Variable or field name
183}
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            // Track primitive types
217            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            // Track primitive types
235            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            // Try to infer type from initialization
251            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}