debtmap/debt/
smells.rs

1use crate::core::{DebtItem, DebtType, FunctionMetrics, Priority};
2use std::path::{Path, PathBuf};
3
4/// Represents different types of code smells
5#[derive(Debug, Clone, PartialEq)]
6pub enum SmellType {
7    LongParameterList,
8    LargeClass,
9    LongMethod,
10    FeatureEnvy,
11    DataClump,
12    DeepNesting,
13    DuplicateCode,
14}
15
16/// A detected code smell with its location and details
17#[derive(Debug, Clone)]
18pub struct CodeSmell {
19    pub smell_type: SmellType,
20    pub location: PathBuf,
21    pub line: usize,
22    pub message: String,
23    pub severity: Priority,
24}
25
26impl CodeSmell {
27    /// Convert a code smell to a debt item
28    pub fn to_debt_item(&self) -> DebtItem {
29        DebtItem {
30            id: format!(
31                "smell-{:?}-{}-{}",
32                self.smell_type,
33                self.location.display(),
34                self.line
35            ),
36            debt_type: DebtType::CodeSmell,
37            priority: self.severity,
38            file: self.location.clone(),
39            line: self.line,
40            message: self.message.clone(),
41            context: None,
42        }
43    }
44}
45
46/// Detect long parameter lists in functions
47pub fn detect_long_parameter_list(func: &FunctionMetrics, param_count: usize) -> Option<CodeSmell> {
48    const THRESHOLD: usize = 5;
49
50    if param_count > THRESHOLD {
51        Some(CodeSmell {
52            smell_type: SmellType::LongParameterList,
53            location: func.file.clone(),
54            line: func.line,
55            message: format!(
56                "Function '{}' has {} parameters (threshold: {})",
57                func.name, param_count, THRESHOLD
58            ),
59            severity: if param_count > THRESHOLD * 2 {
60                Priority::High
61            } else {
62                Priority::Medium
63            },
64        })
65    } else {
66        None
67    }
68}
69
70/// Detect large classes/modules based on line count
71pub fn detect_large_module(path: &Path, line_count: usize) -> Option<CodeSmell> {
72    const THRESHOLD: usize = 300;
73
74    if line_count > THRESHOLD {
75        Some(CodeSmell {
76            smell_type: SmellType::LargeClass,
77            location: path.to_path_buf(),
78            line: 1,
79            message: format!("Module has {line_count} lines (threshold: {THRESHOLD})"),
80            severity: if line_count > THRESHOLD * 2 {
81                Priority::High
82            } else {
83                Priority::Medium
84            },
85        })
86    } else {
87        None
88    }
89}
90
91/// Detect long methods/functions
92pub fn detect_long_method(func: &FunctionMetrics) -> Option<CodeSmell> {
93    const THRESHOLD: usize = 50;
94
95    if func.length > THRESHOLD {
96        Some(CodeSmell {
97            smell_type: SmellType::LongMethod,
98            location: func.file.clone(),
99            line: func.line,
100            message: format!(
101                "Function '{}' has {} lines (threshold: {})",
102                func.name, func.length, THRESHOLD
103            ),
104            severity: if func.length > THRESHOLD * 2 {
105                Priority::High
106            } else {
107                Priority::Medium
108            },
109        })
110    } else {
111        None
112    }
113}
114
115/// Detect deep nesting in functions
116pub fn detect_deep_nesting(func: &FunctionMetrics) -> Option<CodeSmell> {
117    const THRESHOLD: u32 = 4;
118
119    if func.nesting > THRESHOLD {
120        Some(CodeSmell {
121            smell_type: SmellType::DeepNesting,
122            location: func.file.clone(),
123            line: func.line,
124            message: format!(
125                "Function '{}' has nesting depth of {} (threshold: {})",
126                func.name, func.nesting, THRESHOLD
127            ),
128            severity: if func.nesting > THRESHOLD * 2 {
129                Priority::High
130            } else {
131                Priority::Medium
132            },
133        })
134    } else {
135        None
136    }
137}
138
139/// Analyze a function for all code smells
140pub fn analyze_function_smells(func: &FunctionMetrics, param_count: usize) -> Vec<CodeSmell> {
141    let mut smells = Vec::new();
142
143    if let Some(smell) = detect_long_parameter_list(func, param_count) {
144        smells.push(smell);
145    }
146
147    if let Some(smell) = detect_long_method(func) {
148        smells.push(smell);
149    }
150
151    if let Some(smell) = detect_deep_nesting(func) {
152        smells.push(smell);
153    }
154
155    smells
156}
157
158/// Analyze a file for module-level code smells
159pub fn analyze_module_smells(path: &Path, line_count: usize) -> Vec<CodeSmell> {
160    let mut smells = Vec::new();
161
162    if let Some(smell) = detect_large_module(path, line_count) {
163        smells.push(smell);
164    }
165
166    smells
167}
168
169/// Detect feature envy - methods that use other class data more than their own
170/// This is a simplified version that looks for method calls on other objects
171pub fn detect_feature_envy(content: &str, path: &Path) -> Vec<CodeSmell> {
172    let mut smells = Vec::new();
173
174    // Simple heuristic: count method calls on other objects vs self
175    for (line_num, line) in content.lines().enumerate() {
176        let other_calls = line.matches('.').count() - line.matches("self.").count();
177        let self_calls = line.matches("self.").count();
178
179        if other_calls > 3 && other_calls > self_calls * 2 {
180            smells.push(CodeSmell {
181                smell_type: SmellType::FeatureEnvy,
182                location: path.to_path_buf(),
183                line: line_num + 1,
184                message: format!(
185                    "Line has {other_calls} external method calls vs {self_calls} self calls"
186                ),
187                severity: Priority::Low,
188            });
189        }
190    }
191
192    smells
193}
194
195/// Detect data clumps - groups of parameters that often appear together
196pub fn detect_data_clumps(functions: &[FunctionMetrics]) -> Vec<CodeSmell> {
197    let mut smells = Vec::new();
198
199    // This is a simplified implementation
200    // In a real implementation, we'd analyze actual parameter names and types
201    for i in 0..functions.len() {
202        for j in i + 1..functions.len() {
203            // If two functions are in the same file and have similar high parameter counts,
204            // they might have data clumps
205            if functions[i].file == functions[j].file {
206                // This is a placeholder - real implementation would compare actual parameters
207                if functions[i].length > 30 && functions[j].length > 30 {
208                    smells.push(CodeSmell {
209                        smell_type: SmellType::DataClump,
210                        location: functions[i].file.clone(),
211                        line: functions[i].line,
212                        message: format!(
213                            "Functions '{}' and '{}' may share data clumps",
214                            functions[i].name, functions[j].name
215                        ),
216                        severity: Priority::Low,
217                    });
218                    break; // Only report once per function
219                }
220            }
221        }
222    }
223
224    smells
225}
226
227#[cfg(test)]
228mod tests {
229    use super::*;
230    use crate::core::FunctionMetrics;
231    use std::path::PathBuf;
232
233    #[test]
234    fn test_detect_data_clumps_empty_functions() {
235        let functions = vec![];
236        let smells = detect_data_clumps(&functions);
237        assert_eq!(
238            smells.len(),
239            0,
240            "No smells should be detected for empty input"
241        );
242    }
243
244    #[test]
245    fn test_detect_data_clumps_single_function() {
246        let functions = vec![FunctionMetrics {
247            name: "large_function".to_string(),
248            file: PathBuf::from("src/lib.rs"),
249            line: 10,
250            cyclomatic: 5,
251            cognitive: 10,
252            nesting: 2,
253            length: 35,
254            is_test: false,
255            visibility: None,
256        }];
257        let smells = detect_data_clumps(&functions);
258        assert_eq!(
259            smells.len(),
260            0,
261            "Single function cannot have data clumps with itself"
262        );
263    }
264
265    #[test]
266    fn test_detect_data_clumps_different_files() {
267        let functions = vec![
268            FunctionMetrics {
269                name: "function_a".to_string(),
270                file: PathBuf::from("src/module_a.rs"),
271                line: 10,
272                cyclomatic: 5,
273                cognitive: 10,
274                nesting: 2,
275                length: 35,
276                is_test: false,
277                visibility: None,
278            },
279            FunctionMetrics {
280                name: "function_b".to_string(),
281                file: PathBuf::from("src/module_b.rs"),
282                line: 20,
283                cyclomatic: 5,
284                cognitive: 10,
285                nesting: 2,
286                length: 35,
287                is_test: false,
288                visibility: None,
289            },
290        ];
291        let smells = detect_data_clumps(&functions);
292        assert_eq!(
293            smells.len(),
294            0,
295            "Functions in different files should not be reported as data clumps"
296        );
297    }
298
299    #[test]
300    fn test_detect_data_clumps_same_file_large_functions() {
301        let functions = vec![
302            FunctionMetrics {
303                name: "process_user_data".to_string(),
304                file: PathBuf::from("src/user_handler.rs"),
305                line: 10,
306                cyclomatic: 8,
307                cognitive: 15,
308                nesting: 3,
309                length: 40,
310                is_test: false,
311                visibility: None,
312            },
313            FunctionMetrics {
314                name: "validate_user_data".to_string(),
315                file: PathBuf::from("src/user_handler.rs"),
316                line: 60,
317                cyclomatic: 6,
318                cognitive: 12,
319                nesting: 2,
320                length: 35,
321                is_test: false,
322                visibility: None,
323            },
324        ];
325        let smells = detect_data_clumps(&functions);
326        assert_eq!(
327            smells.len(),
328            1,
329            "Should detect data clump for large functions in same file"
330        );
331
332        let smell = &smells[0];
333        assert_eq!(smell.smell_type, SmellType::DataClump);
334        assert_eq!(smell.location, PathBuf::from("src/user_handler.rs"));
335        assert_eq!(smell.line, 10);
336        assert!(smell.message.contains("process_user_data"));
337        assert!(smell.message.contains("validate_user_data"));
338        assert_eq!(smell.severity, Priority::Low);
339    }
340
341    #[test]
342    fn test_detect_data_clumps_multiple_clumps() {
343        let functions = vec![
344            FunctionMetrics {
345                name: "func_a".to_string(),
346                file: PathBuf::from("src/module.rs"),
347                line: 10,
348                cyclomatic: 5,
349                cognitive: 10,
350                nesting: 2,
351                length: 35,
352                is_test: false,
353                visibility: None,
354            },
355            FunctionMetrics {
356                name: "func_b".to_string(),
357                file: PathBuf::from("src/module.rs"),
358                line: 50,
359                cyclomatic: 5,
360                cognitive: 10,
361                nesting: 2,
362                length: 32,
363                is_test: false,
364                visibility: None,
365            },
366            FunctionMetrics {
367                name: "func_c".to_string(),
368                file: PathBuf::from("src/module.rs"),
369                line: 90,
370                cyclomatic: 5,
371                cognitive: 10,
372                nesting: 2,
373                length: 31,
374                is_test: false,
375                visibility: None,
376            },
377            FunctionMetrics {
378                name: "small_func".to_string(),
379                file: PathBuf::from("src/module.rs"),
380                line: 130,
381                cyclomatic: 2,
382                cognitive: 3,
383                nesting: 1,
384                length: 10,
385                is_test: false,
386                visibility: None,
387            },
388        ];
389        let smells = detect_data_clumps(&functions);
390
391        // Should detect clumps between func_a & func_b, func_a & func_c
392        // But due to break after first match per function, we get 2 smells (one for func_a, one for func_b)
393        assert_eq!(smells.len(), 2, "Should detect multiple data clumps");
394
395        // First smell should be between func_a and func_b
396        assert_eq!(smells[0].line, 10);
397        assert!(smells[0].message.contains("func_a"));
398        assert!(smells[0].message.contains("func_b"));
399
400        // Second smell should be between func_b and func_c
401        assert_eq!(smells[1].line, 50);
402        assert!(smells[1].message.contains("func_b"));
403        assert!(smells[1].message.contains("func_c"));
404    }
405}