repopilot 0.12.0

Local-first CLI for repository audit, architecture risk detection, baseline tracking, and CI-friendly code review.
Documentation
use crate::graph::imports::common::{extract_string_literal, is_relative};
use std::collections::HashSet;
use tree_sitter::{Language, Node, Parser};

pub(super) fn extract(content: &str, language: Option<&str>) -> HashSet<String> {
    let Some(tree) = parse(content, language) else {
        return HashSet::new();
    };

    let mut result = HashSet::new();
    visit(tree.root_node(), content, &mut result);
    result
}

fn parse(content: &str, language_name: Option<&str>) -> Option<tree_sitter::Tree> {
    let language: Language = match language_name {
        Some("TypeScript React") => tree_sitter_typescript::LANGUAGE_TSX.into(),
        Some("TypeScript") => tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into(),
        _ => tree_sitter_javascript::LANGUAGE.into(),
    };
    let mut parser = Parser::new();
    parser.set_language(&language).ok()?;
    parser.parse(content, None)
}

fn visit(node: Node<'_>, content: &str, result: &mut HashSet<String>) {
    match node.kind() {
        "import_statement" | "export_statement" => {
            if let Some(path) = module_source(node, content)
                && is_relative(path)
            {
                result.insert(path.to_string());
            }
        }
        "call_expression" => {
            if let Some(path) = require_source(node, content)
                && is_relative(path)
            {
                result.insert(path.to_string());
            }
        }
        _ => {}
    }

    let mut cursor = node.walk();
    for child in node.children(&mut cursor) {
        visit(child, content, result);
    }
}

fn module_source<'a>(node: Node<'_>, content: &'a str) -> Option<&'a str> {
    if let Some(source) = node.child_by_field_name("source") {
        let text = source.utf8_text(content.as_bytes()).ok()?.trim();
        return extract_string_literal(text);
    }

    let text = node.utf8_text(content.as_bytes()).ok()?;
    extract_from_path(text)
}

fn require_source<'a>(node: Node<'_>, content: &'a str) -> Option<&'a str> {
    let function = node.child_by_field_name("function")?;
    if function.utf8_text(content.as_bytes()).ok()? != "require" {
        return None;
    }
    let arguments = node.child_by_field_name("arguments")?;
    let text = arguments.utf8_text(content.as_bytes()).ok()?.trim();
    let text = text.strip_prefix('(')?.trim();
    let text = text.strip_suffix(')')?.trim();
    extract_string_literal(text)
}

fn extract_from_path(line: &str) -> Option<&str> {
    let pos = line.rfind(" from ")?;
    let after = line[pos + 6..].trim();
    extract_string_literal(after)
}