graphyn-core 0.1.3

Language-agnostic code intelligence graph engine — the heart of Graphyn
Documentation
use dashmap::DashMap;
use petgraph::graph::{DiGraph, NodeIndex};

use crate::ir::{ReExportEntry, Relationship, RelationshipKind, Symbol, SymbolId};
use crate::resolver::AliasEntry;

#[derive(Debug, Clone)]
pub struct RelationshipMeta {
    pub kind: RelationshipKind,
    pub alias: Option<String>,
    pub properties_accessed: Vec<String>,
    pub context: String,
    pub file: String,
    pub line: u32,
}

#[derive(Debug)]
pub struct GraphynGraph {
    pub graph: DiGraph<SymbolId, RelationshipMeta>,
    pub node_index: DashMap<SymbolId, NodeIndex>,
    pub name_index: DashMap<String, Vec<SymbolId>>,
    pub file_index: DashMap<String, Vec<SymbolId>>,
    pub symbols: DashMap<SymbolId, Symbol>,
    pub alias_chains: DashMap<SymbolId, Vec<AliasEntry>>,
    pub file_reexports: DashMap<String, Vec<ReExportEntry>>,
}

impl Default for GraphynGraph {
    fn default() -> Self {
        Self::new()
    }
}

impl GraphynGraph {
    pub fn new() -> Self {
        Self {
            graph: DiGraph::new(),
            node_index: DashMap::new(),
            name_index: DashMap::new(),
            file_index: DashMap::new(),
            symbols: DashMap::new(),
            alias_chains: DashMap::new(),
            file_reexports: DashMap::new(),
        }
    }

    pub fn add_symbol(&mut self, symbol: Symbol) {
        if self.node_index.contains_key(&symbol.id) {
            self.replace_symbol(symbol);
            return;
        }

        let symbol_id = symbol.id.clone();
        let symbol_name = symbol.name.clone();
        let file = symbol.file.clone();

        let node = self.graph.add_node(symbol_id.clone());
        self.node_index.insert(symbol_id.clone(), node);
        self.symbols.insert(symbol_id.clone(), symbol);

        self.name_index
            .entry(symbol_name)
            .and_modify(|ids| {
                ids.push(symbol_id.clone());
                ids.sort();
                ids.dedup();
            })
            .or_insert_with(|| vec![symbol_id.clone()]);

        self.file_index
            .entry(file)
            .and_modify(|ids| {
                ids.push(symbol_id.clone());
                ids.sort();
                ids.dedup();
            })
            .or_insert_with(|| vec![symbol_id]);
    }

    pub fn replace_symbol(&mut self, symbol: Symbol) {
        let symbol_id = symbol.id.clone();
        if let Some(existing) = self.symbols.get(&symbol_id) {
            let existing_name = existing.name.clone();
            let existing_file = existing.file.clone();
            drop(existing);

            if existing_name != symbol.name {
                if let Some(mut ids) = self.name_index.get_mut(&existing_name) {
                    ids.retain(|id| id != &symbol_id);
                }
                self.name_index
                    .entry(symbol.name.clone())
                    .and_modify(|ids| {
                        ids.push(symbol_id.clone());
                        ids.sort();
                        ids.dedup();
                    })
                    .or_insert_with(|| vec![symbol_id.clone()]);
            }

            if existing_file != symbol.file {
                if let Some(mut ids) = self.file_index.get_mut(&existing_file) {
                    ids.retain(|id| id != &symbol_id);
                }
                self.file_index
                    .entry(symbol.file.clone())
                    .and_modify(|ids| {
                        ids.push(symbol_id.clone());
                        ids.sort();
                        ids.dedup();
                    })
                    .or_insert_with(|| vec![symbol_id.clone()]);
            }
        }

        self.symbols.insert(symbol_id, symbol);
    }

    pub fn add_relationship(&mut self, relationship: &Relationship) {
        let Some(from) = self.node_index.get(&relationship.from).map(|v| *v) else {
            return;
        };

        // Auto-create external package nodes on first reference
        if !self.node_index.contains_key(&relationship.to) && relationship.to.starts_with("ext::") {
            let package_name = relationship
                .to
                .strip_prefix("ext::")
                .and_then(|s| s.strip_suffix("::package"))
                .unwrap_or(&relationship.to)
                .to_string();
            self.add_symbol(crate::ir::Symbol {
                id: relationship.to.clone(),
                name: package_name,
                kind: crate::ir::SymbolKind::ExternalPackage,
                language: crate::ir::Language::TypeScript,
                file: String::new(),
                line_start: 0,
                line_end: 0,
                signature: None,
            });
        }

        let Some(to) = self.node_index.get(&relationship.to).map(|v| *v) else {
            return;
        };

        let meta = RelationshipMeta {
            kind: relationship.kind.clone(),
            alias: relationship.alias.clone(),
            properties_accessed: relationship.properties_accessed.clone(),
            context: relationship.context.clone(),
            file: relationship.file.clone(),
            line: relationship.line,
        };
        self.graph.add_edge(from, to, meta);
    }

    pub fn remove_relationships_in_file(&mut self, file: &str) -> usize {
        let edge_ids: Vec<_> = self
            .graph
            .edge_indices()
            .filter(|edge_id| {
                self.graph
                    .edge_weight(*edge_id)
                    .map(|meta| meta.file == file)
                    .unwrap_or(false)
            })
            .collect();

        let removed = edge_ids.len();
        for edge_id in edge_ids {
            let _ = self.graph.remove_edge(edge_id);
        }
        removed
    }

    pub fn remove_file(&mut self, file: &str) -> Vec<SymbolId> {
        let mut removed = Vec::new();
        let symbol_ids = self
            .file_index
            .remove(file)
            .map(|(_, ids)| ids)
            .unwrap_or_default();

        for symbol_id in &symbol_ids {
            if let Some((_, symbol)) = self.symbols.remove(symbol_id) {
                if let Some(mut ids) = self.name_index.get_mut(&symbol.name) {
                    ids.retain(|id| id != symbol_id);
                }
            }
            if let Some((_, node)) = self.node_index.remove(symbol_id) {
                let _ = self.graph.remove_node(node);
            }
            self.alias_chains.remove(symbol_id);
            removed.push(symbol_id.clone());
        }
        self.file_reexports.remove(file);

        self.rebuild_node_index();
        removed.sort();
        removed
    }

    fn rebuild_node_index(&self) {
        self.node_index.clear();
        for node_index in self.graph.node_indices() {
            if let Some(symbol_id) = self.graph.node_weight(node_index) {
                self.node_index.insert(symbol_id.clone(), node_index);
            }
        }
    }
}