use std::collections::{HashSet, VecDeque};
use super::super::shared::{
dedupe_nodes, edge_kind_label, node_matches_symbol, sort_nodes_for_output,
};
use crate::types::{Edge, EdgeKind, Node, NodeKind};
pub(super) fn format_explore_blast_radius(
cg: &mut crate::CodeGraph,
nodes: &[Node],
) -> Vec<String> {
let mut rows = Vec::new();
let mut seen_symbols = HashSet::new();
for node in nodes {
if !matches!(
node.kind,
NodeKind::Function | NodeKind::Method | NodeKind::Class | NodeKind::Interface
) {
continue;
}
if !seen_symbols.insert(node.id.clone()) {
continue;
}
let callers = cg
.get_incoming_edges(&node.id)
.into_iter()
.filter(|edge| {
!matches!(
edge.kind,
EdgeKind::Contains | EdgeKind::Imports | EdgeKind::Exports
)
})
.filter_map(|edge| cg.get_node(&edge.source))
.collect::<Vec<_>>();
if callers.is_empty() {
continue;
}
let mut caller_labels = callers
.iter()
.map(|caller| format!("{} ({})", caller.name, caller.file_path))
.collect::<Vec<_>>();
caller_labels.sort();
caller_labels.dedup();
let mut tests = callers
.iter()
.map(|caller| caller.file_path.clone())
.filter(|path| is_test_file_path(path))
.collect::<Vec<_>>();
tests.sort();
tests.dedup();
let tests_label = if tests.is_empty() {
"no covering tests".to_string()
} else {
format!("tests: {}", tests.join(", "))
};
rows.push(format!(
"- `{}`: {} caller(s): {}; {}",
node.name,
caller_labels.len(),
caller_labels.join(", "),
tests_label
));
}
if rows.is_empty() {
Vec::new()
} else {
let mut lines = vec!["### Blast radius".to_string()];
lines.extend(rows);
lines
}
}
pub(super) fn exact_named_query_nodes(nodes: &[Node], query_tokens: &HashSet<String>) -> Vec<Node> {
let mut out = nodes
.iter()
.filter(|node| {
is_dynamic_named_node(node)
&& query_tokens.iter().any(|token| {
node.name == *token
|| node.qualified_name == *token
|| node_matches_symbol(node, token)
})
})
.cloned()
.collect::<Vec<_>>();
sort_nodes_for_output(&mut out);
dedupe_nodes(&mut out);
out
}
fn is_dynamic_named_node(node: &Node) -> bool {
matches!(
node.kind,
NodeKind::Function | NodeKind::Method | NodeKind::Class | NodeKind::Component
)
}
pub(super) fn has_static_flow_between_named(
cg: &mut crate::CodeGraph,
named_nodes: &[Node],
) -> bool {
if named_nodes.len() < 2 {
return false;
}
let targets = named_nodes
.iter()
.map(|node| node.id.clone())
.collect::<HashSet<_>>();
for seed in named_nodes {
let mut seen = HashSet::from([seed.id.clone()]);
let mut frontier = vec![seed.id.clone()];
for _ in 0..6 {
let mut next = Vec::new();
for id in frontier {
for edge in cg.get_outgoing_edges(&id) {
if edge.kind != EdgeKind::Calls {
continue;
}
if edge.target != seed.id && targets.contains(&edge.target) {
return true;
}
if seen.insert(edge.target.clone()) {
next.push(edge.target);
}
}
}
if next.is_empty() {
break;
}
frontier = next;
}
}
false
}
#[derive(Clone)]
struct FlowStep {
node: Node,
edge: Option<Edge>,
}
pub(super) fn format_flow_section(
cg: &mut crate::CodeGraph,
named_nodes: &[Node],
query_tokens: &HashSet<String>,
) -> Vec<String> {
let mut lines = vec!["## Flow (call path among the symbols you queried)".to_string()];
if named_nodes.len() < 2 {
lines.push(format!("- {}", flow_hint_for_query(cg, query_tokens)));
return lines;
}
let mut paths = Vec::new();
let mut seen_paths = HashSet::new();
for from in named_nodes {
for to in named_nodes {
if from.id == to.id {
continue;
}
let Some(path) = find_named_flow_path(cg, from, &to.id, 8) else {
continue;
};
let key = path
.iter()
.map(|step| step.node.id.as_str())
.collect::<Vec<_>>()
.join(">");
if seen_paths.insert(key) {
paths.push(path);
}
if paths.len() >= 3 {
break;
}
}
if paths.len() >= 3 {
break;
}
}
if paths.is_empty() {
lines.push("- No static call path found among the queried symbols; source context is included below.".to_string());
return lines;
}
for path in paths {
lines.push(format!("- {}", format_flow_path(&path)));
}
lines
}
fn find_named_flow_path(
cg: &mut crate::CodeGraph,
from: &Node,
target_id: &str,
max_depth: usize,
) -> Option<Vec<FlowStep>> {
let mut visited = HashSet::from([from.id.clone()]);
let mut queue = VecDeque::from([vec![FlowStep {
node: from.clone(),
edge: None,
}]]);
let mut expansions = 0usize;
while let Some(path) = queue.pop_front() {
let current = path.last()?.node.clone();
if current.id == target_id {
return Some(path);
}
if path.len().saturating_sub(1) >= max_depth {
continue;
}
expansions += 1;
if expansions > 600 {
break;
}
let mut edges = cg
.get_outgoing_edges(¤t.id)
.into_iter()
.filter(|edge| is_flow_edge(edge.kind))
.collect::<Vec<_>>();
edges.sort_by(|a, b| a.target.cmp(&b.target));
for edge in edges {
if !visited.insert(edge.target.clone()) {
continue;
}
let Some(next_node) = cg.get_node(&edge.target) else {
continue;
};
if is_low_value_flow_node(&next_node) && next_node.id != target_id {
continue;
}
let mut next_path = path.clone();
next_path.push(FlowStep {
node: next_node,
edge: Some(edge),
});
queue.push_back(next_path);
}
}
None
}
fn is_flow_edge(kind: EdgeKind) -> bool {
matches!(
kind,
EdgeKind::Calls
| EdgeKind::References
| EdgeKind::Instantiates
| EdgeKind::Decorates
| EdgeKind::Overrides
)
}
fn is_low_value_flow_node(node: &Node) -> bool {
matches!(
node.kind,
NodeKind::File | NodeKind::Import | NodeKind::Export | NodeKind::Parameter
)
}
fn format_flow_path(path: &[FlowStep]) -> String {
let mut rendered = Vec::new();
for (index, step) in path.iter().enumerate() {
if index == 0 {
rendered.push(format_flow_node(&step.node));
continue;
}
let via = step
.edge
.as_ref()
.map(|edge| edge_kind_label(edge.kind))
.unwrap_or("edge");
rendered.push(format!("--{via}--> {}", format_flow_node(&step.node)));
}
rendered.join(" ")
}
fn format_flow_node(node: &Node) -> String {
format!("{} ({}:{})", node.name, node.file_path, node.start_line)
}
fn is_test_file_path(path: &str) -> bool {
let normalized = path.replace('\\', "/").to_ascii_lowercase();
normalized.contains(".test.")
|| normalized.contains(".spec.")
|| normalized.contains("/test/")
|| normalized.contains("/tests/")
}
fn flow_hint_for_query(cg: &mut crate::CodeGraph, query_tokens: &HashSet<String>) -> String {
let mut callables = Vec::new();
for token in query_tokens {
let has_callable = cg.search_nodes(token, None).into_iter().any(|result| {
result.node.name == *token
&& matches!(result.node.kind, NodeKind::Function | NodeKind::Method)
});
if has_callable {
callables.push(token.clone());
}
}
callables.sort();
if callables.is_empty() {
"named symbols gathered into the file context below".to_string()
} else {
callables.join(" -> ")
}
}