rustcodegraph 1.2.10

Local-first code intelligence graph and CLI for AI agents
Documentation
pub(crate) use rustcodegraph::extraction::grammars::{
    detect_language, get_supported_languages, init_grammars, is_language_supported, is_source_file,
    language_key, load_all_grammars,
};
pub(crate) use rustcodegraph::extraction::index::{
    build_default_ignore, extract_from_source, scan_directory,
};
pub(crate) use rustcodegraph::types::{
    EdgeKind, ExtractionResult, Language, Node, NodeKind, ReferenceKind, Visibility,
};
pub(crate) use rustcodegraph::utils::normalize_path;
pub(crate) use rustcodegraph::{CodeGraph, IndexOptions};
pub(crate) use std::ffi::OsStr;
pub(crate) use std::fs;
use std::future::Future;
pub(crate) use std::path::{Path, PathBuf};
use std::pin::Pin;
pub(crate) use std::process::{Command, Stdio};
use std::sync::Once;
use std::task::{Context, Poll, RawWaker, RawWakerVTable, Waker};
use std::time::{SystemTime, UNIX_EPOCH};
pub(crate) static GRAMMAR_INIT: Once = Once::new();
pub(crate) const TS_DESCRIBE_COUNT: usize = 117;
pub(crate) const TS_TEST_CASE_COUNT: usize = 372;
pub(crate) const TS_EXTRACTION_TEST: &str = "__tests__/extraction.test.ts";
pub(crate) const PARITY_IGNORE_REASON: &str = "Rust native extraction parity for this TypeScript case is still in progress; \
     this wrapper runs the original Vitest assertion path when explicitly included";
