Skip to content

Commit 6866932

Browse files
committed
Merge open PRs MadAppGang#59, MadAppGang#60, MadAppGang#61, MadAppGang#62 into fork main
Combines all four currently open pull requests against MadAppGang/dingo into the local fork's main so downstream work (e.g. the nodes.go cascade fix) can build on top of all of them without one-by-one branch juggling. The upstream PRs remain unaffected because their base branch is MadAppGang/dingo:main, not bbodi/dingo:main. Included: MadAppGang#59 fix(ast/transform): recognise method receiver param lists for type annotations MadAppGang#60 test(tokenizer): cover ENUM and GUARD keywords at both surfaces MadAppGang#61 fix(ast/enum_parser): allow arbitrary nesting of '*' and '[]' in variant field types MadAppGang#62 feat(feature): add Validator plugin extension and character-level pre-transform phase
5 parents 9b0fc8a + 9709214 + 7724705 + 808d124 + 704841b commit 6866932

8 files changed

Lines changed: 501 additions & 11 deletions

File tree

pkg/ast/enum_parser.go

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -317,17 +317,19 @@ func (p *EnumParser) parseStructFields() ([]*EnumField, error) {
317317
func (p *EnumParser) parseTypeExpr() (*TypeExpr, error) {
318318
start := p.pos
319319

320-
// Handle pointer types
321-
for p.peek() == '*' {
322-
p.advance()
323-
}
324-
325-
// Handle slice types
326-
if p.peek() == '[' {
327-
p.advance()
328-
if p.peek() == ']' {
320+
// Accept any number of '*' (pointer) and '[]' (slice) prefixes in any order.
321+
// Supports *T, []T, []*T, *[]T, [][]T, **T, etc.
322+
for {
323+
if p.peek() == '*' {
329324
p.advance()
325+
continue
326+
}
327+
if p.peek() == '[' && p.pos+1 < len(p.src) && p.src[p.pos+1] == ']' {
328+
p.advance() // '['
329+
p.advance() // ']'
330+
continue
330331
}
332+
break
331333
}
332334

333335
// Parse base type name

pkg/ast/enum_parser_test.go

Lines changed: 200 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,200 @@
1+
package ast
2+
3+
import (
4+
"strings"
5+
"testing"
6+
)
7+
8+
// TestEnumParser_TypePrefixes covers parseTypeExpr's handling of pointer (*),
9+
// slice ([]), and any nesting/order of the two. The original implementation
10+
// accepted only `**T` or `*[]T`-shaped prefixes (pointers first, then a single
11+
// slice), which silently rejected `[]*T`, `[][]T`, `[]*[]T`, etc. The
12+
// transpilation pipeline turned that rejection into a confusing
13+
// "expected declaration, found enum" error downstream.
14+
func TestEnumParser_TypePrefixes(t *testing.T) {
15+
tests := []struct {
16+
name string
17+
src string
18+
wantType string // expected Text of the parsed field type
19+
}{
20+
{
21+
name: "plain type",
22+
src: `enum E { V { x: int } }`,
23+
wantType: "int",
24+
},
25+
{
26+
name: "pointer",
27+
src: `enum E { V { x: *Foo } }`,
28+
wantType: "*Foo",
29+
},
30+
{
31+
name: "double pointer",
32+
src: `enum E { V { x: **Foo } }`,
33+
wantType: "**Foo",
34+
},
35+
{
36+
name: "slice of value",
37+
src: `enum E { V { x: []Foo } }`,
38+
wantType: "[]Foo",
39+
},
40+
{
41+
name: "slice of pointer",
42+
src: `enum E { V { x: []*Foo } }`,
43+
wantType: "[]*Foo",
44+
},
45+
{
46+
name: "pointer to slice",
47+
src: `enum E { V { x: *[]Foo } }`,
48+
wantType: "*[]Foo",
49+
},
50+
{
51+
name: "slice of slice",
52+
src: `enum E { V { x: [][]int } }`,
53+
wantType: "[][]int",
54+
},
55+
{
56+
name: "slice of slice of pointer",
57+
src: `enum E { V { x: [][]*Foo } }`,
58+
wantType: "[][]*Foo",
59+
},
60+
{
61+
name: "generic with slice arg",
62+
src: `enum E { V { x: Result[[]int, error] } }`,
63+
wantType: "Result[[]int, error]",
64+
},
65+
}
66+
67+
for _, tt := range tests {
68+
t.Run(tt.name, func(t *testing.T) {
69+
p := NewEnumParser([]byte(tt.src), 0)
70+
decl, _, err := p.ParseEnumDecl()
71+
if err != nil {
72+
t.Fatalf("ParseEnumDecl(%q) returned error: %v", tt.src, err)
73+
}
74+
if len(decl.Variants) != 1 {
75+
t.Fatalf("expected 1 variant, got %d", len(decl.Variants))
76+
}
77+
v := decl.Variants[0]
78+
if v.Kind != StructVariant {
79+
t.Fatalf("expected struct variant, got %v", v.Kind)
80+
}
81+
if len(v.Fields) != 1 {
82+
t.Fatalf("expected 1 field, got %d", len(v.Fields))
83+
}
84+
got := v.Fields[0].Type.Text
85+
if got != tt.wantType {
86+
t.Errorf("field type = %q, want %q", got, tt.wantType)
87+
}
88+
})
89+
}
90+
}
91+
92+
// TestEnumParser_TupleTypePrefixes covers the same prefix handling in tuple
93+
// variant fields. parseTupleFields shares parseTypeExpr with parseStructFields,
94+
// so a regression in either context surfaces here too.
95+
func TestEnumParser_TupleTypePrefixes(t *testing.T) {
96+
tests := []struct {
97+
name string
98+
src string
99+
wantTypes []string
100+
}{
101+
{
102+
name: "single slice-of-pointer",
103+
src: `enum E { V([]*Foo) }`,
104+
wantTypes: []string{"[]*Foo"},
105+
},
106+
{
107+
name: "mixed prefixes",
108+
src: `enum E { V(*Foo, []*Bar, [][]int) }`,
109+
wantTypes: []string{"*Foo", "[]*Bar", "[][]int"},
110+
},
111+
}
112+
113+
for _, tt := range tests {
114+
t.Run(tt.name, func(t *testing.T) {
115+
p := NewEnumParser([]byte(tt.src), 0)
116+
decl, _, err := p.ParseEnumDecl()
117+
if err != nil {
118+
t.Fatalf("ParseEnumDecl(%q) returned error: %v", tt.src, err)
119+
}
120+
if len(decl.Variants) != 1 {
121+
t.Fatalf("expected 1 variant, got %d", len(decl.Variants))
122+
}
123+
v := decl.Variants[0]
124+
if v.Kind != TupleVariant {
125+
t.Fatalf("expected tuple variant, got %v", v.Kind)
126+
}
127+
if len(v.Fields) != len(tt.wantTypes) {
128+
t.Fatalf("expected %d fields, got %d", len(tt.wantTypes), len(v.Fields))
129+
}
130+
for i, want := range tt.wantTypes {
131+
got := v.Fields[i].Type.Text
132+
if got != want {
133+
t.Errorf("field %d type = %q, want %q", i, got, want)
134+
}
135+
}
136+
})
137+
}
138+
}
139+
140+
// TestTransformSource_SliceOfPointer is an end-to-end regression test:
141+
// before the fix, a `[]*T` field on an enum variant caused the dingo→go
142+
// transformation to leave the `enum` keyword in the output, producing a
143+
// downstream go/parser error of the form
144+
//
145+
// "expected declaration, found enum"
146+
//
147+
// After the fix the source is transformed cleanly and the generated Go
148+
// contains the variant struct with the correct slice-of-pointer field.
149+
func TestTransformSource_SliceOfPointer(t *testing.T) {
150+
src := []byte(`package main
151+
152+
enum D { Foo { xs: []*Name, n: int } }
153+
154+
type Name struct { v string }
155+
`)
156+
got, err := TransformSource(src, "test.dingo")
157+
if err != nil {
158+
t.Fatalf("TransformSource returned error: %v", err)
159+
}
160+
out := string(got)
161+
if strings.Contains(out, "enum ") {
162+
t.Errorf("transformed output still contains 'enum' keyword:\n%s", out)
163+
}
164+
// The variant struct should carry the field with its original slice-of-pointer type.
165+
if !strings.Contains(out, "xs []*Name") {
166+
t.Errorf("expected `xs []*Name` field in generated Go, got:\n%s", out)
167+
}
168+
}
169+
170+
// TestTransformSource_MultiLineVariantBody confirms that a struct variant
171+
// whose fields are newline-separated (no trailing commas) still parses.
172+
// This was suspected to be broken alongside the slice-of-pointer bug but is
173+
// actually fine; this test guards against future regressions in
174+
// parseStructFields, which relies on skipWhitespaceAndCommas treating
175+
// newlines as separators.
176+
func TestTransformSource_MultiLineVariantBody(t *testing.T) {
177+
src := []byte(`package main
178+
179+
enum D { Foo {
180+
a: int
181+
b: *Name
182+
c: []*Name
183+
} }
184+
185+
type Name struct { v string }
186+
`)
187+
got, err := TransformSource(src, "test.dingo")
188+
if err != nil {
189+
t.Fatalf("TransformSource returned error: %v", err)
190+
}
191+
out := string(got)
192+
if strings.Contains(out, "enum ") {
193+
t.Errorf("transformed output still contains 'enum' keyword:\n%s", out)
194+
}
195+
for _, want := range []string{"a int", "b *Name", "c []*Name"} {
196+
if !strings.Contains(out, want) {
197+
t.Errorf("expected %q in generated Go, got:\n%s", want, out)
198+
}
199+
}
200+
}

pkg/ast/transform.go

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,30 @@ func TransformSource(src []byte, filename string) ([]byte, error) {
104104
break
105105
}
106106
}
107+
} else if prevPrev.tok == gotoken.RPAREN {
108+
// Method declaration: func (recv) name(x: int).
109+
// Walk back to find the matching `(` of the receiver
110+
// list and verify FUNC sits in front of it.
111+
depth := 1
112+
for j := i - 3; j >= 0; j-- {
113+
switch tokens[j].tok {
114+
case gotoken.RPAREN:
115+
depth++
116+
case gotoken.LPAREN:
117+
depth--
118+
if depth == 0 {
119+
if j > 0 && tokens[j-1].tok == gotoken.FUNC {
120+
paramListDepth = parenDepth
121+
}
122+
j = -1 // break outer
123+
}
124+
case gotoken.SEMICOLON, gotoken.LBRACE, gotoken.RBRACE:
125+
j = -1 // break outer
126+
}
127+
if j < 0 {
128+
break
129+
}
130+
}
107131
}
108132
}
109133
}

