Skip to main content

graphyn_core/
query.rs

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}