graphyn-core 0.1.1

Language-agnostic code intelligence graph engine — the heart of Graphyn
Documentation
use graphyn_core::error::GraphynError;
use graphyn_core::graph::GraphynGraph;
use graphyn_core::incremental::replace_file_ir;
use graphyn_core::ir::{FileIR, Language, Relationship, RelationshipKind, Symbol, SymbolKind};
use graphyn_core::query::{blast_radius, dependencies, symbol_usages};

fn symbol(id: &str, name: &str, file: &str, kind: SymbolKind) -> Symbol {
    Symbol {
        id: id.to_string(),
        name: name.to_string(),
        kind,
        language: Language::TypeScript,
        file: file.to_string(),
        line_start: 1,
        line_end: 1,
        signature: None,
    }
}

fn rel(from: &str, to: &str, file: &str, line: u32) -> Relationship {
    Relationship {
        from: from.to_string(),
        to: to.to_string(),
        kind: RelationshipKind::Imports,
        alias: None,
        properties_accessed: vec![],
        context: "import".to_string(),
        file: file.to_string(),
        line,
    }
}

#[test]
fn test_blast_radius_depth_and_direction() {
    let mut graph = GraphynGraph::new();

    let a = symbol("a.ts::A::class", "A", "a.ts", SymbolKind::Class);
    let b = symbol("b.ts::B::class", "B", "b.ts", SymbolKind::Class);
    let c = symbol("c.ts::C::class", "C", "c.ts", SymbolKind::Class);

    graph.add_symbol(a.clone());
    graph.add_symbol(b.clone());
    graph.add_symbol(c.clone());

    graph.add_relationship(&rel(&b.id, &a.id, "b.ts", 10));
    graph.add_relationship(&rel(&c.id, &b.id, "c.ts", 20));

    let depth_1 = blast_radius(&graph, "A", None, Some(1)).expect("depth1 ok");
    assert_eq!(depth_1.len(), 1);
    assert_eq!(depth_1[0].from, b.id);

    let depth_2 = blast_radius(&graph, "A", None, Some(2)).expect("depth2 ok");
    assert_eq!(depth_2.len(), 2);
    assert_eq!(depth_2[0].hop, 1);
    assert_eq!(depth_2[1].hop, 2);

    let deps = dependencies(&graph, "C", None, Some(2)).expect("deps ok");
    assert_eq!(deps.len(), 2);
    assert_eq!(deps[0].to, "b.ts::B::class");
    assert_eq!(deps[1].to, "a.ts::A::class");
}

#[test]
fn test_symbol_lookup_ambiguity_requires_file_disambiguation() {
    let mut graph = GraphynGraph::new();

    let x1 = symbol("a.ts::Thing::class", "Thing", "a.ts", SymbolKind::Class);
    let x2 = symbol("b.ts::Thing::class", "Thing", "b.ts", SymbolKind::Class);

    graph.add_symbol(x1.clone());
    graph.add_symbol(x2.clone());

    let err = blast_radius(&graph, "Thing", None, Some(1)).expect_err("must be ambiguous");
    match err {
        GraphynError::AmbiguousSymbol { symbol, candidates } => {
            assert_eq!(symbol, "Thing");
            assert_eq!(candidates, vec!["a.ts".to_string(), "b.ts".to_string()]);
        }
        other => panic!("unexpected error: {other:?}"),
    }

    let ok = blast_radius(&graph, "Thing", Some("a.ts"), Some(1));
    assert!(ok.is_ok());
}

#[test]
fn test_symbol_usages_dedupes_by_file_line() {
    let mut graph = GraphynGraph::new();

    let target = symbol("t.ts::Target::class", "Target", "t.ts", SymbolKind::Class);
    let user = symbol("u.ts::User::class", "User", "u.ts", SymbolKind::Class);

    graph.add_symbol(target.clone());
    graph.add_symbol(user.clone());

    let mut r1 = rel(&user.id, &target.id, "u.ts", 30);
    r1.alias = Some("AliasT".to_string());
    let r2 = r1.clone();

    graph.add_relationship(&r1);
    graph.add_relationship(&r2);

    let usages = symbol_usages(&graph, "Target", None, true).expect("usages ok");
    assert_eq!(usages.len(), 1);
    assert_eq!(usages[0].line, 30);
}

