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 partition_by_alias<'e>(
39 graph: &GraphynGraph,
40 edges: &'e [QueryEdge],
41) -> (Vec<&'e QueryEdge>, Vec<&'e QueryEdge>) {
42 let mut direct = Vec::new();
43 let mut aliased = Vec::new();
44
45 for edge in edges {
46 if is_renamed(graph, edge) {
47 aliased.push(edge);
48 } else {
49 direct.push(edge);
50 }
51 }
52
53 (direct, aliased)
54}
55
56pub fn is_renamed(graph: &GraphynGraph, edge: &QueryEdge) -> bool {
58 let Some(alias) = edge.alias.as_deref() else {
59 return false;
60 };
61 match graph.symbols.get(&edge.to) {
62 Some(symbol) => alias.rsplit(['.', ':']).next().unwrap_or(alias) != symbol.name,
65 None => true,
67 }
68}
69
70pub fn blast_radius(
71 graph: &GraphynGraph,
72 symbol: &str,
73 file: Option<&str>,
74 depth: Option<usize>,
75) -> Result<Vec<QueryEdge>, GraphynError> {
76 let effective_depth = depth.unwrap_or(DEFAULT_DEPTH);
77 if effective_depth > MAX_DEPTH {
78 return Err(GraphynError::InvalidDepth {
79 depth: effective_depth,
80 max: MAX_DEPTH,
81 });
82 }
83
84 let root = find_symbol_id(graph, symbol, file)?;
85 traverse(graph, &root, effective_depth, Direction::Incoming)
86}
87
88pub fn dependencies(
89 graph: &GraphynGraph,
90 symbol: &str,
91 file: Option<&str>,
92 depth: Option<usize>,
93) -> Result<Vec<QueryEdge>, GraphynError> {
94 let effective_depth = depth.unwrap_or(DEFAULT_DEPTH);
95 if effective_depth > MAX_DEPTH {
96 return Err(GraphynError::InvalidDepth {
97 depth: effective_depth,
98 max: MAX_DEPTH,
99 });
100 }
101
102 let root = find_symbol_id(graph, symbol, file)?;
103 traverse(graph, &root, effective_depth, Direction::Outgoing)
104}
105
106pub fn symbol_usages(
107 graph: &GraphynGraph,
108 symbol: &str,
109 file: Option<&str>,
110 include_aliases: bool,
111) -> Result<Vec<QueryEdge>, GraphynError> {
112 let root = find_symbol_id(graph, symbol, file)?;
113 let mut results = traverse(graph, &root, 1, Direction::Incoming)?;
114
115 if include_aliases {
116 if let Some(aliases) = graph.alias_chains.get(&root) {
117 let alias_set: HashSet<String> = aliases.iter().map(|a| a.alias_name.clone()).collect();
118 for edge in &mut results {
119 if edge.alias.is_none() && edge.context.contains(" as ") {
120 let alias = edge
121 .context
122 .split(" as ")
123 .nth(1)
124 .and_then(|v| v.split_whitespace().next())
125 .map(|s| s.trim_matches(|c: char| c == ',' || c == ';').to_string());
126 if let Some(found) = alias {
127 if alias_set.contains(&found) {
128 edge.alias = Some(found);
129 }
130 }
131 }
132 }
133 }
134 } else {
135 results.retain(|edge| edge.alias.is_none());
136 }
137
138 dedupe_edges(results)
139}
140
141fn traverse(
142 graph: &GraphynGraph,
143 root: &SymbolId,
144 max_depth: usize,
145 direction: Direction,
146) -> Result<Vec<QueryEdge>, GraphynError> {
147 let Some(root_node) = graph.node_index.get(root).map(|v| *v) else {
148 return Err(GraphynError::SymbolNotFound(root.clone()));
149 };
150
151 let mut queue = VecDeque::new();
152 let mut visited = HashSet::new();
153 let mut results = Vec::new();
154
155 queue.push_back((root_node, 0usize));
156 visited.insert(root_node);
157
158 while let Some((node, depth)) = queue.pop_front() {
159 if depth >= max_depth {
160 continue;
161 }
162
163 for edge in graph.graph.edges_directed(node, direction) {
164 let neighbor = if direction == Direction::Incoming {
165 edge.source()
166 } else {
167 edge.target()
168 };
169
170 let from_id = graph
171 .graph
172 .node_weight(edge.source())
173 .cloned()
174 .ok_or_else(|| GraphynError::GraphCorrupt("Missing source node".to_string()))?;
175 let to_id = graph
176 .graph
177 .node_weight(edge.target())
178 .cloned()
179 .ok_or_else(|| GraphynError::GraphCorrupt("Missing target node".to_string()))?;
180 let meta = edge.weight();
181
182 results.push(QueryEdge {
183 from: from_id,
184 to: to_id,
185 file: meta.file.clone(),
186 line: meta.line,
187 alias: meta.alias.clone(),
188 properties_accessed: meta.properties_accessed.clone(),
189 context: meta.context.clone(),
190 hop: depth + 1,
191 });
192
193 if visited.insert(neighbor) {
194 queue.push_back((neighbor, depth + 1));
195 }
196 }
197 }
198
199 dedupe_edges(results)
200}
201
202fn dedupe_edges(mut edges: Vec<QueryEdge>) -> Result<Vec<QueryEdge>, GraphynError> {
203 let mut seen = BTreeSet::new();
204 edges.retain(|edge| {
205 seen.insert((
206 edge.from.clone(),
207 edge.to.clone(),
208 edge.file.clone(),
209 edge.line,
210 edge.alias.clone(),
211 ))
212 });
213
214 edges.sort_by(|a, b| {
215 a.hop
216 .cmp(&b.hop)
217 .then(a.file.cmp(&b.file))
218 .then(a.line.cmp(&b.line))
219 .then(a.from.cmp(&b.from))
220 .then(a.to.cmp(&b.to))
221 });
222
223 Ok(edges)
224}