use std::collections::{HashMap, VecDeque};
use petgraph::graph::NodeIndex;
use serde_json::{json, Value};
use crate::mcp::context::AnalysisContext;
pub fn execute(args: &HashMap<String, Value>, context: &AnalysisContext) -> Result<Value, String> {
let from_name = args
.get("from_function")
.and_then(|v| v.as_str())
.ok_or("Missing required argument 'from_function'")?;
let to_name = args
.get("to_function")
.and_then(|v| v.as_str())
.ok_or("Missing required argument 'to_function'")?;
let max_depth = args.get("max_depth").and_then(|v| v.as_u64()).unwrap_or(10) as usize;
let max_paths = args.get("max_paths").and_then(|v| v.as_u64()).unwrap_or(3) as usize;
let graph = &context.graph;
let from_idx = graph
.find_node_by_name(from_name)
.ok_or_else(|| format!("Function '{}' not found in the code graph", from_name))?;
let to_idx = graph
.find_node_by_name(to_name)
.ok_or_else(|| format!("Function '{}' not found in the code graph", to_name))?;
let from_node = graph.get_node(from_idx).unwrap();
let to_node = graph.get_node(to_idx).unwrap();
let forward_paths = find_paths(from_idx, to_idx, max_depth, max_paths, |idx| {
graph.calls_from_compatible(idx)
});
let reverse_paths = find_paths(to_idx, from_idx, max_depth, max_paths, |idx| {
graph.calls_from_compatible(idx)
});
let format_path = |path: &[NodeIndex]| -> Value {
let steps: Vec<Value> = path
.iter()
.map(|&idx| {
if let Some(n) = graph.get_node(idx) {
json!({
"name": n.name,
"full_name": n.full_name,
"file": n.location.file,
"line": n.location.line_start,
})
} else {
json!({ "index": idx.index() })
}
})
.collect();
let chain: String = path
.iter()
.filter_map(|&idx| graph.get_node(idx).map(|n| n.name.as_str()))
.collect::<Vec<_>>()
.join(" → ");
json!({
"hops": path.len() - 1,
"chain": chain,
"steps": steps,
})
};
let forward_json: Vec<Value> = forward_paths.iter().map(|p| format_path(p)).collect();
let reverse_json: Vec<Value> = reverse_paths.iter().map(|p| format_path(p)).collect();
let connected = !forward_paths.is_empty() || !reverse_paths.is_empty();
let result = json!({
"from": {
"name": from_node.name,
"full_name": from_node.full_name,
"file": from_node.location.file,
"line": from_node.location.line_start,
},
"to": {
"name": to_node.name,
"full_name": to_node.full_name,
"file": to_node.location.file,
"line": to_node.location.line_start,
},
"connected": connected,
"forward": {
"found": !forward_paths.is_empty(),
"count": forward_paths.len(),
"paths": forward_json,
},
"reverse": {
"found": !reverse_paths.is_empty(),
"count": reverse_paths.len(),
"paths": reverse_json,
},
});
Ok(json!({
"content": [{
"type": "text",
"text": serde_json::to_string_pretty(&result).unwrap_or_default()
}]
}))
}
fn find_paths<F>(
start: NodeIndex,
target: NodeIndex,
max_depth: usize,
max_paths: usize,
neighbors: F,
) -> Vec<Vec<NodeIndex>>
where
F: Fn(NodeIndex) -> Vec<NodeIndex>,
{
if start == target {
return vec![vec![start]];
}
let mut results = Vec::new();
let mut queue: VecDeque<(NodeIndex, Vec<NodeIndex>)> = VecDeque::new();
queue.push_back((start, vec![start]));
let mut found_depth: Option<usize> = None;
let mut visited_at_depth: HashMap<NodeIndex, usize> = HashMap::new();
visited_at_depth.insert(start, 0);
while let Some((current, path)) = queue.pop_front() {
let depth = path.len() - 1;
if let Some(fd) = found_depth {
if depth >= fd {
continue;
}
}
if depth >= max_depth {
continue;
}
for next in neighbors(current) {
if path.contains(&next) {
continue;
}
let next_depth = depth + 1;
if let Some(&prev_depth) = visited_at_depth.get(&next) {
if prev_depth < next_depth {
continue;
}
}
visited_at_depth.insert(next, next_depth);
let mut new_path = path.clone();
new_path.push(next);
if next == target {
found_depth = Some(next_depth);
results.push(new_path);
if results.len() >= max_paths {
return results;
}
} else {
queue.push_back((next, new_path));
}
}
}
results
}