graphyn-core 0.1.1

Language-agnostic code intelligence graph engine — the heart of Graphyn
Documentation
use graphyn_core::graph::GraphynGraph;
use graphyn_core::ir::{Language, Relationship, RelationshipKind, Symbol, SymbolKind};
use graphyn_core::query::{blast_radius, symbol_usages};
use graphyn_core::resolver::{AliasResolver, AliasScope};

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,
    }
}

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

    let user_payload = symbol(
        "src/models/user_payload.ts::UserPayload::class",
        "UserPayload",
        "src/models/user_payload.ts",
        SymbolKind::Class,
    );
    let mapper = symbol(
        "src/mappers/response/deep/view_model_mapper.ts::ViewModelMapper::class",
        "ViewModelMapper",
        "src/mappers/response/deep/view_model_mapper.ts",
        SymbolKind::Class,
    );

    graph.add_symbol(user_payload.clone());
    graph.add_symbol(mapper.clone());

    let relationships = vec![Relationship {
        from: mapper.id.clone(),
        to: user_payload.id.clone(),
        kind: RelationshipKind::Imports,
        alias: Some("ResponseModel".to_string()),
        properties_accessed: vec![
            "userId".to_string(),
            "timestamp".to_string(),
            "status".to_string(),
        ],
        context: "import { UserPayload as ResponseModel } from '../../../models/user_payload';"
            .to_string(),
        file: "src/mappers/response/deep/view_model_mapper.ts".to_string(),
        line: 1,
    }];

    for relationship in &relationships {
        graph.add_relationship(relationship);
    }

    let resolver = AliasResolver::default();
    resolver.ingest_relationships(&graph, &relationships);

    let aliases = graph
        .alias_chains
        .get(&user_payload.id)
        .expect("alias chain exists");
    assert_eq!(aliases.len(), 1);
    assert_eq!(aliases[0].alias_name, "ResponseModel");
    assert_eq!(aliases[0].scope, AliasScope::ImportAlias);

    let blast = blast_radius(&graph, "UserPayload", None, Some(2)).expect("blast radius succeeds");
    assert_eq!(blast.len(), 1);
    assert_eq!(
        blast[0].file,
        "src/mappers/response/deep/view_model_mapper.ts"
    );
    assert_eq!(blast[0].alias.as_deref(), Some("ResponseModel"));
    assert_eq!(
        blast[0].properties_accessed,
        vec!["userId", "timestamp", "status"]
    );

    let usages = symbol_usages(&graph, "UserPayload", None, true).expect("usages succeeds");
    assert_eq!(usages.len(), 1);
    assert_eq!(usages[0].alias.as_deref(), Some("ResponseModel"));
}

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

    let canonical = symbol(
        "src/models/user_payload.ts::UserPayload::class",
        "UserPayload",
        "src/models/user_payload.ts",
        SymbolKind::Class,
    );
    let barrel = symbol(
        "src/models/index.ts::Models::module",
        "Models",
        "src/models/index.ts",
        SymbolKind::Module,
    );

    graph.add_symbol(canonical.clone());
    graph.add_symbol(barrel.clone());

    let relationships = vec![
        Relationship {
            from: barrel.id.clone(),
            to: canonical.id.clone(),
            kind: RelationshipKind::ReExports,
            alias: Some("PublicUser".to_string()),
            properties_accessed: vec![],
            context: "export { UserPayload as PublicUser } from './user_payload'".to_string(),
            file: "src/models/index.ts".to_string(),
            line: 1,
        },
        Relationship {
            from: barrel.id.clone(),
            to: canonical.id.clone(),
            kind: RelationshipKind::ReExports,
            alias: Some("UserPayload".to_string()),
            properties_accessed: vec![],
            context: "export * from './user_payload'".to_string(),
            file: "src/models/index.ts".to_string(),
            line: 2,
        },
        Relationship {
            from: barrel.id.clone(),
            to: canonical.id.clone(),
            kind: RelationshipKind::Imports,
            alias: Some("User".to_string()),
            properties_accessed: vec![],
            context: "import User from './user_payload' // default".to_string(),
            file: "src/models/index.ts".to_string(),
            line: 3,
        },
    ];

    let resolver = AliasResolver::default();
    resolver.ingest_relationships(&graph, &relationships);

    let aliases = graph
        .alias_chains
        .get(&canonical.id)
        .expect("alias chain exists");
    assert_eq!(aliases.len(), 3);
    assert!(aliases
        .iter()
        .any(|a| a.alias_name == "PublicUser" && a.scope == AliasScope::ReExport));
    assert!(aliases
        .iter()
        .any(|a| a.alias_name == "UserPayload" && a.scope == AliasScope::BarrelReExport));
    assert!(aliases
        .iter()
        .any(|a| a.alias_name == "User" && a.scope == AliasScope::DefaultImport));

    assert_eq!(
        resolver
            .resolve_alias_in_file("PublicUser", "src/models/index.ts")
            .as_deref(),
        Some(canonical.id.as_str())
    );
}

#[test]
fn test_accesses_property_edge_can_be_canonicalized_from_alias() {
    let mut graph = GraphynGraph::new();
    let canonical = symbol(
        "src/models/user_payload.ts::UserPayload::class",
        "UserPayload",
        "src/models/user_payload.ts",
        SymbolKind::Class,
    );
    let mapper = symbol(
        "src/mappers/response/deep/view_model_mapper.ts::ViewModelMapper::class",
        "ViewModelMapper",
        "src/mappers/response/deep/view_model_mapper.ts",
        SymbolKind::Class,
    );
    graph.add_symbol(canonical.clone());
    graph.add_symbol(mapper.clone());

    let import_rel = Relationship {
        from: mapper.id.clone(),
        to: canonical.id.clone(),
        kind: RelationshipKind::Imports,
        alias: Some("ResponseModel".to_string()),
        properties_accessed: vec![],
        context: "import { UserPayload as ResponseModel } from '../../../models/user_payload';"
            .to_string(),
        file: "src/mappers/response/deep/view_model_mapper.ts".to_string(),
        line: 1,
    };
    let resolver = AliasResolver::default();
    resolver.ingest_relationships(&graph, &[import_rel]);

    let accesses_property = Relationship {
        from: mapper.id.clone(),
        to: "src/mappers/response/deep/view_model_mapper.ts::ResponseModel::alias".to_string(),
        kind: RelationshipKind::AccessesProperty,
        alias: Some("ResponseModel".to_string()),
        properties_accessed: vec!["userId".to_string()],
        context: "id: data.userId".to_string(),
        file: "src/mappers/response/deep/view_model_mapper.ts".to_string(),
        line: 6,
    };

    let normalized = resolver.canonicalize_relationship(&accesses_property);
    assert_eq!(normalized.to, canonical.id);
    assert_eq!(normalized.properties_accessed, vec!["userId"]);
}