Background
Tree-sitter's locals.scm query files define scope boundaries, local variable definitions, and references. This information is critical for accurate call resolution — knowing whether a function is locally defined (closure, nested function) vs module-level eliminates false positive CALLS edges.
Currently, our call resolver uses import-distance heuristics to disambiguate when multiple candidates match a call name. With scope information, we can resolve local calls exactly without heuristics.
Problem
Languages with heavy closure/nested function usage produce incorrect call edges:
def outer():
def helper():
pass
helper() # Should resolve to the local helper, not a module-level one
function createHandler() {
function validate() {}
validate(); // Should resolve locally
}
Without scope tracking, if another helper or validate exists elsewhere in the project, the resolver may pick the wrong one based on import distance.
Approach
Vendor nvim-treesitter locals.scm files (MIT licensed) for all 12 supported languages. nvim-treesitter is the only source with 100% coverage — the pip packages only ship locals.scm for 3 languages (JS, Lua, Scala).
What locals.scm captures
Three capture types defined per language:
@local.scope — block boundaries (function bodies, if blocks, loops, closures)
@local.definition — variable/function definitions within a scope
@local.reference — identifier references within a scope
Example from JavaScript's locals.scm:
[
(statement_block)
(function_expression)
(arrow_function)
(function_declaration)
(method_definition)
] @local.scope
(variable_declarator
name: (identifier) @local.definition)
(identifier) @local.reference
Implementation
- Create
codebase_rag/queries/locals/ directory with .scm files per language
- Build query loading infrastructure in
parser_loader.py to load .scm files alongside existing queries
- In
call_resolver.py, before falling back to trie-based resolution, check if the call name matches a @local.definition within the current @local.scope
- Add tests verifying scope-aware resolution for nested functions, closures, and block scopes
Languages
All 12: Python, JavaScript, TypeScript, Rust, Java, C, C++, Go, Lua, Scala, PHP, C#
References
Background
Tree-sitter's
locals.scmquery files define scope boundaries, local variable definitions, and references. This information is critical for accurate call resolution — knowing whether a function is locally defined (closure, nested function) vs module-level eliminates false positive CALLS edges.Currently, our call resolver uses import-distance heuristics to disambiguate when multiple candidates match a call name. With scope information, we can resolve local calls exactly without heuristics.
Problem
Languages with heavy closure/nested function usage produce incorrect call edges:
Without scope tracking, if another
helperorvalidateexists elsewhere in the project, the resolver may pick the wrong one based on import distance.Approach
Vendor nvim-treesitter
locals.scmfiles (MIT licensed) for all 12 supported languages. nvim-treesitter is the only source with 100% coverage — the pip packages only ship locals.scm for 3 languages (JS, Lua, Scala).What locals.scm captures
Three capture types defined per language:
@local.scope— block boundaries (function bodies, if blocks, loops, closures)@local.definition— variable/function definitions within a scope@local.reference— identifier references within a scopeExample from JavaScript's locals.scm:
Implementation
codebase_rag/queries/locals/directory with .scm files per languageparser_loader.pyto load .scm files alongside existing queriescall_resolver.py, before falling back to trie-based resolution, check if the call name matches a@local.definitionwithin the current@local.scopeLanguages
All 12: Python, JavaScript, TypeScript, Rust, Java, C, C++, Go, Lua, Scala, PHP, C#
References