use std::cell::RefCell;
use std::collections::HashMap;
use std::sync::OnceLock;
use anyhow::{anyhow, Context, Result};
use serde::Serialize;
use streaming_iterator::StreamingIterator;
use tree_sitter::{Language, Node, Parser, Query, QueryCursor, WasmStore};
use crate::registry::{Grammar, Profile};
#[derive(Debug, Serialize)]
pub struct Symbol {
pub id: String,
pub name: String,
pub kind: String,
pub is_definition: bool,
pub file: String,
pub line: usize,
pub col: usize,
pub start_byte: usize,
pub end_byte: usize,
pub signature: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub parent: Option<String>,
}
#[derive(Debug, Serialize)]
pub struct Defect {
pub kind: &'static str,
pub line: usize,
pub col: usize,
pub start_byte: usize,
pub end_byte: usize,
pub text: String,
}
fn engine() -> &'static tree_sitter::wasmtime::Engine {
static E: OnceLock<tree_sitter::wasmtime::Engine> = OnceLock::new();
E.get_or_init(tree_sitter::wasmtime::Engine::default)
}
struct Loaded {
parser: Parser,
#[allow(dead_code)]
language: Language,
tags_query: Query,
capture_names: Vec<String>,
locals: Option<CapturedQuery>,
imports: Option<CapturedQuery>,
}
struct CapturedQuery {
query: Query,
capture_names: Vec<String>,
}
impl CapturedQuery {
fn compile(language: &Language, src: &str, what: &str) -> Result<CapturedQuery> {
let query = Query::new(language, src).with_context(|| format!("compiling {what} query"))?;
let capture_names = query.capture_names().iter().map(|s| s.to_string()).collect();
Ok(CapturedQuery { query, capture_names })
}
fn compile_optional(
language: &Language,
src: &Option<std::sync::Arc<String>>,
what: &str,
) -> Option<CapturedQuery> {
let src = src.as_ref()?;
if has_supertype_pattern(src) {
eprintln!("grove: ignoring {what} query (unsupported supertype `(a/b)` syntax)");
return None;
}
match CapturedQuery::compile(language, src, what) {
Ok(q) => Some(q),
Err(e) => {
eprintln!("grove: ignoring invalid {what} query: {e:#}");
None
}
}
}
}
fn has_supertype_pattern(src: &str) -> bool {
let b = src.as_bytes();
let mut in_str = false;
let mut in_comment = false; let is_ident = |c: u8| c.is_ascii_alphanumeric() || c == b'_';
for i in 0..b.len() {
let c = b[i];
if in_comment {
if c == b'\n' {
in_comment = false;
}
continue;
}
match c {
b'"' if i == 0 || b[i - 1] != b'\\' => in_str = !in_str,
b';' if !in_str => in_comment = true,
b'/' if !in_str => {
let prev = i.checked_sub(1).map(|j| b[j]).unwrap_or(0);
let next = b.get(i + 1).copied().unwrap_or(0);
if is_ident(prev) && is_ident(next) {
return true;
}
}
_ => {}
}
}
false
}
impl Loaded {
fn load(g: &Grammar) -> Result<Loaded> {
let mut store = WasmStore::new(engine()).map_err(|e| anyhow!("wasm store: {e:?}"))?;
let language = store
.load_language(&g.name, &g.wasm)
.map_err(|e| anyhow!("loading `{}` grammar from wasm: {e:?}", g.name))?;
let tags_query =
Query::new(&language, &g.tags_query).context("compiling tags query")?;
let capture_names = tags_query
.capture_names()
.iter()
.map(|s| s.to_string())
.collect();
let locals = CapturedQuery::compile_optional(&language, &g.locals_query, "locals");
let imports = CapturedQuery::compile_optional(&language, &g.imports_query, "imports");
let mut parser = Parser::new();
parser
.set_wasm_store(store)
.map_err(|e| anyhow!("attaching wasm store: {e}"))?;
parser
.set_language(&language)
.map_err(|e| anyhow!("setting language: {e}"))?;
Ok(Loaded { parser, language, tags_query, capture_names, locals, imports })
}
}
thread_local! {
static CACHE: RefCell<HashMap<String, Loaded>> = RefCell::new(HashMap::new());
}
fn with_loaded<R>(g: &Grammar, f: impl FnOnce(&mut Loaded) -> Result<R>) -> Result<R> {
CACHE.with(|c| {
let mut map = c.borrow_mut();
if !map.contains_key(&g.name) {
let loaded = Loaded::load(g)?;
map.insert(g.name.clone(), loaded);
}
f(map.get_mut(&g.name).unwrap())
})
}
fn parse_source(parser: &mut Parser, source: &[u8]) -> Result<tree_sitter::Tree> {
#[cfg(test)]
parse_counter::bump();
parser.parse(source, None).context("parse produced no tree")
}
#[cfg(test)]
pub mod parse_counter {
use std::cell::Cell;
thread_local! {
static COUNT: Cell<usize> = const { Cell::new(0) };
}
pub(super) fn bump() {
COUNT.with(|c| c.set(c.get() + 1));
}
pub fn reset() {
COUNT.with(|c| c.set(0));
}
pub fn get() -> usize {
COUNT.with(Cell::get)
}
}
fn symbol_id(lang: &str, rel: &str, name: &str, line: usize) -> String {
format!("{lang}:{rel}#{name}@{line}")
}
fn line_text(source: &[u8], byte: usize) -> String {
let start = source[..byte.min(source.len())]
.iter()
.rposition(|&b| b == b'\n')
.map_or(0, |i| i + 1);
let end = source[byte.min(source.len())..]
.iter()
.position(|&b| b == b'\n')
.map_or(source.len(), |i| byte + i);
String::from_utf8_lossy(&source[start..end]).trim().to_string()
}
pub fn extract(grammar: &Grammar, rel: &str, source: &[u8]) -> Result<Vec<Symbol>> {
extract_with_tree(grammar, rel, source).map(|(syms, _)| syms)
}
pub fn extract_with_tree(
grammar: &Grammar,
rel: &str,
source: &[u8],
) -> Result<(Vec<Symbol>, tree_sitter::Tree)> {
with_loaded(grammar, |lg| {
let tree = parse_source(&mut lg.parser, source)?;
let mut cursor = QueryCursor::new();
let mut matches = cursor.matches(&lg.tags_query, tree.root_node(), source);
let mut out = Vec::new();
while let Some(m) = matches.next() {
let mut anchor: Option<(Node, String, bool)> = None;
let mut name_node: Option<Node> = None;
for cap in m.captures {
let cn = &lg.capture_names[cap.index as usize];
if let Some(kind) = cn.strip_prefix("definition.") {
anchor = Some((cap.node, kind.to_string(), true));
} else if let Some(kind) = cn.strip_prefix("reference.") {
anchor = Some((cap.node, kind.to_string(), false));
} else if cn == "name" {
name_node = Some(cap.node);
}
}
let Some((node, kind, is_definition)) = anchor else {
continue;
};
let nn = name_node.unwrap_or(node);
let name = nn.utf8_text(source).unwrap_or("").to_string();
if name.is_empty() {
continue;
}
let pos = nn.start_position();
let span = definition_span(node, &kind, &grammar.profile);
let line = pos.row + 1;
out.push(Symbol {
id: symbol_id(&grammar.name, rel, &name, line),
name,
kind,
is_definition,
file: rel.to_string(),
line,
col: pos.column + 1,
start_byte: span.start_byte(),
end_byte: span.end_byte(),
signature: line_text(source, nn.start_byte()),
parent: None,
});
}
let mut seen = std::collections::HashSet::new();
out.retain(|s| seen.insert((s.start_byte, s.end_byte, s.is_definition)));
let root = tree.root_node();
for s in &mut out {
s.parent = root
.descendant_for_byte_range(s.start_byte, s.end_byte)
.and_then(|def| def.parent())
.and_then(|p| nearest_container(p, source, &grammar.profile));
}
Ok((out, tree))
})
}
pub fn slice<'a>(source: &'a [u8], sym: &Symbol) -> &'a str {
std::str::from_utf8(&source[sym.start_byte..sym.end_byte]).unwrap_or("<non-utf8>")
}
pub fn check(grammar: &Grammar, source: &[u8]) -> Result<Vec<Defect>> {
with_loaded(grammar, |lg| {
let tree = parse_source(&mut lg.parser, source)?;
let mut defects = Vec::new();
collect_defects(tree.root_node(), source, &mut defects);
Ok(defects)
})
}
fn collect_defects(node: Node, source: &[u8], out: &mut Vec<Defect>) {
if node.is_error() || node.is_missing() {
let start = node.start_position();
out.push(Defect {
kind: if node.is_missing() { "missing" } else { "error" },
line: start.row + 1,
col: start.column + 1,
start_byte: node.start_byte(),
end_byte: node.end_byte(),
text: String::from_utf8_lossy(&source[node.byte_range()])
.chars()
.take(60)
.collect(),
});
}
let mut cursor = node.walk();
for child in node.children(&mut cursor) {
collect_defects(child, source, out);
}
}
fn definition_span<'a>(node: Node<'a>, kind: &str, profile: &Profile) -> Node<'a> {
if kind != "function" && kind != "method" {
return node;
}
let is_fn_kind = |n: &Node| profile.function_kinds.iter().any(|k| k.as_str() == n.kind());
if is_fn_kind(&node) {
return node;
}
let mut cur = node.parent();
while let Some(n) = cur {
if is_fn_kind(&n) {
return n;
}
cur = n.parent();
}
node
}
fn nearest_container(node: Node, source: &[u8], profile: &Profile) -> Option<String> {
let mut cur = Some(node);
while let Some(n) = cur {
for (kind, field) in &profile.containers {
if kind.as_str() == n.kind() {
if let Some(c) = n.child_by_field_name(field) {
let text = c.utf8_text(source).ok()?;
return Some(text.split('<').next().unwrap_or(text).trim().to_string());
}
}
}
cur = n.parent();
}
None
}
pub fn with_tree<R>(
grammar: &Grammar,
source: &[u8],
f: impl FnOnce(Node, &Profile) -> R,
) -> Result<R> {
with_loaded(grammar, |lg| {
let tree = parse_source(&mut lg.parser, source)?;
Ok(f(tree.root_node(), &grammar.profile))
})
}
fn function_name(node: Node, source: &[u8], profile: &Profile) -> Option<String> {
if let Some(n) = node.child_by_field_name("name") {
return n.utf8_text(source).ok().map(str::to_string);
}
let mut cur = node.child_by_field_name("declarator")?;
loop {
if profile.identifier_kinds.iter().any(|k| k.as_str() == cur.kind()) {
return cur.utf8_text(source).ok().map(str::to_string);
}
cur = cur.child_by_field_name("declarator")?;
}
}
pub fn enclosing_function_at(
root: Node,
byte: usize,
source: &[u8],
profile: &Profile,
) -> Option<String> {
let mut node = root.descendant_for_byte_range(byte, byte)?;
loop {
if profile.function_kinds.iter().any(|k| k.as_str() == node.kind()) {
let fname = function_name(node, source, profile)?;
let container = node
.parent()
.and_then(|p| nearest_container(p, source, profile));
return Some(match container {
Some(c) => format!("{c}::{fname}"),
None => fname,
});
}
node = node.parent()?;
}
}
pub fn identifier_at(
root: Node,
row: usize,
col: usize,
source: &[u8],
profile: &Profile,
) -> Option<String> {
let point = tree_sitter::Point { row, column: col };
let node = root.descendant_for_point_range(point, point)?;
if profile.identifier_kinds.iter().any(|k| k.as_str() == node.kind()) {
node.utf8_text(source).ok().map(str::to_string)
} else {
None
}
}
fn local_symbol(grammar: &Grammar, rel: &str, name_node: Node, source: &[u8]) -> Symbol {
let pos = name_node.start_position();
let line = pos.row + 1;
let name = name_node.utf8_text(source).unwrap_or("").to_string();
let span = name_node.parent().unwrap_or(name_node);
Symbol {
id: symbol_id(&grammar.name, rel, &name, line),
name,
kind: "local".to_string(),
is_definition: true,
file: rel.to_string(),
line,
col: pos.column + 1,
start_byte: span.start_byte(),
end_byte: span.end_byte(),
signature: line_text(source, name_node.start_byte()),
parent: None,
}
}
pub fn resolve_local_at(
grammar: &Grammar,
rel: &str,
source: &[u8],
row: usize,
col: usize,
) -> Result<Option<Symbol>> {
with_loaded(grammar, |lg| {
let Some(locals) = &lg.locals else {
return Ok(None);
};
let tree = parse_source(&mut lg.parser, source)?;
let root = tree.root_node();
let point = tree_sitter::Point { row, column: col };
let Some(ref_node) = root.descendant_for_point_range(point, point) else {
return Ok(None);
};
if !grammar
.profile
.identifier_kinds
.iter()
.any(|k| k.as_str() == ref_node.kind())
{
return Ok(None);
}
let name = ref_node.utf8_text(source).unwrap_or("");
if name.is_empty() {
return Ok(None);
}
let mut scopes: Vec<Node> = Vec::new();
let mut defs: Vec<Node> = Vec::new();
let mut cursor = QueryCursor::new();
let mut matches = cursor.matches(&locals.query, root, source);
while let Some(m) = matches.next() {
for cap in m.captures {
let cn = locals.capture_names[cap.index as usize].as_str();
if cn.starts_with("local.scope") {
scopes.push(cap.node);
} else if cn.starts_with("local.definition") {
defs.push(cap.node);
}
}
}
let (rs, re) = (ref_node.start_byte(), ref_node.end_byte());
let mut enclosing: Vec<Node> = scopes
.into_iter()
.filter(|s| s.start_byte() <= rs && s.end_byte() >= re)
.collect();
enclosing.sort_by_key(|s| s.end_byte() - s.start_byte());
for scope in &enclosing {
let hit = defs.iter().find(|d| {
d.start_byte() >= scope.start_byte()
&& d.end_byte() <= scope.end_byte()
&& d.utf8_text(source).map(|t| t == name).unwrap_or(false)
});
if let Some(d) = hit {
return Ok(Some(local_symbol(grammar, rel, *d, source)));
}
}
Ok(None)
})
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ImportBinding {
pub name: String,
pub source: String,
pub module: String,
}
pub fn extract_imports(grammar: &Grammar, source: &[u8]) -> Result<Vec<ImportBinding>> {
with_loaded(grammar, |lg| {
let Some(imports) = &lg.imports else {
return Ok(Vec::new());
};
let tree = parse_source(&mut lg.parser, source)?;
let mut out = Vec::new();
let mut cursor = QueryCursor::new();
let mut matches = cursor.matches(&imports.query, tree.root_node(), source);
while let Some(m) = matches.next() {
let (mut name, mut src, mut module) = (None, None, None);
for cap in m.captures {
let text = cap.node.utf8_text(source).unwrap_or("").to_string();
match imports.capture_names[cap.index as usize].as_str() {
"import.name" => name = Some(text),
"import.source" => src = Some(text),
"import.module" => module = Some(text),
_ => {}
}
}
if let (Some(name), Some(module)) = (name, module) {
let source = src.unwrap_or_else(|| name.clone());
out.push(ImportBinding { name, source, module });
}
}
Ok(out)
})
}
#[cfg(test)]
mod tests {
use super::*;
use crate::registry;
fn rust() -> Grammar {
registry::resolve("rust").expect("rust grammar (dev stub or cache)")
}
#[test]
fn reported_line_matches_grep_n_per_grammar() {
let cases: &[(&str, &str, &str)] = &[
("rust", "// header\n\nfn target() {}\n", "target"),
("python", "# header\n\ndef target():\n pass\n", "target"),
("javascript", "// header\n\nfunction target() {}\n", "target"),
];
for (lang, src, name) in cases {
let Ok(g) = registry::resolve(lang) else {
eprintln!("skipping {lang}: grammar not resolvable in this environment");
continue;
};
let want_line = src
.lines()
.position(|l| l.contains(&format!(" {name}")) || l.contains(&format!("{name}(")))
.map(|i| i + 1)
.expect("fixture contains the def");
let syms = extract(&g, &format!("demo.{lang}"), src.as_bytes()).unwrap();
let def = syms
.iter()
.find(|s| s.name == *name && s.is_definition)
.unwrap_or_else(|| panic!("{lang}: no def named {name}"));
assert_eq!(
def.line, want_line,
"{lang}: reported line {} != grep -n line {want_line}",
def.line
);
assert!(
def.id.ends_with(&format!("@{want_line}")),
"{lang}: id {} should end with @{want_line}",
def.id
);
}
}
#[test]
fn check_passes_clean_source() {
let defects = check(&rust(), b"fn main() {}\n").unwrap();
assert!(defects.is_empty(), "valid rust has no defects, got {defects:?}");
}
#[test]
fn check_reports_defects_on_broken_source() {
let defects = check(&rust(), b"fn main( {\n").unwrap();
assert!(!defects.is_empty(), "broken rust must report a defect");
assert!(defects.iter().all(|d| d.kind == "error" || d.kind == "missing"));
assert!(defects.iter().all(|d| d.end_byte >= d.start_byte));
}
#[test]
fn extract_finds_definitions_with_container_parent() {
let src = b"struct S;\nimpl S {\n fn method(&self) {}\n}\n";
let syms = extract(&rust(), "lib.rs", src).unwrap();
let m = syms
.iter()
.find(|s| s.name == "method" && s.is_definition)
.expect("method definition");
assert_eq!(m.parent.as_deref(), Some("S"), "method's container is impl S");
assert!(m.id.starts_with("rust:lib.rs#method@"), "stable id, got {}", m.id);
}
#[test]
fn rust_definition_span_covers_the_whole_body() {
let src = b"fn f() {\n let x = 1;\n x + 1\n}\n";
let syms = extract(&rust(), "lib.rs", src).unwrap();
let f = syms.iter().find(|s| s.name == "f" && s.is_definition).unwrap();
let body = slice(src, f);
assert!(body.starts_with("fn f()"), "starts at signature: {body:?}");
assert!(body.trim_end().ends_with('}'), "includes closing brace: {body:?}");
}
#[test]
fn c_function_definition_span_includes_the_body() {
let Ok(c) = registry::resolve("c") else {
eprintln!("skipping: C grammar not resolvable in this environment");
return;
};
let src = b"static int *get_thing(const char *s,\n int n)\n{\n\tint total = 0;\n\treturn &total;\n}\n";
let syms = extract(&c, "demo.c", src).unwrap();
let f = syms
.iter()
.find(|s| s.name == "get_thing" && s.is_definition)
.expect("get_thing definition");
let body = slice(src, f);
assert!(body.contains("int total = 0"), "body included: {body:?}");
assert!(body.contains("return &total"), "body included: {body:?}");
assert!(body.trim_end().ends_with('}'), "closing brace included: {body:?}");
assert!(body.starts_with("static int *"), "return type included: {body:?}");
assert_eq!(f.line, 1, "name on the first line (1-based)");
}
#[test]
fn c_callers_capture_calls_with_enclosing_function() {
let Ok(c) = registry::resolve("c") else {
eprintln!("skipping: C grammar not resolvable in this environment");
return;
};
let src = b"static int helper(int x) { return x + 1; }\nstatic int caller_one(void) { return helper(5); }\n";
let (syms, tree) = extract_with_tree(&c, "demo.c", src).unwrap();
let call = syms
.iter()
.find(|s| s.name == "helper" && !s.is_definition)
.expect("helper call captured as a reference");
assert_eq!(call.kind, "call", "call reference kind");
let enc = enclosing_function_at(tree.root_node(), call.start_byte, src, &c.profile);
assert_eq!(enc.as_deref(), Some("caller_one"), "enclosing fn resolved for C");
}
#[test]
fn extract_with_tree_returns_a_reusable_tree() {
let src = b"fn helper() {}\nfn caller() {\n helper();\n}\n";
let (syms, tree) = extract_with_tree(&rust(), "lib.rs", src).unwrap();
assert!(syms.iter().any(|s| s.name == "helper" && s.is_definition));
let call = syms.iter().find(|s| s.name == "helper" && !s.is_definition).unwrap();
let enc = enclosing_function_at(tree.root_node(), call.start_byte, src, &rust().profile);
assert_eq!(enc.as_deref(), Some("caller"));
}
#[test]
fn slice_returns_the_symbols_bytes() {
let src = b"fn only() { let x = 1; }\n";
let syms = extract(&rust(), "lib.rs", src).unwrap();
let f = syms.iter().find(|s| s.name == "only").unwrap();
let body = slice(src, f);
assert!(body.starts_with("fn only"));
assert!(body.contains("let x = 1"));
}
#[test]
fn identifier_at_resolves_the_name_under_the_cursor() {
let src = b"fn helper() {}\nfn caller() {\n helper();\n}\n";
let g = rust();
let name = with_tree(&g, src, |root, profile| {
identifier_at(root, 2, 4, src, profile)
})
.unwrap();
assert_eq!(name.as_deref(), Some("helper"));
}
#[test]
fn enclosing_function_at_qualifies_method_with_its_type() {
let src = b"struct S;\nimpl S {\n fn m(&self) {\n let _ = 1;\n }\n}\n";
let g = rust();
let needle = src.windows(9).position(|w| w == b"let _ = 1").unwrap();
let enc = with_tree(&g, src, |root, profile| {
enclosing_function_at(root, needle, src, profile)
})
.unwrap();
assert_eq!(enc.as_deref(), Some("S::m"), "method qualified by container type");
}
fn row_col(src: &str, byte: usize) -> (usize, usize) {
let before = &src[..byte];
let row = before.matches('\n').count();
let col = byte - before.rfind('\n').map_or(0, |i| i + 1);
(row, col)
}
fn rust_with_locals() -> Option<Grammar> {
let g = rust();
if g.locals_query.is_none() {
eprintln!("skipping: rust grammar resolved without locals.scm (non-dev-stub root)");
return None;
}
Some(g)
}
#[test]
fn resolve_local_prefers_the_shadowing_binding() {
let Some(g) = rust_with_locals() else { return };
let src = "fn run() {}\nfn caller() {\n let run = 1;\n let _x = run;\n}\n";
let use_byte = src.rfind("run").unwrap(); let (row, col) = row_col(src, use_byte);
let got = resolve_local_at(&g, "demo.rs", src.as_bytes(), row, col)
.unwrap()
.expect("a local binding should resolve");
assert_eq!(got.name, "run");
assert_eq!(got.kind, "local");
assert_eq!(got.line, 3, "must resolve to the local `let run`, not the global fn");
assert!(got.id.ends_with("@3"), "id carries the local's line, got {}", got.id);
}
#[test]
fn resolve_local_returns_none_for_a_global_reference() {
let Some(g) = rust_with_locals() else { return };
let src = "fn helper() {}\nfn caller() {\n helper();\n}\n";
let call_byte = src.rfind("helper").unwrap();
let (row, col) = row_col(src, call_byte);
let got = resolve_local_at(&g, "demo.rs", src.as_bytes(), row, col).unwrap();
assert!(got.is_none(), "a free/global name has no local binding, got {got:?}");
}
#[test]
fn resolve_local_resolves_a_parameter() {
let Some(g) = rust_with_locals() else { return };
let src = "fn f(x: i32) -> i32 {\n x + 1\n}\n";
let use_byte = src.rfind('x').unwrap(); let (row, col) = row_col(src, use_byte);
let got = resolve_local_at(&g, "demo.rs", src.as_bytes(), row, col)
.unwrap()
.expect("parameter should resolve");
assert_eq!(got.name, "x");
assert_eq!(got.line, 1, "parameter is declared on line 1");
}
#[test]
fn resolve_local_returns_none_off_an_identifier() {
let Some(g) = rust_with_locals() else { return };
let src = "fn f() {\n let y = 1;\n}\n";
let (row, col) = row_col(src, src.find('{').unwrap());
let got = resolve_local_at(&g, "demo.rs", src.as_bytes(), row, col).unwrap();
assert!(got.is_none());
}
fn lang_with_imports(lang: &str) -> Option<Grammar> {
let g = registry::resolve(lang).ok()?;
if g.imports_query.is_none() {
eprintln!("skipping {lang}: no imports.scm (non-dev-stub root)");
return None;
}
Some(g)
}
#[test]
fn extract_imports_python_named_and_aliased() {
let Some(g) = lang_with_imports("python") else { return };
let src = b"from pkg.util import helper\nfrom pkg.mod import thing as t\n";
let imps = extract_imports(&g, src).unwrap();
assert!(
imps.contains(&ImportBinding {
name: "helper".into(),
source: "helper".into(),
module: "pkg.util".into(),
}),
"named import: {imps:?}"
);
assert!(
imps.contains(&ImportBinding {
name: "t".into(),
source: "thing".into(),
module: "pkg.mod".into(),
}),
"aliased import binds the alias, sources the original: {imps:?}"
);
}
#[test]
fn extract_imports_javascript_named_and_aliased() {
let Some(g) = lang_with_imports("javascript") else { return };
let src = b"import { compute } from \"./calc\";\nimport { compute as c } from \"./calc\";\n";
let imps = extract_imports(&g, src).unwrap();
assert!(
imps.contains(&ImportBinding {
name: "compute".into(),
source: "compute".into(),
module: "./calc".into(),
}),
"named import: {imps:?}"
);
assert!(
imps.contains(&ImportBinding {
name: "c".into(),
source: "compute".into(),
module: "./calc".into(),
}),
"aliased import: {imps:?}"
);
}
#[test]
fn supertype_pattern_detected_outside_strings() {
assert!(has_supertype_pattern("(pattern/identifier) @local.definition"));
assert!(has_supertype_pattern("(expression/variable) @local.reference"));
assert!(!has_supertype_pattern("(identifier) @local.reference"));
assert!(!has_supertype_pattern("((identifier) @x (#match? @x \"a/b\"))"));
assert!(!has_supertype_pattern("(call function: (identifier) @name)"));
assert!(!has_supertype_pattern("; if/else\n(identifier) @local.reference"));
}
#[test]
fn extract_imports_empty_without_query() {
let imps = extract_imports(&rust(), b"use foo::bar;\n").unwrap();
assert!(imps.is_empty(), "rust has no imports query yet: {imps:?}");
}
#[test]
fn extract_dedups_overlapping_matches() {
let src = b"struct S;\nimpl S {\n fn a(&self) {}\n fn b(&self) {}\n}\n";
let syms = extract(&rust(), "lib.rs", src).unwrap();
let mut seen = std::collections::HashSet::new();
for s in &syms {
assert!(seen.insert((s.start_byte, s.end_byte, s.is_definition)), "duplicate: {s:?}");
}
}
}