car-ast 0.13.0

Tree-sitter AST parsing for code-aware inference
Documentation
use super::node_text;
use crate::types::*;

pub fn extract(tree: &tree_sitter::Tree, source: &[u8]) -> (Vec<Symbol>, Vec<Import>) {
    let root = tree.root_node();
    let mut symbols = Vec::new();

    // JSON root should be a document containing a single value (usually an object)
    let mut cursor = root.walk();
    for child in root.children(&mut cursor) {
        if child.kind() == "object" {
            extract_object_keys(&child, source, &mut symbols, true);
        }
    }

    (symbols, Vec::new())
}

fn extract_object_keys(
    node: &tree_sitter::Node,
    source: &[u8],
    symbols: &mut Vec<Symbol>,
    top_level: bool,
) {
    let mut cursor = node.walk();
    for child in node.children(&mut cursor) {
        if child.kind() == "pair" {
            let key_node = child.child_by_field_name("key");
            let value_node = child.child_by_field_name("value");

            if let Some(key) = key_node {
                let key_text = node_text(&key, source);
                // Strip quotes from the key
                let name = key_text.trim_matches('"').to_string();

                let is_object_value = value_node.map(|v| v.kind() == "object").unwrap_or(false);

                if top_level {
                    if is_object_value {
                        // Nested object at top level -> Module with children
                        let mut children = Vec::new();
                        if let Some(val) = value_node {
                            extract_object_keys(&val, source, &mut children, false);
                        }
                        symbols.push(Symbol {
                            name,
                            kind: SymbolKind::Module,
                            span: Span::from_node(&child),
                            signature: String::new(),
                            doc_comment: None,
                            parent: None,
                            children,
                        });
                    } else {
                        // Scalar or array at top level -> Const
                        let value_text = value_node
                            .map(|v| node_text(&v, source).to_string())
                            .unwrap_or_default();
                        symbols.push(Symbol {
                            name,
                            kind: SymbolKind::Const,
                            span: Span::from_node(&child),
                            signature: value_text,
                            doc_comment: None,
                            parent: None,
                            children: Vec::new(),
                        });
                    }
                } else if is_object_value {
                    // Nested object below top level -> Module (no further recursion)
                    symbols.push(Symbol {
                        name,
                        kind: SymbolKind::Module,
                        span: Span::from_node(&child),
                        signature: String::new(),
                        doc_comment: None,
                        parent: None,
                        children: Vec::new(),
                    });
                } else {
                    // Scalar below top level -> Const
                    let value_text = value_node
                        .map(|v| node_text(&v, source).to_string())
                        .unwrap_or_default();
                    symbols.push(Symbol {
                        name,
                        kind: SymbolKind::Const,
                        span: Span::from_node(&child),
                        signature: value_text,
                        doc_comment: None,
                        parent: None,
                        children: Vec::new(),
                    });
                }
            }
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    fn parse_json(source: &str) -> (Vec<Symbol>, Vec<Import>) {
        let mut parser = tree_sitter::Parser::new();
        parser
            .set_language(&tree_sitter_json::LANGUAGE.into())
            .unwrap();
        let tree = parser.parse(source, None).unwrap();
        extract(&tree, source.as_bytes())
    }

    #[test]
    fn test_json_extraction() {
        let source = r#"{
    "name": "my-project",
    "version": "1.0.0",
    "scripts": {
        "build": "cargo build",
        "test": "cargo test"
    },
    "dependencies": {
        "serde": {
            "version": "1.0"
        }
    }
}"#;

        let (symbols, imports) = parse_json(source);

        // No imports in JSON
        assert!(imports.is_empty());

        // Top-level scalar keys: "name", "version" -> Const
        let consts: Vec<_> = symbols
            .iter()
            .filter(|s| s.kind == SymbolKind::Const)
            .collect();
        assert!(consts.iter().any(|s| s.name == "name"));
        assert!(consts.iter().any(|s| s.name == "version"));

        // Top-level object keys: "scripts", "dependencies" -> Module
        let modules: Vec<_> = symbols
            .iter()
            .filter(|s| s.kind == SymbolKind::Module)
            .collect();
        assert!(modules.iter().any(|s| s.name == "scripts"));
        assert!(modules.iter().any(|s| s.name == "dependencies"));

        // "scripts" module should have children (build, test)
        let scripts = symbols.iter().find(|s| s.name == "scripts").unwrap();
        assert_eq!(scripts.children.len(), 2);
        assert!(scripts.children.iter().any(|c| c.name == "build"));
        assert!(scripts.children.iter().any(|c| c.name == "test"));

        // "dependencies" module has nested object "serde" -> Module child
        let deps = symbols.iter().find(|s| s.name == "dependencies").unwrap();
        assert_eq!(deps.children.len(), 1);
        assert_eq!(deps.children[0].name, "serde");
        assert_eq!(deps.children[0].kind, SymbolKind::Module);
    }
}