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.
This commit is contained in:
2026-06-20 00:54:27 +01:00
parent 2fb9d3e379
commit 99514ff8d6
2 changed files with 89 additions and 21 deletions
+59 -21
View File
@@ -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),
}
}
+30
View File
@@ -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) {