codeprism_mcp/tools/analysis/
complexity.rs1use crate::tools_legacy::{CallToolParams, CallToolResult, Tool, ToolContent};
4use crate::CodePrismMcpServer;
5use anyhow::Result;
6use serde_json::Value;
7
8fn analyze_file_complexity(file_path: &str, metrics: &[String], threshold_warnings: bool) -> Value {
10 let path = std::path::Path::new(file_path);
11
12 if !path.exists() {
13 return serde_json::json!({
14 "target": file_path,
15 "error": "File not found",
16 "file_exists": false
17 });
18 }
19
20 if let Ok(content) = std::fs::read_to_string(path) {
22 let line_count = content.lines().count();
23 let char_count = content.chars().count();
24 let word_count = content.split_whitespace().count();
25
26 let estimated_complexity = calculate_basic_complexity(&content);
28
29 let mut result = serde_json::json!({
30 "target": file_path,
31 "file_analysis": {
32 "file_exists": true,
33 "line_count": line_count,
34 "character_count": char_count,
35 "word_count": word_count,
36 "estimated_complexity": estimated_complexity
37 },
38 "metrics": {}
39 });
40
41 for metric in metrics {
43 match metric.as_str() {
44 "all" | "basic" => {
45 result["metrics"]["basic"] = serde_json::json!({
46 "lines_of_code": line_count,
47 "complexity_score": estimated_complexity,
48 "maintainability_index": calculate_maintainability_index(line_count, estimated_complexity)
49 });
50 }
51 "cyclomatic" => {
52 result["metrics"]["cyclomatic"] = serde_json::json!({
53 "estimated_complexity": estimated_complexity,
54 "note": "Estimated based on control flow indicators"
55 });
56 }
57 _ => {
58 result["metrics"][metric] = serde_json::json!({
59 "status": "not_implemented",
60 "note": format!("Metric '{}' calculation not yet implemented", metric)
61 });
62 }
63 }
64 }
65
66 if threshold_warnings && estimated_complexity > 10 {
67 result["warnings"] = serde_json::json!([format!(
68 "High complexity detected: {} (recommended: < 10)",
69 estimated_complexity
70 )]);
71 }
72
73 result
74 } else {
75 serde_json::json!({
76 "target": file_path,
77 "error": "Failed to read file",
78 "file_exists": true
79 })
80 }
81}
82
83fn analyze_symbol_complexity(
85 node: &codeprism_core::Node,
86 metrics: &[String],
87 threshold_warnings: bool,
88) -> Value {
89 let symbol_complexity = match node.kind {
90 codeprism_core::NodeKind::Function | codeprism_core::NodeKind::Method => {
91 5 + (node.name.len() / 10) }
94 codeprism_core::NodeKind::Class => {
95 3 + (node.name.len() / 20)
97 }
98 _ => {
99 1
101 }
102 };
103
104 let mut result = serde_json::json!({
105 "target": node.name,
106 "symbol_analysis": {
107 "id": node.id.to_hex(),
108 "name": node.name,
109 "kind": format!("{:?}", node.kind),
110 "file": node.file.display().to_string(),
111 "span": {
112 "start_line": node.span.start_line,
113 "end_line": node.span.end_line
114 },
115 "symbol_complexity": symbol_complexity
116 },
117 "metrics": {}
118 });
119
120 for metric in metrics {
122 match metric.as_str() {
123 "all" | "basic" => {
124 result["metrics"]["basic"] = serde_json::json!({
125 "symbol_complexity": symbol_complexity,
126 "maintainability_index": calculate_maintainability_index(1, symbol_complexity)
127 });
128 }
129 "cyclomatic" => {
130 result["metrics"]["cyclomatic"] = serde_json::json!({
131 "estimated_complexity": symbol_complexity,
132 "note": "Estimated based on symbol type and name"
133 });
134 }
135 _ => {
136 result["metrics"][metric] = serde_json::json!({
137 "status": "not_implemented",
138 "note": format!("Metric '{}' calculation not yet implemented for symbols", metric)
139 });
140 }
141 }
142 }
143
144 if threshold_warnings && symbol_complexity > 5 {
145 result["warnings"] = serde_json::json!([format!(
146 "High symbol complexity: {} (recommended: < 5)",
147 symbol_complexity
148 )]);
149 }
150
151 result
152}
153
154fn calculate_basic_complexity(content: &str) -> usize {
156 let mut complexity = 1; for line in content.lines() {
160 let line = line.trim();
161 if line.contains("if ") || line.contains("elif ") {
162 complexity += 1;
163 }
164 if line.contains("for ") || line.contains("while ") {
165 complexity += 1;
166 }
167 if line.contains("try:") || line.contains("except") {
168 complexity += 1;
169 }
170 if line.contains("match ") || line.contains("case ") {
171 complexity += 1;
172 }
173 }
174
175 complexity
176}
177
178fn calculate_maintainability_index(lines: usize, complexity: usize) -> f64 {
180 let halstead_volume = (lines as f64).log2() * 10.0; let mi = 171.0
183 - 5.2 * (halstead_volume).log2()
184 - 0.23 * (complexity as f64)
185 - 16.2 * (lines as f64).log2();
186 mi.clamp(0.0, 100.0)
187}
188
189pub fn list_tools() -> Vec<Tool> {
191 vec![
192 Tool {
193 name: "analyze_complexity".to_string(),
194 title: Some("Analyze Code Complexity".to_string()),
195 description: "Calculate complexity metrics for code elements including cyclomatic, cognitive, and maintainability metrics".to_string(),
196 input_schema: serde_json::json!({
197 "type": "object",
198 "properties": {
199 "target": {
200 "type": "string",
201 "description": "File path or symbol ID to analyze"
202 },
203 "metrics": {
204 "type": "array",
205 "items": {
206 "type": "string",
207 "enum": ["cyclomatic", "cognitive", "halstead", "maintainability_index", "all"]
208 },
209 "description": "Types of complexity metrics to calculate",
210 "default": ["all"]
211 },
212 "threshold_warnings": {
213 "type": "boolean",
214 "description": "Include warnings for metrics exceeding thresholds",
215 "default": true
216 }
217 },
218 "required": ["target"]
219 }),
220 }
221 ]
222}
223
224pub async fn call_tool(
226 server: &CodePrismMcpServer,
227 params: &CallToolParams,
228) -> Result<CallToolResult> {
229 match params.name.as_str() {
230 "analyze_complexity" => analyze_complexity(server, params.arguments.as_ref()).await,
231 _ => Err(anyhow::anyhow!(
232 "Unknown complexity analysis tool: {}",
233 params.name
234 )),
235 }
236}
237
238async fn analyze_complexity(
240 server: &CodePrismMcpServer,
241 arguments: Option<&Value>,
242) -> Result<CallToolResult> {
243 let args = arguments.ok_or_else(|| anyhow::anyhow!("Missing arguments"))?;
244
245 let target = args
247 .get("target")
248 .or_else(|| args.get("path"))
249 .and_then(|v| v.as_str())
250 .ok_or_else(|| anyhow::anyhow!("Missing target parameter (or path)"))?;
251
252 let metrics = args
253 .get("metrics")
254 .and_then(|v| v.as_array())
255 .map(|arr| {
256 arr.iter()
257 .filter_map(|v| v.as_str())
258 .map(|s| s.to_string())
259 .collect::<Vec<_>>()
260 })
261 .unwrap_or_else(|| vec!["all".to_string()]);
262
263 let threshold_warnings = args
264 .get("threshold_warnings")
265 .and_then(|v| v.as_bool())
266 .unwrap_or(true);
267
268 let result = if target.contains('/') || target.contains('.') {
270 analyze_file_complexity(target, &metrics, threshold_warnings)
272 } else {
273 if let Ok(symbol_results) = server.graph_query().search_symbols(target, None, Some(1)) {
275 if let Some(symbol_result) = symbol_results.first() {
276 analyze_symbol_complexity(&symbol_result.node, &metrics, threshold_warnings)
277 } else {
278 serde_json::json!({
279 "target": target,
280 "error": "Symbol not found",
281 "suggestion": "Try using a file path or check if the symbol name is correct"
282 })
283 }
284 } else {
285 serde_json::json!({
286 "target": target,
287 "error": "Failed to search for symbol",
288 "suggestion": "Try using a file path instead"
289 })
290 }
291 };
292
293 Ok(CallToolResult {
294 content: vec![ToolContent::Text {
295 text: serde_json::to_string_pretty(&result)?,
296 }],
297 is_error: Some(result.get("error").is_some()),
298 })
299}