Skip to content

Commit ad31d4a

Browse files
authored
fix(docs): parse Zod 4 function schemas in node API reference generator (#23798)
## Motivation The Node JSON-RPC API reference generator (`docs/scripts/node_api_reference_generation/generate_node_api_reference.ts`) does not instantiate Zod at runtime — it string-parses the schema source (`AztecNodeApiSchema = { ... }`) via the TypeScript compiler API. `parseZodFunctionExpr` only recognized the **Zod 3** DSL: ```ts z.function().args(BlockParameterSchema, schemas.Fr).returns(schemas.Fr) ``` After the repo-wide Zod 3 → 4 migration, every schema moved to the Zod 4 form: ```ts getPublicStorageAt: z.function({ input: z.tuple([BlockParameterSchema, schemas.AztecAddress, schemas.Fr]), output: schemas.Fr, }), ``` There is no `.args(` / `.returns(` left, so `argsStart`/`returnsStart` are `-1` and **every** method is emitted with `Parameters: None` and `Returns: void`. It does not throw — it silently produces a useless reference. This is the breakage referenced in #23660 ("the node-api-reference generator is currently incompatible with the repo's Zod 4 schemas — a pre-existing issue affecting all methods"); it predates that PR and stems from the Zod 4 migration (the generator last regenerated cleanly in #22934, when schemas were still Zod 3). ## Approach Rewrote `parseZodFunctionExpr` to read the Zod 4 `z.function({ input: z.tuple([...]), output: <expr> })` object: it extracts the `input` tuple elements as the parameter list and the `output` expression as the return type, reusing the existing `simplifyZodType` mapping. The legacy `.args().returns()` path is kept as a fallback so older versioned-docs schemas still regenerate. Added small helpers `findMatchingBracket`, `findTopLevelColon`, and `parseInputArgs`. ## Verification The only changed logic is the pure expression parser, so I unit-tested the actual functions (loaded from this file) against real schema strings copied verbatim from `aztec-node.ts`, plus a legacy Zod 3 string: ``` PASS getPublicStorageAt (multi-arg tuple) -> [BlockHash|number|"latest", AztecAddress, Fr] / Fr PASS getWorldStateSyncStatus (empty tuple) -> [] / WorldStateSyncStatus PASS getTxReceipt (class.schema) -> [TxHash] / TxReceipt PASS getPendingTxs (optionals + array output) -> [number|undefined, TxHash|undefined] / Tx[] PASS getBlockNumber (optional ref + branded return) -> [ChainTip|undefined] / number PASS registerContractFunctionSignatures (nested arr) -> [string[]] / void PASS LEGACY Zod 3 .args().returns() -> [number, boolean|undefined] / void 7/7 passed ``` Red→green: pre-fix, the same parser returns `{ paramTypes: [], returnType: "void" }` for every Zod 4 string. **Not done in this PR (needs the built monorepo):** I did not run the full generator end-to-end (`yarn generate:node-api-reference` requires `yarn-project/node_modules`) or commit a regenerated `node-api-reference.md`. A maintainer/CI should run the generator and commit the refreshed reference as a follow-up. ## Out of scope (follow-ups) The generator's static tables have also drifted from the current RPC surface and will produce misplaced/ungrouped output even after this parser fix — kept separate to keep this change focused: - `METHOD_GROUPS` lists removed methods (`getPublicLogs`, `getContractClassLogs`, `getPublicLogsByTagsFromContract`) and omits current ones (`getPrivateLogsByTags`, `getPublicLogsByTags`, `getCheckpointNumber`, `getCheckpointsData`, `getValidatorStats`, …). - `simplifyZodType` still maps deleted schemas (`LogFilterSchema`, `GetPublicLogsResponseSchema`, `GetContractClassLogsResponseSchema`) and lacks `LogResultSchema` / `PrivateLogsQuerySchema` / `PublicLogsQuerySchema` / `TxReceiptSchema`. The separate TypeScript API reference generator (`typescript_api_generation/`) is unrelated to this bug. --- *Created by [claudebox](https://claudebox.work/v2/sessions/46be71fd1d38fe81) · group: `slackbot`*
2 parents f7ff3b9 + 32b1f2c commit ad31d4a

1 file changed

Lines changed: 97 additions & 9 deletions

File tree

docs/scripts/node_api_reference_generation/generate_node_api_reference.ts

Lines changed: 97 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -180,7 +180,7 @@ function parseSchemaObject(
180180
const name = prop.name?.getText(sourceFile);
181181
if (!name) continue;
182182

183-
// The value is a Zod expression like z.function().args(...).returns(...)
183+
// The value is a Zod expression like z.function({ input: z.tuple([...]), output: ... })
184184
const exprText = prop.initializer.getText(sourceFile);
185185
const info = parseZodFunctionExpr(exprText);
186186
result.set(name, info);
@@ -190,19 +190,49 @@ function parseSchemaObject(
190190
/**
191191
* Parse a Zod function expression string to extract simplified parameter types and return type.
192192
*
193+
* Handles the Zod 4 object form used by the schemas, and falls back to the legacy Zod 3
194+
* `.args().returns()` chain so older versioned-docs schemas still regenerate.
195+
*
193196
* Examples:
194-
* "z.function().args(BlockParameterSchema, schemas.Fr).returns(schemas.Fr)"
195-
* "z.function().returns(z.boolean())"
196-
* "z.function().args(z.number(), optional(z.boolean())).returns(z.void())"
197+
* "z.function({ input: z.tuple([BlockParameterSchema, schemas.Fr]), output: schemas.Fr })"
198+
* "z.function({ input: z.tuple([]), output: z.boolean() })"
199+
* "z.function().args(z.number(), optional(z.boolean())).returns(z.void())" // legacy
197200
*/
198201
function parseZodFunctionExpr(expr: string): { paramTypes: string[]; returnType: string } {
199-
const paramTypes: string[] = [];
200-
let returnType = 'void';
201-
202202
// Normalize whitespace (multi-line expressions use \n + indentation)
203203
const normalized = expr.replace(/\s+/g, ' ').trim();
204204

205-
// Extract .args(...) content — match balanced parens for args
205+
// Zod 4 form: z.function({ input: z.tuple([...]), output: <expr> })
206+
const fnStart = normalized.indexOf('z.function(');
207+
if (fnStart !== -1) {
208+
const objStart = normalized.indexOf('{', fnStart);
209+
if (objStart !== -1) {
210+
const objEnd = findMatchingBracket(normalized, objStart);
211+
if (objEnd !== -1) {
212+
let inputExpr: string | undefined;
213+
let outputExpr: string | undefined;
214+
for (const entry of splitTopLevelArgs(normalized.substring(objStart + 1, objEnd))) {
215+
const colon = findTopLevelColon(entry);
216+
if (colon === -1) continue;
217+
const key = entry.substring(0, colon).trim();
218+
const value = entry.substring(colon + 1).trim();
219+
if (key === 'input') inputExpr = value;
220+
else if (key === 'output') outputExpr = value;
221+
}
222+
if (inputExpr !== undefined || outputExpr !== undefined) {
223+
return {
224+
paramTypes: inputExpr ? parseInputArgs(inputExpr) : [],
225+
returnType: outputExpr ? simplifyZodType(outputExpr) : 'void',
226+
};
227+
}
228+
}
229+
}
230+
}
231+
232+
// Legacy Zod 3 form: z.function().args(...).returns(...)
233+
const paramTypes: string[] = [];
234+
let returnType = 'void';
235+
206236
const argsStart = normalized.indexOf('.args(');
207237
if (argsStart !== -1) {
208238
const argsContentStart = argsStart + '.args('.length;
@@ -215,7 +245,6 @@ function parseZodFunctionExpr(expr: string): { paramTypes: string[]; returnType:
215245
}
216246
}
217247

218-
// Extract .returns(...) content — match balanced parens for returns
219248
const returnsStart = normalized.indexOf('.returns(');
220249
if (returnsStart !== -1) {
221250
const returnsContentStart = returnsStart + '.returns('.length;
@@ -228,6 +257,34 @@ function parseZodFunctionExpr(expr: string): { paramTypes: string[]; returnType:
228257
return { paramTypes, returnType };
229258
}
230259

260+
/**
261+
* Extract the parameter type list from a Zod 4 `input:` value, which is normally a
262+
* `z.tuple([...])`. Each tuple element is simplified independently so per-parameter docs
263+
* (names + types) line up with the interface signature.
264+
*/
265+
function parseInputArgs(inputExpr: string): string[] {
266+
const e = inputExpr.trim();
267+
268+
const optionalWrapperMatch = e.match(/^optional\(([\s\S]+)\)$/);
269+
if (optionalWrapperMatch) return parseInputArgs(optionalWrapperMatch[1]);
270+
271+
const tupleStart = e.indexOf('z.tuple(');
272+
if (tupleStart !== -1) {
273+
const bracketOpen = e.indexOf('[', tupleStart);
274+
if (bracketOpen !== -1) {
275+
const bracketClose = findMatchingBracket(e, bracketOpen);
276+
if (bracketClose !== -1) {
277+
const inner = e.substring(bracketOpen + 1, bracketClose).trim();
278+
if (!inner) return [];
279+
return splitTopLevelArgs(inner).map(simplifyZodType);
280+
}
281+
}
282+
}
283+
284+
// Non-tuple input (rare) — treat the whole expression as a single parameter.
285+
return [simplifyZodType(e)];
286+
}
287+
231288
/**
232289
* Find the position of the closing paren that matches the opening paren at `openPos`.
233290
* Returns the index of the closing paren, or -1 if not found.
@@ -244,6 +301,37 @@ function findMatchingParen(text: string, openPos: number): number {
244301
return -1;
245302
}
246303

304+
/**
305+
* Find the index of the bracket matching the opener at `openPos` (one of `(` `[` `{`).
306+
* Returns the index of the matching closer, or -1 if not found.
307+
*/
308+
function findMatchingBracket(text: string, openPos: number): number {
309+
const open = text[openPos];
310+
const close = open === '(' ? ')' : open === '[' ? ']' : open === '{' ? '}' : '';
311+
if (!close) return -1;
312+
let depth = 0;
313+
for (let i = openPos; i < text.length; i++) {
314+
if (text[i] === open) depth++;
315+
else if (text[i] === close) {
316+
depth--;
317+
if (depth === 0) return i;
318+
}
319+
}
320+
return -1;
321+
}
322+
323+
/** Index of the first top-level `:` (depth 0 across `()`, `[]`, `{}`), or -1. */
324+
function findTopLevelColon(text: string): number {
325+
let depth = 0;
326+
for (let i = 0; i < text.length; i++) {
327+
const c = text[i];
328+
if (c === '(' || c === '[' || c === '{') depth++;
329+
else if (c === ')' || c === ']' || c === '}') depth--;
330+
else if (c === ':' && depth === 0) return i;
331+
}
332+
return -1;
333+
}
334+
247335
/**
248336
* Split comma-separated arguments at the top level (respecting nested parentheses and brackets).
249337
*/

0 commit comments

Comments
 (0)