Skip to content

Commit a2b0d2e

Browse files
authored
Merge pull request #314 from constructive-io/feat/traverse-bare-parse-result-roots
feat(traverse): dispatch on bare ParseResult/ScanResult roots + payload guards
2 parents 4efd8a4 + c46d795 commit a2b0d2e

2 files changed

Lines changed: 76 additions & 3 deletions

File tree

‎packages/traverse/__tests__/typed-fields.test.ts‎

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -229,6 +229,33 @@ describe('walk — typed embedded fields (untagged)', () => {
229229
expect(rawStmts).toHaveLength(2);
230230
});
231231

232+
it('detects a bare libpg-query parse result at the root', () => {
233+
// libpg-query parseSync() returns {version, stmts} with no ParseResult wrapper
234+
const parseResult = {
235+
version: 180004,
236+
stmts: [{ stmt: createPolicyAst, stmt_len: 52 }]
237+
};
238+
const tags: string[] = [];
239+
walk(parseResult, (path) => { tags.push(path.tag); });
240+
241+
expect(tags[0]).toBe('ParseResult');
242+
expect(tags[1]).toBe('RawStmt');
243+
expect(tags).toContain('CreatePolicyStmt');
244+
expect(tags).toContain('RangeVar');
245+
});
246+
247+
it('does not treat non-root {version, stmts} shapes as ParseResult', () => {
248+
const tags: string[] = [];
249+
walk({ wrapper: { version: 180004, stmts: [] as any[] } }, (path) => { tags.push(path.tag); });
250+
expect(tags).toEqual([]);
251+
});
252+
253+
it('ignores tagged wrappers whose payload is not an object', () => {
254+
const visited: NodePath[] = [];
255+
walk({ Weird: 'not-an-object' }, (path) => { visited.push(path); });
256+
expect(visited).toEqual([]);
257+
});
258+
232259
it('keeps existing behavior for tagged nodes', () => {
233260
const visited: string[] = [];
234261
const visitor: Visitor = {
@@ -276,6 +303,20 @@ describe('visit — typed embedded fields (untagged)', () => {
276303
expect(rangeVars[0].relname).toBe('posts');
277304
});
278305

306+
it('detects a bare libpg-query parse result at the root', () => {
307+
const parseResult = {
308+
version: 180004,
309+
stmts: [{ stmt: createPolicyAst, stmt_len: 52 }]
310+
};
311+
const visited: string[] = [];
312+
visit(parseResult, {
313+
ParseResult: () => { visited.push('ParseResult'); },
314+
RawStmt: () => { visited.push('RawStmt'); },
315+
RangeVar: (node) => { visited.push(`RangeVar:${node.relname}`); }
316+
});
317+
expect(visited).toEqual(['ParseResult', 'RawStmt', 'RangeVar:posts']);
318+
});
319+
279320
it('keeps existing behavior for tagged nodes', () => {
280321
const visited: string[] = [];
281322
visit(createPolicyAst, {

‎packages/traverse/src/traverse.ts‎

Lines changed: 35 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -61,12 +61,34 @@ export function walk(
6161
const keys = Object.keys(root);
6262
if (keys.length === 1 && /^[A-Z]/.test(keys[0])) {
6363
walkNode(keys[0], root[keys[0]], actualCallback, parent, keyPath);
64-
} else {
65-
for (const key of keys) {
66-
walk(root[key], actualCallback, parent, [...keyPath, key]);
64+
return;
65+
}
66+
if (parent === null && keyPath.length === 0) {
67+
const rootTag = detectUntaggedRootTag(root);
68+
if (rootTag) {
69+
walkNode(rootTag, root, actualCallback, parent, keyPath);
70+
return;
6771
}
6872
}
73+
for (const key of keys) {
74+
walk(root[key], actualCallback, parent, [...keyPath, key]);
75+
}
76+
}
77+
}
78+
79+
/**
80+
* libpg-query returns the top-level ParseResult/ScanResult as a bare object
81+
* (no `{ParseResult: {...}}` wrapper). Detect those shapes at the root so
82+
* their visitors — and typed descendants like RawStmt — are dispatched.
83+
*/
84+
function detectUntaggedRootTag(root: any): string | null {
85+
if (Array.isArray(root.stmts) && typeof root.version === 'number') {
86+
return 'ParseResult';
6987
}
88+
if (Array.isArray(root.tokens) && typeof root.version === 'number') {
89+
return 'ScanResult';
90+
}
91+
return null;
7092
}
7193

7294
function isTaggedNode(value: any): boolean {
@@ -84,6 +106,10 @@ function walkNode(
84106
parent: NodePath | null,
85107
keyPath: readonly (string | number)[],
86108
): void {
109+
if (typeof nodeData !== 'object' || nodeData === null) {
110+
return;
111+
}
112+
87113
const path = new NodePath(tag, nodeData, parent, keyPath);
88114

89115
if (actualCallback(path) === false) {
@@ -159,6 +185,12 @@ export function visit(
159185
): void {
160186
if (node == null || typeof node !== 'object') return;
161187

188+
const rootTag = detectUntaggedRootTag(node);
189+
if (rootTag) {
190+
visitNode(rootTag, node, visitor, ctx);
191+
return;
192+
}
193+
162194
const nodeType = Object.keys(node)[0] as string;
163195
const nodeData = node[nodeType];
164196

0 commit comments

Comments
 (0)