Skip to main content

graphyn_core/
graph.rs

1use dashmap::DashMap;
2use petgraph::graph::{DiGraph, NodeIndex};
3
4use crate::ir::{ReExportEntry, Relationship, RelationshipKind, Symbol, SymbolId};
5use crate::resolver::AliasEntry;
6
7#[derive(Debug, Clone)]
8pub struct RelationshipMeta {
9    pub kind: RelationshipKind,
10    pub alias: Option<String>,
11    pub properties_accessed: Vec<String>,
12    pub context: String,
13    pub file: String,
14    pub line: u32,
15}
16
17#[derive(Debug)]
18pub struct GraphynGraph {
19    pub graph: DiGraph<SymbolId, RelationshipMeta>,
20    pub node_index: DashMap<SymbolId, NodeIndex>,
21    pub name_index: DashMap<String, Vec<SymbolId>>,
22    pub file_index: DashMap<String, Vec<SymbolId>>,
23    pub symbols: DashMap<SymbolId, Symbol>,
24    pub alias_chains: DashMap<SymbolId, Vec<AliasEntry>>,
25    pub file_reexports: DashMap<String, Vec<ReExportEntry>>,
26}
27
28impl Default for GraphynGraph {
29    fn default() -> Self {
30        Self::new()
31    }
32}
33
34impl GraphynGraph {
35    pub fn new() -> Self {
36        Self {
37            graph: DiGraph::new(),
38            node_index: DashMap::new(),
39            name_index: DashMap::new(),
40            file_index: DashMap::new(),
41            symbols: DashMap::new(),
42            alias_chains: DashMap::new(),
43            file_reexports: DashMap::new(),
44        }
45    }
46
47    pub fn add_symbol(&mut self, symbol: Symbol) {
48        if self.node_index.contains_key(&symbol.id) {
49            self.replace_symbol(symbol);
50            return;
51        }
52
53        let symbol_id = symbol.id.clone();
54        let symbol_name = symbol.name.clone();
55        let file = symbol.file.clone();
56
57        let node = self.graph.add_node(symbol_id.clone());
58        self.node_index.insert(symbol_id.clone(), node);
59        self.symbols.insert(symbol_id.clone(), symbol);
60
61        self.name_index
62            .entry(symbol_name)
63            .and_modify(|ids| {
64                ids.push(symbol_id.clone());
65                ids.sort();
66                ids.dedup();
67            })
68            .or_insert_with(|| vec![symbol_id.clone()]);
69
70        self.file_index
71            .entry(file)
72            .and_modify(|ids| {
73                ids.push(symbol_id.clone());
74                ids.sort();
75                ids.dedup();
76            })
77            .or_insert_with(|| vec![symbol_id]);
78    }
79
80    pub fn replace_symbol(&mut self, symbol: Symbol) {
81        let symbol_id = symbol.id.clone();
82        if let Some(existing) = self.symbols.get(&symbol_id) {
83            let existing_name = existing.name.clone();
84            let existing_file = existing.file.clone();
85            drop(existing);
86
87            if existing_name != symbol.name {
88                if let Some(mut ids) = self.name_index.get_mut(&existing_name) {
89                    ids.retain(|id| id != &symbol_id);
90                }
91                self.name_index
92                    .entry(symbol.name.clone())
93                    .and_modify(|ids| {
94                        ids.push(symbol_id.clone());
95                        ids.sort();
96                        ids.dedup();
97                    })
98                    .or_insert_with(|| vec![symbol_id.clone()]);
99            }
100
101            if existing_file != symbol.file {
102                if let Some(mut ids) = self.file_index.get_mut(&existing_file) {
103                    ids.retain(|id| id != &symbol_id);
104                }
105                self.file_index
106                    .entry(symbol.file.clone())
107                    .and_modify(|ids| {
108                        ids.push(symbol_id.clone());
109                        ids.sort();
110                        ids.dedup();
111                    })
112                    .or_insert_with(|| vec![symbol_id.clone()]);
113            }
114        }
115
116        self.symbols.insert(symbol_id, symbol);
117    }
118
119    pub fn add_relationship(&mut self, relationship: &Relationship) {
120        let Some(from) = self.node_index.get(&relationship.from).map(|v| *v) else {
121            return;
122        };
123
124        // Auto-create external package nodes on first reference
125        if !self.node_index.contains_key(&relationship.to) && relationship.to.starts_with("ext::") {
126            let package_name = relationship
127                .to
128                .strip_prefix("ext::")
129                .and_then(|s| s.strip_suffix("::package"))
130                .unwrap_or(&relationship.to)
131                .to_string();
132            self.add_symbol(crate::ir::Symbol {
133                id: relationship.to.clone(),
134                name: package_name,
135                kind: crate::ir::SymbolKind::ExternalPackage,
136                language: crate::ir::Language::TypeScript,
137                file: String::new(),
138                line_start: 0,
139                line_end: 0,
140                signature: None,
141            });
142        }
143
144        let Some(to) = self.node_index.get(&relationship.to).map(|v| *v) else {
145            return;
146        };
147
148        let meta = RelationshipMeta {
149            kind: relationship.kind.clone(),
150            alias: relationship.alias.clone(),
151            properties_accessed: relationship.properties_accessed.clone(),
152            context: relationship.context.clone(),
153            file: relationship.file.clone(),
154            line: relationship.line,
155        };
156        self.graph.add_edge(from, to, meta);
157    }
158
159    pub fn remove_relationships_in_file(&mut self, file: &str) -> usize {
160        let edge_ids: Vec<_> = self
161            .graph
162            .edge_indices()
163            .filter(|edge_id| {
164                self.graph
165                    .edge_weight(*edge_id)
166                    .map(|meta| meta.file == file)
167                    .unwrap_or(false)
168            })
169            .collect();
170
171        let removed = edge_ids.len();
172        for edge_id in edge_ids {
173            let _ = self.graph.remove_edge(edge_id);
174        }
175        removed
176    }
177
178    pub fn remove_file(&mut self, file: &str) -> Vec<SymbolId> {
179        let mut removed = Vec::new();
180        let symbol_ids = self
181            .file_index
182            .remove(file)
183            .map(|(_, ids)| ids)
184            .unwrap_or_default();
185
186        for symbol_id in &symbol_ids {
187            if let Some((_, symbol)) = self.symbols.remove(symbol_id) {
188                if let Some(mut ids) = self.name_index.get_mut(&symbol.name) {
189                    ids.retain(|id| id != symbol_id);
190                }
191            }
192            if let Some((_, node)) = self.node_index.remove(symbol_id) {
193                let _ = self.graph.remove_node(node);
194            }
195            self.alias_chains.remove(symbol_id);
196            removed.push(symbol_id.clone());
197        }
198        self.file_reexports.remove(file);
199
200        self.rebuild_node_index();
201        removed.sort();
202        removed
203    }
204
205    fn rebuild_node_index(&self) {
206        self.node_index.clear();
207        for node_index in self.graph.node_indices() {
208            if let Some(symbol_id) = self.graph.node_weight(node_index) {
209                self.node_index.insert(symbol_id.clone(), node_index);
210            }
211        }
212    }
213}