1use std::cell::RefCell;
10use std::collections::HashMap;
11use std::sync::OnceLock;
12
13use anyhow::{anyhow, Context, Result};
14use serde::Serialize;
15use streaming_iterator::StreamingIterator;
16use tree_sitter::{Language, Node, Parser, Query, QueryCursor, WasmStore};
17
18use crate::registry::{Grammar, Profile};
19
20#[derive(Debug, Serialize)]
22pub struct Symbol {
23 pub id: String,
25 pub name: String,
26 pub kind: String,
28 pub is_definition: bool,
29 pub file: String,
30 pub line: usize,
35 pub col: usize,
36 pub start_byte: usize,
38 pub end_byte: usize,
39 pub signature: String,
41 #[serde(skip_serializing_if = "Option::is_none")]
43 pub parent: Option<String>,
44}
45
46#[derive(Debug, Serialize)]
48pub struct Defect {
49 pub kind: &'static str,
50 pub line: usize,
52 pub col: usize,
53 pub start_byte: usize,
54 pub end_byte: usize,
55 pub text: String,
56}
57
58fn engine() -> &'static tree_sitter::wasmtime::Engine {
64 static E: OnceLock<tree_sitter::wasmtime::Engine> = OnceLock::new();
65 E.get_or_init(tree_sitter::wasmtime::Engine::default)
66}
67
68struct Loaded {
69 parser: Parser,
70 #[allow(dead_code)]
72 language: Language,
73 tags_query: Query,
74 capture_names: Vec<String>,
75 locals: Option<CapturedQuery>,
78 imports: Option<CapturedQuery>,
81}
82
83struct CapturedQuery {
85 query: Query,
86 capture_names: Vec<String>,
87}
88
89impl CapturedQuery {
90 fn compile(language: &Language, src: &str, what: &str) -> Result<CapturedQuery> {
91 let query = Query::new(language, src).with_context(|| format!("compiling {what} query"))?;
92 let capture_names = query.capture_names().iter().map(|s| s.to_string()).collect();
93 Ok(CapturedQuery { query, capture_names })
94 }
95
96 fn compile_optional(
103 language: &Language,
104 src: &Option<std::sync::Arc<String>>,
105 what: &str,
106 ) -> Option<CapturedQuery> {
107 let src = src.as_ref()?;
108 if has_supertype_pattern(src) {
113 eprintln!("grove: ignoring {what} query (unsupported supertype `(a/b)` syntax)");
114 return None;
115 }
116 match CapturedQuery::compile(language, src, what) {
117 Ok(q) => Some(q),
118 Err(e) => {
119 eprintln!("grove: ignoring invalid {what} query: {e:#}");
120 None
121 }
122 }
123 }
124}
125
126fn has_supertype_pattern(src: &str) -> bool {
131 let b = src.as_bytes();
132 let mut in_str = false;
133 let mut in_comment = false; let is_ident = |c: u8| c.is_ascii_alphanumeric() || c == b'_';
135 for i in 0..b.len() {
136 let c = b[i];
137 if in_comment {
138 if c == b'\n' {
139 in_comment = false;
140 }
141 continue;
142 }
143 match c {
144 b'"' if i == 0 || b[i - 1] != b'\\' => in_str = !in_str,
145 b';' if !in_str => in_comment = true,
146 b'/' if !in_str => {
147 let prev = i.checked_sub(1).map(|j| b[j]).unwrap_or(0);
148 let next = b.get(i + 1).copied().unwrap_or(0);
149 if is_ident(prev) && is_ident(next) {
150 return true;
151 }
152 }
153 _ => {}
154 }
155 }
156 false
157}
158
159impl Loaded {
160 fn load(g: &Grammar) -> Result<Loaded> {
161 let mut store = WasmStore::new(engine()).map_err(|e| anyhow!("wasm store: {e:?}"))?;
162 let language = store
163 .load_language(&g.name, &g.wasm)
164 .map_err(|e| anyhow!("loading `{}` grammar from wasm: {e:?}", g.name))?;
165 let tags_query =
166 Query::new(&language, &g.tags_query).context("compiling tags query")?;
167 let capture_names = tags_query
168 .capture_names()
169 .iter()
170 .map(|s| s.to_string())
171 .collect();
172 let locals = CapturedQuery::compile_optional(&language, &g.locals_query, "locals");
173 let imports = CapturedQuery::compile_optional(&language, &g.imports_query, "imports");
174 let mut parser = Parser::new();
175 parser
176 .set_wasm_store(store)
177 .map_err(|e| anyhow!("attaching wasm store: {e}"))?;
178 parser
179 .set_language(&language)
180 .map_err(|e| anyhow!("setting language: {e}"))?;
181 Ok(Loaded { parser, language, tags_query, capture_names, locals, imports })
182 }
183}
184
185thread_local! {
186 static CACHE: RefCell<HashMap<String, Loaded>> = RefCell::new(HashMap::new());
187}
188
189fn with_loaded<R>(g: &Grammar, f: impl FnOnce(&mut Loaded) -> Result<R>) -> Result<R> {
190 CACHE.with(|c| {
191 let mut map = c.borrow_mut();
192 if !map.contains_key(&g.name) {
193 let loaded = Loaded::load(g)?;
194 map.insert(g.name.clone(), loaded);
195 }
196 f(map.get_mut(&g.name).unwrap())
197 })
198}
199
200fn parse_source(parser: &mut Parser, source: &[u8]) -> Result<tree_sitter::Tree> {
205 #[cfg(test)]
206 parse_counter::bump();
207 parser.parse(source, None).context("parse produced no tree")
208}
209
210#[cfg(test)]
214pub mod parse_counter {
215 use std::cell::Cell;
216 thread_local! {
217 static COUNT: Cell<usize> = const { Cell::new(0) };
218 }
219 pub(super) fn bump() {
220 COUNT.with(|c| c.set(c.get() + 1));
221 }
222 pub fn reset() {
223 COUNT.with(|c| c.set(0));
224 }
225 pub fn get() -> usize {
226 COUNT.with(Cell::get)
227 }
228}
229
230fn symbol_id(lang: &str, rel: &str, name: &str, line: usize) -> String {
231 format!("{lang}:{rel}#{name}@{line}")
232}
233
234fn line_text(source: &[u8], byte: usize) -> String {
236 let start = source[..byte.min(source.len())]
237 .iter()
238 .rposition(|&b| b == b'\n')
239 .map_or(0, |i| i + 1);
240 let end = source[byte.min(source.len())..]
241 .iter()
242 .position(|&b| b == b'\n')
243 .map_or(source.len(), |i| byte + i);
244 String::from_utf8_lossy(&source[start..end]).trim().to_string()
245}
246
247pub fn extract(grammar: &Grammar, rel: &str, source: &[u8]) -> Result<Vec<Symbol>> {
249 extract_with_tree(grammar, rel, source).map(|(syms, _)| syms)
250}
251
252pub fn extract_with_tree(
256 grammar: &Grammar,
257 rel: &str,
258 source: &[u8],
259) -> Result<(Vec<Symbol>, tree_sitter::Tree)> {
260 with_loaded(grammar, |lg| {
261 let tree = parse_source(&mut lg.parser, source)?;
262 let mut cursor = QueryCursor::new();
263 let mut matches = cursor.matches(&lg.tags_query, tree.root_node(), source);
264
265 let mut out = Vec::new();
266 while let Some(m) = matches.next() {
267 let mut anchor: Option<(Node, String, bool)> = None;
268 let mut name_node: Option<Node> = None;
269 for cap in m.captures {
270 let cn = &lg.capture_names[cap.index as usize];
271 if let Some(kind) = cn.strip_prefix("definition.") {
272 anchor = Some((cap.node, kind.to_string(), true));
273 } else if let Some(kind) = cn.strip_prefix("reference.") {
274 anchor = Some((cap.node, kind.to_string(), false));
275 } else if cn == "name" {
276 name_node = Some(cap.node);
277 }
278 }
279 let Some((node, kind, is_definition)) = anchor else {
280 continue;
281 };
282 let nn = name_node.unwrap_or(node);
283 let name = nn.utf8_text(source).unwrap_or("").to_string();
284 if name.is_empty() {
285 continue;
286 }
287 let pos = nn.start_position();
288 let span = definition_span(node, &kind, &grammar.profile);
294 let line = pos.row + 1;
297 out.push(Symbol {
298 id: symbol_id(&grammar.name, rel, &name, line),
299 name,
300 kind,
301 is_definition,
302 file: rel.to_string(),
303 line,
304 col: pos.column + 1,
305 start_byte: span.start_byte(),
306 end_byte: span.end_byte(),
307 signature: line_text(source, nn.start_byte()),
308 parent: None,
309 });
310 }
311
312 let mut seen = std::collections::HashSet::new();
317 out.retain(|s| seen.insert((s.start_byte, s.end_byte, s.is_definition)));
318
319 let root = tree.root_node();
322 for s in &mut out {
323 s.parent = root
324 .descendant_for_byte_range(s.start_byte, s.end_byte)
325 .and_then(|def| def.parent())
326 .and_then(|p| nearest_container(p, source, &grammar.profile));
327 }
328 Ok((out, tree))
329 })
330}
331
332pub fn slice<'a>(source: &'a [u8], sym: &Symbol) -> &'a str {
334 std::str::from_utf8(&source[sym.start_byte..sym.end_byte]).unwrap_or("<non-utf8>")
335}
336
337pub fn check(grammar: &Grammar, source: &[u8]) -> Result<Vec<Defect>> {
339 with_loaded(grammar, |lg| {
340 let tree = parse_source(&mut lg.parser, source)?;
341 let mut defects = Vec::new();
342 collect_defects(tree.root_node(), source, &mut defects);
343 Ok(defects)
344 })
345}
346
347fn collect_defects(node: Node, source: &[u8], out: &mut Vec<Defect>) {
348 if node.is_error() || node.is_missing() {
349 let start = node.start_position();
350 out.push(Defect {
351 kind: if node.is_missing() { "missing" } else { "error" },
352 line: start.row + 1,
353 col: start.column + 1,
354 start_byte: node.start_byte(),
355 end_byte: node.end_byte(),
356 text: String::from_utf8_lossy(&source[node.byte_range()])
357 .chars()
358 .take(60)
359 .collect(),
360 });
361 }
362 let mut cursor = node.walk();
363 for child in node.children(&mut cursor) {
364 collect_defects(child, source, out);
365 }
366}
367
368fn definition_span<'a>(node: Node<'a>, kind: &str, profile: &Profile) -> Node<'a> {
378 if kind != "function" && kind != "method" {
379 return node;
380 }
381 let is_fn_kind = |n: &Node| profile.function_kinds.iter().any(|k| k.as_str() == n.kind());
382 if is_fn_kind(&node) {
383 return node;
384 }
385 let mut cur = node.parent();
386 while let Some(n) = cur {
387 if is_fn_kind(&n) {
388 return n;
389 }
390 cur = n.parent();
391 }
392 node
393}
394
395fn nearest_container(node: Node, source: &[u8], profile: &Profile) -> Option<String> {
401 let mut cur = Some(node);
402 while let Some(n) = cur {
403 for (kind, field) in &profile.containers {
404 if kind.as_str() == n.kind() {
405 if let Some(c) = n.child_by_field_name(field) {
406 let text = c.utf8_text(source).ok()?;
407 return Some(text.split('<').next().unwrap_or(text).trim().to_string());
408 }
409 }
410 }
411 cur = n.parent();
412 }
413 None
414}
415
416pub fn with_tree<R>(
419 grammar: &Grammar,
420 source: &[u8],
421 f: impl FnOnce(Node, &Profile) -> R,
422) -> Result<R> {
423 with_loaded(grammar, |lg| {
424 let tree = parse_source(&mut lg.parser, source)?;
425 Ok(f(tree.root_node(), &grammar.profile))
426 })
427}
428
429fn function_name(node: Node, source: &[u8], profile: &Profile) -> Option<String> {
434 if let Some(n) = node.child_by_field_name("name") {
435 return n.utf8_text(source).ok().map(str::to_string);
436 }
437 let mut cur = node.child_by_field_name("declarator")?;
438 loop {
439 if profile.identifier_kinds.iter().any(|k| k.as_str() == cur.kind()) {
440 return cur.utf8_text(source).ok().map(str::to_string);
441 }
442 cur = cur.child_by_field_name("declarator")?;
443 }
444}
445
446pub fn enclosing_function_at(
448 root: Node,
449 byte: usize,
450 source: &[u8],
451 profile: &Profile,
452) -> Option<String> {
453 let mut node = root.descendant_for_byte_range(byte, byte)?;
454 loop {
455 if profile.function_kinds.iter().any(|k| k.as_str() == node.kind()) {
456 let fname = function_name(node, source, profile)?;
457 let container = node
458 .parent()
459 .and_then(|p| nearest_container(p, source, profile));
460 return Some(match container {
461 Some(c) => format!("{c}::{fname}"),
462 None => fname,
463 });
464 }
465 node = node.parent()?;
466 }
467}
468
469pub fn identifier_at(
471 root: Node,
472 row: usize,
473 col: usize,
474 source: &[u8],
475 profile: &Profile,
476) -> Option<String> {
477 let point = tree_sitter::Point { row, column: col };
478 let node = root.descendant_for_point_range(point, point)?;
479 if profile.identifier_kinds.iter().any(|k| k.as_str() == node.kind()) {
480 node.utf8_text(source).ok().map(str::to_string)
481 } else {
482 None
483 }
484}
485
486fn local_symbol(grammar: &Grammar, rel: &str, name_node: Node, source: &[u8]) -> Symbol {
491 let pos = name_node.start_position();
492 let line = pos.row + 1;
493 let name = name_node.utf8_text(source).unwrap_or("").to_string();
494 let span = name_node.parent().unwrap_or(name_node);
495 Symbol {
496 id: symbol_id(&grammar.name, rel, &name, line),
497 name,
498 kind: "local".to_string(),
499 is_definition: true,
500 file: rel.to_string(),
501 line,
502 col: pos.column + 1,
503 start_byte: span.start_byte(),
504 end_byte: span.end_byte(),
505 signature: line_text(source, name_node.start_byte()),
506 parent: None,
507 }
508}
509
510pub fn resolve_local_at(
519 grammar: &Grammar,
520 rel: &str,
521 source: &[u8],
522 row: usize,
523 col: usize,
524) -> Result<Option<Symbol>> {
525 with_loaded(grammar, |lg| {
526 let Some(locals) = &lg.locals else {
527 return Ok(None);
528 };
529 let tree = parse_source(&mut lg.parser, source)?;
530 let root = tree.root_node();
531
532 let point = tree_sitter::Point { row, column: col };
533 let Some(ref_node) = root.descendant_for_point_range(point, point) else {
534 return Ok(None);
535 };
536 if !grammar
537 .profile
538 .identifier_kinds
539 .iter()
540 .any(|k| k.as_str() == ref_node.kind())
541 {
542 return Ok(None);
543 }
544 let name = ref_node.utf8_text(source).unwrap_or("");
545 if name.is_empty() {
546 return Ok(None);
547 }
548
549 let mut scopes: Vec<Node> = Vec::new();
551 let mut defs: Vec<Node> = Vec::new();
552 let mut cursor = QueryCursor::new();
553 let mut matches = cursor.matches(&locals.query, root, source);
554 while let Some(m) = matches.next() {
555 for cap in m.captures {
556 let cn = locals.capture_names[cap.index as usize].as_str();
559 if cn.starts_with("local.scope") {
560 scopes.push(cap.node);
561 } else if cn.starts_with("local.definition") {
562 defs.push(cap.node);
563 }
564 }
565 }
566
567 let (rs, re) = (ref_node.start_byte(), ref_node.end_byte());
569 let mut enclosing: Vec<Node> = scopes
570 .into_iter()
571 .filter(|s| s.start_byte() <= rs && s.end_byte() >= re)
572 .collect();
573 enclosing.sort_by_key(|s| s.end_byte() - s.start_byte());
574
575 for scope in &enclosing {
576 let hit = defs.iter().find(|d| {
577 d.start_byte() >= scope.start_byte()
578 && d.end_byte() <= scope.end_byte()
579 && d.utf8_text(source).map(|t| t == name).unwrap_or(false)
580 });
581 if let Some(d) = hit {
582 return Ok(Some(local_symbol(grammar, rel, *d, source)));
583 }
584 }
585 Ok(None)
586 })
587}
588
589#[derive(Debug, Clone, PartialEq, Eq)]
591pub struct ImportBinding {
592 pub name: String,
595 pub source: String,
598 pub module: String,
600}
601
602pub fn extract_imports(grammar: &Grammar, source: &[u8]) -> Result<Vec<ImportBinding>> {
606 with_loaded(grammar, |lg| {
607 let Some(imports) = &lg.imports else {
608 return Ok(Vec::new());
609 };
610 let tree = parse_source(&mut lg.parser, source)?;
611 let mut out = Vec::new();
612 let mut cursor = QueryCursor::new();
613 let mut matches = cursor.matches(&imports.query, tree.root_node(), source);
614 while let Some(m) = matches.next() {
615 let (mut name, mut src, mut module) = (None, None, None);
616 for cap in m.captures {
617 let text = cap.node.utf8_text(source).unwrap_or("").to_string();
618 match imports.capture_names[cap.index as usize].as_str() {
619 "import.name" => name = Some(text),
620 "import.source" => src = Some(text),
621 "import.module" => module = Some(text),
622 _ => {}
623 }
624 }
625 if let (Some(name), Some(module)) = (name, module) {
627 let source = src.unwrap_or_else(|| name.clone());
628 out.push(ImportBinding { name, source, module });
629 }
630 }
631 Ok(out)
632 })
633}
634
635#[cfg(test)]
636mod tests {
637 use super::*;
638 use crate::registry;
639
640 fn rust() -> Grammar {
641 registry::resolve("rust").expect("rust grammar (dev stub or cache)")
642 }
643
644 #[test]
649 fn reported_line_matches_grep_n_per_grammar() {
650 let cases: &[(&str, &str, &str)] = &[
653 ("rust", "// header\n\nfn target() {}\n", "target"),
654 ("python", "# header\n\ndef target():\n pass\n", "target"),
655 ("javascript", "// header\n\nfunction target() {}\n", "target"),
656 ];
657 for (lang, src, name) in cases {
658 let Ok(g) = registry::resolve(lang) else {
659 eprintln!("skipping {lang}: grammar not resolvable in this environment");
660 continue;
661 };
662 let want_line = src
663 .lines()
664 .position(|l| l.contains(&format!(" {name}")) || l.contains(&format!("{name}(")))
665 .map(|i| i + 1)
666 .expect("fixture contains the def");
667 let syms = extract(&g, &format!("demo.{lang}"), src.as_bytes()).unwrap();
668 let def = syms
669 .iter()
670 .find(|s| s.name == *name && s.is_definition)
671 .unwrap_or_else(|| panic!("{lang}: no def named {name}"));
672 assert_eq!(
673 def.line, want_line,
674 "{lang}: reported line {} != grep -n line {want_line}",
675 def.line
676 );
677 assert!(
679 def.id.ends_with(&format!("@{want_line}")),
680 "{lang}: id {} should end with @{want_line}",
681 def.id
682 );
683 }
684 }
685
686 #[test]
687 fn check_passes_clean_source() {
688 let defects = check(&rust(), b"fn main() {}\n").unwrap();
689 assert!(defects.is_empty(), "valid rust has no defects, got {defects:?}");
690 }
691
692 #[test]
693 fn check_reports_defects_on_broken_source() {
694 let defects = check(&rust(), b"fn main( {\n").unwrap();
696 assert!(!defects.is_empty(), "broken rust must report a defect");
697 assert!(defects.iter().all(|d| d.kind == "error" || d.kind == "missing"));
698 assert!(defects.iter().all(|d| d.end_byte >= d.start_byte));
699 }
700
701 #[test]
702 fn extract_finds_definitions_with_container_parent() {
703 let src = b"struct S;\nimpl S {\n fn method(&self) {}\n}\n";
704 let syms = extract(&rust(), "lib.rs", src).unwrap();
705 let m = syms
706 .iter()
707 .find(|s| s.name == "method" && s.is_definition)
708 .expect("method definition");
709 assert_eq!(m.parent.as_deref(), Some("S"), "method's container is impl S");
710 assert!(m.id.starts_with("rust:lib.rs#method@"), "stable id, got {}", m.id);
711 }
712
713 #[test]
714 fn rust_definition_span_covers_the_whole_body() {
715 let src = b"fn f() {\n let x = 1;\n x + 1\n}\n";
719 let syms = extract(&rust(), "lib.rs", src).unwrap();
720 let f = syms.iter().find(|s| s.name == "f" && s.is_definition).unwrap();
721 let body = slice(src, f);
722 assert!(body.starts_with("fn f()"), "starts at signature: {body:?}");
723 assert!(body.trim_end().ends_with('}'), "includes closing brace: {body:?}");
724 }
725
726 #[test]
727 fn c_function_definition_span_includes_the_body() {
728 let Ok(c) = registry::resolve("c") else {
734 eprintln!("skipping: C grammar not resolvable in this environment");
735 return;
736 };
737 let src = b"static int *get_thing(const char *s,\n int n)\n{\n\tint total = 0;\n\treturn &total;\n}\n";
738 let syms = extract(&c, "demo.c", src).unwrap();
739 let f = syms
740 .iter()
741 .find(|s| s.name == "get_thing" && s.is_definition)
742 .expect("get_thing definition");
743 let body = slice(src, f);
744 assert!(body.contains("int total = 0"), "body included: {body:?}");
745 assert!(body.contains("return &total"), "body included: {body:?}");
746 assert!(body.trim_end().ends_with('}'), "closing brace included: {body:?}");
747 assert!(body.starts_with("static int *"), "return type included: {body:?}");
750 assert_eq!(f.line, 1, "name on the first line (1-based)");
752 }
753
754 #[test]
755 fn c_callers_capture_calls_with_enclosing_function() {
756 let Ok(c) = registry::resolve("c") else {
761 eprintln!("skipping: C grammar not resolvable in this environment");
762 return;
763 };
764 let src = b"static int helper(int x) { return x + 1; }\nstatic int caller_one(void) { return helper(5); }\n";
765 let (syms, tree) = extract_with_tree(&c, "demo.c", src).unwrap();
766 let call = syms
767 .iter()
768 .find(|s| s.name == "helper" && !s.is_definition)
769 .expect("helper call captured as a reference");
770 assert_eq!(call.kind, "call", "call reference kind");
771 let enc = enclosing_function_at(tree.root_node(), call.start_byte, src, &c.profile);
772 assert_eq!(enc.as_deref(), Some("caller_one"), "enclosing fn resolved for C");
773 }
774
775 #[test]
776 fn extract_with_tree_returns_a_reusable_tree() {
777 let src = b"fn helper() {}\nfn caller() {\n helper();\n}\n";
778 let (syms, tree) = extract_with_tree(&rust(), "lib.rs", src).unwrap();
779 assert!(syms.iter().any(|s| s.name == "helper" && s.is_definition));
780 let call = syms.iter().find(|s| s.name == "helper" && !s.is_definition).unwrap();
782 let enc = enclosing_function_at(tree.root_node(), call.start_byte, src, &rust().profile);
783 assert_eq!(enc.as_deref(), Some("caller"));
784 }
785
786 #[test]
787 fn slice_returns_the_symbols_bytes() {
788 let src = b"fn only() { let x = 1; }\n";
789 let syms = extract(&rust(), "lib.rs", src).unwrap();
790 let f = syms.iter().find(|s| s.name == "only").unwrap();
791 let body = slice(src, f);
792 assert!(body.starts_with("fn only"));
793 assert!(body.contains("let x = 1"));
794 }
795
796 #[test]
797 fn identifier_at_resolves_the_name_under_the_cursor() {
798 let src = b"fn helper() {}\nfn caller() {\n helper();\n}\n";
799 let g = rust();
800 let name = with_tree(&g, src, |root, profile| {
801 identifier_at(root, 2, 4, src, profile)
803 })
804 .unwrap();
805 assert_eq!(name.as_deref(), Some("helper"));
806 }
807
808 #[test]
809 fn enclosing_function_at_qualifies_method_with_its_type() {
810 let src = b"struct S;\nimpl S {\n fn m(&self) {\n let _ = 1;\n }\n}\n";
811 let g = rust();
812 let needle = src.windows(9).position(|w| w == b"let _ = 1").unwrap();
814 let enc = with_tree(&g, src, |root, profile| {
815 enclosing_function_at(root, needle, src, profile)
816 })
817 .unwrap();
818 assert_eq!(enc.as_deref(), Some("S::m"), "method qualified by container type");
819 }
820
821 fn row_col(src: &str, byte: usize) -> (usize, usize) {
823 let before = &src[..byte];
824 let row = before.matches('\n').count();
825 let col = byte - before.rfind('\n').map_or(0, |i| i + 1);
826 (row, col)
827 }
828
829 fn rust_with_locals() -> Option<Grammar> {
835 let g = rust();
836 if g.locals_query.is_none() {
837 eprintln!("skipping: rust grammar resolved without locals.scm (non-dev-stub root)");
838 return None;
839 }
840 Some(g)
841 }
842
843 #[test]
844 fn resolve_local_prefers_the_shadowing_binding() {
845 let Some(g) = rust_with_locals() else { return };
846 let src = "fn run() {}\nfn caller() {\n let run = 1;\n let _x = run;\n}\n";
849 let use_byte = src.rfind("run").unwrap(); let (row, col) = row_col(src, use_byte);
851 let got = resolve_local_at(&g, "demo.rs", src.as_bytes(), row, col)
852 .unwrap()
853 .expect("a local binding should resolve");
854 assert_eq!(got.name, "run");
855 assert_eq!(got.kind, "local");
856 assert_eq!(got.line, 3, "must resolve to the local `let run`, not the global fn");
857 assert!(got.id.ends_with("@3"), "id carries the local's line, got {}", got.id);
858 }
859
860 #[test]
861 fn resolve_local_returns_none_for_a_global_reference() {
862 let Some(g) = rust_with_locals() else { return };
863 let src = "fn helper() {}\nfn caller() {\n helper();\n}\n";
866 let call_byte = src.rfind("helper").unwrap();
867 let (row, col) = row_col(src, call_byte);
868 let got = resolve_local_at(&g, "demo.rs", src.as_bytes(), row, col).unwrap();
869 assert!(got.is_none(), "a free/global name has no local binding, got {got:?}");
870 }
871
872 #[test]
873 fn resolve_local_resolves_a_parameter() {
874 let Some(g) = rust_with_locals() else { return };
875 let src = "fn f(x: i32) -> i32 {\n x + 1\n}\n";
877 let use_byte = src.rfind('x').unwrap(); let (row, col) = row_col(src, use_byte);
879 let got = resolve_local_at(&g, "demo.rs", src.as_bytes(), row, col)
880 .unwrap()
881 .expect("parameter should resolve");
882 assert_eq!(got.name, "x");
883 assert_eq!(got.line, 1, "parameter is declared on line 1");
884 }
885
886 #[test]
887 fn resolve_local_returns_none_off_an_identifier() {
888 let Some(g) = rust_with_locals() else { return };
889 let src = "fn f() {\n let y = 1;\n}\n";
891 let (row, col) = row_col(src, src.find('{').unwrap());
892 let got = resolve_local_at(&g, "demo.rs", src.as_bytes(), row, col).unwrap();
893 assert!(got.is_none());
894 }
895
896 fn lang_with_imports(lang: &str) -> Option<Grammar> {
900 let g = registry::resolve(lang).ok()?;
901 if g.imports_query.is_none() {
902 eprintln!("skipping {lang}: no imports.scm (non-dev-stub root)");
903 return None;
904 }
905 Some(g)
906 }
907
908 #[test]
909 fn extract_imports_python_named_and_aliased() {
910 let Some(g) = lang_with_imports("python") else { return };
911 let src = b"from pkg.util import helper\nfrom pkg.mod import thing as t\n";
912 let imps = extract_imports(&g, src).unwrap();
913 assert!(
914 imps.contains(&ImportBinding {
915 name: "helper".into(),
916 source: "helper".into(),
917 module: "pkg.util".into(),
918 }),
919 "named import: {imps:?}"
920 );
921 assert!(
922 imps.contains(&ImportBinding {
923 name: "t".into(),
924 source: "thing".into(),
925 module: "pkg.mod".into(),
926 }),
927 "aliased import binds the alias, sources the original: {imps:?}"
928 );
929 }
930
931 #[test]
932 fn extract_imports_javascript_named_and_aliased() {
933 let Some(g) = lang_with_imports("javascript") else { return };
934 let src = b"import { compute } from \"./calc\";\nimport { compute as c } from \"./calc\";\n";
935 let imps = extract_imports(&g, src).unwrap();
936 assert!(
937 imps.contains(&ImportBinding {
938 name: "compute".into(),
939 source: "compute".into(),
940 module: "./calc".into(),
941 }),
942 "named import: {imps:?}"
943 );
944 assert!(
945 imps.contains(&ImportBinding {
946 name: "c".into(),
947 source: "compute".into(),
948 module: "./calc".into(),
949 }),
950 "aliased import: {imps:?}"
951 );
952 }
953
954 #[test]
955 fn supertype_pattern_detected_outside_strings() {
956 assert!(has_supertype_pattern("(pattern/identifier) @local.definition"));
958 assert!(has_supertype_pattern("(expression/variable) @local.reference"));
959 assert!(!has_supertype_pattern("(identifier) @local.reference"));
961 assert!(!has_supertype_pattern("((identifier) @x (#match? @x \"a/b\"))"));
962 assert!(!has_supertype_pattern("(call function: (identifier) @name)"));
963 assert!(!has_supertype_pattern("; if/else\n(identifier) @local.reference"));
966 }
967
968 #[test]
969 fn extract_imports_empty_without_query() {
970 let imps = extract_imports(&rust(), b"use foo::bar;\n").unwrap();
972 assert!(imps.is_empty(), "rust has no imports query yet: {imps:?}");
973 }
974
975 #[test]
976 fn extract_dedups_overlapping_matches() {
977 let src = b"struct S;\nimpl S {\n fn a(&self) {}\n fn b(&self) {}\n}\n";
979 let syms = extract(&rust(), "lib.rs", src).unwrap();
980 let mut seen = std::collections::HashSet::new();
981 for s in &syms {
982 assert!(seen.insert((s.start_byte, s.end_byte, s.is_definition)), "duplicate: {s:?}");
983 }
984 }
985}
986