cxpak 0.10.0

Spends CPU cycles so you don't spend tokens. The LLM gets a briefing packet instead of a flashlight in a dark room.
Documentation
use crate::parser::language::{
    Export, Import, LanguageSupport, ParseResult, Symbol, SymbolKind, Visibility,
};
use tree_sitter::Language as TsLanguage;

pub struct TomlLangLanguage;

impl TomlLangLanguage {
    fn node_text<'a>(node: &tree_sitter::Node, source: &'a [u8]) -> &'a str {
        node.utf8_text(source).unwrap_or("")
    }

    fn first_line(node: &tree_sitter::Node, source: &[u8]) -> String {
        let text = Self::node_text(node, source);
        text.lines().next().unwrap_or("").trim().to_string()
    }

    fn full_text(node: &tree_sitter::Node, source: &[u8]) -> String {
        Self::node_text(node, source).to_string()
    }

    /// Extract the table header name (e.g., `[package]` -> `package`, `[dependencies]` -> `dependencies`).
    /// The grammar always produces `bare_key`, `quoted_key`, or `dotted_key` children.
    fn extract_table_name(node: &tree_sitter::Node, source: &[u8]) -> String {
        let mut cursor = node.walk();
        for child in node.children(&mut cursor) {
            let kind = child.kind();
            if kind.contains("key") {
                return Self::node_text(&child, source)
                    .trim_matches('"')
                    .trim_matches('\'')
                    .to_string();
            }
        }
        String::new()
    }

    /// Extract the key name from a pair node.
    /// The grammar always produces `bare_key`, `quoted_key`, or `dotted_key` children.
    fn extract_key_name(node: &tree_sitter::Node, source: &[u8]) -> String {
        let mut cursor = node.walk();
        for child in node.children(&mut cursor) {
            let kind = child.kind();
            if kind.contains("key") {
                return Self::node_text(&child, source)
                    .trim_matches('"')
                    .trim_matches('\'')
                    .to_string();
            }
        }
        String::new()
    }
}

impl LanguageSupport for TomlLangLanguage {
    fn ts_language(&self) -> TsLanguage {
        tree_sitter_toml_updated::language()
    }

    fn name(&self) -> &str {
        "toml"
    }