pub(crate) fn noop_raw_waker() -> RawWaker {
    fn clone(_: *const ()) -> RawWaker {
        noop_raw_waker()
    }
    fn wake(_: *const ()) {}
    fn wake_by_ref(_: *const ()) {}
    fn drop(_: *const ()) {}
    static VTABLE: RawWakerVTable = RawWakerVTable::new(clone, wake, wake_by_ref, drop);
    RawWaker::new(std::ptr::null(), &VTABLE)
}
pub(crate) fn block_on<F: Future>(future: F) -> F::Output {
    let waker = unsafe { Waker::from_raw(noop_raw_waker()) };
    let mut cx = Context::from_waker(&waker);
    let mut future = Pin::from(Box::new(future));
    loop {
        match future.as_mut().poll(&mut cx) {
            Poll::Ready(value) => return value,
            Poll::Pending => std::thread::yield_now(),
        }
    }
}
pub(crate) fn before_all_init_grammars() {
    GRAMMAR_INIT.call_once(|| {
        let _ = block_on(init_grammars());
        let _ = block_on(load_all_grammars());
    });
}
pub(crate) fn assert_detected_language(path: &str, content: Option<&str>, expected: Language) {
    before_all_init_grammars();
    assert_eq!(detect_language(path, content), expected);
}
pub(crate) fn assert_language_support(language: Language, expected: bool) {
    before_all_init_grammars();
    assert_eq!(is_language_supported(language), expected);
}
pub(crate) fn assert_supported_languages_include(expected_languages: &[Language]) {
    before_all_init_grammars();
    let languages = get_supported_languages();
    for expected in expected_languages {
        assert!(languages.contains(expected), "missing {expected:?}");
    }
}
pub(crate) fn extract(file_path: &str, source: &str) -> ExtractionResult {
    before_all_init_grammars();
    extract_from_source(file_path, source, None, None)
}
pub(crate) fn find_node<'a>(
    result: &'a ExtractionResult,
    kind: NodeKind,
    name: &str,
) -> Option<&'a Node> {
    result
        .nodes
        .iter()
        .find(|node| node.kind == kind && node.name == name)
}
pub(crate) fn nodes_by_kind(result: &ExtractionResult, kind: NodeKind) -> Vec<&Node> {
    result
        .nodes
        .iter()
        .filter(|node| node.kind == kind)
        .collect()
}
pub(crate) fn names_by_kind(result: &ExtractionResult, kind: NodeKind) -> Vec<String> {
    nodes_by_kind(result, kind)
        .into_iter()
        .map(|node| node.name.clone())
        .collect()
}
pub(crate) fn is_exported(node: &Node) -> bool {
    node.is_exported.unwrap_or(false)
}
pub(crate) fn references_by_kind(result: &ExtractionResult, kind: ReferenceKind) -> Vec<String> {
    result
        .unresolved_references
        .iter()
        .filter(|reference| reference.reference_kind == kind)
        .map(|reference| reference.reference_name.clone())
        .collect()
}
pub(crate) fn import_nodes(result: &ExtractionResult) -> Vec<&Node> {
    nodes_by_kind(result, NodeKind::Import)
}
pub(crate) fn single_import<'a>(result: &'a ExtractionResult, expected_name: &str) -> &'a Node {
    let imports = import_nodes(result);
    assert_eq!(imports.len(), 1, "imports: {imports:?}");
    let import = imports[0];
    assert_eq!(import.name, expected_name);
    import
}
pub(crate) fn assert_import_names(result: &ExtractionResult, expected: &[&str]) {
    let names = names_by_kind(result, NodeKind::Import);
    assert_eq!(names.len(), expected.len(), "imports: {names:?}");
    for name in expected {
        assert_contains(&names, name);
    }
}
pub(crate) fn assert_no_imports(result: &ExtractionResult) {
    let imports = import_nodes(result);
    assert!(imports.is_empty(), "imports: {imports:?}");
}
pub(crate) fn assert_signature_eq(node: &Node, expected: &str) {
    assert_eq!(node.signature.as_deref(), Some(expected));
}
pub(crate) fn assert_signature_contains(node: &Node, expected: &str) {
    let signature = node.signature.as_deref().unwrap_or_default();
    assert!(
        signature.contains(expected),
        "expected signature {signature:?} to contain {expected:?}"
    );
}
pub(crate) fn assert_return_type(
    result: &ExtractionResult,
    kind: NodeKind,
    name: &str,
    expected: Option<&str>,
) {
    let node = find_node(result, kind, name).unwrap_or_else(|| {
        panic!(
            "missing {kind:?} {name:?}; nodes: {:?}",
            result
                .nodes
                .iter()
                .map(|node| (&node.kind, &node.name))
                .collect::<Vec<_>>()
        )
    });
    assert_eq!(node.return_type.as_deref(), expected);
}
pub(crate) fn expect_node<'a>(
    result: &'a ExtractionResult,
    kind: NodeKind,
    name: &str,
) -> &'a Node {
    find_node(result, kind, name).unwrap_or_else(|| {
        panic!(
            "missing {kind:?} {name:?}; nodes: {:?}",
            result
                .nodes
                .iter()
                .map(|node| (&node.kind, &node.name, &node.file_path))
                .collect::<Vec<_>>()
        )
    })
}
pub(crate) fn assert_names_include(result: &ExtractionResult, kind: NodeKind, expected: &[&str]) {
    let names = names_by_kind(result, kind);
    for name in expected {
        assert_contains(&names, name);
    }
}
pub(crate) fn reference_names(result: &ExtractionResult, kind: ReferenceKind) -> Vec<String> {
    references_by_kind(result, kind)
}
pub(crate) fn assert_reference_names_include(
    result: &ExtractionResult,
    kind: ReferenceKind,
    expected: &[&str],
) {
    let names = reference_names(result, kind);
    for name in expected {
        assert_contains(&names, name);
    }
}
pub(crate) fn index_project(temp: &TempDir) -> CodeGraph {
    let mut cg = CodeGraph::init_sync(temp.path()).expect("CodeGraph should initialize");
    let result = cg.index_all(IndexOptions::default());
    assert!(result.success, "index_all failed: {:?}", result.errors);
    let _ = cg.resolve_references();
    cg
}
pub(crate) struct TempDir {
    path: PathBuf,
}
impl TempDir {
    pub(crate) fn new(prefix: &str) -> Self {
        for attempt in 0..100 {
            let unique = SystemTime::now()
                .duration_since(UNIX_EPOCH)
                .expect("system time should be after Unix epoch")
                .as_nanos();
            let path = std::env::temp_dir().join(format!(
                "{prefix}-{}-{unique}-{attempt}",
                std::process::id()
            ));
            match fs::create_dir(&path) {
                Ok(()) => return Self { path },
                Err(err) if err.kind() == std::io::ErrorKind::AlreadyExists => continue,
                Err(err) => panic!("failed to create temp dir {}: {err}", path.display()),
            }
        }
        panic!("failed to create unique temp dir for {prefix}");
    }
    pub(crate) fn path(&self) -> &Path {
        &self.path
    }
    pub(crate) fn write(&self, relative_path: &str, content: &str) {
        let path = self.path.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()));
    }
}
impl Drop for TempDir {
    fn drop(&mut self) {
        let _ = fs::remove_dir_all(&self.path);
    }
}
pub(crate) fn assert_contains(actual: &[String], expected: &str) {
    assert!(
        actual.iter().any(|value| value == expected),
        "expected {actual:?} to contain {expected:?}"
    );
}
pub(crate) fn assert_not_contains(actual: &[String], unexpected: &str) {
    assert!(
        actual.iter().all(|value| value != unexpected),
        "expected {actual:?} not to contain {unexpected:?}"
    );
}
pub(crate) fn impact_names(cg: &mut CodeGraph, node_id: &str, depth: u64) -> Vec<String> {
    cg.get_impact_radius(node_id, depth)
        .nodes
        .into_values()
        .map(|node| node.name)
        .collect()
}
pub(crate) fn impact_file_paths(cg: &mut CodeGraph, node_id: &str, depth: u64) -> Vec<String> {
    cg.get_impact_radius(node_id, depth)
        .nodes
        .into_values()
        .map(|node| node.file_path)
        .collect()
}
pub(crate) fn assert_not_contains_fragment(actual: &[String], unexpected: &str) {
    assert!(
        actual.iter().all(|value| !value.contains(unexpected)),
        "expected {actual:?} not to contain fragment {unexpected:?}"
    );
}
pub(crate) fn git<I, S>(cwd: &Path, args: I)
where
    I: IntoIterator<Item = S>,
    S: AsRef<OsStr>,
{
    let args = args
        .into_iter()
        .map(|arg| arg.as_ref().to_os_string())
        .collect::<Vec<_>>();
    let output = Command::new("git")
        .args(&args)
        .current_dir(cwd)
        .stdin(Stdio::null())
        .stdout(Stdio::null())
        .stderr(Stdio::piped())
        .output()
        .unwrap_or_else(|err| panic!("failed to run git {args:?} in {}: {err}", cwd.display()));
    assert!(
        output.status.success(),
        "git {args:?} failed in {}\nstatus: {}\nstderr:\n{}",
        cwd.display(),
        output.status,
        String::from_utf8_lossy(&output.stderr)
    );
}
pub(crate) fn git_commit_all(cwd: &Path, message: &str) {
    git(cwd, ["add", "-A"]);
    git(
        cwd,
        [
            "-c",
            "user.email=test@test.com",
            "-c",
            "user.name=Test",
            "-c",
            "commit.gpgsign=false",
            "commit",
            "-q",
            "-m",
            message,
        ],
    );
}
#[allow(dead_code)]
pub(crate) fn regex_escape(input: &str) -> String {
    let mut escaped = String::new();
    for ch in input.chars() {
        if matches!(
            ch,
            '.' | '+' | '*' | '?' | '^' | '$' | '(' | ')' | '[' | ']' | '{' | '}' | '|' | '\\'
        ) {
            escaped.push('\\');
        }
        escaped.push(ch);
    }
    escaped
}
#[allow(dead_code)]
pub(crate) fn run_ts_case(suite: &[&str], case_name: &str) {
    let mut full_name = suite.join(" ");
    if !full_name.is_empty() {
        full_name.push(' ');
    }
    full_name.push_str(case_name);
    let pattern = regex_escape(&full_name);
    let vitest = Path::new(env!("CARGO_MANIFEST_DIR")).join("node_modules/.bin/vitest");
    assert!(
        vitest.is_file(),
        "cannot run Vitest extraction case {full_name:?}: local node_modules/.bin/vitest is missing"
    );
    let output = Command::new(&vitest)
        .args(["run", TS_EXTRACTION_TEST, "-t", &pattern])
        .current_dir(env!("CARGO_MANIFEST_DIR"))
        .output()
        .unwrap_or_else(|err| panic!("failed to spawn Vitest for {full_name:?}: {err}"));
    assert!(
        output.status.success(),
        "Vitest extraction case failed: {full_name}\nstatus: {:?}\nstdout:\n{}\nstderr:\n{}",
        output.status.code(),
        String::from_utf8_lossy(&output.stdout),
        String::from_utf8_lossy(&output.stderr)
    );
}