codebase-graph 1.1.6

Native codebaseGraph CLI and MCP server for local code knowledge graphs.
use crate::protocol::CaptureMapping;
use serde_json::Value;
use std::collections::BTreeMap;

#[derive(Debug, Clone)]
pub(crate) struct SyntaxNode {
    pub(crate) node_type: String,
    pub(crate) text: String,
    pub(crate) line_start: Option<i64>,
    pub(crate) line_end: Option<i64>,
    pub(crate) byte_start: Option<i64>,
    pub(crate) byte_end: Option<i64>,
    pub(crate) capture_name: String,
    pub(crate) children: Vec<SyntaxNode>,
    pub(crate) fields: BTreeMap<String, Value>,
}

pub(crate) fn mapping_for_syntax_node<'a>(
    node: &SyntaxNode,
    mappings: &'a [CaptureMapping],
    ancestors: &[String],
) -> Option<&'a CaptureMapping> {
    let candidates = mappings
        .iter()
        .filter(|mapping| {
            mapping
                .parser_node_types
                .iter()
                .any(|node_type| node_type == &node.node_type)
        })
        .collect::<Vec<_>>();
    for mapping in &candidates {
        if !mapping.context_rule.is_empty()
            && context_rule_matches(&mapping.context_rule, node, ancestors)
        {
            return Some(mapping);
        }
    }
    candidates
        .into_iter()
        .find(|mapping| mapping.context_rule.is_empty())
}

fn context_rule_matches(rule: &str, node: &SyntaxNode, ancestors: &[String]) -> bool {
    let normalized = rule.trim().to_lowercase();
    if let Some(expected) = normalized.strip_prefix("inside ") {
        return ancestors
            .iter()
            .any(|ancestor| context_name_matches(ancestor, expected));
    }
    if let Some(expected_type) = normalized.strip_prefix("type is ") {
        return field_type_matches(node, "type", expected_type);
    }
    if normalized == "qualified declarator" {
        return field_descendant_has(node, "declarator", "qualified_identifier");
    }
    if normalized == "function declarator" {
        return field_type_matches(node, "declarator", "function_declarator")
            || field_descendant_has(node, "declarator", "function_declarator");
    }
    false
}

fn context_name_matches(node_type: &str, expected: &str) -> bool {
    match expected {
        "impl" => node_type == "impl_item",
        "class" => matches!(node_type, "class_specifier" | "struct_specifier"),
        value => node_type == value,
    }
}

fn field_type_matches(node: &SyntaxNode, field_name: &str, expected_type: &str) -> bool {
    node.fields
        .get("_field_types")
        .and_then(Value::as_object)
        .and_then(|values| values.get(field_name))
        .and_then(Value::as_str)
        .is_some_and(|value| value == expected_type)
}

fn field_descendant_has(node: &SyntaxNode, field_name: &str, expected_type: &str) -> bool {
    node.fields
        .get("_field_descendant_types")
        .and_then(Value::as_object)
        .and_then(|values| values.get(field_name))
        .and_then(Value::as_array)
        .is_some_and(|values| {
            values
                .iter()
                .filter_map(Value::as_str)
                .any(|value| value == expected_type)
        })
}