loctree 0.13.1

Structural code intelligence for AI agents. Scan once, query everything.
Documentation
#![cfg(feature = "deep-index")]

use std::fs;
use std::path::{Path, PathBuf};

use loctree::analyzer::scip::proto::{
    Document, Index, Occurrence, Relationship, SymbolInformation,
};
use loctree::analyzer::scip::{import_index_at, merge_graphs};
use loctree::symbols::{
    Confidence, LanguageId, OccurrenceRole, SymbolEdge, SymbolEdgeKind, SymbolGraph, SymbolId,
    SymbolKind, SymbolNode, SymbolProvenance,
};
use prost::Message;

const ADD_SYMBOL: &str = "scip-clang c main.c add().";
const MAIN_SYMBOL: &str = "scip-clang c main.c main().";

fn write_fixture_project() -> tempfile::TempDir {
    let dir = tempfile::tempdir().expect("tempdir");
    let source = include_str!("fixtures/cfamily/scip/main.c");
    fs::write(dir.path().join("main.c"), source).expect("write source");

    let index = Index {
        external_symbols: Vec::new(),
        documents: vec![Document {
            relative_path: "main.c".to_string(),
            language: "c".to_string(),
            occurrences: vec![
                Occurrence {
                    range: vec![0, 4, 7],
                    symbol: ADD_SYMBOL.to_string(),
                    symbol_roles: 1,
                    relationships: Vec::new(),
                },
                Occurrence {
                    range: vec![5, 11, 14],
                    symbol: ADD_SYMBOL.to_string(),
                    symbol_roles: 0,
                    relationships: Vec::new(),
                },
            ],
            symbols: vec![
                SymbolInformation {
                    symbol: ADD_SYMBOL.to_string(),
                    display_name: "add".to_string(),
                    signature_documentation: "int add(int lhs, int rhs)".to_string(),
                    relationships: Vec::new(),
                },
                SymbolInformation {
                    symbol: MAIN_SYMBOL.to_string(),
                    display_name: "main".to_string(),
                    signature_documentation: "int main(void)".to_string(),
                    relationships: vec![Relationship {
                        symbol: ADD_SYMBOL.to_string(),
                        is_reference: true,
                        is_implementation: false,
                        is_type_definition: false,
                        is_definition: false,
                    }],
                },
            ],
        }],
    };

    let mut bytes = Vec::new();
    index.encode(&mut bytes).expect("encode scip fixture");
    fs::write(dir.path().join("index.scip"), bytes).expect("write index.scip");
    dir
}

#[test]
fn decodes_scip_index_as_precise_c_symbols() {
    let project = write_fixture_project();

    let graph = import_index_at(project.path())
        .expect("import succeeds")
        .expect("fixture index exists");

    assert_eq!(graph.symbols.len(), 2);
    assert!(graph.symbols.iter().all(|s| {
        s.language == LanguageId::C
            && s.kind == SymbolKind::Func
            && s.provenance == SymbolProvenance::ScipClang
    }));
    assert!(graph.occurrences.iter().all(|o| {
        o.file == PathBuf::from("main.c")
            && o.confidence == Confidence::Precise
            && o.engine == SymbolProvenance::ScipClang
    }));
    assert!(
        graph
            .occurrences
            .iter()
            .any(|o| o.symbol_id.as_str() == ADD_SYMBOL && o.role == OccurrenceRole::Definition)
    );
    assert!(graph.edges.iter().any(|e| e.from.as_str() == MAIN_SYMBOL
        && e.to.as_str() == ADD_SYMBOL
        && e.kind == SymbolEdgeKind::References
        && e.confidence == Confidence::Precise));
}

#[test]
fn merges_precise_scip_without_replacing_heuristic_graph() {
    let project = write_fixture_project();
    let precise = import_index_at(project.path())
        .expect("import succeeds")
        .expect("fixture index exists");

    let heuristic_id = SymbolId::new(ADD_SYMBOL);
    let mut base = SymbolGraph::new();
    base.symbols.push(SymbolNode {
        id: heuristic_id.clone(),
        language: LanguageId::C,
        kind: SymbolKind::Func,
        name: "add".to_string(),
        qualified_name: None,
        module: None,
        usr: None,
        file: Some(PathBuf::from("main.c")),
        range: None,
        signature: None,
        visibility: None,
        provenance: SymbolProvenance::TreeSitter,
    });
    base.edges.push(SymbolEdge {
        from: SymbolId::new("heuristic-caller"),
        to: heuristic_id,
        kind: SymbolEdgeKind::Calls,
        provenance: SymbolProvenance::TreeSitter,
        confidence: Confidence::Heuristic,
    });

    merge_graphs(&mut base, precise);

    assert_eq!(
        base.symbols
            .iter()
            .filter(|s| s.id.as_str() == ADD_SYMBOL)
            .count(),
        1,
        "same SCIP descriptor must dedupe symbol nodes"
    );
    assert!(
        base.symbols.iter().any(|s| {
            s.id.as_str() == ADD_SYMBOL && s.provenance == SymbolProvenance::ScipClang
        })
    );
    assert!(base.edges.iter().any(|e| {
        e.from.as_str() == "heuristic-caller"
            && e.to.as_str() == ADD_SYMBOL
            && e.confidence == Confidence::Heuristic
    }));
    assert!(base.edges.iter().any(|e| {
        e.from.as_str() == MAIN_SYMBOL
            && e.to.as_str() == ADD_SYMBOL
            && e.confidence == Confidence::Precise
    }));
}

#[test]
fn scip_missing_or_corrupt_index_degrades_without_panicking() {
    let missing = tempfile::tempdir().expect("tempdir");
    assert!(
        import_index_at(missing.path())
            .expect("missing index is not an error")
            .is_none()
    );

    fs::write(missing.path().join("index.scip"), b"not protobuf").expect("write corrupt index");
    assert!(
        import_index_at(missing.path())
            .expect("corrupt index is logged and skipped")
            .is_none()
    );
}

#[test]
fn import_path_is_named_index_scip() {
    let project = write_fixture_project();
    assert!(Path::new(project.path()).join("index.scip").exists());
}