scryer-engine 0.3.0

Tree-sitter AST indexing and reference resolution engine for Scryer code intelligence
//! `self.m()`, `this.m()`, `Self::m()` and `Type::m()` pick the method of the right type, and
//! a method call whose receiver is unknown stays a match by name.

mod support;

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

/// (line, qualified target, via) of the call references to methods 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)
}

fn row(line: u32, target: &str, via: &str) -> (u32, String, String) {
    (line, target.to_string(), via.to_string())
}

#[tokio::test]
async fn rust_self_and_type_paths_pick_the_right_impl() -> anyhow::Result<()> {
    let (db, project) = index_files(
        &[(
            "src/lib.rs",
            "pub struct A;\n\
             pub struct B;\n\
             impl A {\n\
                 pub fn new() -> A { A }\n\
                 pub fn helper(&self) -> u32 { 1 }\n\
                 pub fn go(&self) -> u32 { self.helper() }\n\
                 pub fn make() -> A { Self::new() }\n\
             }\n\
             impl B {\n\
                 pub fn new() -> B { B }\n\
                 pub fn helper(&self) -> u32 { 2 }\n\
                 pub fn go(&self) -> u32 { self.helper() }\n\
             }\n\
             pub fn build() { A::new(); B::new(); }\n\
             pub trait Tr { fn defaulted(&self) -> u32 { 3 } fn needs(&self) -> u32; }\n\
             impl Tr for A { fn needs(&self) -> u32 { self.defaulted() } }\n\
             pub fn unknown(x: B) -> u32 { x.helper() }\n",
        )],
        IndexOptions::default(),
    )
    .await?;

    // `A::new()` and `B::new()` are told apart, and `Self::new()` is `A::new`.
    assert_eq!(
        calls(&db, project, "new").await?,
        vec![
            row(7, "crate::A::new", "exact"),
            row(14, "crate::A::new", "exact"),
            row(14, "crate::B::new", "exact"),
        ]
    );
    let helper = calls(&db, project, "helper").await?;
    assert_eq!(helper[0], row(6, "crate::A::helper", "exact"), "{helper:?}");
    assert_eq!(
        helper[1],
        row(12, "crate::B::helper", "exact"),
        "{helper:?}"
    );
    // An argument of unknown type: only the name matches.
    assert_eq!(helper[2].0, 17, "{helper:?}");
    assert_eq!(helper[2].2, "name", "{helper:?}");
    // `A` has no `defaulted`; the trait's default method is found by name.
    assert_eq!(
        calls(&db, project, "defaulted").await?,
        vec![row(16, "crate::Tr::defaulted", "name")]
    );
    Ok(())
}

#[tokio::test]
async fn python_self_picks_its_own_class() -> anyhow::Result<()> {
    let (db, project) = index_files(
        &[(
            "m.py",
            "class A:\n\
             \x20   def helper(self):\n\
             \x20       return 1\n\
             \x20   def go(self):\n\
             \x20       return self.helper()\n\
             class B:\n\
             \x20   def helper(self):\n\
             \x20       return 2\n\
             \x20   def go(self):\n\
             \x20       return self.helper()\n",
        )],
        IndexOptions::default(),
    )
    .await?;
    assert_eq!(
        calls(&db, project, "helper").await?,
        vec![
            row(5, "m.A.helper", "exact"),
            row(10, "m.B.helper", "exact")
        ]
    );
    Ok(())
}

#[tokio::test]
async fn typescript_this_picks_its_own_class() -> anyhow::Result<()> {
    let (db, project) = index_files(
        &[(
            "m.ts",
            "export class A {\n\
             \x20 helper() { return 1; }\n\
             \x20 go() { return this.helper(); }\n\
             }\n\
             export class B {\n\
             \x20 helper() { return 2; }\n\
             \x20 go() { return this.helper(); }\n\
             }\n",
        )],
        IndexOptions::default(),
    )
    .await?;
    let helper = calls(&db, project, "helper").await?;
    assert_eq!(helper.len(), 2, "{helper:?}");
    assert!(
        helper[0].1.ends_with("A.helper") || helper[0].1.ends_with("A::helper"),
        "{helper:?}"
    );
    assert!(
        helper[1].1.ends_with("B.helper") || helper[1].1.ends_with("B::helper"),
        "{helper:?}"
    );
    assert!(helper.iter().all(|r| r.2 == "exact"), "{helper:?}");
    Ok(())
}