From 85a539b18b4ce8bba25217c6551cdd3f82730019 Mon Sep 17 00:00:00 2001 From: Lukasz Raczylo Date: Sat, 20 Jun 2026 00:04:36 +0100 Subject: [PATCH] fix(parser): exclude function-local var/const from symbols MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit extractGoSymbols/extractJSSymbols matched var/const/let at any tree depth, so every function-local var, const (Go) and let/const/var (JS/TS) was emitted as a top-level symbol — flooding include_ast/ast_query symbol lists with locals (inaccurate + token-wasteful). Skip declarations nested inside a function/block body (isNestedInBody); module-level and 'export const' declarations are still kept. Adds TestExtractSymbolsExcludesLocals. --- internal/parser/symbols.go | 31 ++++++++++++++++++++++ internal/parser/symbols_test.go | 46 +++++++++++++++++++++++++++++++++ 2 files changed, 77 insertions(+) diff --git a/internal/parser/symbols.go b/internal/parser/symbols.go index 94242ea..8ae4872 100644 --- a/internal/parser/symbols.go +++ b/internal/parser/symbols.go @@ -35,6 +35,28 @@ func ExtractSymbols(tree *sitter.Tree, content []byte, lang protocol.Language, f } // extractGoSymbols extracts symbols from Go code. +// goBodyTypes are the node types whose descendants are function-local (not +// package-level) declarations in Go. +var goBodyTypes = map[string]bool{"block": true, "func_literal": true} + +// jsBodyTypes are the node types whose descendants are function-local (not +// module/class-member) declarations in JS/TS. +var jsBodyTypes = map[string]bool{ + "statement_block": true, "function_declaration": true, "method_definition": true, + "arrow_function": true, "function_expression": true, "generator_function_declaration": true, +} + +// isNestedInBody reports whether n has an ancestor in bodyTypes, i.e. it is a +// function-local declaration rather than a top-level/module one. +func isNestedInBody(n *sitter.Node, bodyTypes map[string]bool) bool { + for p := n.Parent(); p != nil; p = p.Parent() { + if bodyTypes[p.Type()] { + return true + } + } + return false +} + func extractGoSymbols(root *sitter.Node, content []byte, filename string) []protocol.Symbol { var symbols []protocol.Symbol @@ -49,6 +71,10 @@ func extractGoSymbols(root *sitter.Node, content []byte, filename string) []prot case "type_declaration": symbol = extractGoType(n, content, filename) case "const_declaration", "var_declaration": + // Skip function-local var/const; only package-level ones are symbols. + if isNestedInBody(n, goBodyTypes) { + return true + } syms := extractGoVarConst(n, content, filename) symbols = append(symbols, syms...) return true @@ -190,6 +216,11 @@ func extractJSSymbols(root *sitter.Node, content []byte, filename string) []prot case "method_definition": symbol = extractJSMethod(n, content, filename) case "lexical_declaration", "variable_declaration": + // Skip function-local let/const/var; only module/top-level ones + // (including `export const`) are symbols. + if isNestedInBody(n, jsBodyTypes) { + return true + } syms := extractJSVariable(n, content, filename) symbols = append(symbols, syms...) return true diff --git a/internal/parser/symbols_test.go b/internal/parser/symbols_test.go index ed67e0b..8cd5550 100644 --- a/internal/parser/symbols_test.go +++ b/internal/parser/symbols_test.go @@ -125,6 +125,52 @@ var globalVar = "test" // Grouped var/const specs declare multiple names on one spec (var a, b = 1, 2); // every name must surface as its own symbol, not just the first. +// Function-local var/const declarations must NOT appear as top-level symbols +// (they flood the list and aren't package/module symbols). +func TestExtractSymbolsExcludesLocals(t *testing.T) { + r := NewRegistry() + defer r.Close() + ctx := context.Background() + + t.Run("go", func(t *testing.T) { + content := "package main\n\nvar TopLevel = 1\n\nfunc f() {\n\tvar localVar = 2\n\tconst localConst = 3\n\t_ = localVar\n\t_ = localConst\n}\n" + result, err := r.Parse(ctx, "m.go", []byte(content)) + if err != nil { + t.Fatalf("parse: %v", err) + } + names := symbolNameSet(ExtractSymbols(result.Tree, []byte(content), protocol.LangGo, "m.go")) + if !names["TopLevel"] { + t.Errorf("expected TopLevel symbol, got %v", names) + } + if names["localVar"] || names["localConst"] { + t.Errorf("function-local var/const leaked into symbols: %v", names) + } + }) + + t.Run("js exported and module consts kept, locals dropped", func(t *testing.T) { + content := "export const Exported = 1;\nconst ModuleConst = 2;\nfunction f() {\n const localOne = 3;\n return localOne;\n}\n" + result, err := r.Parse(ctx, "m.js", []byte(content)) + if err != nil { + t.Fatalf("parse: %v", err) + } + names := symbolNameSet(ExtractSymbols(result.Tree, []byte(content), protocol.LangJavaScript, "m.js")) + if !names["Exported"] || !names["ModuleConst"] { + t.Errorf("expected Exported and ModuleConst symbols, got %v", names) + } + if names["localOne"] { + t.Errorf("function-local const leaked into symbols: %v", names) + } + }) +} + +func symbolNameSet(syms []protocol.Symbol) map[string]bool { + m := make(map[string]bool, len(syms)) + for _, s := range syms { + m[s.Name] = true + } + return m +} + // An Elixir defstruct must be named after its enclosing module (%Mod{}), // not the useless literal "defstruct". func TestExtractElixirStructNamedByModule(t *testing.T) {