crabmap 0.1.1

Rust code satellite map — index, query, and navigate your entire codebase
use super::commands::{file, module, neighbors, symbol};
use super::traversal::path;
use crate::model::{
    BuildProfile, CodeGraph, Edge, EdgeCertainty, EdgeKind, EdgeSource, Node, NodeKind, Package,
    Project, Range, Target,
};
use std::collections::BTreeMap;

#[test]
fn symbol_reports_ambiguous_short_names() {
    let graph = ambiguous_graph();

    let result = symbol(&graph, "index_project").unwrap();

    assert_eq!(result["kind"], "ambiguous");
    let matches = result["matches"].as_array().unwrap();
    assert_eq!(matches.len(), 2);
    assert!(
        matches
            .iter()
            .any(|node| node["qualified_name"] == "crabmap::index_project")
    );
    assert!(
        matches
            .iter()
            .any(|node| node["qualified_name"] == "crabmap::analyzer::index_project")
    );
}

#[test]
fn symbol_prefers_exact_qualified_name_match() {
    let graph = ambiguous_graph();

    let result = symbol(&graph, "crabmap::index_project").unwrap();

    assert_eq!(result["kind"], "symbol");
    assert_eq!(result["node"]["qualified_name"], "crabmap::index_project");
}

#[test]
fn neighbors_fail_on_ambiguous_short_names() {
    let graph = ambiguous_graph();

    let error = neighbors(&graph, "index_project", "calls", true, 1, 10).unwrap_err();

    assert_eq!(
        error.to_string(),
        "symbol `index_project` is ambiguous, matches: crabmap::index_project, crabmap::analyzer::index_project"
    );
}

#[test]
fn path_fails_when_source_symbol_is_ambiguous() {
    let graph = ambiguous_graph();

    let error = path(&graph, "index_project", "helper", 3).unwrap_err();

    assert_eq!(
        error.to_string(),
        "source `index_project` is ambiguous, matches: crabmap::index_project, crabmap::analyzer::index_project"
    );
}

#[test]
fn file_lists_symbols_declared_in_matching_path() {
    let graph = file_and_module_graph();

    let result = file(&graph, "src/main.rs").unwrap();
    let declares = result["declares"].as_array().unwrap();

    assert_eq!(declares.len(), 3);
    assert!(
        declares
            .iter()
            .any(|item| item["node"]["qualified_name"] == "crabmap::model")
    );
    assert!(
        declares
            .iter()
            .any(|item| item["node"]["qualified_name"] == "crabmap::main_fn")
    );
    assert!(
        declares
            .iter()
            .any(|item| item["node"]["qualified_name"] == "crabmap::App")
    );
}

#[test]
fn module_prefers_declaring_module_node_with_matching_qualified_name() {
    let graph = file_and_module_graph();

    let result = module(&graph, "crabmap::model").unwrap();
    let declares = result["declares"].as_array().unwrap();

    assert_eq!(result["node"]["id"], "module:crabmap::model#2");
    assert_eq!(declares.len(), 1);
    assert_eq!(
        declares[0]["node"]["qualified_name"],
        "crabmap::model::Node"
    );
}

fn ambiguous_graph() -> CodeGraph {
    CodeGraph {
        schema_version: 1,
        project: project(),
        nodes: vec![
            node(
                "function:crabmap::index_project",
                NodeKind::Function,
                "index_project",
                "crabmap::index_project",
                "src/main.rs",
                10,
            ),
            node(
                "function:crabmap::analyzer::index_project",
                NodeKind::Function,
                "index_project",
                "crabmap::analyzer::index_project",
                "src/analyzer.rs",
                30,
            ),
            node(
                "function:crabmap::helper",
                NodeKind::Function,
                "helper",
                "crabmap::helper",
                "src/main.rs",
                50,
            ),
        ],
        edges: vec![Edge {
            from: "function:crabmap::index_project".to_string(),
            to: "function:crabmap::helper".to_string(),
            kind: EdgeKind::Calls,
            label: None,
            evidence: None,
            weight: 1,
            source: EdgeSource::Ast,
            certainty: EdgeCertainty::Definite,
            profiles: vec![],
        }],
        warnings: vec![],
        semantic: None,
        mir: None,
        profiles: vec![profile()],
        generated_at_ms: 0,
    }
}