    fn extract(&self, source: &str, tree: &tree_sitter::Tree) -> ParseResult {
        let source_bytes = source.as_bytes();
        let root = tree.root_node();

        let mut symbols: Vec<Symbol> = Vec::new();
        let imports: Vec<Import> = Vec::new();
        let exports: Vec<Export> = Vec::new();

        let mut cursor = root.walk();

        for node in root.children(&mut cursor) {
            match node.kind() {
                "table" | "table_array_element" => {
                    let name = Self::extract_table_name(&node, source_bytes);
                    if !name.is_empty() {
                        let start_line = node.start_position().row + 1;
                        let end_line = node.end_position().row + 1;

                        symbols.push(Symbol {
                            name,
                            kind: SymbolKind::Table,
                            visibility: Visibility::Public,
                            signature: Self::first_line(&node, source_bytes),
                            body: Self::full_text(&node, source_bytes),
                            start_line,
                            end_line,
                        });
                    }
                }

                "pair" => {
                    let name = Self::extract_key_name(&node, source_bytes);
                    if !name.is_empty() {
                        let start_line = node.start_position().row + 1;
                        let end_line = node.end_position().row + 1;

                        symbols.push(Symbol {
                            name,
                            kind: SymbolKind::Key,
                            visibility: Visibility::Public,
                            signature: Self::first_line(&node, source_bytes),
                            body: Self::full_text(&node, source_bytes),
                            start_line,
                            end_line,
                        });
                    }
                }

                _ => {}
            }
        }

        ParseResult {
            symbols,
            imports,
            exports,
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::parser::language::{SymbolKind, Visibility};

    fn make_parser() -> tree_sitter::Parser {
        let mut parser = tree_sitter::Parser::new();
        parser
            .set_language(&tree_sitter_toml_updated::language())
            .expect("failed to set language");
        parser
    }

    #[test]
    fn test_extract_tables() {
        let source = r#"[package]
name = "my-crate"
version = "1.0.0"

[dependencies]
serde = "1"
"#;
        let mut parser = make_parser();
        let tree = parser.parse(source, None).expect("parse failed");
        let lang = TomlLangLanguage;
        let result = lang.extract(source, &tree);

        let tables: Vec<_> = result
            .symbols
            .iter()
            .filter(|s| s.kind == SymbolKind::Table)
            .collect();
        assert!(
            tables.len() >= 2,
            "expected at least 2 tables, got: {:?}",
            tables.iter().map(|t| &t.name).collect::<Vec<_>>()
        );
    }

    #[test]
    fn test_extract_top_level_keys() {
        let source = r#"name = "my-project"
version = "1.0.0"
edition = "2021"
"#;
        let mut parser = make_parser();
        let tree = parser.parse(source, None).expect("parse failed");
        let lang = TomlLangLanguage;
        let result = lang.extract(source, &tree);

        let keys: Vec<_> = result
            .symbols
            .iter()
            .filter(|s| s.kind == SymbolKind::Key)
            .collect();
        assert!(
            keys.len() >= 3,
            "expected at least 3 keys, got: {:?}",
            keys.iter().map(|k| &k.name).collect::<Vec<_>>()
        );
    }

    #[test]
    fn test_empty_source() {
        let source = "";
        let mut parser = make_parser();
        let tree = parser.parse(source, None).unwrap();
        let lang = TomlLangLanguage;
        let result = lang.extract(source, &tree);
        assert!(result.symbols.is_empty());
        assert!(result.imports.is_empty());
        assert!(result.exports.is_empty());
    }

    #[test]
    fn test_complex_toml() {
        let source = r#"[package]
name = "cxpak"
version = "0.9.0"
edition = "2021"

[dependencies]
clap = { version = "4", features = ["derive"] }
tree-sitter = "0.25"
serde = { version = "1", features = ["derive"] }

[features]
default = ["lang-rust", "lang-python"]
lang-rust = ["dep:tree-sitter-rust"]
lang-python = ["dep:tree-sitter-python"]

[[bin]]
name = "cxpak"
path = "src/main.rs"
"#;
        let mut parser = make_parser();
        let tree = parser.parse(source, None).expect("parse failed");
        let lang = TomlLangLanguage;
        let result = lang.extract(source, &tree);

        assert!(
            !result.symbols.is_empty(),
            "expected symbols from complex TOML"
        );

        let tables: Vec<_> = result
            .symbols
            .iter()
            .filter(|s| s.kind == SymbolKind::Table)
            .collect();
        assert!(
            tables.len() >= 3,
            "expected at least 3 tables, got: {:?}",
            tables.iter().map(|t| &t.name).collect::<Vec<_>>()
        );
    }

    #[test]
    fn test_symbol_kinds() {
        let source = "[section]\nkey = \"value\"\n";
        let mut parser = make_parser();
        let tree = parser.parse(source, None).expect("parse failed");
        let lang = TomlLangLanguage;
        let result = lang.extract(source, &tree);

        let has_table = result.symbols.iter().any(|s| s.kind == SymbolKind::Table);
        assert!(has_table, "expected Table symbol kind");

        assert!(
            result
                .symbols
                .iter()
                .all(|s| s.visibility == Visibility::Public),
            "all TOML symbols should be public"
        );
    }

    #[test]
    fn test_no_imports_exports() {
        let source = "key = \"value\"\n";
        let mut parser = make_parser();
        let tree = parser.parse(source, None).expect("parse failed");
        let lang = TomlLangLanguage;
        let result = lang.extract(source, &tree);
        assert!(result.imports.is_empty(), "toml should have no imports");
        assert!(result.exports.is_empty(), "toml should have no exports");
    }

    #[test]
    fn test_extract_table_name_fallback() {
        // A table with a quoted key exercises the trim_matches('"') path,
        // and an empty-ish table name exercises the fallback first_line path.
        let source = "[\"quoted-section\"]\nval = 1\n";
        let mut parser = make_parser();
        let tree = parser.parse(source, None).expect("parse failed");
        let lang = TomlLangLanguage;
        let result = lang.extract(source, &tree);

        let tables: Vec<_> = result
            .symbols
            .iter()
            .filter(|s| s.kind == SymbolKind::Table)
            .collect();
        assert!(!tables.is_empty(), "expected table from quoted key");
        assert_eq!(tables[0].name, "quoted-section");
    }

    #[test]
    fn test_extract_key_name_fallback() {
        // The extract_key_name fallback path parses text before '=' when no
        // matching child kind is found.  We test the normal path and verify
        // that a pair whose key text is empty is skipped by the `!name.is_empty()` guard.
        let source = "dotted.key.path = true\n";
        let mut parser = make_parser();
        let tree = parser.parse(source, None).expect("parse failed");
        let lang = TomlLangLanguage;
        let result = lang.extract(source, &tree);

        let keys: Vec<_> = result
            .symbols
            .iter()
            .filter(|s| s.kind == SymbolKind::Key)
            .collect();
        assert!(!keys.is_empty(), "expected key from dotted key");
    }

    #[test]
    fn test_table_array() {
        let source = r#"[[bin]]
name = "tool"
path = "src/main.rs"

[[bin]]
name = "helper"
path = "src/helper.rs"
"#;
        let mut parser = make_parser();
        let tree = parser.parse(source, None).expect("parse failed");
        let lang = TomlLangLanguage;
        let result = lang.extract(source, &tree);

        let tables: Vec<_> = result
            .symbols
            .iter()
            .filter(|s| s.kind == SymbolKind::Table)
            .collect();
        assert!(
            tables.len() >= 2,
            "expected at least 2 table array elements, got: {:?}",
            tables.iter().map(|t| &t.name).collect::<Vec<_>>()
        );
    }
}