repopilot 0.18.0

Local-first CLI for reviewing Git changes, security boundaries, and blast radius before merge.
Documentation
use crate::review::signals::behavioral::{
    BehavioralKind, BehavioralSignal, BehavioralSignalSource, DependencyContext, truncate_str,
};
use tree_sitter::Node;

pub(super) fn match_rust(
    node: Node<'_>,
    content: &str,
    path_str: &str,
    line: usize,
    dependencies: &DependencyContext,
) -> Option<BehavioralSignal> {
    match node.kind() {
        "call_expression" => {
            let function = node.child_by_field_name("function")?;
            let func_text = function.utf8_text(content.as_bytes()).ok()?.trim();
            let text = node.utf8_text(content.as_bytes()).unwrap_or("");

            if func_text.contains("reqwest::") || func_text == "TcpStream::connect" {
                return Some(BehavioralSignal {
                    kind: BehavioralKind::NetworkCallAdded,
                    path: path_str.to_string(),
                    line,
                    detail: truncate_str(text, 60),
                    source: BehavioralSignalSource::Ast,
                });
            }
            if func_text.contains("Command::new") {
                return Some(BehavioralSignal {
                    kind: BehavioralKind::SubprocessAdded,
                    path: path_str.to_string(),
                    line,
                    detail: truncate_str(text, 60),
                    source: BehavioralSignalSource::Ast,
                });
            }
            if func_text.contains("fs::write")
                || func_text.contains("File::create")
                || func_text.contains("OpenOptions::new")
            {
                return Some(BehavioralSignal {
                    kind: BehavioralKind::FsWriteAdded,
                    path: path_str.to_string(),
                    line,
                    detail: truncate_str(text, 60),
                    source: BehavioralSignalSource::Ast,
                });
            }
            if func_text.contains("env::var") || func_text.contains("std::env::var") {
                return Some(BehavioralSignal {
                    kind: BehavioralKind::EnvVarIntroduced,
                    path: path_str.to_string(),
                    line,
                    detail: truncate_str(text, 60),
                    source: BehavioralSignalSource::Ast,
                });
            }
        }
        "macro_invocation" => {
            let macro_name = node
                .child(0)
                .and_then(|n| n.utf8_text(content.as_bytes()).ok())
                .unwrap_or("")
                .trim();
            let text = node.utf8_text(content.as_bytes()).unwrap_or("");
            if macro_name == "query" || macro_name == "sql" || macro_name == "query_as" {
                return Some(BehavioralSignal {
                    kind: BehavioralKind::RawSqlAdded,
                    path: path_str.to_string(),
                    line,
                    detail: truncate_str(text, 60),
                    source: BehavioralSignalSource::Ast,
                });
            }
        }
        "use_declaration" => {
            let text = node.utf8_text(content.as_bytes()).ok()?.trim();
            let path = text
                .trim_start_matches("pub ")
                .trim_start_matches("use ")
                .trim();
            let root = path
                .split("::")
                .next()
                .unwrap_or(path)
                .trim_end_matches(';');
            if !path.starts_with("crate::")
                && !path.starts_with("self::")
                && !path.starts_with("super::")
                && !matches!(root, "std" | "core" | "alloc")
                && !dependencies.is_local_package(root)
                && !module_declared_in_scope(node, content, root)
            {
                return Some(BehavioralSignal {
                    kind: BehavioralKind::DependencyImportAdded,
                    path: path_str.to_string(),
                    line,
                    detail: format!("Imported Rust crate/module: {}", text.trim_end_matches(';')),
                    source: BehavioralSignalSource::Ast,
                });
            }
        }
        _ => {}
    }
    None
}

fn module_declared_in_scope(node: Node<'_>, content: &str, root: &str) -> bool {
    let Some(scope) = node.parent() else {
        return false;
    };
    (0..scope.named_child_count()).any(|index| {
        let Ok(index) = u32::try_from(index) else {
            return false;
        };
        let Some(child) = scope.named_child(index) else {
            return false;
        };
        if child.kind() != "mod_item" {
            return false;
        }
        child
            .child_by_field_name("name")
            .and_then(|name| name.utf8_text(content.as_bytes()).ok())
            .is_some_and(|name| name == root)
    })
}