mod support;
use scryer_db::{CodeGraphEdge, ScryerDb, Symbol, SymbolReference};
use scryer_engine::IndexOptions;
use support::fixture::index_files;
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)
}
async fn call_edges(db: &ScryerDb, project: u64) -> anyhow::Result<Vec<String>> {
let mut conn = db.lock().await;
let symbols = Symbol::filter_by_project_id(project)
.exec(&mut *conn)
.await?;
let name = |id: u64| {
symbols
.iter()
.find(|s| s.id == id)
.map(|s| s.qualified_name.clone())
.unwrap_or_default()
};
let mut edges: Vec<String> = CodeGraphEdge::filter_by_project_id(project)
.exec(&mut *conn)
.await?
.into_iter()
.filter(|e| e.edge_type == "calls")
.map(|e| {
format!(
"{} -> {}",
name(e.source_symbol_id),
name(e.target_symbol_id)
)
})
.collect();
edges.sort();
Ok(edges)
}
#[tokio::test]
async fn trait_qualified_calls_resolve_to_the_trait_method() -> anyhow::Result<()> {
let (db, project) = index_files(
&[(
"src/lib.rs",
"pub trait Walk { fn step(&self) -> u32; }\n\
pub trait Run { fn step(&self) -> u32; fn sprint(&self) -> u32; }\n\
pub struct Dog;\n\
impl Walk for Dog { fn step(&self) -> u32 { 1 } }\n\
pub fn direct(d: &Dog) -> u32 { Walk::step(d) }\n\
pub fn qualified(d: &Dog) -> u32 { <Dog as Walk>::step(d) }\n\
pub fn generic<T: Walk>(t: &T) -> u32 { T::step(t) }\n\
pub fn where_clause<T>(t: &T) -> u32 where T: Walk { T::step(t) }\n\
pub fn two_bounds<T: Run + Walk>(t: &T) -> u32 { T::sprint(t) }\n\
pub fn picks_the_declaring_trait<T: Run + Walk>(t: &T) -> u32 { T::step(t) }\n",
)],
IndexOptions::default(),
)
.await?;
let step = calls(&db, project, "step").await?;
let target = |line: u32| {
step.iter()
.find(|(l, _, _)| *l == line)
.map(|(_, t, v)| (t.as_str(), v.as_str()))
};
assert_eq!(target(5), Some(("crate::Walk::step", "exact")), "{step:?}");
assert_eq!(target(6), Some(("crate::Walk::step", "exact")), "{step:?}");
assert_eq!(target(7), Some(("crate::Walk::step", "exact")), "{step:?}");
assert_eq!(target(8), Some(("crate::Walk::step", "exact")), "{step:?}");
assert_eq!(target(10), Some(("crate::Run::step", "exact")), "{step:?}");
assert_eq!(
calls(&db, project, "sprint").await?,
vec![(9, "crate::Run::sprint".into(), "exact".into())]
);
Ok(())
}
#[tokio::test]
async fn initialisers_have_a_calling_symbol() -> anyhow::Result<()> {
let (db, project) = index_files(
&[
(
"src/lib.rs",
"pub fn helper() -> u32 { 1 }\npub const LIMIT: u32 = helper() + 1;\n",
),
(
"m.py",
"def helper():\n return 1\n\n\
class Holder:\n attribute = helper()\n\n\
def with_default(x=helper()):\n return x\n\n\
top_level = helper()\n",
),
(
"m.ts",
"export function helper() { return 1; }\n\
export class Holder {\n property = helper();\n}\n",
),
],
IndexOptions::default(),
)
.await?;
let edges = call_edges(&db, project).await?;
for expected in [
"crate::LIMIT -> crate::helper",
"m.Holder.attribute -> m.helper",
"m.with_default -> m.helper",
"m.top_level -> m.helper",
"m.Holder -> m.helper",
] {
assert!(
edges.iter().any(|e| e == expected),
"{expected} in {edges:#?}"
);
}
Ok(())
}