codemem_engine/enrichment/
code_smells.rs1use 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 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 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 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 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 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 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}