scryer-engine 0.3.0

Tree-sitter AST indexing and reference resolution engine for Scryer code intelligence
//! A call that starts or sits inside a method chain is still a call to that function.
//! `make().map(f).unwrap_or(0)` calls `make`, though the outermost call is `unwrap_or`.

use std::fs;
use tempfile::tempdir;

use scryer_db::{CodeGraphEdge, Project, ScryerDb, Symbol, SymbolReference};
use scryer_engine::EngineService;

async fn index(files: &[(&str, &str)]) -> anyhow::Result<(ScryerDb, u64)> {
    let temp = tempdir()?;
    let root = temp.path();
    for (path, content) in files {
        fs::write(root.join(path), content)?;
    }
    let db = ScryerDb::new_in_memory().await?;
    let project = {
        let mut conn = db.lock().await;
        Project::create()
            .slug("chains".to_string())
            .root_path(root.to_string_lossy().to_string())
            .default_branch("main".to_string())
            .is_active(true)
            .created_at("2026-10-08T00:00:00Z".to_string())
            .updated_at("2026-10-08T00:00:00Z".to_string())
            .exec(&mut *conn)
            .await?
    };
    EngineService::new(db.clone())
        .index_project(project.id, root)
        .await?;
    Ok((db, project.id))
}

/// Lines of the call references that resolve to the function named `name`.
async fn call_lines(db: &ScryerDb, project: u64, name: &str) -> anyhow::Result<Vec<u32>> {
    let mut conn = db.lock().await;
    let ids: Vec<u64> = Symbol::filter_by_project_id(project)
        .exec(&mut *conn)
        .await?
        .into_iter()
        .filter(|s| s.name == name)
        .map(|s| s.id)
        .collect();
    let mut lines: Vec<u32> = SymbolReference::filter_by_project_id(project)
        .exec(&mut *conn)
        .await?
        .into_iter()
        .filter(|r| r.role == "call" && ids.contains(&r.symbol_id))
        .map(|r| r.line_number)
        .collect();
    lines.sort_unstable();
    lines.dedup();
    Ok(lines)
}

#[tokio::test]
async fn rust_calls_in_method_chains_are_all_found() -> anyhow::Result<()> {
    let (db, project) = index(&[(
        "lib.rs",
        "pub fn make() -> Option<u32> { Some(1) }\n\
         pub fn tail() -> Option<u32> { Some(2) }\n\
         pub fn direct() { make(); }\n\
         pub fn chain_end() { let _ = tail().map(|x| x); }\n\
         pub fn chain_head() { let _ = make().map(|x| x + 1).unwrap_or(0); }\n\
         pub fn chain_long() { let _ = make().ok_or(1).map(|x| x).is_ok(); }\n\
         pub fn awaited() { let _ = make().map(|x| x).ok(); }\n\
         pub fn nested_arg() { let _ = tail().map(|x| make().unwrap_or(x)); }\n\
         pub fn turbofish() { let _ = make().into_iter().collect::<Vec<_>>(); }\n",
    )])
    .await?;

    assert_eq!(
        call_lines(&db, project, "make").await?,
        vec![3, 5, 6, 7, 8, 9]
    );
    assert_eq!(call_lines(&db, project, "tail").await?, vec![4, 8]);

    // Each of those calls is also a call-graph edge from its enclosing function.
    let mut conn = db.lock().await;
    let symbols = Symbol::filter_by_project_id(project)
        .exec(&mut *conn)
        .await?;
    let id = |n: &str| symbols.iter().find(|s| s.name == n).unwrap().id;
    let edges = CodeGraphEdge::filter_by_project_id(project)
        .exec(&mut *conn)
        .await?;
    for caller in [
        "direct",
        "chain_head",
        "chain_long",
        "awaited",
        "nested_arg",
        "turbofish",
    ] {
        assert!(
            edges.iter().any(|e| e.source_symbol_id == id(caller)
                && e.target_symbol_id == id("make")
                && e.edge_type == "calls"),
            "missing edge {caller} -> make"
        );
    }
    Ok(())
}

#[tokio::test]
async fn python_calls_in_method_chains_are_all_found() -> anyhow::Result<()> {
    let (db, project) = index(&[(
        "m.py",
        "def make():\n    return []\n\n\
         def direct():\n    make()\n\n\
         def chained():\n    return make().copy().pop()\n\n\
         def arg():\n    return str(make().copy())\n",
    )])
    .await?;
    assert_eq!(call_lines(&db, project, "make").await?, vec![5, 8, 11]);
    Ok(())
}

#[tokio::test]
async fn typescript_calls_in_method_chains_are_all_found() -> anyhow::Result<()> {
    let (db, project) = index(&[(
        "m.ts",
        "export function make(): number[] { return []; }\n\
         export function direct() { make(); }\n\
         export function chained() { return make().slice().pop(); }\n\
         export function mapped() { return make().map((x) => x).filter(Boolean); }\n",
    )])
    .await?;
    assert_eq!(call_lines(&db, project, "make").await?, vec![2, 3, 4]);
    Ok(())
}