rustcodegraph 1.2.10

Local-first code intelligence graph and CLI for AI agents
Documentation
use std::collections::{HashMap, HashSet};
use std::fs;
use std::path::{Path, PathBuf};
use std::time::{SystemTime, UNIX_EPOCH};

use rustcodegraph::resolution::import_resolver::clear_cpp_include_dir_cache;
use rustcodegraph::resolution::types::{ImportMapping, ResolutionContext, UnresolvedRef, now_ms};
use rustcodegraph::types::{Language, Node, NodeKind, ReferenceKind};

const BACKEND_BLOCKER: &str =
    "Rust CodeGraph end-to-end extraction/query/reference backend is not at TypeScript parity yet";

#[derive(Default)]
pub(crate) struct MockResolutionContext {
    nodes: Vec<Node>,
    file_contents: HashMap<String, String>,
    files: HashSet<String>,
    all_files: Vec<String>,
    project_root: String,
    file_exists_default: bool,
    cpp_include_dirs: Vec<String>,
    import_mappings: Vec<ImportMapping>,
}

impl MockResolutionContext {
    pub(crate) fn new() -> Self {
        Self {
            project_root: String::new(),
            ..Self::default()
        }
    }

    pub(crate) fn with_nodes(nodes: Vec<Node>) -> Self {
        Self {
            nodes,
            ..Self::new()
        }
    }

    pub(crate) fn with_files(files: &[&str]) -> Self {
        Self {
            files: files.iter().map(|file| (*file).to_owned()).collect(),
            all_files: files.iter().map(|file| (*file).to_owned()).collect(),
            ..Self::new()
        }
    }

    pub(crate) fn with_project_root(mut self, project_root: impl Into<String>) -> Self {
        self.project_root = project_root.into();
        self
    }

    pub(crate) fn with_file_contents(mut self, entries: &[(&str, &str)]) -> Self {
        for (path, content) in entries {
            self.file_contents
                .insert((*path).to_owned(), (*content).to_owned());
            self.files.insert((*path).to_owned());
        }
        self
    }

    pub(crate) fn with_all_files(mut self, files: &[&str]) -> Self {
        self.all_files = files.iter().map(|file| (*file).to_owned()).collect();
        for file in files {
            self.files.insert((*file).to_owned());
        }
        self
    }

    pub(crate) fn with_cpp_include_dirs(mut self, dirs: &[&str]) -> Self {
        self.cpp_include_dirs = dirs.iter().map(|dir| (*dir).to_owned()).collect();
        self
    }

    pub(crate) fn with_file_exists_default(mut self, value: bool) -> Self {
        self.file_exists_default = value;
        self
    }
}

impl ResolutionContext for MockResolutionContext {
    fn get_nodes_in_file(&mut self, file_path: &str) -> Vec<Node> {
        self.nodes
            .iter()
            .filter(|node| node.file_path == file_path)
            .cloned()
            .collect()
    }

    fn get_nodes_by_name(&mut self, name: &str) -> Vec<Node> {
        self.nodes
            .iter()
            .filter(|node| node.name == name)
            .cloned()
            .collect()
    }

    fn get_nodes_by_qualified_name(&mut self, qualified_name: &str) -> Vec<Node> {
        self.nodes
            .iter()
            .filter(|node| node.qualified_name == qualified_name)
            .cloned()
            .collect()
    }

    fn get_nodes_by_kind(&mut self, kind: NodeKind) -> Vec<Node> {
        self.nodes
            .iter()
            .filter(|node| node.kind == kind)
            .cloned()
            .collect()
    }

    fn file_exists(&mut self, file_path: &str) -> bool {
        self.file_exists_default
            || self.files.contains(file_path)
            || self.file_contents.contains_key(file_path)
    }

    fn read_file(&mut self, file_path: &str) -> Option<String> {
        self.file_contents.get(file_path).cloned()
    }

    fn get_project_root(&self) -> String {
        self.project_root.clone()
    }

