gobby-code 1.0.0

Fast Rust CLI for Gobby's code index — AST-aware search, symbol navigation, and dependency graph
Documentation
use std::fs;
use std::path::PathBuf;

use tempfile::TempDir;

use crate::index::semantic::{SemanticCallRequest, SemanticCallResolver};

use super::super::{build_import_resolution_context, parse_file_with_semantic};
use super::common::discover_supported_files;

struct FakeSemanticResolver {
    target: Option<crate::index::semantic::SemanticCallTarget>,
    expected_language: &'static str,
    expected_callee: &'static str,
    requests: Vec<CapturedSemanticRequest>,
    error: Option<&'static str>,
}

struct CapturedSemanticRequest {
    language: String,
    file_path: PathBuf,
    root_path: PathBuf,
    callee_name: String,
    line: usize,
    column: usize,
}

impl SemanticCallResolver for FakeSemanticResolver {
    fn resolve(
        &mut self,
        request: &SemanticCallRequest<'_>,
    ) -> anyhow::Result<Option<crate::index::semantic::SemanticCallTarget>> {
        self.requests.push(CapturedSemanticRequest {
            language: request.language.to_string(),
            file_path: request.file_path.to_path_buf(),
            root_path: request.root_path.to_path_buf(),
            callee_name: request.callee_name.to_string(),
            line: request.line,
            column: request.column,
        });
        if let Some(error) = self.error {
            anyhow::bail!("{error}");
        }
        if request.language == self.expected_language && request.callee_name == self.expected_callee
        {
            Ok(self.target.clone())
        } else {
            Ok(None)
        }
    }
}

#[test]
fn semantic_resolver_can_classify_cpp_calls_as_external() {
    let tempdir = TempDir::new().expect("create tempdir");
    let root = tempdir.path();
    let path = root.join("src/main.cpp");
    fs::create_dir_all(path.parent().expect("parent")).expect("create parent dirs");
    fs::write(
        &path,
        r#"
void run() {
    printf("x");
}
"#,
    )
    .expect("write source");
    let candidates = discover_supported_files(root);
    let context = build_import_resolution_context(root, &candidates);
    let mut resolver = FakeSemanticResolver {
        target: Some(crate::index::semantic::SemanticCallTarget {
            callee_name: "printf".to_string(),
            external_module: "/usr/include/stdio.h".to_string(),
        }),
        expected_language: "cpp",
        expected_callee: "printf",
        requests: Vec::new(),
        error: None,
    };
    let parsed = parse_file_with_semantic(
        &path,
        "proj",
        root,
        &[] as &[&str],
        &context,
        Some(&mut resolver),
    )
    .expect("parse result")
    .expect("parse file");

    let call = parsed.calls.first().expect("printf call");
    assert_eq!(call.callee_target_kind.as_str(), "external");
    assert_eq!(
        call.callee_external_module.as_deref(),
        Some("/usr/include/stdio.h")
    );
}

#[test]
fn semantic_resolver_can_classify_textual_dart_calls_as_external() {
    let tempdir = TempDir::new().expect("create tempdir");
    let root = tempdir.path();
    let path = root.join("lib/sample.dart");
    fs::create_dir_all(path.parent().expect("parent")).expect("create parent dirs");
    fs::write(
        &path,
        r#"
void run() {
  Tooltip(message: 'x');
}
"#,
    )
    .expect("write source");
    let candidates = discover_supported_files(root);
    let context = build_import_resolution_context(root, &candidates);
    let mut resolver = FakeSemanticResolver {
        target: Some(crate::index::semantic::SemanticCallTarget {
            callee_name: "Tooltip".to_string(),
            external_module: "package:flutter/material.dart".to_string(),
        }),
        expected_language: "dart",
        expected_callee: "Tooltip",
        requests: Vec::new(),
        error: None,
    };
    let parsed = parse_file_with_semantic(
        &path,
        "proj",
        root,
        &[] as &[&str],
        &context,
        Some(&mut resolver),
    )
    .expect("parse result")
    .expect("parse file");

    let call = parsed
        .calls
        .iter()
        .find(|call| call.callee_name == "Tooltip")
        .expect("Tooltip call");
    assert_eq!(call.callee_target_kind.as_str(), "external");
    assert_eq!(
        call.callee_external_module.as_deref(),
        Some("package:flutter/material.dart")
    );
    assert!(resolver.requests.iter().any(|request| {
        request.language == "dart"
            && request.file_path == path
            && request.root_path == root
            && request.callee_name == "Tooltip"
    }));
}

