mirror of
https://github.com/lukaszraczylo/filepuff-mcp.git
synced 2026-10-12 04:54:27 +00:00
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.
1094 lines
29 KiB
Go
1094 lines
29 KiB
Go
package parser
|
|
|
|
import (
|
|
"strings"
|
|
|
|
"github.com/lukaszraczylo/mcp-filepuff/pkg/protocol"
|
|
sitter "github.com/smacker/go-tree-sitter"
|
|
)
|
|
|
|
// ExtractSymbols extracts symbols from a parsed tree.
|
|
func ExtractSymbols(tree *sitter.Tree, content []byte, lang protocol.Language, filename string) []protocol.Symbol {
|
|
if tree == nil {
|
|
return nil
|
|
}
|
|
|
|
root := tree.RootNode()
|
|
if root == nil {
|
|
return nil
|
|
}
|
|
|
|
switch lang {
|
|
case protocol.LangGo:
|
|
return extractGoSymbols(root, content, filename)
|
|
case protocol.LangTypeScript, protocol.LangJavaScript:
|
|
return extractJSSymbols(root, content, filename)
|
|
case protocol.LangPython:
|
|
return extractPythonSymbols(root, content, filename)
|
|
case protocol.LangC, protocol.LangCpp:
|
|
return extractCSymbols(root, content, filename)
|
|
case protocol.LangElixir:
|
|
return extractElixirSymbols(root, content, filename)
|
|
case protocol.LangRust:
|
|
return extractRustSymbols(root, content, filename)
|
|
default:
|
|
return nil
|
|
}
|
|
}
|
|
|
|
// 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
|
|
|
|
WalkTree(root, func(n *sitter.Node) bool {
|
|
var symbol *protocol.Symbol
|
|
|
|
switch n.Type() {
|
|
case "function_declaration":
|
|
symbol = extractGoFunction(n, content, filename)
|
|
case "method_declaration":
|
|
symbol = extractGoMethod(n, content, filename)
|
|
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
|
|
}
|
|
|
|
if symbol != nil {
|
|
if doc := ExtractDocComment(n, content, protocol.LangGo); doc != nil {
|
|
symbol.Doc = FormatDocComment(doc)
|
|
}
|
|
symbols = append(symbols, *symbol)
|
|
}
|
|
|
|
return true
|
|
})
|
|
|
|
return symbols
|
|
}
|
|
|
|
func extractGoFunction(n *sitter.Node, content []byte, filename string) *protocol.Symbol {
|
|
nameNode := n.ChildByFieldName("name")
|
|
if nameNode == nil {
|
|
return nil
|
|
}
|
|
|
|
return &protocol.Symbol{
|
|
Name: GetNodeText(nameNode, content),
|
|
Kind: protocol.SymbolFunction,
|
|
Location: NodeLocation(n, filename),
|
|
}
|
|
}
|
|
|
|
func extractGoMethod(n *sitter.Node, content []byte, filename string) *protocol.Symbol {
|
|
nameNode := n.ChildByFieldName("name")
|
|
if nameNode == nil {
|
|
return nil
|
|
}
|
|
|
|
// Get receiver type
|
|
receiver := n.ChildByFieldName("receiver")
|
|
receiverType := ""
|
|
if receiver != nil {
|
|
// Find the type in the receiver
|
|
WalkTree(receiver, func(node *sitter.Node) bool {
|
|
if node.Type() == "type_identifier" {
|
|
receiverType = GetNodeText(node, content)
|
|
return false
|
|
}
|
|
return true
|
|
})
|
|
}
|
|
|
|
name := GetNodeText(nameNode, content)
|
|
if receiverType != "" {
|
|
name = "(" + receiverType + ")." + name
|
|
}
|
|
|
|
return &protocol.Symbol{
|
|
Name: name,
|
|
Kind: protocol.SymbolMethod,
|
|
Location: NodeLocation(n, filename),
|
|
}
|
|
}
|
|
|
|
func extractGoType(n *sitter.Node, content []byte, filename string) *protocol.Symbol {
|
|
// Find type_spec child
|
|
for i := 0; i < int(n.NamedChildCount()); i++ {
|
|
child := n.NamedChild(i)
|
|
if child != nil && child.Type() == "type_spec" {
|
|
nameNode := child.ChildByFieldName("name")
|
|
if nameNode == nil {
|
|
continue
|
|
}
|
|
|
|
kind := protocol.SymbolType
|
|
typeNode := child.ChildByFieldName("type")
|
|
if typeNode != nil {
|
|
switch typeNode.Type() {
|
|
case "struct_type":
|
|
kind = protocol.SymbolStruct
|
|
case "interface_type":
|
|
kind = protocol.SymbolInterface
|
|
}
|
|
}
|
|
|
|
return &protocol.Symbol{
|
|
Name: GetNodeText(nameNode, content),
|
|
Kind: kind,
|
|
Location: NodeLocation(child, filename),
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func extractGoVarConst(n *sitter.Node, content []byte, filename string) []protocol.Symbol {
|
|
var symbols []protocol.Symbol
|
|
kind := protocol.SymbolVariable
|
|
if n.Type() == "const_declaration" {
|
|
kind = protocol.SymbolConstant
|
|
}
|
|
|
|
WalkTree(n, func(node *sitter.Node) bool {
|
|
if node.Type() == "const_spec" || node.Type() == "var_spec" {
|
|
// A spec may declare multiple names (e.g. var a, b = 1, 2);
|
|
// ChildByFieldName returns only the first, so iterate all "name" fields.
|
|
for i := 0; i < int(node.ChildCount()); i++ {
|
|
if node.FieldNameForChild(i) != "name" {
|
|
continue
|
|
}
|
|
nameNode := node.Child(i)
|
|
if nameNode == nil {
|
|
continue
|
|
}
|
|
symbols = append(symbols, protocol.Symbol{
|
|
Name: GetNodeText(nameNode, content),
|
|
Kind: kind,
|
|
Location: NodeLocation(nameNode, filename),
|
|
})
|
|
}
|
|
}
|
|
return true
|
|
})
|
|
|
|
return symbols
|
|
}
|
|
|
|
// extractJSSymbols extracts symbols from JavaScript/TypeScript code.
|
|
func extractJSSymbols(root *sitter.Node, content []byte, filename string) []protocol.Symbol {
|
|
var symbols []protocol.Symbol
|
|
|
|
WalkTree(root, func(n *sitter.Node) bool {
|
|
var symbol *protocol.Symbol
|
|
|
|
switch n.Type() {
|
|
case "function_declaration":
|
|
symbol = extractJSFunction(n, content, filename)
|
|
case "class_declaration":
|
|
symbol = extractJSClass(n, content, filename)
|
|
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
|
|
case "interface_declaration":
|
|
symbol = extractTSInterface(n, content, filename)
|
|
case "type_alias_declaration":
|
|
symbol = extractTSTypeAlias(n, content, filename)
|
|
}
|
|
|
|
if symbol != nil {
|
|
if doc := ExtractDocComment(n, content, protocol.LangJavaScript); doc != nil {
|
|
symbol.Doc = FormatDocComment(doc)
|
|
}
|
|
symbols = append(symbols, *symbol)
|
|
}
|
|
|
|
return true
|
|
})
|
|
|
|
return symbols
|
|
}
|
|
|
|
func extractJSFunction(n *sitter.Node, content []byte, filename string) *protocol.Symbol {
|
|
nameNode := n.ChildByFieldName("name")
|
|
if nameNode == nil {
|
|
return nil
|
|
}
|
|
|
|
return &protocol.Symbol{
|
|
Name: GetNodeText(nameNode, content),
|
|
Kind: protocol.SymbolFunction,
|
|
Location: NodeLocation(n, filename),
|
|
}
|
|
}
|
|
|
|
func extractJSClass(n *sitter.Node, content []byte, filename string) *protocol.Symbol {
|
|
nameNode := n.ChildByFieldName("name")
|
|
if nameNode == nil {
|
|
return nil
|
|
}
|
|
|
|
return &protocol.Symbol{
|
|
Name: GetNodeText(nameNode, content),
|
|
Kind: protocol.SymbolClass,
|
|
Location: NodeLocation(n, filename),
|
|
}
|
|
}
|
|
|
|
func extractJSMethod(n *sitter.Node, content []byte, filename string) *protocol.Symbol {
|
|
nameNode := n.ChildByFieldName("name")
|
|
if nameNode == nil {
|
|
return nil
|
|
}
|
|
|
|
return &protocol.Symbol{
|
|
Name: GetNodeText(nameNode, content),
|
|
Kind: protocol.SymbolMethod,
|
|
Location: NodeLocation(n, filename),
|
|
}
|
|
}
|
|
|
|
func extractJSVariable(n *sitter.Node, content []byte, filename string) []protocol.Symbol {
|
|
var symbols []protocol.Symbol
|
|
|
|
WalkTree(n, func(node *sitter.Node) bool {
|
|
if node.Type() == "variable_declarator" {
|
|
nameNode := node.ChildByFieldName("name")
|
|
if nameNode != nil {
|
|
symbols = append(symbols, protocol.Symbol{
|
|
Name: GetNodeText(nameNode, content),
|
|
Kind: protocol.SymbolVariable,
|
|
Location: NodeLocation(node, filename),
|
|
})
|
|
}
|
|
}
|
|
return true
|
|
})
|
|
|
|
return symbols
|
|
}
|
|
|
|
func extractTSInterface(n *sitter.Node, content []byte, filename string) *protocol.Symbol {
|
|
nameNode := n.ChildByFieldName("name")
|
|
if nameNode == nil {
|
|
return nil
|
|
}
|
|
|
|
return &protocol.Symbol{
|
|
Name: GetNodeText(nameNode, content),
|
|
Kind: protocol.SymbolInterface,
|
|
Location: NodeLocation(n, filename),
|
|
}
|
|
}
|
|
|
|
func extractTSTypeAlias(n *sitter.Node, content []byte, filename string) *protocol.Symbol {
|
|
nameNode := n.ChildByFieldName("name")
|
|
if nameNode == nil {
|
|
return nil
|
|
}
|
|
|
|
return &protocol.Symbol{
|
|
Name: GetNodeText(nameNode, content),
|
|
Kind: protocol.SymbolType,
|
|
Location: NodeLocation(n, filename),
|
|
}
|
|
}
|
|
|
|
// extractPythonSymbols extracts symbols from Python code.
|
|
func extractPythonSymbols(root *sitter.Node, content []byte, filename string) []protocol.Symbol {
|
|
var symbols []protocol.Symbol
|
|
|
|
WalkTree(root, func(n *sitter.Node) bool {
|
|
var symbol *protocol.Symbol
|
|
|
|
switch n.Type() {
|
|
case "function_definition":
|
|
symbol = extractPythonFunction(n, content, filename)
|
|
case "class_definition":
|
|
symbol = extractPythonClass(n, content, filename)
|
|
}
|
|
|
|
if symbol != nil {
|
|
if doc := ExtractDocComment(n, content, protocol.LangPython); doc != nil {
|
|
symbol.Doc = FormatDocComment(doc)
|
|
}
|
|
symbols = append(symbols, *symbol)
|
|
}
|
|
|
|
return true
|
|
})
|
|
|
|
return symbols
|
|
}
|
|
|
|
func extractPythonFunction(n *sitter.Node, content []byte, filename string) *protocol.Symbol {
|
|
nameNode := n.ChildByFieldName("name")
|
|
if nameNode == nil {
|
|
return nil
|
|
}
|
|
|
|
// Check if this is a method (inside a class)
|
|
parent := n.Parent()
|
|
kind := protocol.SymbolFunction
|
|
if parent != nil && parent.Type() == "block" {
|
|
grandparent := parent.Parent()
|
|
if grandparent != nil && grandparent.Type() == "class_definition" {
|
|
kind = protocol.SymbolMethod
|
|
}
|
|
}
|
|
|
|
return &protocol.Symbol{
|
|
Name: GetNodeText(nameNode, content),
|
|
Kind: kind,
|
|
Location: NodeLocation(n, filename),
|
|
}
|
|
}
|
|
|
|
func extractPythonClass(n *sitter.Node, content []byte, filename string) *protocol.Symbol {
|
|
nameNode := n.ChildByFieldName("name")
|
|
if nameNode == nil {
|
|
return nil
|
|
}
|
|
|
|
return &protocol.Symbol{
|
|
Name: GetNodeText(nameNode, content),
|
|
Kind: protocol.SymbolClass,
|
|
Location: NodeLocation(n, filename),
|
|
}
|
|
}
|
|
|
|
// extractCSymbols extracts symbols from C/C++ code.
|
|
func extractCSymbols(root *sitter.Node, content []byte, filename string) []protocol.Symbol {
|
|
var symbols []protocol.Symbol
|
|
|
|
WalkTree(root, func(n *sitter.Node) bool {
|
|
var symbol *protocol.Symbol
|
|
|
|
switch n.Type() {
|
|
case "function_definition":
|
|
symbol = extractCFunction(n, content, filename)
|
|
case "struct_specifier":
|
|
symbol = extractCStruct(n, content, filename)
|
|
case "class_specifier":
|
|
symbol = extractCppClass(n, content, filename)
|
|
case "declaration":
|
|
// Could be function declaration or variable
|
|
if hasFunctionDeclarator(n) {
|
|
symbol = extractCFunctionDecl(n, content, filename)
|
|
}
|
|
}
|
|
|
|
if symbol != nil {
|
|
if doc := ExtractDocComment(n, content, protocol.LangC); doc != nil {
|
|
symbol.Doc = FormatDocComment(doc)
|
|
}
|
|
symbols = append(symbols, *symbol)
|
|
}
|
|
|
|
return true
|
|
})
|
|
|
|
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
|
|
}
|
|
|
|
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: kind,
|
|
Location: NodeLocation(n, filename),
|
|
}
|
|
}
|
|
|
|
func extractCStruct(n *sitter.Node, content []byte, filename string) *protocol.Symbol {
|
|
nameNode := n.ChildByFieldName("name")
|
|
if nameNode == nil {
|
|
return nil
|
|
}
|
|
|
|
return &protocol.Symbol{
|
|
Name: GetNodeText(nameNode, content),
|
|
Kind: protocol.SymbolStruct,
|
|
Location: NodeLocation(n, filename),
|
|
}
|
|
}
|
|
|
|
func extractCppClass(n *sitter.Node, content []byte, filename string) *protocol.Symbol {
|
|
nameNode := n.ChildByFieldName("name")
|
|
if nameNode == nil {
|
|
return nil
|
|
}
|
|
|
|
return &protocol.Symbol{
|
|
Name: GetNodeText(nameNode, content),
|
|
Kind: protocol.SymbolClass,
|
|
Location: NodeLocation(n, filename),
|
|
}
|
|
}
|
|
|
|
func extractCFunctionDecl(n *sitter.Node, content []byte, filename string) *protocol.Symbol {
|
|
declarator := n.ChildByFieldName("declarator")
|
|
if declarator == nil {
|
|
return nil
|
|
}
|
|
|
|
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: kind,
|
|
Location: NodeLocation(n, filename),
|
|
}
|
|
}
|
|
|
|
func hasFunctionDeclarator(n *sitter.Node) bool {
|
|
found := false
|
|
WalkTree(n, func(node *sitter.Node) bool {
|
|
if node.Type() == "function_declarator" {
|
|
found = true
|
|
return false
|
|
}
|
|
return true
|
|
})
|
|
return found
|
|
}
|
|
|
|
// extractElixirSymbols extracts symbols from Elixir code.
|
|
// Elixir uses `defmodule` for modules, `def`/`defp` for functions, and `defmacro`/`defmacrop` for macros.
|
|
func extractElixirSymbols(root *sitter.Node, content []byte, filename string) []protocol.Symbol {
|
|
var symbols []protocol.Symbol
|
|
|
|
WalkTree(root, func(n *sitter.Node) bool {
|
|
var symbol *protocol.Symbol
|
|
|
|
switch n.Type() {
|
|
case "call":
|
|
symbol = extractElixirCall(n, content, filename)
|
|
}
|
|
|
|
if symbol != nil {
|
|
if doc := ExtractDocComment(n, content, protocol.LangElixir); doc != nil {
|
|
symbol.Doc = FormatDocComment(doc)
|
|
}
|
|
symbols = append(symbols, *symbol)
|
|
}
|
|
|
|
return true
|
|
})
|
|
|
|
return symbols
|
|
}
|
|
|
|
// extractElixirCall extracts symbols from Elixir call nodes (def, defp, defmodule, defmacro, etc.).
|
|
func extractElixirCall(n *sitter.Node, content []byte, filename string) *protocol.Symbol {
|
|
// Get the function being called (first child is usually the target)
|
|
if n.NamedChildCount() < 1 {
|
|
return nil
|
|
}
|
|
|
|
target := n.NamedChild(0)
|
|
if target == nil {
|
|
return nil
|
|
}
|
|
|
|
targetText := GetNodeText(target, content)
|
|
|
|
switch targetText {
|
|
case "defmodule":
|
|
return extractElixirModule(n, content, filename)
|
|
case "def", "defp":
|
|
return extractElixirFunction(n, content, filename, targetText == "defp")
|
|
case "defmacro", "defmacrop":
|
|
return extractElixirMacro(n, content, filename)
|
|
case "defstruct":
|
|
return extractElixirStruct(n, content, filename)
|
|
case "defprotocol":
|
|
return extractElixirProtocol(n, content, filename)
|
|
case "defimpl":
|
|
return extractElixirImpl(n, content, filename)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// extractElixirModule extracts a module definition.
|
|
func extractElixirModule(n *sitter.Node, content []byte, filename string) *protocol.Symbol {
|
|
// defmodule ModuleName do ... end
|
|
// The module name is in the arguments
|
|
args := n.ChildByFieldName("arguments")
|
|
if args == nil {
|
|
// Try finding it as the second named child
|
|
if n.NamedChildCount() >= 2 {
|
|
args = n.NamedChild(1)
|
|
}
|
|
}
|
|
if args == nil {
|
|
return nil
|
|
}
|
|
|
|
// Find the alias (module name) in the arguments
|
|
var moduleName string
|
|
WalkTree(args, func(node *sitter.Node) bool {
|
|
if node.Type() == "alias" {
|
|
moduleName = GetNodeText(node, content)
|
|
return false
|
|
}
|
|
return true
|
|
})
|
|
|
|
if moduleName == "" {
|
|
return nil
|
|
}
|
|
|
|
return &protocol.Symbol{
|
|
Name: moduleName,
|
|
Kind: protocol.SymbolModule,
|
|
Location: NodeLocation(n, filename),
|
|
}
|
|
}
|
|
|
|
// extractElixirFunction extracts a function definition.
|
|
func extractElixirFunction(n *sitter.Node, content []byte, filename string, isPrivate bool) *protocol.Symbol {
|
|
// def function_name(args) do ... end
|
|
// The function name and args are in the arguments of the call
|
|
if n.NamedChildCount() < 2 {
|
|
return nil
|
|
}
|
|
|
|
// Second child contains the function definition
|
|
funcDef := n.NamedChild(1)
|
|
if funcDef == nil {
|
|
return nil
|
|
}
|
|
|
|
var funcName string
|
|
|
|
// The function definition can be:
|
|
// 1. A call node (function with args): func_name(arg1, arg2)
|
|
// 2. An identifier (function without args): func_name
|
|
switch funcDef.Type() {
|
|
case "call":
|
|
// Get the function name from the call target
|
|
if funcDef.NamedChildCount() >= 1 {
|
|
nameNode := funcDef.NamedChild(0)
|
|
if nameNode != nil {
|
|
funcName = GetNodeText(nameNode, content)
|
|
}
|
|
}
|
|
case "identifier":
|
|
funcName = GetNodeText(funcDef, content)
|
|
case "binary_operator":
|
|
// Guard clause: def func_name(args) when guard do ... end
|
|
// The left side contains the actual function call
|
|
WalkTree(funcDef, func(node *sitter.Node) bool {
|
|
if node.Type() == "call" && node.NamedChildCount() >= 1 {
|
|
nameNode := node.NamedChild(0)
|
|
if nameNode != nil && nameNode.Type() == "identifier" {
|
|
funcName = GetNodeText(nameNode, content)
|
|
return false
|
|
}
|
|
}
|
|
if node.Type() == "identifier" && funcName == "" {
|
|
funcName = GetNodeText(node, content)
|
|
return false
|
|
}
|
|
return true
|
|
})
|
|
}
|
|
|
|
if funcName == "" {
|
|
return nil
|
|
}
|
|
|
|
kind := protocol.SymbolFunction
|
|
if isPrivate {
|
|
funcName = funcName + " (private)"
|
|
}
|
|
|
|
return &protocol.Symbol{
|
|
Name: funcName,
|
|
Kind: kind,
|
|
Location: NodeLocation(n, filename),
|
|
}
|
|
}
|
|
|
|
// extractElixirMacro extracts a macro definition.
|
|
func extractElixirMacro(n *sitter.Node, content []byte, filename string) *protocol.Symbol {
|
|
// Similar to function extraction
|
|
if n.NamedChildCount() < 2 {
|
|
return nil
|
|
}
|
|
|
|
funcDef := n.NamedChild(1)
|
|
if funcDef == nil {
|
|
return nil
|
|
}
|
|
|
|
var macroName string
|
|
|
|
switch funcDef.Type() {
|
|
case "call":
|
|
if funcDef.NamedChildCount() >= 1 {
|
|
nameNode := funcDef.NamedChild(0)
|
|
if nameNode != nil {
|
|
macroName = GetNodeText(nameNode, content)
|
|
}
|
|
}
|
|
case "identifier":
|
|
macroName = GetNodeText(funcDef, content)
|
|
}
|
|
|
|
if macroName == "" {
|
|
return nil
|
|
}
|
|
|
|
return &protocol.Symbol{
|
|
Name: macroName + " (macro)",
|
|
Kind: protocol.SymbolFunction,
|
|
Location: NodeLocation(n, filename),
|
|
}
|
|
}
|
|
|
|
// 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 {
|
|
name := "defstruct"
|
|
if mod := enclosingElixirModuleName(n, content); mod != "" {
|
|
name = "%" + mod + "{}"
|
|
}
|
|
return &protocol.Symbol{
|
|
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
|
|
if n.NamedChildCount() < 2 {
|
|
return nil
|
|
}
|
|
|
|
args := n.NamedChild(1)
|
|
if args == nil {
|
|
return nil
|
|
}
|
|
|
|
var protocolName string
|
|
WalkTree(args, func(node *sitter.Node) bool {
|
|
if node.Type() == "alias" {
|
|
protocolName = GetNodeText(node, content)
|
|
return false
|
|
}
|
|
return true
|
|
})
|
|
|
|
if protocolName == "" {
|
|
return nil
|
|
}
|
|
|
|
return &protocol.Symbol{
|
|
Name: protocolName,
|
|
Kind: protocol.SymbolInterface,
|
|
Location: NodeLocation(n, filename),
|
|
}
|
|
}
|
|
|
|
// extractRustSymbols extracts symbols from Rust code.
|
|
func extractRustSymbols(root *sitter.Node, content []byte, filename string) []protocol.Symbol {
|
|
var symbols []protocol.Symbol
|
|
|
|
WalkTree(root, func(n *sitter.Node) bool {
|
|
var symbol *protocol.Symbol
|
|
|
|
switch n.Type() {
|
|
case "function_item":
|
|
nameNode := n.ChildByFieldName("name")
|
|
if nameNode != nil {
|
|
symbol = &protocol.Symbol{
|
|
Name: GetNodeText(nameNode, content),
|
|
Kind: protocol.SymbolFunction,
|
|
Location: NodeLocation(n, filename),
|
|
}
|
|
}
|
|
case "struct_item":
|
|
nameNode := n.ChildByFieldName("name")
|
|
if nameNode != nil {
|
|
symbol = &protocol.Symbol{
|
|
Name: GetNodeText(nameNode, content),
|
|
Kind: protocol.SymbolStruct,
|
|
Location: NodeLocation(n, filename),
|
|
}
|
|
}
|
|
case "enum_item":
|
|
nameNode := n.ChildByFieldName("name")
|
|
if nameNode != nil {
|
|
symbol = &protocol.Symbol{
|
|
Name: GetNodeText(nameNode, content),
|
|
Kind: protocol.SymbolEnum,
|
|
Location: NodeLocation(n, filename),
|
|
}
|
|
}
|
|
case "trait_item":
|
|
nameNode := n.ChildByFieldName("name")
|
|
if nameNode != nil {
|
|
symbol = &protocol.Symbol{
|
|
Name: GetNodeText(nameNode, content),
|
|
Kind: protocol.SymbolTrait,
|
|
Location: NodeLocation(n, filename),
|
|
}
|
|
}
|
|
case "impl_item":
|
|
symbol = extractRustImpl(n, content, filename)
|
|
case "type_item":
|
|
nameNode := n.ChildByFieldName("name")
|
|
if nameNode != nil {
|
|
symbol = &protocol.Symbol{
|
|
Name: GetNodeText(nameNode, content),
|
|
Kind: protocol.SymbolType,
|
|
Location: NodeLocation(n, filename),
|
|
}
|
|
}
|
|
case "const_item":
|
|
nameNode := n.ChildByFieldName("name")
|
|
if nameNode != nil {
|
|
symbol = &protocol.Symbol{
|
|
Name: GetNodeText(nameNode, content),
|
|
Kind: protocol.SymbolConstant,
|
|
Location: NodeLocation(n, filename),
|
|
}
|
|
}
|
|
case "static_item":
|
|
nameNode := n.ChildByFieldName("name")
|
|
if nameNode != nil {
|
|
symbol = &protocol.Symbol{
|
|
Name: GetNodeText(nameNode, content),
|
|
Kind: protocol.SymbolVariable,
|
|
Location: NodeLocation(n, filename),
|
|
}
|
|
}
|
|
case "macro_definition":
|
|
nameNode := n.ChildByFieldName("name")
|
|
if nameNode != nil {
|
|
symbol = &protocol.Symbol{
|
|
Name: GetNodeText(nameNode, content) + " (macro)",
|
|
Kind: protocol.SymbolFunction,
|
|
Location: NodeLocation(n, filename),
|
|
}
|
|
}
|
|
case "mod_item":
|
|
nameNode := n.ChildByFieldName("name")
|
|
if nameNode != nil {
|
|
symbol = &protocol.Symbol{
|
|
Name: GetNodeText(nameNode, content),
|
|
Kind: protocol.SymbolModule,
|
|
Location: NodeLocation(n, filename),
|
|
}
|
|
}
|
|
}
|
|
|
|
if symbol != nil {
|
|
if doc := ExtractDocComment(n, content, protocol.LangRust); doc != nil {
|
|
symbol.Doc = FormatDocComment(doc)
|
|
}
|
|
symbols = append(symbols, *symbol)
|
|
}
|
|
|
|
return true
|
|
})
|
|
|
|
return symbols
|
|
}
|
|
|
|
// extractRustImpl extracts an impl block symbol from Rust code.
|
|
func extractRustImpl(n *sitter.Node, content []byte, filename string) *protocol.Symbol {
|
|
typeNode := n.ChildByFieldName("type")
|
|
traitNode := n.ChildByFieldName("trait")
|
|
|
|
var name string
|
|
if traitNode != nil && typeNode != nil {
|
|
name = "impl " + GetNodeText(traitNode, content) + " for " + GetNodeText(typeNode, content)
|
|
} else if typeNode != nil {
|
|
name = "impl " + GetNodeText(typeNode, content)
|
|
} else {
|
|
return nil
|
|
}
|
|
|
|
return &protocol.Symbol{
|
|
Name: name,
|
|
Kind: protocol.SymbolType,
|
|
Location: NodeLocation(n, filename),
|
|
}
|
|
}
|
|
|
|
// extractElixirImpl extracts a protocol implementation.
|
|
func extractElixirImpl(n *sitter.Node, content []byte, filename string) *protocol.Symbol {
|
|
// defimpl Protocol, for: Type do ... end
|
|
if n.NamedChildCount() < 2 {
|
|
return nil
|
|
}
|
|
|
|
args := n.NamedChild(1)
|
|
if args == nil {
|
|
return nil
|
|
}
|
|
|
|
var implName string
|
|
WalkTree(args, func(node *sitter.Node) bool {
|
|
if node.Type() == "alias" {
|
|
if implName == "" {
|
|
implName = GetNodeText(node, content)
|
|
} else {
|
|
implName = implName + " for " + GetNodeText(node, content)
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
})
|
|
|
|
if implName == "" {
|
|
return nil
|
|
}
|
|
|
|
return &protocol.Symbol{
|
|
Name: implName,
|
|
Kind: protocol.SymbolClass,
|
|
Location: NodeLocation(n, filename),
|
|
}
|
|
}
|
|
|
|
// kindMatchesNode returns true if the given SymbolKind matches the node type.
|
|
// An empty kind matches all symbol-bearing nodes.
|
|
func kindMatchesNode(kind protocol.SymbolKind, nodeType string) bool {
|
|
if kind == "" {
|
|
return true
|
|
}
|
|
switch kind {
|
|
case protocol.SymbolFunction:
|
|
return nodeType == "function_declaration" || nodeType == "function_definition" || nodeType == "function_item"
|
|
case protocol.SymbolMethod:
|
|
return nodeType == "method_declaration" || nodeType == "method_definition"
|
|
case protocol.SymbolClass:
|
|
return nodeType == "class_declaration" || nodeType == "class_definition"
|
|
case protocol.SymbolStruct:
|
|
return nodeType == "struct_item" || nodeType == "type_declaration"
|
|
case protocol.SymbolInterface:
|
|
return nodeType == "interface_declaration"
|
|
case protocol.SymbolType:
|
|
return nodeType == "type_declaration" || nodeType == "type_alias_declaration" || nodeType == "type_item"
|
|
case protocol.SymbolEnum:
|
|
return nodeType == "enum_item"
|
|
case protocol.SymbolTrait:
|
|
return nodeType == "trait_item"
|
|
case protocol.SymbolConstant:
|
|
return nodeType == "const_item"
|
|
case protocol.SymbolModule:
|
|
return nodeType == "mod_item"
|
|
}
|
|
return true
|
|
}
|
|
|
|
// SymbolCandidate is one symbol matching a name (and optional kind) lookup,
|
|
// with its declaration node type and 1-indexed inclusive line range. NameLine
|
|
// and NameColumn point at the name identifier itself (1-indexed), which is the
|
|
// position LSP queries (hover/definition/references) expect.
|
|
type SymbolCandidate struct {
|
|
NodeType string
|
|
StartLine int
|
|
EndLine int
|
|
NameLine int
|
|
NameColumn int
|
|
}
|
|
|
|
// symbolDeclNodeTypes is the set of declaration node types FindSymbolCandidates
|
|
// treats as nameable symbols across supported languages.
|
|
var symbolDeclNodeTypes = map[string]bool{
|
|
"function_declaration": true, "method_declaration": true, "type_declaration": true,
|
|
"function_definition": true, "class_definition": true, "class_declaration": true,
|
|
"interface_declaration": true, "type_alias_declaration": true,
|
|
"function_item": true, "struct_item": true, "enum_item": true, "trait_item": true,
|
|
"type_item": true, "const_item": true, "mod_item": true,
|
|
}
|
|
|
|
// FindSymbolCandidates returns every declaration whose name equals symbolName and
|
|
// whose kind matches symbolKind (empty matches any). Multiple results indicate an
|
|
// ambiguous lookup (e.g. two methods named String on different receivers).
|
|
func FindSymbolCandidates(tree *sitter.Tree, content []byte, filename, symbolName string, symbolKind protocol.SymbolKind) []SymbolCandidate {
|
|
if tree == nil || symbolName == "" {
|
|
return nil
|
|
}
|
|
var candidates []SymbolCandidate
|
|
WalkTree(tree.RootNode(), func(n *sitter.Node) bool {
|
|
nameNode := n.ChildByFieldName("name")
|
|
if nameNode == nil {
|
|
return true
|
|
}
|
|
if GetNodeText(nameNode, content) != symbolName {
|
|
return true
|
|
}
|
|
if !symbolDeclNodeTypes[n.Type()] {
|
|
return true
|
|
}
|
|
if !kindMatchesNode(symbolKind, n.Type()) {
|
|
return true // kind mismatch, keep searching
|
|
}
|
|
r := NodeRange(n, filename)
|
|
nameLoc := NodeLocation(nameNode, filename)
|
|
candidates = append(candidates, SymbolCandidate{
|
|
NodeType: n.Type(),
|
|
StartLine: r.Start.Line,
|
|
EndLine: r.End.Line,
|
|
NameLine: nameLoc.Line,
|
|
NameColumn: nameLoc.Column,
|
|
})
|
|
return true
|
|
})
|
|
return candidates
|
|
}
|
|
|
|
// FindSymbolRange finds a named symbol in the AST and returns its line range (1-indexed, inclusive).
|
|
// symbolKind filters by symbol kind (e.g. "function", "struct"); empty string matches any kind.
|
|
// Returns (0, 0, false) if the symbol is not found. On ambiguity it returns the first match;
|
|
// callers that need to detect ambiguity should use FindSymbolCandidates.
|
|
func FindSymbolRange(tree *sitter.Tree, content []byte, filename, symbolName string, symbolKind protocol.SymbolKind) (startLine, endLine int, found bool) {
|
|
candidates := FindSymbolCandidates(tree, content, filename, symbolName, symbolKind)
|
|
if len(candidates) == 0 {
|
|
return 0, 0, false
|
|
}
|
|
return candidates[0].StartLine, candidates[0].EndLine, true
|
|
}
|