ochna 0.3.1

A structural code graph indexing and analysis CLI using Tree-sitter and SQLite
//! The project-wide symbol index and its string interner. Symbols are stored as
//! integer surrogates ([`SymbolIx`]) over interned strings to keep the call-edge
//! resolution tables compact and cache-friendly.

use crate::db::Node;
use rustc_hash::FxHashMap;
use smallvec::SmallVec;

pub type SymbolIx = u32;
pub type InternedString = u32;
pub type CandidateList = SmallVec<[SymbolIx; 4]>;
pub type CandidateByFile = FxHashMap<InternedString, CandidateList>;

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SymbolCandidate {
    pub id: InternedString,
    pub file_path: InternedString,
    pub namespace: Option<InternedString>,
    pub qualified_name: InternedString,
}

#[derive(Debug, Default)]
pub struct SymbolIndex {
    pub symbols: Vec<SymbolCandidate>,
    pub strings: Vec<String>,
    pub by_name: FxHashMap<String, CandidateList>,
    pub by_name_file: FxHashMap<String, CandidateByFile>,
    pub by_qualified_name: FxHashMap<String, CandidateList>,
    pub by_owner_and_name: FxHashMap<(String, String), CandidateList>,
    pub by_id: FxHashMap<String, SymbolIx>,
}

#[derive(Debug, Default)]
pub struct SymbolIndexBuilder {
    symbols: Vec<SymbolCandidate>,
    by_name: FxHashMap<String, CandidateList>,
    by_name_file: FxHashMap<String, CandidateByFile>,
    by_qualified_name: FxHashMap<String, CandidateList>,
    by_owner_and_name: FxHashMap<(String, String), CandidateList>,
    by_id: FxHashMap<String, SymbolIx>,
    interner: StringInterner,
}

impl SymbolIndexBuilder {
    pub fn push(
        &mut self,
        id: &str,
        name: &str,
        file_path: &str,
        qualified_name: Option<&str>,
    ) -> SymbolIx {
        let ix = self.symbols.len() as SymbolIx;
        let id_ix = self.interner.intern(id);
        let file_path_ix = self.interner.intern(file_path);
        let namespace_ix = qualified_name
            .and_then(parent_namespace)
            .map(|namespace| self.interner.intern(namespace));

        let qname = if let Some(q) = qualified_name {
            q
        } else {
            let prefix = format!("{}::", file_path);
            if let Some(stripped) = id.strip_prefix(&prefix) {
                stripped
            } else {
                id
            }
        };
        let qualified_name_ix = self.interner.intern(qname);

        self.symbols.push(SymbolCandidate::new(
            id_ix,
            file_path_ix,
            namespace_ix,
            qualified_name_ix,
        ));
        self.by_name.entry(name.to_string()).or_default().push(ix);
        self.by_name_file
            .entry(name.to_string())
            .or_default()
            .entry(file_path_ix)
            .or_default()
            .push(ix);
        if let Some(qname) = qualified_name {
            self.by_qualified_name
                .entry(qname.to_string())
                .or_default()
                .push(ix);
            if let Some((owner, _)) = qname.rsplit_once("::") {
                self.by_owner_and_name
                    .entry((owner.to_string(), name.to_string()))
                    .or_default()
                    .push(ix);
            }
        }
        self.by_id.insert(id.to_string(), ix);
        ix
    }

    pub fn finish(self) -> SymbolIndex {
        SymbolIndex {
            symbols: self.symbols,
            strings: self.interner.into_strings(),
            by_name: self.by_name,
            by_name_file: self.by_name_file,
            by_qualified_name: self.by_qualified_name,
            by_owner_and_name: self.by_owner_and_name,
            by_id: self.by_id,
        }
    }
}

impl SymbolIndex {
    pub fn from_nodes(nodes: &[Node]) -> Self {
        let mut builder = SymbolIndexBuilder::default();
        for node in nodes {
            builder.push(
                &node.id,
                &node.name,
                &node.file_path,
                node.qualified_name.as_deref(),
            );
        }
        builder.finish()
    }
}

#[derive(Debug, Default)]
pub struct StringInterner {
    strings: Vec<String>,
    by_value: FxHashMap<String, InternedString>,
}

impl StringInterner {
    pub fn intern(&mut self, value: &str) -> InternedString {
        if let Some(ix) = self.by_value.get(value) {
            return *ix;
        }

        let ix = self.strings.len() as InternedString;
        let value = value.to_string();
        self.strings.push(value.clone());
        self.by_value.insert(value, ix);
        ix
    }

    pub fn resolve(&self, ix: InternedString) -> &str {
        &self.strings[ix as usize]
    }

    pub fn into_strings(self) -> Vec<String> {
        self.strings
    }
}

impl SymbolCandidate {
    pub fn new(
        id: InternedString,
        file_path: InternedString,
        namespace: Option<InternedString>,
        qualified_name: InternedString,
    ) -> Self {
        Self {
            id,
            file_path,
            namespace,
            qualified_name,
        }
    }
}

fn parent_namespace(qualified_name: &str) -> Option<&str> {
    let (parent, _) = qualified_name.rsplit_once("::")?;
    parent.rsplit("::").next()
}