#[test]
fn test_symbol_usages_respects_include_aliases_flag() {
    let mut graph = GraphynGraph::new();

    let target = symbol("t.ts::Target::class", "Target", "t.ts", SymbolKind::Class);
    let direct = symbol(
        "d.ts::DirectUser::class",
        "DirectUser",
        "d.ts",
        SymbolKind::Class,
    );
    let alias_user = symbol(
        "a.ts::AliasUser::class",
        "AliasUser",
        "a.ts",
        SymbolKind::Class,
    );

    graph.add_symbol(target.clone());
    graph.add_symbol(direct.clone());
    graph.add_symbol(alias_user.clone());

    let mut direct_rel = rel(&direct.id, &target.id, "d.ts", 10);
    direct_rel.alias = None;
    let mut alias_rel = rel(&alias_user.id, &target.id, "a.ts", 20);
    alias_rel.alias = Some("AliasT".to_string());

    graph.add_relationship(&direct_rel);
    graph.add_relationship(&alias_rel);

    let with_aliases = symbol_usages(&graph, "Target", None, true).expect("with aliases");
    assert_eq!(with_aliases.len(), 2);

    let without_aliases = symbol_usages(&graph, "Target", None, false).expect("without aliases");
    assert_eq!(without_aliases.len(), 1);
    assert_eq!(without_aliases[0].from, direct.id);
    assert!(without_aliases[0].alias.is_none());
}

#[test]
fn test_incremental_replace_file_preserves_indexes() {
    let mut graph = GraphynGraph::new();

    let old_symbol = symbol("x.ts::X::class", "X", "x.ts", SymbolKind::Class);
    graph.add_symbol(old_symbol);

    let file_ir = FileIR {
        file: "x.ts".to_string(),
        language: Language::TypeScript,
        symbols: vec![symbol("x.ts::X2::class", "X2", "x.ts", SymbolKind::Class)],
        relationships: vec![],
        parse_errors: vec![],
    };

    let result = replace_file_ir(&mut graph, &file_ir);
    assert_eq!(
        result.removed_symbol_ids,
        vec!["x.ts::X::class".to_string()]
    );
    assert_eq!(result.added_symbol_ids, vec!["x.ts::X2::class".to_string()]);
    assert_eq!(result.removed_relationships, 0);
    assert_eq!(result.added_relationships, 0);

    assert!(graph.symbols.get("x.ts::X::class").is_none());
    assert!(graph.symbols.get("x.ts::X2::class").is_some());
}

#[test]
fn test_invalid_depth_is_rejected() {
    let graph = GraphynGraph::new();
    let err = blast_radius(&graph, "Any", None, Some(11)).expect_err("invalid depth");
    match err {
        GraphynError::InvalidDepth { depth, max } => {
            assert_eq!(depth, 11);
            assert_eq!(max, 10);
        }
        other => panic!("unexpected error: {other:?}"),
    }
}

#[test]
fn test_remove_file_keeps_remaining_node_indexes_valid() {
    let mut graph = GraphynGraph::new();

    let a = symbol("a.ts::A::class", "A", "a.ts", SymbolKind::Class);
    let b = symbol("b.ts::B::class", "B", "b.ts", SymbolKind::Class);
    let c = symbol("c.ts::C::class", "C", "c.ts", SymbolKind::Class);
    graph.add_symbol(a.clone());
    graph.add_symbol(b.clone());
    graph.add_symbol(c.clone());

    graph.add_relationship(&rel(&b.id, &a.id, "b.ts", 1));
    graph.add_relationship(&rel(&c.id, &a.id, "c.ts", 2));
    graph.remove_file("b.ts");

    let blast = blast_radius(&graph, "A", None, Some(1)).expect("blast ok");
    assert_eq!(blast.len(), 1);
    assert_eq!(blast[0].from, c.id);
}