pkg/ast/transform_test.go

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
package ast
2+
3+
import (
4+
"go/parser"
5+
gotoken "go/token"
6+
"strings"
7+
"testing"
8+
)
9+
10+
// TestTransformSource_MethodReceiverTypeAnnotations exercises the fix for
11+
// type-annotated parameter lists on methods (receiver functions).
12+
//
13+
// Without the receiver-aware branch in TransformSource, the param-list detector
14+
// only recognised `func name(...)` and `func(...)`. As soon as a receiver was
15+
// present, e.g. `func (r *T) Foo(x: int)`, the `(x: int)` group was treated as
16+
// an arbitrary parenthesised expression, so the `:` was never converted into a
17+
// space and the generated Go was syntactically invalid.
18+
func TestTransformSource_MethodReceiverTypeAnnotations(t *testing.T) {
19+
tests := []struct {
20+
name string
21+
input string
22+
wantAbsent []string
23+
}{
24+
{
25+
name: "pointer receiver single param",
26+
input: "package p\nfunc (r *T) Foo(x: int) {}\ntype T struct{}\n",
27+
wantAbsent: []string{
28+
"x: int",
29+
},
30+
},
31+
{
32+
name: "value receiver multiple params",
33+
input: "package p\nfunc (r T) Bar(x: int, y: string) {}\ntype T struct{}\n",
34+
wantAbsent: []string{
35+
"x: int",
36+
"y: string",
37+
},
38+
},
39+
{
40+
name: "method body struct literal colons are preserved",
41+
input: "package p\n" +
42+
"type T struct{}\n" +
43+
"type rw struct{ X int }\n" +
44+
"func (r *T) Foo(x: int) { _ = rw{X: 1} }\n",
45+
wantAbsent: []string{
46+
"x: int",
47+
},
48+
},
49+
{
50+
name: "named receiver pointer to generic type",
51+
input: "package p\nfunc (r *T[U]) Foo(x: U) {}\ntype T[U any] struct{}\n",
52+
wantAbsent: []string{
53+
"x: U",
54+
},
55+
},
56+
}
57+
58+
for _, tt := range tests {
59+
t.Run(tt.name, func(t *testing.T) {
60+
out, err := TransformSource([]byte(tt.input), "test.dingo")
61+
if err != nil {
62+
t.Fatalf("TransformSource() error = %v", err)
63+
}
64+
got := string(out)
65+
// Goal: transformer must produce valid Go for method declarations
66+
// with type-annotated params. Without the fix, the `:` survives and
67+
// go/parser rejects it.
68+
fset := gotoken.NewFileSet()
69+
if _, err := parser.ParseFile(fset, "out.go", got, 0); err != nil {
70+
t.Errorf("transformed output is not valid Go: %v\n---got---\n%s", err, got)
71+
}
72+
for _, bad := range tt.wantAbsent {
73+
if strings.Contains(got, bad) {
74+
t.Errorf("output unexpectedly contains %q\n---got---\n%s", bad, got)
75+
}
76+
}
77+
})
78+
}
79+
}

0 commit comments

Comments
 (0)