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 codeprism_analysis::complexity::ComplexityAnalyzer;
7use serde_json::Value;
8use std::path::Path;
9
10/// Analyze complexity for a specific file
11fn analyze_file_complexity(file_path: &str, metrics: &[String], threshold_warnings: bool) -> Value {
12    let path = Path::new(file_path);
13
14    if !path.exists() {
15        return serde_json::json!({
16            "target": file_path,
17            "error": "File not found",
18            "file_exists": false
19        });
20    }
21
22    let analyzer = ComplexityAnalyzer::new();
23
24    match analyzer.analyze_file_complexity(path, metrics, threshold_warnings) {
25        Ok(mut result) => {
26            // Add target information for consistency
27            result["target"] = serde_json::Value::String(file_path.to_string());
28            result["file_exists"] = serde_json::Value::Bool(true);
29            result
30        }
31        Err(e) => {
32            serde_json::json!({
33                "target": file_path,
34                "error": format!("Failed to analyze file: {}", e),
35                "file_exists": true
36            })
37        }
38    }
39}
40
41/// Analyze complexity for a specific symbol/node
42fn analyze_symbol_complexity(
43    node: &codeprism_core::Node,
44    metrics: &[String],
45    threshold_warnings: bool,
46) -> Value {
47    // For symbol-specific analysis, we need to extract content from the file at the symbol's location
48    let file_path = &node.file;
49
50    if let Ok(content) = std::fs::read_to_string(file_path) {
51        let lines: Vec<&str> = content.lines().collect();
52
53        // Extract symbol content based on span
54        let start_line = node.span.start_line.saturating_sub(1); // Convert to 0-based
55        let end_line = (node.span.end_line.saturating_sub(1)).min(lines.len().saturating_sub(1));
56
57        if start_line <= end_line && start_line < lines.len() {
58            let symbol_content = lines[start_line..=end_line].join("\n");
59            let symbol_lines = end_line - start_line + 1;
60
61            let analyzer = ComplexityAnalyzer::new();
62            let complexity_metrics = analyzer.calculate_all_metrics(&symbol_content, symbol_lines);
63
64            let mut result = serde_json::json!({
65                "target": node.name,
66                "symbol_analysis": {
67                    "id": node.id.to_hex(),
68                    "name": node.name,
69                    "kind": format!("{:?}", node.kind),
70                    "file": node.file.display().to_string(),
71                    "span": {
72                        "start_line": node.span.start_line,
73                        "end_line": node.span.end_line
74                    },
75                    "lines_of_code": symbol_lines
76                },
77                "metrics": {}
78            });
79
80            // Add requested metrics
81            for metric in metrics {
82                match metric.as_str() {
83                    "all" => {
84                        result["metrics"]["cyclomatic_complexity"] =
85                            complexity_metrics.cyclomatic.into();
86                        result["metrics"]["cognitive_complexity"] =
87                            complexity_metrics.cognitive.into();
88                        result["metrics"]["halstead"] = serde_json::json!({
89                            "volume": complexity_metrics.halstead_volume,
90                            "difficulty": complexity_metrics.halstead_difficulty,
91                            "effort": complexity_metrics.halstead_effort
92                        });
93                        result["metrics"]["maintainability_index"] =
94                            complexity_metrics.maintainability_index.into();
95                    }
96                    "cyclomatic" => {
97                        result["metrics"]["cyclomatic_complexity"] =
98                            complexity_metrics.cyclomatic.into();
99                    }
100                    "cognitive" => {
101                        result["metrics"]["cognitive_complexity"] =
102                            complexity_metrics.cognitive.into();
103                    }
104                    "halstead" => {
105                        result["metrics"]["halstead"] = serde_json::json!({
106                            "volume": complexity_metrics.halstead_volume,
107                            "difficulty": complexity_metrics.halstead_difficulty,
108                            "effort": complexity_metrics.halstead_effort
109                        });
110                    }
111                    "maintainability_index" | "maintainability" => {
112                        result["metrics"]["maintainability_index"] =
113                            complexity_metrics.maintainability_index.into();
114                    }
115                    _ => {
116                        result["metrics"][metric] = serde_json::json!({
117                            "status": "unknown_metric",
118                            "note": format!("Unknown metric '{}'. Available metrics: cyclomatic, cognitive, halstead, maintainability_index, all", metric)
119                        });
120                    }
121                }
122            }
123
124            // Add threshold warnings
125            if threshold_warnings {
126                let mut warnings = Vec::new();
127
128                if complexity_metrics.cyclomatic > 10 {
129                    warnings.push(format!(
130                        "High cyclomatic complexity: {} (recommended: < 10)",
131                        complexity_metrics.cyclomatic
132                    ));
133                }
134
135                if complexity_metrics.cognitive > 15 {
136                    warnings.push(format!(
137                        "High cognitive complexity: {} (recommended: < 15)",
138                        complexity_metrics.cognitive
139                    ));
140                }
141
142                if complexity_metrics.maintainability_index < 50.0 {
143                    warnings.push(format!(
144                        "Low maintainability index: {:.1} (recommended: > 50.0)",
145                        complexity_metrics.maintainability_index
146                    ));
147                }
148
149                if !warnings.is_empty() {
150                    result["warnings"] = serde_json::json!(warnings);
151                }
152            }
153
154            result
155        } else {
156            serde_json::json!({
157                "target": node.name,
158                "error": "Invalid line range for symbol",
159                "symbol_analysis": {
160                    "id": node.id.to_hex(),
161                    "name": node.name,
162                    "kind": format!("{:?}", node.kind),
163                    "file": node.file.display().to_string(),
164                    "span": {
165                        "start_line": node.span.start_line,
166                        "end_line": node.span.end_line
167                    }
168                }
169            })
170        }
171    } else {
172        serde_json::json!({
173            "target": node.name,
174            "error": "Failed to read symbol's source file",
175            "symbol_analysis": {
176                "id": node.id.to_hex(),
177                "name": node.name,
178                "file": node.file.display().to_string()
179            }
180        })
181    }
182}
183
184/// List complexity analysis tools
185pub fn list_tools() -> Vec<Tool> {
186    vec![
187        Tool {
188            name: "analyze_complexity".to_string(),
189            title: Some("Analyze Code Complexity".to_string()),
190            description: "Calculate comprehensive complexity metrics including cyclomatic, cognitive, Halstead metrics, and maintainability index for code elements".to_string(),
191            input_schema: serde_json::json!({
192                "type": "object",
193                "properties": {
194                    "target": {
195                        "type": "string",
196                        "description": "File path or symbol ID to analyze"
197                    },
198                    "metrics": {
199                        "type": "array",
200                        "items": {
201                            "type": "string",
202                            "enum": ["cyclomatic", "cognitive", "halstead", "maintainability_index", "maintainability", "all"]
203                        },
204                        "description": "Types of complexity metrics to calculate. 'all' includes all available metrics.",
205                        "default": ["all"]
206                    },
207                    "threshold_warnings": {
208                        "type": "boolean",
209                        "description": "Include warnings for metrics exceeding recommended thresholds",
210                        "default": true
211                    }
212                },
213                "required": ["target"],
214                "additionalProperties": false
215            }),
216        }
217    ]
218}
219
220/// Route complexity analysis tool calls
221pub async fn call_tool(
222    server: &CodePrismMcpServer,
223    params: &CallToolParams,
224) -> Result<CallToolResult> {
225    match params.name.as_str() {
226        "analyze_complexity" => analyze_complexity(server, params.arguments.as_ref()).await,
227        _ => Err(anyhow::anyhow!(
228            "Unknown complexity analysis tool: {}",
229            params.name
230        )),
231    }
232}
233
234/// Analyze code complexity
235async fn analyze_complexity(
236    server: &CodePrismMcpServer,
237    arguments: Option<&Value>,
238) -> Result<CallToolResult> {
239    let args = arguments.ok_or_else(|| anyhow::anyhow!("Missing arguments"))?;
240
241    // Support both "target" and "path" parameter names for backward compatibility
242    let target = args
243        .get("target")
244        .or_else(|| args.get("path"))
245        .and_then(|v| v.as_str())
246        .ok_or_else(|| anyhow::anyhow!("Missing target parameter (or path)"))?;
247
248    let metrics = args
249        .get("metrics")
250        .and_then(|v| v.as_array())
251        .map(|arr| {
252            arr.iter()
253                .filter_map(|v| v.as_str())
254                .map(|s| s.to_string())
255                .collect::<Vec<_>>()
256        })
257        .unwrap_or_else(|| vec!["all".to_string()]);
258
259    let threshold_warnings = args
260        .get("threshold_warnings")
261        .and_then(|v| v.as_bool())
262        .unwrap_or(true);
263
264    // Try to resolve as symbol identifier or file path
265    let result = if target.contains('/') || target.contains('.') {
266        // Handle as file path
267        analyze_file_complexity(target, &metrics, threshold_warnings)
268    } else {
269        // Try to resolve as symbol name using search, then analyze
270        match server.graph_query().search_symbols(target, None, Some(1)) {
271            Ok(symbol_results) => {
272                if let Some(symbol_result) = symbol_results.first() {
273                    analyze_symbol_complexity(&symbol_result.node, &metrics, threshold_warnings)
274                } else {
275                    serde_json::json!({
276                        "target": target,
277                        "error": "Symbol not found",
278                        "suggestion": "Try using a file path or check if the symbol name is correct",
279                        "available_metrics": ["cyclomatic", "cognitive", "halstead", "maintainability_index", "all"]
280                    })
281                }
282            }
283            Err(e) => {
284                serde_json::json!({
285                    "target": target,
286                    "error": format!("Failed to search for symbol: {}", e),
287                    "suggestion": "Try using a file path instead",
288                    "available_metrics": ["cyclomatic", "cognitive", "halstead", "maintainability_index", "all"]
289                })
290            }
291        }
292    };
293
294    Ok(CallToolResult {
295        content: vec![ToolContent::Text {
296            text: serde_json::to_string_pretty(&result)?,
297        }],
298        is_error: Some(result.get("error").is_some()),
299    })
300}