From 9709214d51977fe43de1515a3be4b3f00d5d7154 Mon Sep 17 00:00:00 2001 From: bbodi Date: Sun, 17 May 2026 12:28:32 +0200 Subject: [PATCH] fix(ast/transform): recognise method receiver param lists for type annotations The token-walking transformer in `TransformSource` only entered the parameter-list context when it saw `func name(` or `func(`. For methods with a receiver, e.g. `func (r *T) Foo(x: int)`, the second `(` is preceded by `IDENT RPAREN`, which the previous logic treated as an arbitrary parenthesised expression. As a result the `:` in `x: int` was never converted into a space and the generated Go was invalid. Detect the receiver form by walking backwards from the param list `(` through a matching `RPAREN ... LPAREN` group with depth tracking, then checking that `FUNC` sits immediately before the receiver's `(`. The walk bails on `;`, `{` or `}` so unrelated parenthesised expressions preceding a function call are not misclassified. Added `pkg/ast/transform_test.go` with four cases (pointer receiver, value receiver + multiple params, body struct literal colons preserved, generic receiver type). Each parses the transformer output with `go/parser` so the assertion is exactly the property we care about: valid Go on the way out. Without this fix all four cases fail because the surviving `:` makes go/parser reject the param list. --- pkg/ast/transform.go | 24 ++++++++++++ pkg/ast/transform_test.go | 79 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 103 insertions(+) create mode 100644 pkg/ast/transform_test.go diff --git a/pkg/ast/transform.go b/pkg/ast/transform.go index 30bf4ac2..a88d818b 100644 --- a/pkg/ast/transform.go +++ b/pkg/ast/transform.go @@ -104,6 +104,30 @@ func TransformSource(src []byte, filename string) ([]byte, error) { break } } + } else if prevPrev.tok == gotoken.RPAREN { + // Method declaration: func (recv) name(x: int). + // Walk back to find the matching `(` of the receiver + // list and verify FUNC sits in front of it. + depth := 1 + for j := i - 3; j >= 0; j-- { + switch tokens[j].tok { + case gotoken.RPAREN: + depth++ + case gotoken.LPAREN: + depth-- + if depth == 0 { + if j > 0 && tokens[j-1].tok == gotoken.FUNC { + paramListDepth = parenDepth + } + j = -1 // break outer + } + case gotoken.SEMICOLON, gotoken.LBRACE, gotoken.RBRACE: + j = -1 // break outer + } + if j < 0 { + break + } + } } } } diff --git a/pkg/ast/transform_test.go b/pkg/ast/transform_test.go new file mode 100644 index 00000000..eccee813 --- /dev/null +++ b/pkg/ast/transform_test.go @@ -0,0 +1,79 @@ +package ast + +import ( + "go/parser" + gotoken "go/token" + "strings" + "testing" +) + +// TestTransformSource_MethodReceiverTypeAnnotations exercises the fix for +// type-annotated parameter lists on methods (receiver functions). +// +// Without the receiver-aware branch in TransformSource, the param-list detector +// only recognised `func name(...)` and `func(...)`. As soon as a receiver was +// present, e.g. `func (r *T) Foo(x: int)`, the `(x: int)` group was treated as +// an arbitrary parenthesised expression, so the `:` was never converted into a +// space and the generated Go was syntactically invalid. +func TestTransformSource_MethodReceiverTypeAnnotations(t *testing.T) { + tests := []struct { + name string + input string + wantAbsent []string + }{ + { + name: "pointer receiver single param", + input: "package p\nfunc (r *T) Foo(x: int) {}\ntype T struct{}\n", + wantAbsent: []string{ + "x: int", + }, + }, + { + name: "value receiver multiple params", + input: "package p\nfunc (r T) Bar(x: int, y: string) {}\ntype T struct{}\n", + wantAbsent: []string{ + "x: int", + "y: string", + }, + }, + { + name: "method body struct literal colons are preserved", + input: "package p\n" + + "type T struct{}\n" + + "type rw struct{ X int }\n" + + "func (r *T) Foo(x: int) { _ = rw{X: 1} }\n", + wantAbsent: []string{ + "x: int", + }, + }, + { + name: "named receiver pointer to generic type", + input: "package p\nfunc (r *T[U]) Foo(x: U) {}\ntype T[U any] struct{}\n", + wantAbsent: []string{ + "x: U", + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + out, err := TransformSource([]byte(tt.input), "test.dingo") + if err != nil { + t.Fatalf("TransformSource() error = %v", err) + } + got := string(out) + // Goal: transformer must produce valid Go for method declarations + // with type-annotated params. Without the fix, the `:` survives and + // go/parser rejects it. + fset := gotoken.NewFileSet() + if _, err := parser.ParseFile(fset, "out.go", got, 0); err != nil { + t.Errorf("transformed output is not valid Go: %v\n---got---\n%s", err, got) + } + for _, bad := range tt.wantAbsent { + if strings.Contains(got, bad) { + t.Errorf("output unexpectedly contains %q\n---got---\n%s", bad, got) + } + } + }) + } +}