1use std::collections::{BTreeSet, HashSet, VecDeque};
2
3use petgraph::visit::EdgeRef;
4use petgraph::Direction;
5
6use crate::error::GraphynError;
7use crate::graph::GraphynGraph;
8use crate::index::find_symbol_id;
9use crate::ir::SymbolId;
10
11const DEFAULT_DEPTH: usize = 3;
12const MAX_DEPTH: usize = 10;
13
14#[derive(Debug, Clone, PartialEq, Eq)]
15pub struct QueryEdge {
16 pub from: SymbolId,
17 pub to: SymbolId,
18 pub file: String,
19 pub line: u32,
20 pub alias: Option<String>,
21 pub properties_accessed: Vec<String>,
22 pub context: String,
23 pub hop: usize,
24}
25
26pub fn blast_radius(
27 graph: &GraphynGraph,
28 symbol: &str,
29 file: Option<&str>,
30 depth: Option<usize>,
31) -> Result<Vec<QueryEdge>, GraphynError> {
32 let effective_depth = depth.unwrap_or(DEFAULT_DEPTH);
33 if effective_depth > MAX_DEPTH {
34 return Err(GraphynError::InvalidDepth {
35 depth: effective_depth,
36 max: MAX_DEPTH,
37 });
38 }
39
40 let root = find_symbol_id(graph, symbol, file)?;
41 traverse(graph, &root, effective_depth, Direction::Incoming)
42}
43
44pub fn dependencies(
45 graph: &GraphynGraph,
46 symbol: &str,
47 file: Option<&str>,
48 depth: Option<usize>,
49) -> Result<Vec<QueryEdge>, GraphynError> {
50 let effective_depth = depth.unwrap_or(DEFAULT_DEPTH);
51 if effective_depth > MAX_DEPTH {
52 return Err(GraphynError::InvalidDepth {
53 depth: effective_depth,
54 max: MAX_DEPTH,
55 });
56 }
57
58 let root = find_symbol_id(graph, symbol, file)?;
59 traverse(graph, &root, effective_depth, Direction::Outgoing)
60}
61
62pub fn symbol_usages(
63 graph: &GraphynGraph,
64 symbol: &str,
65 file: Option<&str>,
66 include_aliases: bool,
67) -> Result<Vec<QueryEdge>, GraphynError> {
68 let root = find_symbol_id(graph, symbol, file)?;
69 let mut results = traverse(graph, &root, 1, Direction::Incoming)?;
70
71 if include_aliases {
72 if let Some(aliases) = graph.alias_chains.get(&root) {
73 let alias_set: HashSet<String> = aliases.iter().map(|a| a.alias_name.clone()).collect();
74 for edge in &mut results {
75 if edge.alias.is_none() && edge.context.contains(" as ") {
76 let alias = edge
77 .context
78 .split(" as ")
79 .nth(1)
80 .and_then(|v| v.split_whitespace().next())
81 .map(|s| s.trim_matches(|c: char| c == ',' || c == ';').to_string());
82 if let Some(found) = alias {
83 if alias_set.contains(&found) {
84 edge.alias = Some(found);
85 }
86 }
87 }
88 }
89 }
90 } else {
91 results.retain(|edge| edge.alias.is_none());
92 }
93
94 dedupe_edges(results)
95}
96
97fn traverse(
98 graph: &GraphynGraph,
99 root: &SymbolId,
100 max_depth: usize,
101 direction: Direction,
102) -> Result<Vec<QueryEdge>, GraphynError> {
103 let Some(root_node) = graph.node_index.get(root).map(|v| *v) else {
104 return Err(GraphynError::SymbolNotFound(root.clone()));
105 };
106
107 let mut queue = VecDeque::new();
108 let mut visited = HashSet::new();
109 let mut results = Vec::new();
110
111 queue.push_back((root_node, 0usize));
112 visited.insert(root_node);
113
114 while let Some((node, depth)) = queue.pop_front() {
115 if depth >= max_depth {
116 continue;
117 }
118
119 for edge in graph.graph.edges_directed(node, direction) {
120 let neighbor = if direction == Direction::Incoming {
121 edge.source()
122 } else {
123 edge.target()
124 };
125
126 let from_id = graph
127 .graph
128 .node_weight(edge.source())
129 .cloned()
130 .ok_or_else(|| GraphynError::GraphCorrupt("Missing source node".to_string()))?;
131 let to_id = graph
132 .graph
133 .node_weight(edge.target())
134 .cloned()
135 .ok_or_else(|| GraphynError::GraphCorrupt("Missing target node".to_string()))?;
136 let meta = edge.weight();
137
138 results.push(QueryEdge {
139 from: from_id,
140 to: to_id,
141 file: meta.file.clone(),
142 line: meta.line,
143 alias: meta.alias.clone(),
144 properties_accessed: meta.properties_accessed.clone(),
145 context: meta.context.clone(),
146 hop: depth + 1,
147 });
148
149 if visited.insert(neighbor) {
150 queue.push_back((neighbor, depth + 1));
151 }
152 }
153 }
154
155 dedupe_edges(results)
156}
157
158fn dedupe_edges(mut edges: Vec<QueryEdge>) -> Result<Vec<QueryEdge>, GraphynError> {
159 let mut seen = BTreeSet::new();
160 edges.retain(|edge| {
161 seen.insert((
162 edge.from.clone(),
163 edge.to.clone(),
164 edge.file.clone(),
165 edge.line,
166 edge.alias.clone(),
167 ))
168 });
169
170 edges.sort_by(|a, b| {
171 a.hop
172 .cmp(&b.hop)
173 .then(a.file.cmp(&b.file))
174 .then(a.line.cmp(&b.line))
175 .then(a.from.cmp(&b.from))
176 .then(a.to.cmp(&b.to))
177 });
178
179 Ok(edges)
180}