From 99514ff8d64eb6eac701d1b32092c9828a15818a Mon Sep 17 00:00:00 2001 From: Lukasz Raczylo Date: Sat, 20 Jun 2026 00:54:27 +0100 Subject: [PATCH] fix(parser): extract C++ in-class methods correctly extractCFunction/extractCFunctionDecl scanned for the first 'identifier' in the declarator, which for C++ methods grabbed a parameter name (e.g. method setName surfaced as 'n') or, when the name was a field_identifier with no parameter identifier, dropped the symbol entirely. cDeclaratorName now descends the declarator field-chain to the real name (identifier/field_identifier/qualified_identifier/destructor/operator); in-class definitions are kinded as methods and qualified Class::method (out-of-line Class::method definitions detected via '::'). Adds TestExtractCppMethods. --- internal/parser/symbols.go | 80 ++++++++++++++++++++++++--------- internal/parser/symbols_test.go | 30 +++++++++++++ 2 files changed, 89 insertions(+), 21 deletions(-) diff --git a/internal/parser/symbols.go b/internal/parser/symbols.go index 8ae4872..18f0eff 100644 --- a/internal/parser/symbols.go +++ b/internal/parser/symbols.go @@ -1,6 +1,8 @@ package parser import ( + "strings" + "github.com/lukaszraczylo/mcp-filepuff/pkg/protocol" sitter "github.com/smacker/go-tree-sitter" ) @@ -425,29 +427,65 @@ func extractCSymbols(root *sitter.Node, content []byte, filename string) []proto return symbols } +// cDeclaratorName descends the declarator field-chain (pointer/reference/array/ +// function declarators) to the innermost name node and returns its text. Unlike a +// first-identifier scan, this returns the function/method NAME (not a parameter) +// and handles C++ method names (field_identifier), out-of-line definitions +// (qualified_identifier like User::foo), destructors and operators. +func cDeclaratorName(declarator *sitter.Node, content []byte) string { + node := declarator + for node != nil { + switch node.Type() { + case "identifier", "field_identifier", "qualified_identifier", + "destructor_name", "operator_name": + return GetNodeText(node, content) + } + next := node.ChildByFieldName("declarator") + if next == nil { + return "" + } + node = next + } + return "" +} + +// enclosingCppClassName returns the name of the nearest class/struct ancestor of +// n (for qualifying in-class method names), or "" if n is not inside one. +func enclosingCppClassName(n *sitter.Node, content []byte) string { + for p := n.Parent(); p != nil; p = p.Parent() { + if p.Type() == "class_specifier" || p.Type() == "struct_specifier" { + if name := p.ChildByFieldName("name"); name != nil { + return GetNodeText(name, content) + } + } + } + return "" +} + func extractCFunction(n *sitter.Node, content []byte, filename string) *protocol.Symbol { declarator := n.ChildByFieldName("declarator") if declarator == nil { return nil } - // Find the function name within the declarator - var name string - WalkTree(declarator, func(node *sitter.Node) bool { - if node.Type() == "identifier" { - name = GetNodeText(node, content) - return false - } - return true - }) - + name := cDeclaratorName(declarator, content) if name == "" { return nil } + // In-class definitions are methods; qualify the name with the class. Out-of-line + // definitions already carry a qualified name (Class::method). + kind := protocol.SymbolFunction + if cls := enclosingCppClassName(n, content); cls != "" { + kind = protocol.SymbolMethod + name = cls + "::" + name + } else if strings.Contains(name, "::") { + kind = protocol.SymbolMethod + } + return &protocol.Symbol{ Name: name, - Kind: protocol.SymbolFunction, + Kind: kind, Location: NodeLocation(n, filename), } } @@ -484,22 +522,22 @@ func extractCFunctionDecl(n *sitter.Node, content []byte, filename string) *prot return nil } - var name string - WalkTree(declarator, func(node *sitter.Node) bool { - if node.Type() == "identifier" { - name = GetNodeText(node, content) - return false - } - return true - }) - + name := cDeclaratorName(declarator, content) if name == "" { return nil } + kind := protocol.SymbolFunction + if cls := enclosingCppClassName(n, content); cls != "" { + kind = protocol.SymbolMethod + name = cls + "::" + name + } else if strings.Contains(name, "::") { + kind = protocol.SymbolMethod + } + return &protocol.Symbol{ Name: name, - Kind: protocol.SymbolFunction, + Kind: kind, Location: NodeLocation(n, filename), } } diff --git a/internal/parser/symbols_test.go b/internal/parser/symbols_test.go index 8cd5550..b355f53 100644 --- a/internal/parser/symbols_test.go +++ b/internal/parser/symbols_test.go @@ -125,6 +125,36 @@ 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. +// C++ in-class methods must be extracted as methods qualified by their class +// (Class::method), not misnamed after a parameter or dropped entirely. +func TestExtractCppMethods(t *testing.T) { + r := NewRegistry() + defer r.Close() + content := "class User {\npublic:\n void setName(const char* n) { name_ = n; }\n int id() const { return id_; }\nprivate:\n const char* name_;\n int id_;\n};\n\nvoid freeFunc() {}\n" + result, err := r.Parse(context.Background(), "u.cpp", []byte(content)) + if err != nil { + t.Fatalf("parse: %v", err) + } + syms := ExtractSymbols(result.Tree, []byte(content), protocol.LangCpp, "u.cpp") + + kindByName := make(map[string]protocol.SymbolKind, len(syms)) + for _, s := range syms { + kindByName[s.Name] = s.Kind + } + if kindByName["User::setName"] != protocol.SymbolMethod { + t.Errorf("setName: want method User::setName, got %v", kindByName) + } + if kindByName["User::id"] != protocol.SymbolMethod { + t.Errorf("id: want method User::id, got %v", kindByName) + } + if kindByName["freeFunc"] != protocol.SymbolFunction { + t.Errorf("freeFunc: want function, got %v", kindByName) + } + if _, leaked := kindByName["n"]; leaked { + t.Errorf("parameter 'n' leaked as a symbol: %v", kindByName) + } +} + // 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) {