scryer-engine 0.3.0

Tree-sitter AST indexing and reference resolution engine for Scryer code intelligence
//! Trait-qualified calls resolve to the trait's method, and calls in initialisers have a
//! calling symbol.

mod support;

use scryer_db::{CodeGraphEdge, ScryerDb, Symbol, SymbolReference};
use scryer_engine::IndexOptions;
use support::fixture::index_files;

/// (line, qualified target, via) of every call reference whose target is named `name`.
async fn calls(
    db: &ScryerDb,
    project: u64,
    name: &str,
) -> anyhow::Result<Vec<(u32, String, String)>> {
    let mut conn = db.lock().await;
    let symbols = Symbol::filter_by_project_id(project)
        .exec(&mut *conn)
        .await?;
    let mut out: Vec<(u32, String, String)> = SymbolReference::filter_by_project_id(project)
        .exec(&mut *conn)
        .await?
        .into_iter()
        .filter(|r| r.role == "call")
        .filter_map(|r| {
            let target = symbols.iter().find(|s| s.id == r.symbol_id)?;
            (target.name == name).then(|| (r.line_number, target.qualified_name.clone(), r.via))
        })
        .collect();
    out.sort();
    Ok(out)
}

/// `source -> target` call edges as qualified names.
async fn call_edges(db: &ScryerDb, project: u64) -> anyhow::Result<Vec<String>> {
    let mut conn = db.lock().await;
    let symbols = Symbol::filter_by_project_id(project)
        .exec(&mut *conn)
        .await?;
    let name = |id: u64| {
        symbols
            .iter()
            .find(|s| s.id == id)
            .map(|s| s.qualified_name.clone())
            .unwrap_or_default()
    };
    let mut edges: Vec<String> = CodeGraphEdge::filter_by_project_id(project)
        .exec(&mut *conn)
        .await?
        .into_iter()
        .filter(|e| e.edge_type == "calls")
        .map(|e| {
            format!(
                "{} -> {}",
                name(e.source_symbol_id),
                name(e.target_symbol_id)
            )
        })
        .collect();
    edges.sort();
    Ok(edges)
}

#[tokio::test]
async fn trait_qualified_calls_resolve_to_the_trait_method() -> anyhow::Result<()> {
    let (db, project) = index_files(
        &[(
            "src/lib.rs",
            "pub trait Walk { fn step(&self) -> u32; }\n\
             pub trait Run { fn step(&self) -> u32; fn sprint(&self) -> u32; }\n\
             pub struct Dog;\n\
             impl Walk for Dog { fn step(&self) -> u32 { 1 } }\n\
             pub fn direct(d: &Dog) -> u32 { Walk::step(d) }\n\
             pub fn qualified(d: &Dog) -> u32 { <Dog as Walk>::step(d) }\n\
             pub fn generic<T: Walk>(t: &T) -> u32 { T::step(t) }\n\
             pub fn where_clause<T>(t: &T) -> u32 where T: Walk { T::step(t) }\n\
             pub fn two_bounds<T: Run + Walk>(t: &T) -> u32 { T::sprint(t) }\n\
             pub fn picks_the_declaring_trait<T: Run + Walk>(t: &T) -> u32 { T::step(t) }\n",
        )],
        IndexOptions::default(),
    )
    .await?;

    let step = calls(&db, project, "step").await?;
    let target = |line: u32| {
        step.iter()
            .find(|(l, _, _)| *l == line)
            .map(|(_, t, v)| (t.as_str(), v.as_str()))
    };
    assert_eq!(target(5), Some(("crate::Walk::step", "exact")), "{step:?}");
    assert_eq!(target(6), Some(("crate::Walk::step", "exact")), "{step:?}");
    assert_eq!(target(7), Some(("crate::Walk::step", "exact")), "{step:?}");
    assert_eq!(target(8), Some(("crate::Walk::step", "exact")), "{step:?}");
    // Both traits declare `step`: the first bound that does is taken (`Run`), and the other
    // trait's `sprint` is found through the bound that declares it.
    assert_eq!(target(10), Some(("crate::Run::step", "exact")), "{step:?}");
    assert_eq!(
        calls(&db, project, "sprint").await?,
        vec![(9, "crate::Run::sprint".into(), "exact".into())]
    );
    Ok(())
}

#[tokio::test]
async fn initialisers_have_a_calling_symbol() -> anyhow::Result<()> {
    let (db, project) = index_files(
        &[
            (
                "src/lib.rs",
                "pub fn helper() -> u32 { 1 }\npub const LIMIT: u32 = helper() + 1;\n",
            ),
            (
                "m.py",
                "def helper():\n    return 1\n\n\
                 class Holder:\n    attribute = helper()\n\n\
                 def with_default(x=helper()):\n    return x\n\n\
                 top_level = helper()\n",
            ),
            (
                "m.ts",
                "export function helper() { return 1; }\n\
                 export class Holder {\n  property = helper();\n}\n",
            ),
        ],
        IndexOptions::default(),
    )
    .await?;
    let edges = call_edges(&db, project).await?;
    for expected in [
        // A Rust `const`, a Python class attribute, a default argument and a top-level
        // assignment, and a TypeScript property initializer.
        "crate::LIMIT -> crate::helper",
        "m.Holder.attribute -> m.helper",
        "m.with_default -> m.helper",
        "m.top_level -> m.helper",
        "m.Holder -> m.helper",
    ] {
        assert!(
            edges.iter().any(|e| e == expected),
            "{expected} in {edges:#?}"
        );
    }
    Ok(())
}