    fn get_all_files(&mut self) -> Vec<String> {
        if !self.all_files.is_empty() {
            return self.all_files.clone();
        }
        let mut files = self.files.iter().cloned().collect::<Vec<_>>();
        for node in &self.nodes {
            if !files.iter().any(|file| file == &node.file_path) {
                files.push(node.file_path.clone());
            }
        }
        files.sort();
        files
    }

    fn get_nodes_by_lower_name(&mut self, lower_name: &str) -> Vec<Node> {
        self.nodes
            .iter()
            .filter(|node| node.name.to_ascii_lowercase() == lower_name)
            .cloned()
            .collect()
    }

    fn get_import_mappings(&mut self, _file_path: &str, _language: Language) -> Vec<ImportMapping> {
        self.import_mappings.clone()
    }

    fn get_cpp_include_dirs(&mut self) -> Vec<String> {
        self.cpp_include_dirs.clone()
    }
}

pub(crate) struct TempProject {
    root: PathBuf,
}

impl TempProject {
    pub(crate) fn new(prefix: &str) -> Self {
        let nanos = SystemTime::now()
            .duration_since(UNIX_EPOCH)
            .expect("system time should be after Unix epoch")
            .as_nanos();
        let root = std::env::temp_dir().join(format!("{prefix}-{}-{nanos}", std::process::id()));
        fs::create_dir_all(&root).unwrap_or_else(|err| {
            panic!("failed to create temp project {}: {err}", root.display())
        });
        Self { root }
    }

    pub(crate) fn path(&self) -> &Path {
        &self.root
    }

    pub(crate) fn write(&self, relative_path: &str, content: &str) {
        let path = self.root.join(relative_path);
        if let Some(parent) = path.parent() {
            fs::create_dir_all(parent)
                .unwrap_or_else(|err| panic!("failed to create {}: {err}", parent.display()));
        }
        fs::write(&path, content)
            .unwrap_or_else(|err| panic!("failed to write {}: {err}", path.display()));
    }

    pub(crate) fn mkdir(&self, relative_path: &str) {
        fs::create_dir_all(self.root.join(relative_path))
            .unwrap_or_else(|err| panic!("failed to create fixture dir {relative_path}: {err}"));
    }
}

impl Drop for TempProject {
    fn drop(&mut self) {
        let _ = fs::remove_dir_all(&self.root);
    }
}

pub(crate) struct CppIncludeCacheGuard;

impl CppIncludeCacheGuard {
    pub(crate) fn new() -> Self {
        clear_cpp_include_dir_cache();
        Self
    }
}

impl Drop for CppIncludeCacheGuard {
    fn drop(&mut self) {
        clear_cpp_include_dir_cache();
    }
}

pub(crate) fn test_node(
    id: &str,
    kind: NodeKind,
    name: &str,
    qualified_name: &str,
    file_path: &str,
    language: Language,
    start_line: u64,
) -> Node {
    Node {
        id: id.to_owned(),
        kind,
        name: name.to_owned(),
        qualified_name: qualified_name.to_owned(),
        file_path: file_path.to_owned(),
        language,
        start_line,
        end_line: start_line + 10,
        start_column: 0,
        end_column: 0,
        docstring: None,
        signature: None,
        visibility: None,
        is_exported: None,
        is_async: None,
        is_static: None,
        is_abstract: None,
        decorators: None,
        type_parameters: None,
        return_type: None,
        updated_at: now_ms(),
    }
}

pub(crate) fn unresolved_ref(
    reference_name: &str,
    reference_kind: ReferenceKind,
    file_path: &str,
    language: Language,
) -> UnresolvedRef {
    UnresolvedRef {
        from_node_id: format!("caller:{file_path}:caller:5"),
        reference_name: reference_name.to_owned(),
        reference_kind,
        line: 5,
        column: 10,
        file_path: file_path.to_owned(),
        language,
        candidates: None,
    }
}

pub(crate) fn record_backend_blocker(case_name: &str) {
    assert!(
        !case_name.is_empty(),
        "ignored backend-parity case must record the TypeScript case name"
    );
    eprintln!("{BACKEND_BLOCKER}: {case_name}");
}