scryer-engine 0.3.0

Tree-sitter AST indexing and reference resolution engine for Scryer code intelligence
//! A reference never resolves into a file of another language, however the name matches.

mod support;

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

/// Files holding the targets of every call reference to a symbol named `name`, by line.
async fn target_files(
    db: &ScryerDb,
    project: u64,
    file: &str,
) -> anyhow::Result<Vec<(u32, String)>> {
    use scryer_db::SourceFile;
    let mut conn = db.lock().await;
    let files = SourceFile::filter_by_project_id(project)
        .exec(&mut *conn)
        .await?;
    let path = |id: u64| files.iter().find(|f| f.id == id).map(|f| f.path.clone());
    let symbols = Symbol::filter_by_project_id(project)
        .exec(&mut *conn)
        .await?;
    let mut out: Vec<(u32, String)> = SymbolReference::filter_by_project_id(project)
        .exec(&mut *conn)
        .await?
        .into_iter()
        .filter(|r| r.role == "call" && path(r.file_id).as_deref() == Some(file))
        .filter_map(|r| {
            let target = symbols.iter().find(|s| s.id == r.symbol_id)?;
            Some((r.line_number, path(target.file_id)?))
        })
        .collect();
    out.sort();
    Ok(out)
}

#[tokio::test]
async fn calls_resolve_within_their_own_language() -> anyhow::Result<()> {
    let (db, project) = index_files(
        &[
            // `m.py` sorts before `src/lib.rs` and `z.ts`: path order alone would pick it.
            ("m.py", "def helper():\n    return 1\n\n\ndef go():\n    return helper()\n"),
            (
                "src/lib.rs",
                "pub fn helper() -> u32 { 1 }\npub fn go() -> u32 { helper() }\n",
            ),
            (
                "z.ts",
                "export function helper() { return 1; }\nexport function go() { return helper(); }\n",
            ),
        ],
        IndexOptions::default(),
    )
    .await?;
    assert_eq!(
        target_files(&db, project, "m.py").await?,
        [(6, "m.py".into())]
    );
    assert_eq!(
        target_files(&db, project, "src/lib.rs").await?,
        [(2, "src/lib.rs".into())]
    );
    assert_eq!(
        target_files(&db, project, "z.ts").await?,
        [(2, "z.ts".into())]
    );
    Ok(())
}

#[tokio::test]
async fn a_name_defined_only_in_another_language_is_unresolved() -> anyhow::Result<()> {
    let dir = tempfile::tempdir()?;
    std::fs::write(
        dir.path().join("only.py"),
        "def only_python():\n    return 1\n",
    )?;
    std::fs::write(
        dir.path().join("lib.rs"),
        "pub fn go() { only_python(); }\n",
    )?;
    let db = ScryerDb::new_in_memory().await?;
    let (project, report) = index_dir(&db, dir.path(), IndexOptions::default()).await?;
    assert_eq!(report.unresolved_references, 1, "{report:?}");
    assert!(target_files(&db, project, "lib.rs").await?.is_empty());
    Ok(())
}