Skip to main content

codemem_engine/enrichment/
code_smells.rs

1//! Code smell detection: long functions, many parameters, deep nesting, long files.
2
3use super::{resolve_path, EnrichResult};
4use crate::CodememEngine;
5use codemem_core::{CodememError, NodeKind};
6use serde_json::json;
7use std::collections::HashMap;
8use std::path::Path;
9
10impl CodememEngine {
11    /// Detect common code smells: long functions (>50 lines), too many parameters (>5),
12    /// deep nesting (>4 levels), and long files (>500 lines).
13    ///
14    /// Stores findings as Pattern memories with importance 0.5.
15    pub fn enrich_code_smells(
16        &self,
17        namespace: Option<&str>,
18        project_root: Option<&Path>,
19    ) -> Result<EnrichResult, CodememError> {
20        let all_nodes = {
21            let graph = self.lock_graph()?;
22            graph.get_all_nodes()
23        };
24
25        let mut smells_stored = 0;
26
27        // Check functions/methods for long bodies and deep nesting
28        let mut file_cache: HashMap<String, Vec<String>> = HashMap::new();
29
30        for node in &all_nodes {
31            if !matches!(node.kind, NodeKind::Function | NodeKind::Method) {
32                continue;
33            }
34            let file_path = match node.payload.get("file_path").and_then(|v| v.as_str()) {
35                Some(fp) => fp.to_string(),
36                None => continue,
37            };
38            let line_start = node
39                .payload
40                .get("line_start")
41                .and_then(|v| v.as_u64())
42                .unwrap_or(0) as usize;
43            let line_end = node
44                .payload
45                .get("line_end")
46                .and_then(|v| v.as_u64())
47                .unwrap_or(0) as usize;
48
49            let fn_length = line_end.saturating_sub(line_start);
50
51            // Long function (>50 lines)
52            if fn_length > 50 {
53                let content = format!(
54                    "Code smell: Long function {} ({} lines) in {} — consider splitting",
55                    node.label, fn_length, file_path
56                );
57                if self
58                    .store_pattern_memory(&content, namespace, std::slice::from_ref(&node.id))
59                    .is_some()
60                {
61                    smells_stored += 1;
62                }
63            }
64
65            // Check parameter count from signature
66            let signature = node
67                .payload
68                .get("signature")
69                .and_then(|v| v.as_str())
70                .unwrap_or("");
71            if let Some(params_str) = signature
72                .split('(')
73                .nth(1)
74                .and_then(|s| s.split(')').next())
75            {
76                let param_count = if params_str.trim().is_empty() {
77                    0
78                } else {
79                    params_str.split(',').count()
80                };
81                if param_count > 5 {
82                    let content = format!(
83                        "Code smell: {} has {} parameters in {} — consider using a struct",
84                        node.label, param_count, file_path
85                    );
86                    if self
87                        .store_pattern_memory(&content, namespace, std::slice::from_ref(&node.id))
88                        .is_some()
89                    {
90                        smells_stored += 1;
91                    }
92                }
93            }
94
95            // Check nesting depth
96            if fn_length > 0 {
97                let lines = file_cache.entry(file_path.clone()).or_insert_with(|| {
98                    std::fs::read_to_string(resolve_path(&file_path, project_root))
99                        .unwrap_or_default()
100                        .lines()
101                        .map(String::from)
102                        .collect()
103                });
104
105                let start = line_start.saturating_sub(1);
106                let end = line_end.min(lines.len());
107                if start < end {
108                    let mut max_depth = 0usize;
109                    let mut depth = 0usize;
110                    for line in &lines[start..end] {
111                        for ch in line.chars() {
112                            match ch {
113                                '{' => {
114                                    depth += 1;
115                                    max_depth = max_depth.max(depth);
116                                }
117                                '}' => depth = depth.saturating_sub(1),
118                                _ => {}
119                            }
120                        }
121                    }
122                    if max_depth > 4 {
123                        let content = format!(
124                            "Code smell: Deep nesting ({} levels) in {} in {} — consider extracting",
125                            max_depth, node.label, file_path
126                        );
127                        if self
128                            .store_pattern_memory(
129                                &content,
130                                namespace,
131                                std::slice::from_ref(&node.id),
132                            )
133                            .is_some()
134                        {
135                            smells_stored += 1;
136                        }
137                    }
138                }
139            }
140        }
141
142        // Check for long files (>500 lines)
143        for node in &all_nodes {
144            if node.kind != NodeKind::File {
145                continue;
146            }
147            let file_path = &node.label;
148            let line_count = file_cache
149                .get(file_path)
150                .map(|lines| lines.len())
151                .unwrap_or_else(|| {
152                    std::fs::read_to_string(resolve_path(file_path, project_root))
153                        .map(|s| s.lines().count())
154                        .unwrap_or(0)
155                });
156            if line_count > 500 {
157                let content = format!(
158                    "Code smell: Long file {} ({} lines) — consider splitting into modules",
159                    file_path, line_count
160                );
161                if self
162                    .store_pattern_memory(&content, namespace, std::slice::from_ref(&node.id))
163                    .is_some()
164                {
165                    smells_stored += 1;
166                }
167            }
168        }
169
170        self.save_index();
171
172        Ok(EnrichResult {
173            insights_stored: smells_stored,
174            details: json!({
175                "smells_detected": smells_stored,
176            }),
177        })
178    }
179}