use bynkc::lexer::{self, TokenKind};
use bynkc::locals::{LocalBinding, binding_at_def, locals_at};
use bynkc::span::Span;
fn ident_at(text: &str, offset: usize) -> Option<(&str, Span)> {
let toks = lexer::tokenize(text).ok()?;
toks.into_iter()
.find(|t| t.kind == TokenKind::Ident && t.span.start <= offset && offset <= t.span.end)
.map(|t| (&text[t.span.start..t.span.end], t.span))
}
fn target_at<'a>(
locals: &'a [LocalBinding],
text: &str,
offset: usize,
) -> Option<&'a LocalBinding> {
let (name, _) = ident_at(text, offset)?;
binding_at_def(locals, offset)
.filter(|b| b.name == name)
.or_else(|| {
locals_at(locals, offset)
.into_iter()
.find(|b| b.name == name)
})
}
pub fn local_sites_at(locals: &[LocalBinding], text: &str, offset: usize) -> Option<Vec<Span>> {
let target = target_at(locals, text, offset)?;
let toks = lexer::tokenize(text).ok()?;
let mut sites = vec![target.def_span];
for t in &toks {
if t.kind != TokenKind::Ident || text[t.span.start..t.span.end] != target.name {
continue;
}
if t.span == target.def_span {
continue; }
if locals.iter().any(|b| b.def_span == t.span) {
continue;
}
if t.span.start < target.scope.start || t.span.end > target.scope.end {
continue; }
let resolves = locals_at(locals, t.span.start)
.into_iter()
.find(|b| b.name == target.name)
.map(|b| b.def_span);
if resolves == Some(target.def_span) {
sites.push(t.span);
}
}
Some(sites)
}
pub fn local_definition_at(locals: &[LocalBinding], text: &str, offset: usize) -> Option<Span> {
target_at(locals, text, offset).map(|b| b.def_span)
}
pub fn local_token_sites(locals: &[LocalBinding], text: &str) -> Vec<(Span, bool)> {
let Ok(toks) = lexer::tokenize(text) else {
return Vec::new();
};
let mut out = Vec::new();
for t in &toks {
if t.kind != TokenKind::Ident {
continue;
}
let name = &text[t.span.start..t.span.end];
if locals.iter().any(|b| b.def_span == t.span) {
out.push((t.span, true)); } else if locals_at(locals, t.span.start)
.into_iter()
.any(|b| b.name == name)
{
out.push((t.span, false)); }
}
out
}
#[cfg(test)]
mod tests {
use super::*;
fn bindings() -> Vec<LocalBinding> {
vec![
LocalBinding {
name: "n".into(),
def_span: Span { start: 5, end: 6 },
ty: "Int".into(),
scope: Span { start: 20, end: 60 },
},
LocalBinding {
name: "x".into(),
def_span: Span { start: 26, end: 27 },
ty: "Int".into(),
scope: Span { start: 34, end: 60 },
},
]
}
const TEXT: &str = "fn f(n: Int) -> Int { let x = n\n x + x\n}";
#[test]
fn sites_for_a_use_collect_def_plus_uses() {
let locals = bindings();
let x_use = TEXT.match_indices('x').nth(1).unwrap().0; let sites = local_sites_at(&locals, TEXT, x_use).expect("on a local");
assert!(
sites.contains(&Span { start: 26, end: 27 }),
"includes def: {sites:?}"
);
assert!(sites.len() >= 2, "def + at least one use: {sites:?}");
}
#[test]
fn definition_resolves_from_a_use() {
let locals = bindings();
let n_use = TEXT.rfind('n').unwrap(); assert_eq!(
local_definition_at(&locals, TEXT, n_use),
Some(Span { start: 5, end: 6 })
);
}
#[test]
fn not_on_a_local_yields_none() {
let locals = bindings();
assert!(local_sites_at(&locals, TEXT, 0).is_none()); }
#[test]
fn token_sites_mark_definitions_and_uses() {
let sites = local_token_sites(&bindings(), TEXT);
assert!(
sites.iter().any(|(_, decl)| *decl),
"has a definition token"
);
assert!(sites.iter().any(|(_, decl)| !*decl), "has a use token");
assert!(
sites.contains(&(Span { start: 26, end: 27 }, true)),
"x def is a declaration: {sites:?}"
);
}
#[test]
fn resolves_a_real_local_from_diagnose_project() {
let root = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
.join("../bynkc/tests/fixtures/inlay/clean/src");
let r = bynkc::diagnose_project(&root, &std::collections::HashMap::new());
let file = r
.files
.iter()
.find(|f| f.source_path.to_string_lossy().ends_with("util.bynk"))
.expect("util.bynk analysed");
let text = &file.text;
let locals = r
.locals
.iter()
.find(|(p, _)| p.to_string_lossy().ends_with("util.bynk"))
.map(|(_, l)| l.clone())
.expect("util.bynk locals");
let use_off = text.rfind("total").expect("total use");
let sites = local_sites_at(&locals, text, use_off).expect("on a local");
assert!(sites.len() >= 2, "def + use: {sites:?}");
let def = text.find("total").expect("total def");
assert_eq!(sites[0].start, def, "def first");
}
}