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()
};
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:?}"
);
}
assert!(
calls_from("lib.rs").is_empty(),
"{:?}",
calls_from("lib.rs")
);
Ok(())
}