fix(parser): name Elixir structs after their enclosing module

extractElixirStruct hardcoded Name="defstruct", so every Elixir struct surfaced as a symbol literally called "defstruct" — useless for navigation and colliding across modules. Walk ancestors to the enclosing defmodule and name the struct %ModuleName{}. Adds TestExtractElixirStructNamedByModule.
This commit is contained in:
2026-06-20 00:33:33 +01:00
parent 1b41e43bf3
commit 2e444401f5
2 changed files with 68 additions and 4 deletions
+39 -4
View File
@@ -681,17 +681,52 @@ func extractElixirMacro(n *sitter.Node, content []byte, filename string) *protoc
}
}
// extractElixirStruct extracts a struct definition.
// extractElixirStruct extracts a struct definition. A defstruct has no name of
// its own — it defines the struct of its enclosing module — so name it after
// that module (%ModuleName{}) rather than the useless literal "defstruct".
func extractElixirStruct(n *sitter.Node, content []byte, filename string) *protocol.Symbol {
// defstruct is typically inside a module, the struct name is the module name
// We just mark this as a struct symbol
name := "defstruct"
if mod := enclosingElixirModuleName(n, content); mod != "" {
name = "%" + mod + "{}"
}
return &protocol.Symbol{
Name: "defstruct",
Name: name,
Kind: protocol.SymbolStruct,
Location: NodeLocation(n, filename),
}
}
// enclosingElixirModuleName walks ancestors of n to the nearest defmodule call
// and returns its module alias (e.g. "MyApp.User"), or "" if none is found.
func enclosingElixirModuleName(n *sitter.Node, content []byte) string {
for p := n.Parent(); p != nil; p = p.Parent() {
if p.Type() != "call" || p.NamedChildCount() < 1 {
continue
}
target := p.NamedChild(0)
if target == nil || GetNodeText(target, content) != "defmodule" {
continue
}
args := p.ChildByFieldName("arguments")
if args == nil && p.NamedChildCount() >= 2 {
args = p.NamedChild(1)
}
if args == nil {
return ""
}
var moduleName string
WalkTree(args, func(node *sitter.Node) bool {
if node.Type() == "alias" {
moduleName = GetNodeText(node, content)
return false
}
return true
})
return moduleName
}
return ""
}
// extractElixirProtocol extracts a protocol definition.
func extractElixirProtocol(n *sitter.Node, content []byte, filename string) *protocol.Symbol {
// defprotocol ProtocolName do ... end
+29
View File
@@ -125,6 +125,35 @@ 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.
// An Elixir defstruct must be named after its enclosing module (%Mod{}),
// not the useless literal "defstruct".
func TestExtractElixirStructNamedByModule(t *testing.T) {
r := NewRegistry()
defer r.Close()
content := "defmodule MyApp.User do\n defstruct [:name, :email]\nend\n"
result, err := r.Parse(context.Background(), "user.ex", []byte(content))
if err != nil {
t.Fatalf("parse failed: %v", err)
}
symbols := ExtractSymbols(result.Tree, []byte(content), protocol.LangElixir, "user.ex")
var structName string
found := false
for _, s := range symbols {
if s.Kind == protocol.SymbolStruct {
structName = s.Name
found = true
}
}
if !found {
t.Fatalf("no struct symbol extracted, got: %+v", symbols)
}
if structName != "%MyApp.User{}" {
t.Errorf("struct name = %q, want %q", structName, "%MyApp.User{}")
}
}
func TestExtractGoVarConstMultiName(t *testing.T) {
r := NewRegistry()
defer r.Close()