1use bynk_check::locals::{LocalBinding, binding_at_def, locals_at};
12use bynk_syntax::lexer::{self, TokenKind};
13use bynk_syntax::span::Span;
14
15fn ident_at(text: &str, offset: usize) -> Option<(&str, Span)> {
17 let toks = lexer::tokenize(text).ok()?;
18 toks.into_iter()
19 .find(|t| t.kind == TokenKind::Ident && t.span.start <= offset && offset <= t.span.end)
20 .map(|t| (&text[t.span.start..t.span.end], t.span))
21}
22
23fn target_at<'a>(
26 locals: &'a [LocalBinding],
27 text: &str,
28 offset: usize,
29) -> Option<&'a LocalBinding> {
30 let (name, _) = ident_at(text, offset)?;
31 binding_at_def(locals, offset)
32 .filter(|b| b.name == name)
33 .or_else(|| {
34 locals_at(locals, offset)
35 .into_iter()
36 .find(|b| b.name == name)
37 })
38}
39
40pub fn local_sites_at(locals: &[LocalBinding], text: &str, offset: usize) -> Option<Vec<Span>> {
44 let target = target_at(locals, text, offset)?;
45 let toks = lexer::tokenize(text).ok()?;
46 let mut sites = vec![target.def_span];
47 for t in &toks {
48 if t.kind != TokenKind::Ident || text[t.span.start..t.span.end] != target.name {
49 continue;
50 }
51 if t.span == target.def_span {
52 continue; }
54 if locals.iter().any(|b| b.def_span == t.span) {
56 continue;
57 }
58 if t.span.start < target.scope.start || t.span.end > target.scope.end {
59 continue; }
61 let resolves = locals_at(locals, t.span.start)
63 .into_iter()
64 .find(|b| b.name == target.name)
65 .map(|b| b.def_span);
66 if resolves == Some(target.def_span) {
67 sites.push(t.span);
68 }
69 }
70 Some(sites)
71}
72
73pub fn local_definition_at(locals: &[LocalBinding], text: &str, offset: usize) -> Option<Span> {
75 target_at(locals, text, offset).map(|b| b.def_span)
76}
77
78pub fn local_token_sites(locals: &[LocalBinding], text: &str) -> Vec<(Span, bool)> {
82 let Ok(toks) = lexer::tokenize(text) else {
83 return Vec::new();
84 };
85 let mut out = Vec::new();
86 for t in &toks {
87 if t.kind != TokenKind::Ident {
88 continue;
89 }
90 let name = &text[t.span.start..t.span.end];
91 if locals.iter().any(|b| b.def_span == t.span) {
92 out.push((t.span, true)); } else if locals_at(locals, t.span.start)
94 .into_iter()
95 .any(|b| b.name == name)
96 {
97 out.push((t.span, false)); }
99 }
100 out
101}
102
103#[cfg(test)]
104mod tests {
105 use super::*;
106
107 fn bindings() -> Vec<LocalBinding> {
109 vec![
111 LocalBinding {
112 name: "n".into(),
113 def_span: Span { start: 5, end: 6 },
114 ty: "Int".into(),
115 scope: Span { start: 20, end: 60 },
116 },
117 LocalBinding {
118 name: "x".into(),
119 def_span: Span { start: 26, end: 27 },
120 ty: "Int".into(),
121 scope: Span { start: 34, end: 60 },
122 },
123 ]
124 }
125
126 const TEXT: &str = "fn f(n: Int) -> Int { let x = n\n x + x\n}";
127 #[test]
131 fn sites_for_a_use_collect_def_plus_uses() {
132 let locals = bindings();
133 let x_use = TEXT.match_indices('x').nth(1).unwrap().0; let sites = local_sites_at(&locals, TEXT, x_use).expect("on a local");
136 assert!(
137 sites.contains(&Span { start: 26, end: 27 }),
138 "includes def: {sites:?}"
139 );
140 assert!(sites.len() >= 2, "def + at least one use: {sites:?}");
141 }
142
143 #[test]
144 fn definition_resolves_from_a_use() {
145 let locals = bindings();
146 let n_use = TEXT.rfind('n').unwrap(); assert_eq!(
148 local_definition_at(&locals, TEXT, n_use),
149 Some(Span { start: 5, end: 6 })
150 );
151 }
152
153 #[test]
154 fn not_on_a_local_yields_none() {
155 let locals = bindings();
156 assert!(local_sites_at(&locals, TEXT, 0).is_none()); }
158
159 #[test]
160 fn token_sites_mark_definitions_and_uses() {
161 let sites = local_token_sites(&bindings(), TEXT);
162 assert!(
163 sites.iter().any(|(_, decl)| *decl),
164 "has a definition token"
165 );
166 assert!(sites.iter().any(|(_, decl)| !*decl), "has a use token");
167 assert!(
169 sites.contains(&(Span { start: 26, end: 27 }, true)),
170 "x def is a declaration: {sites:?}"
171 );
172 }
173
174 #[test]
177 fn resolves_a_real_local_from_diagnose_project() {
178 let root = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
179 .join("../bynkc/tests/fixtures/inlay/clean/src");
180 let r = bynk_ide::diagnose_project(&root, &std::collections::HashMap::new());
181 let file = r
182 .files
183 .iter()
184 .find(|f| f.source_path.to_string_lossy().ends_with("util.bynk"))
185 .expect("util.bynk analysed");
186 let text = &file.text;
187 let locals = r
188 .locals
189 .iter()
190 .find(|(p, _)| p.to_string_lossy().ends_with("util.bynk"))
191 .map(|(_, l)| l.clone())
192 .expect("util.bynk locals");
193
194 let use_off = text.rfind("total").expect("total use");
196 let sites = local_sites_at(&locals, text, use_off).expect("on a local");
197 assert!(sites.len() >= 2, "def + use: {sites:?}");
198 let def = text.find("total").expect("total def");
200 assert_eq!(sites[0].start, def, "def first");
201 }
202}