codemem_engine/enrichment/
complexity.rs1use super::{resolve_path, EnrichResult};
4use crate::CodememEngine;
5use codemem_core::{CodememError, GraphBackend, NodeKind};
6use serde_json::json;
7use std::collections::HashMap;
8use std::path::Path;
9
10impl CodememEngine {
11 pub fn enrich_complexity(
18 &self,
19 namespace: Option<&str>,
20 project_root: Option<&Path>,
21 ) -> Result<EnrichResult, CodememError> {
22 let all_nodes = {
23 let graph = self.lock_graph()?;
24 graph.get_all_nodes()
25 };
26
27 struct SymbolInfo {
29 node_id: String,
30 label: String,
31 file_path: String,
32 line_start: usize,
33 line_end: usize,
34 }
35
36 let mut symbols: Vec<SymbolInfo> = Vec::new();
37 for node in &all_nodes {
38 if !matches!(node.kind, NodeKind::Function | NodeKind::Method) {
39 continue;
40 }
41 let file_path = match node.payload.get("file_path").and_then(|v| v.as_str()) {
42 Some(fp) => fp.to_string(),
43 None => continue,
44 };
45 let line_start = node
46 .payload
47 .get("line_start")
48 .and_then(|v| v.as_u64())
49 .unwrap_or(0) as usize;
50 let line_end = node
51 .payload
52 .get("line_end")
53 .and_then(|v| v.as_u64())
54 .unwrap_or(0) as usize;
55 if line_end <= line_start {
56 continue;
57 }
58 symbols.push(SymbolInfo {
59 node_id: node.id.clone(),
60 label: node.label.clone(),
61 file_path,
62 line_start,
63 line_end,
64 });
65 }
66
67 let mut file_cache: HashMap<String, Vec<String>> = HashMap::new();
69 let mut annotated = 0usize;
70 let mut insights_stored = 0usize;
71
72 struct ComplexityData {
74 node_id: String,
75 cyclomatic: usize,
76 cognitive: usize,
77 }
78 let mut complexity_data: Vec<ComplexityData> = Vec::new();
79
80 struct ComplexityInsight {
82 content: String,
83 importance: f64,
84 node_id: String,
85 }
86 let mut pending_insights: Vec<ComplexityInsight> = Vec::new();
87
88 for sym in &symbols {
89 let lines = file_cache.entry(sym.file_path.clone()).or_insert_with(|| {
90 std::fs::read_to_string(resolve_path(&sym.file_path, project_root))
91 .unwrap_or_default()
92 .lines()
93 .map(String::from)
94 .collect()
95 });
96
97 let start = sym.line_start.saturating_sub(1);
99 let end = sym.line_end.min(lines.len());
100 if start >= end {
101 continue;
102 }
103 let fn_lines = &lines[start..end];
104
105 let mut cyclomatic: usize = 1; let mut max_depth: usize = 0;
108 let mut current_depth: usize = 0;
109
110 for line in fn_lines {
111 let trimmed = line.trim();
112
113 for keyword in &[
115 "if ", "if(", "else if", "match ", "for ", "for(", "while ", "while(", "loop ",
116 "loop{",
117 ] {
118 if trimmed.starts_with(keyword) || trimmed.contains(&format!(" {keyword}")) {
119 cyclomatic += 1;
120 break;
121 }
122 }
123 cyclomatic += trimmed.matches("&&").count();
125 cyclomatic += trimmed.matches("||").count();
126
127 for ch in trimmed.chars() {
129 match ch {
130 '{' => {
131 current_depth += 1;
132 max_depth = max_depth.max(current_depth);
133 }
134 '}' => {
135 current_depth = current_depth.saturating_sub(1);
136 }
137 _ => {}
138 }
139 }
140 }
141
142 complexity_data.push(ComplexityData {
143 node_id: sym.node_id.clone(),
144 cyclomatic,
145 cognitive: max_depth,
146 });
147 annotated += 1;
148
149 if cyclomatic > 10 {
151 let importance = if cyclomatic > 20 { 0.9 } else { 0.7 };
152 pending_insights.push(ComplexityInsight {
153 content: format!(
154 "High complexity: {} — cyclomatic={}, max_nesting={} in {}",
155 sym.label, cyclomatic, max_depth, sym.file_path
156 ),
157 importance,
158 node_id: sym.node_id.clone(),
159 });
160 }
161 }
162
163 {
165 let mut graph = self.lock_graph()?;
166 for data in &complexity_data {
167 if let Ok(Some(mut node)) = graph.get_node(&data.node_id) {
168 node.payload
169 .insert("cyclomatic_complexity".into(), json!(data.cyclomatic));
170 node.payload
171 .insert("cognitive_complexity".into(), json!(data.cognitive));
172 let _ = graph.add_node(node);
173 }
174 }
175 }
176
177 for insight in &pending_insights {
179 if self
180 .store_insight(
181 &insight.content,
182 "complexity",
183 &[],
184 insight.importance,
185 namespace,
186 std::slice::from_ref(&insight.node_id),
187 )
188 .is_some()
189 {
190 insights_stored += 1;
191 }
192 }
193
194 self.save_index();
195
196 Ok(EnrichResult {
197 insights_stored,
198 details: json!({
199 "symbols_analyzed": annotated,
200 "high_complexity_count": pending_insights.len(),
201 "insights_stored": insights_stored,
202 }),
203 })
204 }
205}