Scope analysis engine using tree-sitter locals queries.
Parses locals.scm query files to resolve symbol references to their
definitions within a single source file. Uses the tree-sitter locals
convention:
@local.scope— marks a node that creates a new lexical scope@local.definition/@local.definition.*— marks a name-binding site@local.reference— marks an identifier that refers to a bound name
Handling destructuring patterns: @local.definition.each + @local.binding-leaf
Tree-sitter queries only match direct children ((A (B)) requires B to be
a named child of A). Arbitrarily nested destructuring ({ a: { b } })
can't be expressed in a finite set of fixed-depth query patterns.
This engine supports two extension captures that work together:
@local.binding-leaf— declares which node kinds count as binding identifiers in this language. The engine collects these kinds from all matches in the query pass.@local.definition.each— captures a container node (e.g. a pattern or parameter node) and triggers recursive descent, emitting a definition for every descendant leaf whose kind is in the@local.binding-leafset.
Example (javascript.locals.scm):
; Declare binding leaf kinds for this language
(identifier) @local.binding-leaf
(shorthand_property_identifier_pattern) @local.binding-leaf
; Recurse into each parameter child — handles f(x), f({ a: { b } }), f([x, y])
(formal_parameters (_) @local.definition.each)
The engine has no hardcoded knowledge of which node kinds are bindings in any
given language — that belongs entirely in the .scm file.
Usage
use normalize_scope::ScopeEngine;
use normalize_languages::GrammarLoader;
let loader = GrammarLoader::new();
let engine = ScopeEngine::new(&loader);
let refs = engine.find_references("javascript", source, "myVar");
for r in refs {
println!("{}:{} -> def at {:?}", r.location.line, r.location.column, r.definition);
}