codeprism_mcp/tools/analysis/
complexity.rs1use 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
10fn 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 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
41fn analyze_symbol_complexity(
43 node: &codeprism_core::Node,
44 metrics: &[String],
45 threshold_warnings: bool,
46) -> Value {
47 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 let start_line = node.span.start_line.saturating_sub(1); 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 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 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
184pub 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
220pub 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
234async 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 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 let result = if target.contains('/') || target.contains('.') {
266 analyze_file_complexity(target, &metrics, threshold_warnings)
268 } else {
269 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}