scryer-engine 0.3.0

Tree-sitter AST indexing and reference resolution engine for Scryer code intelligence
//! A symbol is a declaration (Plan 23): module- and class-level bindings and functions at any
//! depth. What a function body binds to a name is a local and is not indexed, so outlines list
//! declarations and a call is blamed on the function it sits in, not on a local it is assigned
//! to.

mod support;

use std::path::Path;

use scryer_engine::IndexOptions;
use scryer_engine::parsers::{PythonAstParser, TypeScriptAstParser};
use support::fingerprint::fingerprint;
use support::fixture::index_files;

fn names(payload: &scryer_engine::ParsedFilePayload) -> Vec<(String, String)> {
    let mut out: Vec<_> = payload
        .symbols
        .iter()
        .map(|s| {
            (
                s.qualified_name
                    .rsplit_once('.')
                    .map_or(s.qualified_name.clone(), |(_, tail)| tail.to_string()),
                s.kind.clone(),
            )
        })
        .collect();
    out.sort();
    out
}

fn has(names: &[(String, String)], name: &str, kind: &str) -> bool {
    names.iter().any(|(n, k)| n == name && k == kind)
}

#[test]
fn python_indexes_module_and_class_bindings_but_not_function_locals() {
    let code = r#"
LIMIT = 5
registry = {}

def outer(url):
    host = parse(url)
    def inner():
        tmp = 1
        return tmp
    class Local:
        attr = 1
    return host

class C:
    CLASSES = {}
    label = "x"

    def method(self):
        local = 1
        def nested():
            pass

    class Inner:
        flag = True
"#;
    let payload =
        PythonAstParser::parse(Path::new("pkg/mod.py"), code.as_bytes(), "h").expect("parses");
    let names = names(&payload);

    for (name, kind) in [
        ("LIMIT", "const"),
        ("registry", "var"),
        ("outer", "fn"),
        ("inner", "fn"),
        ("Local", "class"),
        ("attr", "var"),
        ("C", "class"),
        ("CLASSES", "const"),
        ("label", "var"),
        ("method", "fn"),
        ("nested", "fn"),
        ("Inner", "class"),
        ("flag", "var"),
    ] {
        assert!(
            has(&names, name, kind),
            "{name} ({kind}) is a declaration: {names:?}"
        );
    }
    for local in ["host", "tmp", "local"] {
        assert!(
            !names.iter().any(|(n, _)| n == local),
            "{local} is a function-body local: {names:?}"
        );
    }
}

#[test]
fn typescript_indexes_functions_and_top_level_bindings_but_not_function_locals() {
    let code = r#"
export const TOP = 1;

export function run(x: number) {
  const response = f(x);
  let count = 0;
  const helper = () => g();
  function inner() {
    const a = 1;
    return a;
  }
  return response + count;
}

export class K {
  method() {
    const z = 1;
    const cb = () => z;
    return cb();
  }
}
"#;
    let payload =
        TypeScriptAstParser::parse(Path::new("src/mod.ts"), code.as_bytes(), "h").expect("parses");
    let names = names(&payload);

    for (name, kind) in [
        ("TOP", "const"),
        ("run", "fn"),
        ("helper", "fn"),
        ("inner", "fn"),
        ("K", "class"),
        ("method", "method"),
        ("cb", "fn"),
    ] {
        assert!(
            has(&names, name, kind),
            "{name} ({kind}) is a declaration: {names:?}"
        );
    }
    for local in ["response", "count", "a", "z"] {
        assert!(
            !names.iter().any(|(n, _)| n == local),
            "{local} is a function-body local: {names:?}"
        );
    }
}

#[tokio::test]
async fn a_call_is_blamed_on_the_function_not_on_the_local_it_is_assigned_to() -> anyhow::Result<()>
{
    let (db, project) = index_files(
        &[
            ("a.ts", "export function f(): number { return 1; }\n"),
            (
                "b.ts",
                "import { f } from './a';\nexport function run(): number {\n  const response = f();\n  return response;\n}\n",
            ),
            ("m.py", "def g():\n    return 1\n"),
            (
                "n.py",
                "from m import g\n\ndef run():\n    result = g()\n    return result\n",
            ),
        ],
        IndexOptions::default(),
    )
    .await?;
    let fp = fingerprint(&db, project).await?;
    for (caller, file) in [(".run@b.ts->", "b.ts"), (".run@n.py->", "n.py")] {
        assert!(
            fp.edges.iter().any(|e| e.contains(caller)),
            "{file}: the call is an edge from run: {:?}",
            fp.edges
        );
    }
    assert!(
        !fp.edges
            .iter()
            .any(|e| e.contains(".response@") || e.contains(".result@")),
        "no edge starts at a local: {:?}",
        fp.edges
    );
    Ok(())
}

#[tokio::test]
async fn a_call_through_a_local_binding_never_links_to_a_same_named_symbol_elsewhere()
-> anyhow::Result<()> {
    let (db, project) = index_files(
        &[
            (
                "other.ts",
                "export function cb(): number { return 1; }\nexport function send(): number { return 2; }\n",
            ),
            (
                "use.ts",
                "export function run(cb: () => number): number {\n  const send = () => 3;\n  return cb() + send();\n}\n",
            ),
            (
                "other.py",
                "def cb():\n    return 1\n\ndef send():\n    return 2\n",
            ),
            (
                "use.py",
                "def run(cb):\n    def send():\n        return 3\n    return cb() + send()\n",
            ),
            ("lib.rs", "pub fn cb() {}\npub fn go(cb: fn()) { cb(); }\n"),
        ],
        IndexOptions::default(),
    )
    .await?;
    let fp = fingerprint(&db, project).await?;
    let calls_from = |file: &str| -> Vec<&String> {
        fp.references
            .iter()
            .filter(|r| r.starts_with(&format!("{file}:")) && r.contains(":call["))
            .collect()
    };

    // `cb` is a parameter: it is not `other.cb`. `send` is a nested function of the file.
    for file in ["use.ts", "use.py"] {
        let calls = calls_from(file);
        assert!(
            calls.iter().all(|r| !r.contains("::other.")
                && !r.contains("other.ts::")
                && !r.contains("other.py::")),
            "{file} calls nothing in other: {calls:?}"
        );
        assert!(
            calls.iter().any(|r| r.contains(&format!("{file}::"))),
            "{file}: the nested `send` is called: {calls:?}"
        );
    }
    // Rust: a `fn()` parameter called `cb` is not the `cb` function of the same file.
    assert!(
        calls_from("lib.rs").is_empty(),
        "{:?}",
        calls_from("lib.rs")
    );
    Ok(())
}