feat: add value-expression AST nodes for composing call chains#84
Merged
Conversation
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.
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.
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Adds seven bindings nodes that let mutations build TypeScript value expressions structurally instead of via string concatenation:
IdentifierExpression{Name}z,BaseSchema(bare value reference)PropertyAccessExpression{Expr, Name}expr.nameCallExpression{Expr, Args}expr(args...), composes for chainsObjectLiteralExpression{Props}+PropertyAssignment{Name, Init}{ k: v, ... }in value positionArrowFunction{Params, RetType, Body}+Parameter{Name, Type}(params): retType => bodyTypeQuery{Name}typeof Name(type position)Foundation for a stacked PR that adds a
zodmutation; useful on its own to any caller that wants to inject computed value expressions into the AST.Implementation notes and test coverage
Why these aren't covered by existing nodes
ReferenceTypeis a TypeScript type-reference node.IdentifierExpressionis needed for the value-position case (thezinz.string()); reusingReferenceTypethere would emit a type reference where TypeScript expects an expression.TypeLiteralNodeis a TypeScript object-type literal ({ k: string }).ObjectLiteralExpressionis its value-position sibling ({ k: z.string() }). They share no semantics so they stay distinct.ArrayLiteralTypeandLiteralTypealready cover[...]and primitive literals, so this PR doesn't redefine them.Wiring
ts.factory.create*wrappers added totypescript-engine/src/index.ts.dist/main.jsrebuilt withpnpm run build.bindings/bindings.go.ToTypescriptNodedispatches*PropertyAssignmentand*Parameterdirectly (neither is anExpressionTypeorDeclarationTypebecause they only appear as children of object literals / arrow functions).ToTypescriptExpressionNodedispatches the six newExpressionTypecases.Test coverage
bindings/expressions_test.goround-trips every node through goja and verifies the emitted text:TestIdentifierExpression— bare identifier.TestPropertyAccessExpression—z.string.TestCallExpression— no-args, one-arg, chained (z.string().optional()), and nested (z.array(z.string())).TestObjectLiteralExpression— empty, single property, multiple with declaration-order preservation.TestPropertyAssignment— standalone, easier failure bisect.TestArrowFunction— no params, with return type, with typed parameter.TestTypeQuery— standalone and as a generic argument (Foo<typeof FooSchema>).TestComposeZodObjectandTestComposeZodLazyRef— integration cases that build thez.object({...})andz.lazy((): z.ZodType => Schema)shapes the stacked zod mutation will need. If these fail, the follow-up cannot succeed.go test ./...andgo vet ./...clean (one pre-existing unreachable-code warning inconvert.gopredates this branch).Stacked follow-up
A
feat/zod-mutationPR will add azodpackage with anAsSchemasmutation that walks everyInterface/Aliasints.typescriptNodesand replaces it with aVariableStatement(export const FooSchema = z.object({...})) plus anAlias(export type Foo = z.infer<typeof FooSchema>). The schema expression tree is built from the nodes in this PR. Replaces #82's string-builder serializer with an AST mutation composable withExportTypes,SimplifyOptional, and the other config mutations.Generated by Coder Agents on behalf of @Emyrk.