Skip to content

feat: add scope tracking via nvim-treesitter locals.scm queries #523

Description

@vitali87

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

  1. Create codebase_rag/queries/locals/ directory with .scm files per language
  2. Build query loading infrastructure in parser_loader.py to load .scm files alongside existing queries
  3. 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
  4. 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

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Projects

    Status
    No status

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions