mirror of
https://github.com/lukaszraczylo/filepuff-mcp.git
synced 2026-10-12 04:54:27 +00:00
skeletonPythonNode took lines[0] of a decorated_definition, which is the first @decorator line, dropping the def/class signature entirely. Now emit the decorator line(s) then recurse into the wrapped definition so its signature is preserved. Adds skeleton_test.go (decorated + plain function).
372 lines
10 KiB
Go
372 lines
10 KiB
Go
// Package parser provides skeleton rendering for source files.
|
|
package parser
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"strings"
|
|
|
|
"github.com/lukaszraczylo/mcp-filepuff/pkg/protocol"
|
|
sitter "github.com/smacker/go-tree-sitter"
|
|
)
|
|
|
|
// SkeletonFile returns a skeleton representation of the file: top-level declarations
|
|
// with signatures and doc-comments intact, but function/method bodies replaced with
|
|
// a language-appropriate placeholder.
|
|
//
|
|
// Supported: go, typescript, javascript, python, rust.
|
|
// Other languages fall back to symbols_only (AST summary text).
|
|
// Returns (skeletonText, isFullSkeleton, error).
|
|
func SkeletonFile(ctx context.Context, reg *Registry, filename string, content []byte) (string, bool, error) {
|
|
result, err := reg.Parse(ctx, filename, content)
|
|
if err != nil {
|
|
return "", false, err
|
|
}
|
|
lang := protocol.DetectLanguage(filename)
|
|
|
|
switch lang {
|
|
case protocol.LangGo:
|
|
return skeletonGo(result.Tree, content), true, nil
|
|
case protocol.LangTypeScript, protocol.LangJavaScript:
|
|
return skeletonTS(result.Tree, content), true, nil
|
|
case protocol.LangPython:
|
|
return skeletonPython(result.Tree, content), true, nil
|
|
case protocol.LangRust:
|
|
return skeletonRust(result.Tree, content), true, nil
|
|
default:
|
|
// TODO: skeleton for c, cpp, elixir, html, vue — fall back to symbols_only
|
|
syms := ExtractSymbols(result.Tree, content, lang, filename)
|
|
return renderSymbolsOnly(syms, lang), false, nil
|
|
}
|
|
}
|
|
|
|
// renderSymbolsOnly renders a simple symbol list (fallback for unsupported languages).
|
|
// It deliberately does not fabricate "<source line> { ... }" output: the raw start
|
|
// line plus a guessed brace is wrong for brace-less languages (e.g. Elixir's "def f do",
|
|
// HTML) and only wastes tokens. The symbol list (kind, name, line, doc) is accurate.
|
|
func renderSymbolsOnly(syms []protocol.Symbol, lang protocol.Language) string {
|
|
var sb strings.Builder
|
|
fmt.Fprintf(&sb, "// skeleton unavailable for %s — symbol list only\n", lang)
|
|
for _, s := range syms {
|
|
fmt.Fprintf(&sb, "// %s %s (line %d)\n", s.Kind, s.Name, s.Location.Line)
|
|
if s.Doc != "" {
|
|
fmt.Fprintf(&sb, "// doc: %s\n", s.Doc)
|
|
}
|
|
}
|
|
return sb.String()
|
|
}
|
|
|
|
// ---- Go skeleton ----
|
|
|
|
// skeletonGoBodyNodes lists Go node types that have a body field to replace.
|
|
var skeletonGoBodyNodes = map[string]string{
|
|
"function_declaration": "body",
|
|
"method_declaration": "body",
|
|
}
|
|
|
|
func skeletonGo(tree *sitter.Tree, content []byte) string {
|
|
if tree == nil {
|
|
return string(content)
|
|
}
|
|
root := tree.RootNode()
|
|
var sb strings.Builder
|
|
skeletonGoNode(root, content, &sb)
|
|
return sb.String()
|
|
}
|
|
|
|
func skeletonGoNode(node *sitter.Node, content []byte, sb *strings.Builder) {
|
|
if node == nil {
|
|
return
|
|
}
|
|
nodeType := node.Type()
|
|
|
|
if bodyField, ok := skeletonGoBodyNodes[nodeType]; ok {
|
|
body := node.ChildByFieldName(bodyField)
|
|
if body != nil {
|
|
sig := strings.TrimRight(string(content[node.StartByte():body.StartByte()]), " \t")
|
|
sb.WriteString(sig)
|
|
sb.WriteString("{ ... }\n\n")
|
|
return
|
|
}
|
|
sb.WriteString(string(content[node.StartByte():node.EndByte()]))
|
|
sb.WriteString("\n")
|
|
return
|
|
}
|
|
|
|
switch nodeType {
|
|
case "source_file":
|
|
for i := 0; i < int(node.ChildCount()); i++ {
|
|
child := node.Child(i)
|
|
if child == nil {
|
|
continue
|
|
}
|
|
skeletonGoNode(child, content, sb)
|
|
}
|
|
return
|
|
case "comment":
|
|
sb.WriteString(string(content[node.StartByte():node.EndByte()]))
|
|
sb.WriteString("\n")
|
|
return
|
|
default:
|
|
sb.WriteString(string(content[node.StartByte():node.EndByte()]))
|
|
sb.WriteString("\n")
|
|
}
|
|
}
|
|
|
|
// ---- TypeScript / JavaScript skeleton ----
|
|
|
|
func skeletonTS(tree *sitter.Tree, content []byte) string {
|
|
if tree == nil {
|
|
return string(content)
|
|
}
|
|
root := tree.RootNode()
|
|
var sb strings.Builder
|
|
skeletonTSNode(root, content, &sb)
|
|
return sb.String()
|
|
}
|
|
|
|
func skeletonTSNode(node *sitter.Node, content []byte, sb *strings.Builder) {
|
|
if node == nil {
|
|
return
|
|
}
|
|
nodeType := node.Type()
|
|
|
|
switch nodeType {
|
|
case "program":
|
|
for i := 0; i < int(node.ChildCount()); i++ {
|
|
child := node.Child(i)
|
|
if child == nil {
|
|
continue
|
|
}
|
|
skeletonTSNode(child, content, sb)
|
|
}
|
|
return
|
|
case "function_declaration":
|
|
body := node.ChildByFieldName("body")
|
|
if body != nil {
|
|
sig := strings.TrimRight(string(content[node.StartByte():body.StartByte()]), " \t")
|
|
sb.WriteString(sig)
|
|
sb.WriteString("{ ... }\n\n")
|
|
return
|
|
}
|
|
case "class_declaration":
|
|
body := node.ChildByFieldName("body")
|
|
if body != nil {
|
|
header := strings.TrimRight(string(content[node.StartByte():body.StartByte()]), " \t")
|
|
sb.WriteString(header)
|
|
sb.WriteString("{\n")
|
|
for i := 0; i < int(body.ChildCount()); i++ {
|
|
child := body.Child(i)
|
|
if child == nil {
|
|
continue
|
|
}
|
|
if child.Type() == "method_definition" {
|
|
methBody := child.ChildByFieldName("body")
|
|
if methBody != nil {
|
|
methSig := strings.TrimRight(string(content[child.StartByte():methBody.StartByte()]), " \t")
|
|
sb.WriteString(" ")
|
|
sb.WriteString(methSig)
|
|
sb.WriteString("{ ... }\n")
|
|
continue
|
|
}
|
|
}
|
|
sb.WriteString(" ")
|
|
sb.WriteString(string(content[child.StartByte():child.EndByte()]))
|
|
sb.WriteString("\n")
|
|
}
|
|
sb.WriteString("}\n\n")
|
|
return
|
|
}
|
|
case "comment":
|
|
sb.WriteString(string(content[node.StartByte():node.EndByte()]))
|
|
sb.WriteString("\n")
|
|
return
|
|
}
|
|
|
|
sb.WriteString(string(content[node.StartByte():node.EndByte()]))
|
|
sb.WriteString("\n")
|
|
}
|
|
|
|
// ---- Python skeleton ----
|
|
|
|
func skeletonPython(tree *sitter.Tree, content []byte) string {
|
|
if tree == nil {
|
|
return string(content)
|
|
}
|
|
root := tree.RootNode()
|
|
var sb strings.Builder
|
|
skeletonPythonNode(root, content, &sb, "")
|
|
return sb.String()
|
|
}
|
|
|
|
func skeletonPythonNode(node *sitter.Node, content []byte, sb *strings.Builder, indent string) {
|
|
if node == nil {
|
|
return
|
|
}
|
|
nodeType := node.Type()
|
|
|
|
switch nodeType {
|
|
case "module":
|
|
for i := 0; i < int(node.ChildCount()); i++ {
|
|
child := node.Child(i)
|
|
if child == nil {
|
|
continue
|
|
}
|
|
skeletonPythonNode(child, content, sb, indent)
|
|
}
|
|
return
|
|
case "decorated_definition":
|
|
// Emit the decorator line(s), then render the wrapped definition so its
|
|
// signature (def/class) is preserved — taking lines[0] of the whole node
|
|
// would keep only the first decorator and drop the signature entirely.
|
|
var def *sitter.Node
|
|
for i := 0; i < int(node.NamedChildCount()); i++ {
|
|
if child := node.NamedChild(i); child != nil {
|
|
if t := child.Type(); t == "function_definition" || t == "class_definition" {
|
|
def = child
|
|
break
|
|
}
|
|
}
|
|
}
|
|
if def != nil {
|
|
decText := strings.TrimRight(string(content[node.StartByte():def.StartByte()]), "\n")
|
|
for _, dl := range strings.Split(decText, "\n") {
|
|
if strings.TrimSpace(dl) == "" {
|
|
continue
|
|
}
|
|
sb.WriteString(indent)
|
|
sb.WriteString(dl)
|
|
sb.WriteString("\n")
|
|
}
|
|
skeletonPythonNode(def, content, sb, indent)
|
|
return
|
|
}
|
|
fallthrough
|
|
case "function_definition":
|
|
nodeText := string(content[node.StartByte():node.EndByte()])
|
|
lines := strings.SplitN(nodeText, "\n", 2)
|
|
sb.WriteString(indent)
|
|
sb.WriteString(lines[0])
|
|
sb.WriteString("\n")
|
|
sb.WriteString(indent)
|
|
sb.WriteString(" ...\n\n")
|
|
return
|
|
case "class_definition":
|
|
body := node.ChildByFieldName("body")
|
|
if body != nil {
|
|
header := string(content[node.StartByte():body.StartByte()])
|
|
firstLine := strings.SplitN(header, "\n", 2)[0]
|
|
sb.WriteString(indent)
|
|
sb.WriteString(firstLine)
|
|
sb.WriteString("\n")
|
|
for i := 0; i < int(body.ChildCount()); i++ {
|
|
child := body.Child(i)
|
|
if child == nil {
|
|
continue
|
|
}
|
|
if child.Type() == "function_definition" || child.Type() == "decorated_definition" {
|
|
childText := string(content[child.StartByte():child.EndByte()])
|
|
childLines := strings.SplitN(childText, "\n", 2)
|
|
sb.WriteString(indent + " ")
|
|
sb.WriteString(childLines[0])
|
|
sb.WriteString("\n")
|
|
sb.WriteString(indent + " ...")
|
|
sb.WriteString("\n")
|
|
continue
|
|
}
|
|
if child.Type() == "expression_statement" {
|
|
sb.WriteString(indent + " ")
|
|
sb.WriteString(string(content[child.StartByte():child.EndByte()]))
|
|
sb.WriteString("\n")
|
|
continue
|
|
}
|
|
}
|
|
sb.WriteString("\n")
|
|
return
|
|
}
|
|
case "comment":
|
|
sb.WriteString(indent)
|
|
sb.WriteString(string(content[node.StartByte():node.EndByte()]))
|
|
sb.WriteString("\n")
|
|
return
|
|
}
|
|
|
|
sb.WriteString(indent)
|
|
sb.WriteString(string(content[node.StartByte():node.EndByte()]))
|
|
sb.WriteString("\n")
|
|
}
|
|
|
|
// ---- Rust skeleton ----
|
|
|
|
func skeletonRust(tree *sitter.Tree, content []byte) string {
|
|
if tree == nil {
|
|
return string(content)
|
|
}
|
|
root := tree.RootNode()
|
|
var sb strings.Builder
|
|
skeletonRustNode(root, content, &sb)
|
|
return sb.String()
|
|
}
|
|
|
|
func skeletonRustNode(node *sitter.Node, content []byte, sb *strings.Builder) {
|
|
if node == nil {
|
|
return
|
|
}
|
|
nodeType := node.Type()
|
|
|
|
switch nodeType {
|
|
case "source_file":
|
|
for i := 0; i < int(node.ChildCount()); i++ {
|
|
child := node.Child(i)
|
|
if child == nil {
|
|
continue
|
|
}
|
|
skeletonRustNode(child, content, sb)
|
|
}
|
|
return
|
|
case "function_item":
|
|
body := node.ChildByFieldName("body")
|
|
if body != nil {
|
|
sig := strings.TrimRight(string(content[node.StartByte():body.StartByte()]), " \t")
|
|
sb.WriteString(sig)
|
|
sb.WriteString("{ ... }\n\n")
|
|
return
|
|
}
|
|
case "impl_item":
|
|
body := node.ChildByFieldName("body")
|
|
if body != nil {
|
|
header := strings.TrimRight(string(content[node.StartByte():body.StartByte()]), " \t")
|
|
sb.WriteString(header)
|
|
sb.WriteString("{\n")
|
|
for i := 0; i < int(body.ChildCount()); i++ {
|
|
child := body.Child(i)
|
|
if child == nil {
|
|
continue
|
|
}
|
|
if child.Type() == "function_item" {
|
|
methBody := child.ChildByFieldName("body")
|
|
if methBody != nil {
|
|
methSig := strings.TrimRight(string(content[child.StartByte():methBody.StartByte()]), " \t")
|
|
sb.WriteString(" ")
|
|
sb.WriteString(methSig)
|
|
sb.WriteString("{ ... }\n")
|
|
continue
|
|
}
|
|
}
|
|
sb.WriteString(" ")
|
|
sb.WriteString(string(content[child.StartByte():child.EndByte()]))
|
|
sb.WriteString("\n")
|
|
}
|
|
sb.WriteString("}\n\n")
|
|
return
|
|
}
|
|
case "line_comment", "block_comment":
|
|
sb.WriteString(string(content[node.StartByte():node.EndByte()]))
|
|
sb.WriteString("\n")
|
|
return
|
|
}
|
|
|
|
sb.WriteString(string(content[node.StartByte():node.EndByte()]))
|
|
sb.WriteString("\n")
|
|
}
|