cha-parser 1.17.0

Tree-sitter based AST parser for Cha (TypeScript, Rust)
Documentation
//! Rust `use` declaration → ImportsMap resolver.

use cha_core::{ImportInfo, TypeOrigin, TypeRef};
use tree_sitter::Node;

use crate::type_ref::{self, ImportsMap};

/// Read the `return_type` field of a `function_item`, resolving the origin
/// via the file's imports map.
pub fn rust_return_type(node: Node, src: &[u8], imports: &ImportsMap) -> Option<TypeRef> {
    let rt = node.child_by_field_name("return_type")?;
    let raw = rt.utf8_text(src).unwrap_or("").to_string();
    Some(type_ref::resolve(raw, imports))
}

/// Parallel to `extract_param_types` — returns identifier names in the
/// same order. `self` parameters are skipped to keep the vector
/// length-aligned with `parameter_types` (which only records typed
/// parameters). Anonymous `_` surfaces as empty string.
pub fn rust_param_names(node: Node, src: &[u8]) -> Vec<String> {
    let Some(params) = node.child_by_field_name("parameters") else {
        return vec![];
    };
    let mut names = Vec::new();
    let mut cursor = params.walk();
    for child in params.children(&mut cursor) {
        if child.kind() == "parameter"
            && let Some(pat) = child.child_by_field_name("pattern")
        {
            let text = pat.utf8_text(src).unwrap_or("");
            names.push(text.trim_start_matches("mut ").trim().to_string());
        } else if child.kind() == "parameter" {
            names.push(String::new());
        }
    }
    names
}

/// Collect match-arm literal values from a Rust function body.
pub fn rust_collect_arm_values(body: Node, src: &[u8]) -> Vec<cha_core::ArmValue> {
    let mut out = Vec::new();
    crate::switch_arms::walk_arms(body, src, &mut out, &|n| n.kind() == "match_arm");
    out
}

/// Build an `ImportInfo` from a `use_declaration` node.
pub fn extract_use(node: Node, src: &[u8]) -> Option<ImportInfo> {
    let source = node
        .utf8_text(src)
        .unwrap_or("")
        .strip_prefix("use ")?
        .trim_end_matches(';')
        .trim()
        .to_string();
    Some(ImportInfo {
        source,
        line: node.start_position().row + 1,
        col: node.start_position().column,
        ..Default::default()
    })
}

/// Scan the whole tree for `use` declarations and build a short-name ->
/// TypeOrigin map. Call once per file before collecting functions.
pub fn build(root: Node, src: &[u8]) -> ImportsMap {
    let mut map = ImportsMap::new();
    walk(root, src, &mut map);
    map
}

fn walk(node: Node, src: &[u8], map: &mut ImportsMap) {
    if node.kind() == "use_declaration" {
        let text = node.utf8_text(src).unwrap_or("").trim();
        if let Some(rest) = text.strip_prefix("use ") {
            let path = rest.trim_end_matches(';').trim();
            parse_use_path(path, map);
        }
    }
    let mut cursor = node.walk();
    for child in node.children(&mut cursor) {
        walk(child, src, map);
    }
}

/// Parse one `use` path and insert all short-name bindings into map.
/// Handles `foo::Bar`, `foo::Bar as Baz`, `foo::{Bar, Baz}`.
fn parse_use_path(path: &str, map: &mut ImportsMap) {
    // Group expansion: `foo::{A, B as C}`.
    if let Some(open) = path.find('{')
        && let Some(close) = path.rfind('}')
    {
        let prefix = path[..open].trim_end_matches("::").trim();
        for item in path[open + 1..close].split(',') {
            let item = item.trim();
            if !item.is_empty() {
                parse_use_path(&format!("{prefix}::{item}"), map);
            }
        }
        return;
    }
    let (path_part, alias) = match path.split_once(" as ") {
        Some((p, a)) => (p.trim(), Some(a.trim().to_string())),
        None => (path, None),
    };
    if path_part.ends_with("::*") || path_part == "*" {
        return;
    }
    let short = alias.unwrap_or_else(|| {
        path_part
            .rsplit("::")
            .next()
            .unwrap_or(path_part)
            .to_string()
    });
    map.insert(short, classify(path_part));
}

fn classify(path: &str) -> TypeOrigin {
    let root = path.split("::").next().unwrap_or("").trim();
    match root {
        "crate" | "self" | "super" => TypeOrigin::Local,
        "std" | "core" | "alloc" => TypeOrigin::Primitive,
        "" => TypeOrigin::Unknown,
        other => TypeOrigin::External(other.to_string()),
    }
}

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

    fn parse(src: &str) -> ImportsMap {
        let mut parser = tree_sitter::Parser::new();
        parser
            .set_language(&tree_sitter_rust::LANGUAGE.into())
            .unwrap();
        let tree = parser.parse(src, None).unwrap();
        build(tree.root_node(), src.as_bytes())
    }

    #[test]
    fn simple_external() {
        let m = parse("use tree_sitter::Node;");
        assert_eq!(
            m.get("Node"),
            Some(&TypeOrigin::External("tree_sitter".into()))
        );
    }

    #[test]
    fn crate_local() {
        let m = parse("use crate::model::Finding;");
        assert_eq!(m.get("Finding"), Some(&TypeOrigin::Local));
    }

    #[test]
    fn std_primitive() {
        let m = parse("use std::collections::HashMap;");
        assert_eq!(m.get("HashMap"), Some(&TypeOrigin::Primitive));
    }

    #[test]
    fn alias_rename() {
        let m = parse("use tree_sitter::Node as TsNode;");
        assert_eq!(
            m.get("TsNode"),
            Some(&TypeOrigin::External("tree_sitter".into()))
        );
        assert!(m.get("Node").is_none());
    }

    #[test]
    fn group_expansion() {
        let m = parse("use tree_sitter::{Node, Parser};");
        assert_eq!(
            m.get("Node"),
            Some(&TypeOrigin::External("tree_sitter".into()))
        );
        assert_eq!(
            m.get("Parser"),
            Some(&TypeOrigin::External("tree_sitter".into()))
        );
    }

    #[test]
    fn glob_ignored() {
        let m = parse("use tree_sitter::*;");
        assert!(m.is_empty());
    }
}