From 5214ad2b66b2cec5a2b6ab05095cc3b79060a557 Mon Sep 17 00:00:00 2001 From: Lukasz Raczylo Date: Fri, 19 Jun 2026 22:19:24 +0100 Subject: [PATCH] fix(parser): keep signature in decorated Python skeleton 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). --- internal/parser/skeleton.go | 29 ++++++++++++++++++- internal/parser/skeleton_test.go | 48 ++++++++++++++++++++++++++++++++ 2 files changed, 76 insertions(+), 1 deletion(-) create mode 100644 internal/parser/skeleton_test.go diff --git a/internal/parser/skeleton.go b/internal/parser/skeleton.go index ab97514..c37540f 100644 --- a/internal/parser/skeleton.go +++ b/internal/parser/skeleton.go @@ -215,7 +215,34 @@ func skeletonPythonNode(node *sitter.Node, content []byte, sb *strings.Builder, skeletonPythonNode(child, content, sb, indent) } return - case "function_definition", "decorated_definition": + 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) diff --git a/internal/parser/skeleton_test.go b/internal/parser/skeleton_test.go new file mode 100644 index 0000000..4780b39 --- /dev/null +++ b/internal/parser/skeleton_test.go @@ -0,0 +1,48 @@ +package parser + +import ( + "context" + "strings" + "testing" +) + +// A decorated Python function must keep BOTH its decorator(s) and its def +// signature in the skeleton — previously only the first decorator survived and +// the signature was dropped. +func TestSkeletonPythonDecoratedFunction(t *testing.T) { + r := NewRegistry() + defer r.Close() + + src := "@app.route(\"/\")\n" + + "def home():\n" + + " return \"hi\"\n" + + out, _, err := SkeletonFile(context.Background(), r, "app.py", []byte(src)) + if err != nil { + t.Fatalf("SkeletonFile error: %v", err) + } + if !strings.Contains(out, "@app.route(\"/\")") { + t.Errorf("skeleton lost decorator, got:\n%s", out) + } + if !strings.Contains(out, "def home():") { + t.Errorf("skeleton lost def signature, got:\n%s", out) + } + if strings.Contains(out, "return \"hi\"") { + t.Errorf("skeleton should elide the body, got:\n%s", out) + } +} + +// A plain (undecorated) Python function still renders its signature + body stub. +func TestSkeletonPythonPlainFunction(t *testing.T) { + r := NewRegistry() + defer r.Close() + + src := "def add(a, b):\n return a + b\n" + out, _, err := SkeletonFile(context.Background(), r, "m.py", []byte(src)) + if err != nil { + t.Fatalf("SkeletonFile error: %v", err) + } + if !strings.Contains(out, "def add(a, b):") { + t.Errorf("skeleton lost signature, got:\n%s", out) + } +}