Skip to content

Commit 7b8e1a8

Browse files
authored
feat: add value-expression AST nodes for composing call chains (#84)
* feat: add value-expression AST nodes for composing call chains Adds seven bindings nodes that let mutations build TypeScript value expressions structurally instead of via string concatenation: - IdentifierExpression: bare value-position identifier (`z`, `BaseSchema`). - PropertyAccessExpression: `expr.name`, the building block for method chains like `z.string`. - CallExpression: `expr(args...)`, composes with PropertyAccessExpression to build `z.string()` and chained `z.string().optional()`. - ObjectLiteralExpression + PropertyAssignment: `{ k: v, ... }` in value position, distinct from TypeLiteralNode which emits a type literal. - ArrowFunction + Parameter: `(params): retType => body` so callers can produce things like `(): z.ZodType => FooSchema` for cyclic references. - TypeQuery: `typeof name`, used as a generic argument such as the `typeof FooSchema` inside `z.infer<typeof FooSchema>`. Each node has a corresponding `ts.factory.create*` wrapper in typescript-engine/src/index.ts, a goja-backed builder in bindings.go, and dispatch through ToTypescriptNode / ToTypescriptExpressionNode. typescript-engine/dist/main.js is rebuilt to ship the new factories. bindings/expressions_test.go round-trips every node and several compositions through goja, including the chained-call shape `z.string().optional()`, an inline object literal with property-order preservation, and the `z.lazy((): z.ZodType => Schema)` self-reference form. These cover the surface a follow-up zod mutation needs without binding the bindings to any zod-specific opinions. Generated by Coder Agents on behalf of @Emyrk. * refactor: type IdentifierExpression.Name as Identifier Align with ReferenceType.Name, VariableDeclaration.Name, and Alias.Name so the same qualified-name handle flows through value-position references. Cross-package Prefix set during Go parsing now reaches the emitted name via .Ref() instead of being silently dropped. Doc comment on IdentifierExpression now spells out the distinction from bindings.Identifier: Identifier is a parser-layer qualified-name handle and is not itself a Node; IdentifierExpression is a tree-layer Node that embeds an Identifier in expression position. Test split into bare-name and prefix-is-applied subcases. The prefix case pins the new behavior; without it, an IdentifierExpression carrying a prefixed Identifier would emit "Schema" instead of "ExternalSchema" and would not match the prefixed declaration the rest of guts emits for the same Identifier. Generated by Coder Agents on behalf of @Emyrk. * refactor: review fixes for expression bindings Three review findings, applied together: 1. TypeQuery.Name is now Identifier instead of string. Same reason as the IdentifierExpression refactor: `typeof FooSchema` references a declared TS name, and the prefix set during Go parsing must flow through .Ref() so the TypeQuery and a matching IdentifierExpression for the same prefixed Identifier emit aligned names. Without this, a prefixed declaration would emit `typeof Foo` while the value-position reference emits `ExternalFoo`. Doc updated to spell out the invariant and a prefix_is_applied subtest pins the new behavior. 2. Test helper zMethodCall renamed to methodCall(receiver, method, args). The bindings know nothing about zod; the helper shouldn't either. The helper had "z" hardcoded as the receiver, which leaked the zod motivation into general-purpose tests. The new signature reads methodCall("z", "string"), exactly as descriptive at the call site, without baking domain assumptions into the helper name. 3. ObjectLiteralExpression doc now calls out the modeled subset. TS's ObjectLiteralElementLike covers PropertyAssignment, ShorthandPropertyAssignment, SpreadAssignment, MethodDeclaration, and accessors. We model only PropertyAssignment; the doc says so, matching the existing pattern on ImportDeclaration ("Only the named-imports form is modeled"). Generated by Coder Agents on behalf of @Emyrk.
1 parent 955fb63 commit 7b8e1a8

5 files changed

Lines changed: 649 additions & 1 deletion

File tree

bindings/bindings.go

Lines changed: 176 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,10 @@ func (b *Bindings) ToTypescriptNode(ety Node) (*goja.Object, error) {
3333
siObj, err = b.PropertySignature(node)
3434
case *TypeParameter:
3535
siObj, err = b.TypeParameter(node)
36+
case *PropertyAssignment:
37+
siObj, err = b.PropertyAssignment(node)
38+
case *Parameter:
39+
siObj, err = b.Parameter(node)
3640
case DeclarationType:
3741
// Defer to the ExpressionType implementation
3842
siObj, err = b.ToTypescriptDeclarationNode(node)
@@ -135,6 +139,18 @@ func (b *Bindings) ToTypescriptExpressionNode(ety ExpressionType) (*goja.Object,
135139
siObj, err = b.TypeLiteralNode(ety)
136140
case *TypeIntersection:
137141
siObj, err = b.TypeIntersection(ety)
142+
case *IdentifierExpression:
143+
siObj, err = b.IdentifierExpression(ety)
144+
case *PropertyAccessExpression:
145+
siObj, err = b.PropertyAccessExpression(ety)
146+
case *CallExpression:
147+
siObj, err = b.CallExpression(ety)
148+
case *ObjectLiteralExpression:
149+
siObj, err = b.ObjectLiteralExpression(ety)
150+
case *ArrowFunction:
151+
siObj, err = b.ArrowFunction(ety)
152+
case *TypeQuery:
153+
siObj, err = b.TypeQuery(ety)
138154
default:
139155
return nil, xerrors.Errorf("unsupported type for field type: %T", ety)
140156
}
@@ -864,3 +880,163 @@ func (b *Bindings) ImportDeclaration(decl *ImportDeclaration) (*goja.Object, err
864880
}
865881
return res.ToObject(b.vm), nil
866882
}
883+
884+
// IdentifierExpression builds the goja node for a value-position identifier
885+
// such as `z` or `BaseSchema`.
886+
func (b *Bindings) IdentifierExpression(expr *IdentifierExpression) (*goja.Object, error) {
887+
idF, err := b.f("identifierExpression")
888+
if err != nil {
889+
return nil, err
890+
}
891+
res, err := idF(goja.Undefined(), b.vm.ToValue(expr.Name.Ref()))
892+
if err != nil {
893+
return nil, xerrors.Errorf("call identifierExpression: %w", err)
894+
}
895+
return res.ToObject(b.vm), nil
896+
}
897+
898+
// PropertyAccessExpression builds `<expression>.<name>`.
899+
func (b *Bindings) PropertyAccessExpression(expr *PropertyAccessExpression) (*goja.Object, error) {
900+
paF, err := b.f("propertyAccessExpression")
901+
if err != nil {
902+
return nil, err
903+
}
904+
inner, err := b.ToTypescriptNode(expr.Expression)
905+
if err != nil {
906+
return nil, fmt.Errorf("property access expression: %w", err)
907+
}
908+
res, err := paF(goja.Undefined(), inner, b.vm.ToValue(expr.Name))
909+
if err != nil {
910+
return nil, xerrors.Errorf("call propertyAccessExpression: %w", err)
911+
}
912+
return res.ToObject(b.vm), nil
913+
}
914+
915+
// CallExpression builds `<expression>(args...)`.
916+
func (b *Bindings) CallExpression(expr *CallExpression) (*goja.Object, error) {
917+
callF, err := b.f("callExpression")
918+
if err != nil {
919+
return nil, err
920+
}
921+
callee, err := b.ToTypescriptNode(expr.Expression)
922+
if err != nil {
923+
return nil, fmt.Errorf("call expression callee: %w", err)
924+
}
925+
args := make([]interface{}, 0, len(expr.Arguments))
926+
for i, a := range expr.Arguments {
927+
v, err := b.ToTypescriptNode(a)
928+
if err != nil {
929+
return nil, fmt.Errorf("call expression arg %d: %w", i, err)
930+
}
931+
args = append(args, v)
932+
}
933+
res, err := callF(goja.Undefined(), callee, b.vm.NewArray(args...))
934+
if err != nil {
935+
return nil, xerrors.Errorf("call callExpression: %w", err)
936+
}
937+
return res.ToObject(b.vm), nil
938+
}
939+
940+
// ObjectLiteralExpression builds `{ k: v, ... }` in expression position.
941+
func (b *Bindings) ObjectLiteralExpression(expr *ObjectLiteralExpression) (*goja.Object, error) {
942+
objF, err := b.f("objectLiteralExpression")
943+
if err != nil {
944+
return nil, err
945+
}
946+
props := make([]interface{}, 0, len(expr.Properties))
947+
for _, p := range expr.Properties {
948+
v, err := b.PropertyAssignment(p)
949+
if err != nil {
950+
return nil, fmt.Errorf("object literal property %q: %w", p.Name, err)
951+
}
952+
props = append(props, v)
953+
}
954+
res, err := objF(goja.Undefined(), b.vm.NewArray(props...))
955+
if err != nil {
956+
return nil, xerrors.Errorf("call objectLiteralExpression: %w", err)
957+
}
958+
return res.ToObject(b.vm), nil
959+
}
960+
961+
// PropertyAssignment builds `<name>: <initializer>` for an
962+
// ObjectLiteralExpression child.
963+
func (b *Bindings) PropertyAssignment(pa *PropertyAssignment) (*goja.Object, error) {
964+
paF, err := b.f("propertyAssignment")
965+
if err != nil {
966+
return nil, err
967+
}
968+
init, err := b.ToTypescriptNode(pa.Initializer)
969+
if err != nil {
970+
return nil, fmt.Errorf("property assignment %q initializer: %w", pa.Name, err)
971+
}
972+
res, err := paF(goja.Undefined(), b.vm.ToValue(pa.Name), init)
973+
if err != nil {
974+
return nil, xerrors.Errorf("call propertyAssignment: %w", err)
975+
}
976+
return res.ToObject(b.vm), nil
977+
}
978+
979+
// Parameter builds a single `name: type` arrow-function parameter.
980+
func (b *Bindings) Parameter(p *Parameter) (*goja.Object, error) {
981+
pF, err := b.f("parameter")
982+
if err != nil {
983+
return nil, err
984+
}
985+
var typeNode goja.Value = goja.Undefined()
986+
if p.Type != nil {
987+
typeNode, err = b.ToTypescriptNode(p.Type)
988+
if err != nil {
989+
return nil, fmt.Errorf("parameter %q type: %w", p.Name, err)
990+
}
991+
}
992+
res, err := pF(goja.Undefined(), b.vm.ToValue(p.Name), typeNode)
993+
if err != nil {
994+
return nil, xerrors.Errorf("call parameter: %w", err)
995+
}
996+
return res.ToObject(b.vm), nil
997+
}
998+
999+
// ArrowFunction builds `(parameters): returnType => body`.
1000+
func (b *Bindings) ArrowFunction(af *ArrowFunction) (*goja.Object, error) {
1001+
afF, err := b.f("arrowFunction")
1002+
if err != nil {
1003+
return nil, err
1004+
}
1005+
params := make([]interface{}, 0, len(af.Parameters))
1006+
for _, p := range af.Parameters {
1007+
v, err := b.Parameter(p)
1008+
if err != nil {
1009+
return nil, fmt.Errorf("arrow function parameter %q: %w", p.Name, err)
1010+
}
1011+
params = append(params, v)
1012+
}
1013+
var returnType goja.Value = goja.Undefined()
1014+
if af.ReturnType != nil {
1015+
returnType, err = b.ToTypescriptNode(af.ReturnType)
1016+
if err != nil {
1017+
return nil, fmt.Errorf("arrow function return type: %w", err)
1018+
}
1019+
}
1020+
body, err := b.ToTypescriptNode(af.Body)
1021+
if err != nil {
1022+
return nil, fmt.Errorf("arrow function body: %w", err)
1023+
}
1024+
res, err := afF(goja.Undefined(), b.vm.NewArray(params...), returnType, body)
1025+
if err != nil {
1026+
return nil, xerrors.Errorf("call arrowFunction: %w", err)
1027+
}
1028+
return res.ToObject(b.vm), nil
1029+
}
1030+
1031+
// TypeQuery builds `typeof <name>` as a TypeNode.
1032+
func (b *Bindings) TypeQuery(tq *TypeQuery) (*goja.Object, error) {
1033+
tqF, err := b.f("typeQuery")
1034+
if err != nil {
1035+
return nil, err
1036+
}
1037+
res, err := tqF(goja.Undefined(), b.vm.ToValue(tq.Name.Ref()))
1038+
if err != nil {
1039+
return nil, xerrors.Errorf("call typeQuery: %w", err)
1040+
}
1041+
return res.ToObject(b.vm), nil
1042+
}

bindings/expressions.go

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -197,3 +197,105 @@ type TypeIntersection struct {
197197

198198
func (*TypeIntersection) isNode() {}
199199
func (*TypeIntersection) isExpressionType() {}
200+
201+
// IdentifierExpression is a value-position TypeScript identifier such as
202+
// the `z` in `z.string()` or `BaseSchema` in `BaseSchema.extend({...})`.
203+
//
204+
// It is distinct from bindings.Identifier despite the similar name.
205+
// Identifier is parser-layer plumbing: a qualified-name handle
206+
// (Name + Package + Prefix) used to resolve and disambiguate references
207+
// across Go packages, and it is not itself a Node. IdentifierExpression is
208+
// tree-layer plumbing: a Node that implements ExpressionType so callers
209+
// can place a value-position identifier inside expression slots like
210+
// CallExpression.Expression.
211+
//
212+
// The Name field is itself an Identifier so cross-package prefixing flows
213+
// through .Ref() the same way it does for ReferenceType.Name and
214+
// VariableDeclaration.Name.
215+
type IdentifierExpression struct {
216+
Name Identifier
217+
}
218+
219+
func (*IdentifierExpression) isNode() {}
220+
func (*IdentifierExpression) isExpressionType() {}
221+
222+
// PropertyAccessExpression is `<expression>.<name>`, used to chain method
223+
// names or member references such as `z.string` or `BaseSchema.extend`.
224+
type PropertyAccessExpression struct {
225+
Expression ExpressionType
226+
Name string
227+
}
228+
229+
func (*PropertyAccessExpression) isNode() {}
230+
func (*PropertyAccessExpression) isExpressionType() {}
231+
232+
// CallExpression is `<expression>(args...)`. It composes with
233+
// PropertyAccessExpression to build chained calls like
234+
// `z.string().optional()`.
235+
type CallExpression struct {
236+
Expression ExpressionType
237+
Arguments []ExpressionType
238+
}
239+
240+
func (*CallExpression) isNode() {}
241+
func (*CallExpression) isExpressionType() {}
242+
243+
// ObjectLiteralExpression is `{ k: v, ... }` in expression position. It is
244+
// distinct from TypeLiteralNode, which emits a TypeScript object type.
245+
//
246+
// Only the PropertyAssignment form is modeled. ShorthandPropertyAssignment
247+
// (`{ x }`), SpreadAssignment (`{ ...rest }`), MethodDeclaration, and
248+
// accessor properties are not yet supported; add them if you need them.
249+
type ObjectLiteralExpression struct {
250+
Properties []*PropertyAssignment
251+
}
252+
253+
func (*ObjectLiteralExpression) isNode() {}
254+
func (*ObjectLiteralExpression) isExpressionType() {}
255+
256+
// PropertyAssignment is `<name>: <initializer>` inside an
257+
// ObjectLiteralExpression. It is a node but not an ExpressionType or a
258+
// DeclarationType because it only appears as a child of
259+
// ObjectLiteralExpression.
260+
type PropertyAssignment struct {
261+
Name string
262+
Initializer ExpressionType
263+
}
264+
265+
func (*PropertyAssignment) isNode() {}
266+
267+
// Parameter is a single parameter in an ArrowFunction signature. Name is
268+
// required; Type may be nil to omit the annotation.
269+
type Parameter struct {
270+
Name string
271+
Type ExpressionType
272+
}
273+
274+
func (*Parameter) isNode() {}
275+
276+
// ArrowFunction is `(parameters): returnType => body`. ReturnType may be
277+
// nil to omit the annotation. Body is currently required to be a single
278+
// expression; statement bodies are not yet modeled.
279+
type ArrowFunction struct {
280+
Parameters []*Parameter
281+
ReturnType ExpressionType
282+
Body ExpressionType
283+
}
284+
285+
func (*ArrowFunction) isNode() {}
286+
func (*ArrowFunction) isExpressionType() {}
287+
288+
// TypeQuery is `typeof <name>`. It appears in type position, typically as
289+
// a generic argument such as the `typeof FooSchema` inside
290+
// `z.infer<typeof FooSchema>`.
291+
//
292+
// Name is an Identifier so cross-package prefixing flows through .Ref(),
293+
// matching the rest of the AST. Without this, a TypeQuery for a prefixed
294+
// declaration would emit `typeof Foo` while the matching value-position
295+
// IdentifierExpression emits `ExternalFoo`, and the two would not line up.
296+
type TypeQuery struct {
297+
Name Identifier
298+
}
299+
300+
func (*TypeQuery) isNode() {}
301+
func (*TypeQuery) isExpressionType() {}

0 commit comments

Comments
 (0)