scryer-engine 0.3.0

Tree-sitter AST indexing and reference resolution engine for Scryer code intelligence
//! A function named as a value is a `value` reference; a local or non-function of the same
//! name, a call, and a definition are not.

mod support;

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

/// Lines of the `value` references to symbols named `name`.
async fn value_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 == "value" && ids.contains(&r.symbol_id))
        .map(|r| r.line_number)
        .collect();
    lines.sort_unstable();
    Ok(lines)
}

#[tokio::test]
async fn rust_values_respect_shadowing_and_kind() -> anyhow::Result<()> {
    let (db, project) = index_files(
        &[(
            "src/lib.rs",
            "pub fn target() -> u32 { 1 }\n\
             pub const LIMIT: u32 = 3;\n\
             pub fn take(f: fn() -> u32) -> u32 { f() }\n\
             pub fn as_argument() -> u32 { take(target) }\n\
             pub fn in_let() { let _f: fn() -> u32 = target; }\n\
             pub fn returned() -> fn() -> u32 { return target; }\n\
             pub fn in_array() { let _a = [target, target]; }\n\
             pub fn shadowed_param(target: u32) -> u32 { take_u(target) }\n\
             pub fn shadowed_let() { let target = 1; take_u(target); }\n\
             pub fn shadowed_closure() { let _ = |target: u32| take_u(target); }\n\
             pub fn called() { target(); }\n\
             pub fn constant() { take_u(LIMIT); }\n\
             pub fn take_u(x: u32) -> u32 { x }\n",
        )],
        IndexOptions::default(),
    )
    .await?;

    // Lines 4, 5, 6 and 7 (twice) use the function as a value; the shadowed uses on lines 8
    // to 10, the call on line 11 and the constant on line 12 do not count.
    assert_eq!(
        value_lines(&db, project, "target").await?,
        vec![4, 5, 6, 7, 7]
    );
    assert!(value_lines(&db, project, "LIMIT").await?.is_empty());
    Ok(())
}

#[tokio::test]
async fn python_values_respect_shadowing() -> anyhow::Result<()> {
    let (db, project) = index_files(
        &[(
            "m.py",
            "def target():\n    return 1\n\n\
             def take(f):\n    return f\n\n\
             def as_argument():\n    return take(target)\n\n\
             def assigned():\n    f = target\n    return f\n\n\
             def shadowed_param(target):\n    return take(target)\n\n\
             def shadowed_assign():\n    target = 3\n    return take(target)\n\n\
             def default_value(f=target):\n    return f\n\n\
             def called():\n    return target()\n",
        )],
        IndexOptions::default(),
    )
    .await?;
    // `take(target)` (8) and `f = target` (11); not the shadowed uses (14, 17-18) or the call
    // (23). The default `f=target` (20) is Phase 5 (default-argument expressions are not
    // visited yet).
    assert_eq!(value_lines(&db, project, "target").await?, vec![8, 11]);
    Ok(())
}

#[tokio::test]
async fn names_bound_by_struct_and_destructuring_patterns_are_locals() -> anyhow::Result<()> {
    let (db, project) = index_files(
        &[
            (
                "src/lib.rs",
                "pub fn path() -> u32 { 1 }\n\
                 pub fn parent() -> u32 { 2 }\n\
                 pub struct P { pub path: u32, pub parent: u32 }\n\
                 pub fn take(x: u32) -> u32 { x }\n\
                 pub fn in_let(p: P) -> u32 { let P { path, .. } = p; take(path) }\n\
                 pub fn in_match(p: P) -> u32 { match p { P { parent, .. } => take(parent) } }\n\
                 pub fn in_if_let(p: Option<P>) -> u32 { if let Some(P { path, .. }) = p { take(path) } else { 0 } }\n",
            ),
            (
                "m.ts",
                "export function target() { return 1; }\n\
                 export function take(x: number) { return x; }\n\
                 export function destructured(o: { target: number }) { const { target } = o; return take(target); }\n",
            ),
        ],
        IndexOptions::default(),
    )
    .await?;
    assert!(value_lines(&db, project, "path").await?.is_empty());
    assert!(value_lines(&db, project, "parent").await?.is_empty());
    assert!(value_lines(&db, project, "target").await?.is_empty());
    Ok(())
}

#[tokio::test]
async fn a_method_is_not_a_value_and_an_associated_name_is_not_a_function() -> anyhow::Result<()> {
    let (db, project) = index_files(
        &[(
            "src/lib.rs",
            "pub struct S;\n\
             impl S { pub fn drop(&self) {} pub fn free_name(&self) {} }\n\
             pub fn free_name() {}\n\
             pub fn take<T>(_f: T) {}\n\
             pub fn uses() { take(drop); take(free_name); }\n",
        )],
        IndexOptions::default(),
    )
    .await?;
    // `drop` only exists as a method; `free_name` as both, and the free function is the target.
    assert!(value_lines(&db, project, "drop").await?.is_empty());
    let mut conn = db.lock().await;
    let free = Symbol::filter_by_project_id(project)
        .exec(&mut *conn)
        .await?
        .into_iter()
        .find(|s| s.name == "free_name" && !s.qualified_name.contains("S::"))
        .unwrap();
    let refs: Vec<_> = SymbolReference::filter_by_project_id(project)
        .exec(&mut *conn)
        .await?
        .into_iter()
        .filter(|r| r.role == "value")
        .collect();
    assert_eq!(refs.len(), 1, "{refs:?}");
    assert_eq!(refs[0].symbol_id, free.id);
    Ok(())
}

#[tokio::test]
async fn a_function_in_another_file_can_be_a_value() -> anyhow::Result<()> {
    let (db, project) = index_files(
        &[
            ("a.py", "def take(f):\n    return f\n\n\ndef go():\n    return take(target)\n"),
            ("z.py", "def target():\n    return 1\n\n\nclass K:\n    def method(self):\n        return 2\n"),
            ("b.py", "def go2():\n    return [method]\n"),
        ],
        IndexOptions::default(),
    )
    .await?;
    assert_eq!(value_lines(&db, project, "target").await?, vec![6]);
    // `method` is a method of K: a bare name cannot refer to it.
    assert!(value_lines(&db, project, "method").await?.is_empty());
    Ok(())
}