codeprism_mcp/tools/analysis/
complexity.rs

1//! Complexity analysis tools
2
3use crate::tools_legacy::{CallToolParams, CallToolResult, Tool, ToolContent};
4use crate::CodePrismMcpServer;
5use anyhow::Result;
6use serde_json::Value;
7
8/// Analyze complexity for a specific file
9fn 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    // Read file and calculate basic metrics
21    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        // Basic complexity estimation based on file content
27        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        // Add requested metrics
42        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
83/// Analyze complexity for a specific symbol/node
84fn 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            // Functions typically have higher complexity
92            5 + (node.name.len() / 10) // Simple heuristic
93        }
94        codeprism_core::NodeKind::Class => {
95            // Classes have moderate complexity
96            3 + (node.name.len() / 20)
97        }
98        _ => {
99            // Other symbols have low complexity
100            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    // Add requested metrics
121    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
154/// Calculate basic complexity estimation from code content
155fn calculate_basic_complexity(content: &str) -> usize {
156    let mut complexity = 1; // Base complexity
157
158    // Count control flow statements
159    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
178/// Calculate maintainability index (simplified)
179fn calculate_maintainability_index(lines: usize, complexity: usize) -> f64 {
180    // Simplified maintainability index calculation
181    let halstead_volume = (lines as f64).log2() * 10.0; // Rough approximation
182    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
189/// List complexity analysis tools
190pub 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
224/// Route complexity analysis tool calls
225pub 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
238/// Analyze code complexity
239async 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    // Support both "target" and "path" parameter names for backward compatibility
246    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    // Try to resolve as symbol identifier or file path
269    let result = if target.contains('/') || target.contains('.') {
270        // Handle as file path
271        analyze_file_complexity(target, &metrics, threshold_warnings)
272    } else {
273        // Try to resolve as symbol name using search, then analyze
274        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}