fn file_and_module_graph() -> CodeGraph {
    CodeGraph {
        schema_version: 1,
        project: project(),
        nodes: vec![
            node(
                "file:src/main.rs",
                NodeKind::File,
                "src/main.rs",
                "src/main.rs",
                "src/main.rs",
                1,
            ),
            node(
                "module:codegraph",
                NodeKind::Module,
                "codegraph",
                "codegraph",
                "src/main.rs",
                1,
            ),
            node(
                "module:crabmap::model",
                NodeKind::Module,
                "model",
                "crabmap::model",
                "src/main.rs",
                12,
            ),
            node(
                "module:crabmap::model#2",
                NodeKind::Module,
                "crabmap::model",
                "crabmap::model",
                "src/model.rs",
                1,
            ),
            node(
                "function:crabmap::main_fn",
                NodeKind::Function,
                "main_fn",
                "crabmap::main_fn",
                "src/main.rs",
                20,
            ),
            node(
                "struct:crabmap::App",
                NodeKind::Struct,
                "App",
                "crabmap::App",
                "src/main.rs",
                30,
            ),
            node(
                "struct:crabmap::model::Node",
                NodeKind::Struct,
                "Node",
                "crabmap::model::Node",
                "src/model.rs",
                5,
            ),
        ],
        edges: vec![
            Edge {
                from: "file:src/main.rs".to_string(),
                to: "module:codegraph".to_string(),
                kind: EdgeKind::ModuleFile,
                label: None,
                evidence: None,
                weight: 1,
                source: EdgeSource::Ast,
                certainty: EdgeCertainty::Definite,
                profiles: vec![],
            },
            Edge {
                from: "module:codegraph".to_string(),
                to: "module:crabmap::model".to_string(),
                kind: EdgeKind::Declares,
                label: None,
                evidence: None,
                weight: 1,
                source: EdgeSource::Ast,
                certainty: EdgeCertainty::Definite,
                profiles: vec![],
            },
            Edge {
                from: "module:codegraph".to_string(),
                to: "function:crabmap::main_fn".to_string(),
                kind: EdgeKind::Declares,
                label: None,
                evidence: None,
                weight: 1,
                source: EdgeSource::Ast,
                certainty: EdgeCertainty::Definite,
                profiles: vec![],
            },
            Edge {
                from: "module:codegraph".to_string(),
                to: "struct:crabmap::App".to_string(),
                kind: EdgeKind::Declares,
                label: None,
                evidence: None,
                weight: 1,
                source: EdgeSource::Ast,
                certainty: EdgeCertainty::Definite,
                profiles: vec![],
            },
            Edge {
                from: "module:crabmap::model#2".to_string(),
                to: "struct:crabmap::model::Node".to_string(),
                kind: EdgeKind::Declares,
                label: None,
                evidence: None,
                weight: 1,
                source: EdgeSource::Ast,
                certainty: EdgeCertainty::Definite,
                profiles: vec![],
            },
        ],
        warnings: vec![],
        semantic: None,
        mir: None,
        profiles: vec![profile()],
        generated_at_ms: 0,
    }
}

fn project() -> Project {
    Project {
        root: "/tmp/demo".to_string(),
        workspace_root: "/tmp/demo".to_string(),
        packages: vec![Package {
            name: "codegraph".to_string(),
            manifest_path: "/tmp/demo/Cargo.toml".to_string(),
            targets: vec![Target {
                name: "codegraph".to_string(),
                kind: vec!["bin".to_string()],
                src_path: "src/main.rs".to_string(),
            }],
        }],
    }
}

fn profile() -> BuildProfile {
    BuildProfile {
        name: "default".to_string(),
        features: vec![],
        all_features: false,
        no_default_features: false,
        target: None,
    }
}

fn node(
    id: &str,
    kind: NodeKind,
    name: &str,
    qualified_name: &str,
    file: &str,
    line: usize,
) -> Node {
    Node {
        id: id.to_string(),
        kind,
        name: name.to_string(),
        qualified_name: qualified_name.to_string(),
        file: Some(file.to_string()),
        range: Some(Range {
            start_line: line,
            end_line: line,
        }),
        visibility: Some("pub".to_string()),
        signature: Some(format!("fn {name}()")),
        docs: None,
        metrics: BTreeMap::new(),
    }
}