#[test]
fn semantic_resolver_receives_utf16_columns_for_ast_calls() {
    let tempdir = TempDir::new().expect("create tempdir");
    let root = tempdir.path();
    let path = root.join("src/main.cpp");
    fs::create_dir_all(path.parent().expect("parent")).expect("create parent dirs");
    let source = format!(
        "void run() {{\n    auto s = \"{}\"; printf(\"x\");\n}}\n",
        '\u{1F600}'
    );
    fs::write(&path, source.as_bytes()).expect("write source");
    let candidates = discover_supported_files(root);
    let context = build_import_resolution_context(root, &candidates);
    let mut resolver = FakeSemanticResolver {
        target: None,
        expected_language: "cpp",
        expected_callee: "printf",
        requests: Vec::new(),
        error: None,
    };

    parse_file_with_semantic(
        &path,
        "proj",
        root,
        &[] as &[&str],
        &context,
        Some(&mut resolver),
    )
    .expect("parse result")
    .expect("parse file");

    let request = resolver
        .requests
        .iter()
        .find(|request| request.callee_name == "printf")
        .expect("printf semantic request");
    let prefix = format!("    auto s = \"{}\"; ", '\u{1F600}');
    assert_eq!(request.line, 2);
    assert_eq!(request.column, prefix.encode_utf16().count());
}

#[test]
fn semantic_resolver_receives_utf16_columns_for_textual_dart_calls() {
    let tempdir = TempDir::new().expect("create tempdir");
    let root = tempdir.path();
    let path = root.join("lib/sample.dart");
    fs::create_dir_all(path.parent().expect("parent")).expect("create parent dirs");
    let source = format!(
        "void run() {{\n  final s = '{}'; Tooltip(message: 'x');\n}}\n",
        '\u{1F600}'
    );
    fs::write(&path, source.as_bytes()).expect("write source");
    let candidates = discover_supported_files(root);
    let context = build_import_resolution_context(root, &candidates);
    let mut resolver = FakeSemanticResolver {
        target: None,
        expected_language: "dart",
        expected_callee: "Tooltip",
        requests: Vec::new(),
        error: None,
    };

    parse_file_with_semantic(
        &path,
        "proj",
        root,
        &[] as &[&str],
        &context,
        Some(&mut resolver),
    )
    .expect("parse result")
    .expect("parse file");

    let request = resolver
        .requests
        .iter()
        .find(|request| request.callee_name == "Tooltip")
        .expect("Tooltip semantic request");
    let prefix = format!("  final s = '{}'; ", '\u{1F600}');
    assert_eq!(request.line, 2);
    assert_eq!(request.column, prefix.encode_utf16().count());
}

#[test]
fn semantic_resolver_receives_dart_byte_offsets_across_crlf_lines() {
    let tempdir = TempDir::new().expect("create tempdir");
    let root = tempdir.path();
    let path = root.join("lib/sample.dart");
    fs::create_dir_all(path.parent().expect("parent")).expect("create parent dirs");
    let source = format!(
        "void run() {{\r\n  final s = '{}';\r\n  Tooltip(message: 'x');\r\n}}\r\n",
        '\u{1F600}'
    );
    fs::write(&path, source.as_bytes()).expect("write source");
    let candidates = discover_supported_files(root);
    let context = build_import_resolution_context(root, &candidates);
    let mut resolver = FakeSemanticResolver {
        target: None,
        expected_language: "dart",
        expected_callee: "Tooltip",
        requests: Vec::new(),
        error: None,
    };

    parse_file_with_semantic(
        &path,
        "proj",
        root,
        &[] as &[&str],
        &context,
        Some(&mut resolver),
    )
    .expect("parse result")
    .expect("parse file");

    let request = resolver
        .requests
        .iter()
        .find(|request| request.callee_name == "Tooltip")
        .expect("Tooltip semantic request");
    assert_eq!(request.line, 3);
    assert_eq!(request.column, 2);
}

#[test]
fn semantic_resolver_errors_are_propagated() {
    let tempdir = TempDir::new().expect("create tempdir");
    let root = tempdir.path();
    let path = root.join("src/main.cpp");
    fs::create_dir_all(path.parent().expect("parent")).expect("create parent dirs");
    fs::write(
        &path,
        r#"
void run() {
    printf("x");
}
"#,
    )
    .expect("write source");
    let candidates = discover_supported_files(root);
    let context = build_import_resolution_context(root, &candidates);
    let mut resolver = FakeSemanticResolver {
        target: None,
        expected_language: "cpp",
        expected_callee: "printf",
        requests: Vec::new(),
        error: Some("semantic resolver failed"),
    };

    let err = match parse_file_with_semantic(
        &path,
        "proj",
        root,
        &[] as &[&str],
        &context,
        Some(&mut resolver),
    ) {
        Err(err) => err,
        Ok(_) => panic!("expected semantic resolver error"),
    };

    assert_eq!(err.to_string(), "semantic resolver failed");
}