diff --git a/internal/parser/symbols.go b/internal/parser/symbols.go index 819e86b..94242ea 100644 --- a/internal/parser/symbols.go +++ b/internal/parser/symbols.go @@ -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 diff --git a/internal/parser/symbols_test.go b/internal/parser/symbols_test.go index fed45dc..ed67e0b 100644 --- a/internal/parser/symbols_test.go +++ b/internal/parser/symbols_test.go @@ -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()