grapha 0.1.0

Blazingly fast code intelligence CLI and MCP server for Swift and Rust
Documentation
use std::collections::HashMap;

use serde::Serialize;

use grapha_core::graph::{Graph, Node, NodeKind};

use crate::localization::{
    LocalizationCatalogIndex, LocalizationCatalogRecord, LocalizationReference, edges_by_source,
    localization_usage_nodes, node_index, resolve_usage,
};

use super::SymbolInfo;
use super::l10n::{contains_parents, to_symbol_info, ui_path};

#[derive(Debug, Serialize)]
pub struct UsagesResult {
    pub query: UsageQuery,
    pub records: Vec<RecordUsages>,
}

#[derive(Debug, Serialize)]
pub struct UsageQuery {
    pub key: String,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub table: Option<String>,
}

#[derive(Debug, Serialize)]
pub struct RecordUsages {
    pub record: LocalizationCatalogRecord,
    pub usages: Vec<UsageSite>,
}

#[derive(Debug, Serialize)]
pub struct UsageSite {
    pub owner: SymbolInfo,
    pub view: SymbolInfo,
    #[serde(skip_serializing_if = "Vec::is_empty")]
    pub ui_path: Vec<String>,
    pub reference: LocalizationReference,
}

pub fn query_usages(
    graph: &Graph,
    catalogs: &LocalizationCatalogIndex,
    key: &str,
    table: Option<&str>,
) -> UsagesResult {
    let records = if let Some(table) = table {
        catalogs.records_for(table, key)
    } else {
        catalogs.records_for_key(key)
    };

    let node_index = node_index(graph);
    let edges_by_source = edges_by_source(graph);
    let parents = contains_parents(graph);

    let mut record_groups = Vec::new();
    for record in records {
        let usages = localization_usage_nodes(graph)
            .into_iter()
            .filter_map(|usage_node| {
                let resolution =
                    resolve_usage(usage_node, &edges_by_source, &node_index, catalogs)?;
                let matched_reference = resolution.matches.into_iter().find_map(|item| {
                    (item.record.table == record.table
                        && item.record.key == record.key
                        && item.record.catalog_file == record.catalog_file)
                        .then_some(item.reference)
                })?;

                let owner = owning_symbol(usage_node.id.as_str(), &parents, &node_index)
                    .unwrap_or(usage_node);
                Some(UsageSite {
                    owner: to_symbol_info(owner),
                    view: to_symbol_info(usage_node),
                    ui_path: ui_path(
                        usage_node.id.as_str(),
                        owner.id.as_str(),
                        &parents,
                        &node_index,
                    ),
                    reference: matched_reference,
                })
            })
            .collect();

        record_groups.push(RecordUsages { record, usages });
    }

    UsagesResult {
        query: UsageQuery {
            key: key.to_string(),
            table: table.map(ToString::to_string),
        },
        records: record_groups,
    }
}

fn owning_symbol<'a>(
    node_id: &'a str,
    parents: &HashMap<&'a str, &'a str>,
    node_index: &HashMap<&'a str, &'a Node>,
) -> Option<&'a Node> {
    let mut current = Some(node_id);
    while let Some(id) = current {
        let node = node_index.get(id).copied()?;
        if !matches!(node.kind, NodeKind::View | NodeKind::Branch) {
            return Some(node);
        }
        current = parents.get(id).copied();
    }
    None
}