Skip to content

Commit 62af2ed

Browse files
authored
Validate its own generated schemas (#1017)
* test(validators): reproduce the crash on a nested definedTypeLink getValidationItemsVisitor throws `Expected node of kind [definedTypeLinkNode]` whenever it walks a definedTypeLinkNode that sits below the top of the tree: a struct field, an argument, an array element, or a PDA seed. That is most real programs, and six of this repo's own test IDLs (example-idl, system, token, token-2022, mpl-token-metadata, pmp) hit it. Add two tests that build the smallest such shape from the node constructors, both of which throw today: one asserts a resolvable link validates cleanly, the other asserts a missing link is reported as an error. So the fix must restore the visitor's ability to catch a broken link, not merely stop the crash. * fix(validators): make recordNodeStackVisitor follow prefix traversal rules getValidationItemsVisitor was the only visitor to run its per-node logic before committing the current node to the stack, so validation ran in the gap between arriving at a node and recording it. Two bugs fell out of that single ordering: - visitDefinedTypeLink calls stack.getPath(node.kind), which asserts the top of the stack is the link node; the link node was not committed yet, so the top was its parent, and any nested definedTypeLink (struct field, argument, array, PDA seed) threw `Expected node of kind [definedTypeLinkNode]`. Six of this repo's own test IDLs hit it, including example-idl.json, System, Token, and Token-2022. - every validationItem recorded an ancestors-only path, missing the node itself, which contradicts the documented ValidationItem.path contract ("the path of nodes that led to the node above, including the node itself"). Commit the node before the per-node logic runs, matching the eleven other recordNodeStackVisitor consumers (getByteSizeVisitor, and the rest): the node is on the stack when the visit runs, so getPath(node.kind) resolves, missing links are reported instead of throwing, and recorded paths satisfy the contract. The two existing tests asserted the off-by-one paths, so they are corrected to the documented behaviour. * prettier(validators): fix write :)
1 parent 3a75e06 commit 62af2ed

3 files changed

Lines changed: 85 additions & 9 deletions

File tree

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@codama/validators': patch
3+
---
4+
5+
Fix `getValidationItemsVisitor` throwing `Expected node of kind [definedTypeLinkNode]` on any nested `definedTypeLinkNode` (a struct field, argument, array element, or PDA seed), and recording `ValidationItem` paths that omitted the node itself. The visitor now composes `recordNodeStackVisitor` outermost, like every other visitor, so the current node is on the stack when validation runs.

packages/validators/src/getValidationItemsVisitor.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,6 @@ export function getValidationItemsVisitor(): Visitor<readonly ValidationItem[]>
2323
() => [] as readonly ValidationItem[],
2424
(_, items) => items.flat(),
2525
),
26-
v => recordLinkablesOnFirstVisitVisitor(v, linkables),
27-
v => recordNodeStackVisitor(v, stack),
2826
v =>
2927
extendVisitor(v, {
3028
visitAccount(node, { next }) {
@@ -243,5 +241,11 @@ export function getValidationItemsVisitor(): Visitor<readonly ValidationItem[]>
243241
return [...items, ...next(node)];
244242
},
245243
}),
244+
// Pipe stages run outermost-first, i.e. in reverse of their listing here:
245+
// pipe(init, g, h, i) => i(h(g(init))) -- i is the outer layer, runs first
246+
// so these record the node (onto the stack, and into linkables) BEFORE the
247+
// extendVisitor logic above runs and reads that state to validate the node.
248+
v => recordNodeStackVisitor(v, stack),
249+
v => recordLinkablesOnFirstVisitVisitor(v, linkables),
246250
);
247251
}

packages/validators/test/getValidationItemsVisitor.test.ts

Lines changed: 74 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,13 @@
1-
import { programNode, publicKeyTypeNode, structFieldTypeNode, structTypeNode, tupleTypeNode } from '@codama/nodes';
1+
import {
2+
definedTypeLinkNode,
3+
definedTypeNode,
4+
numberTypeNode,
5+
programNode,
6+
publicKeyTypeNode,
7+
structFieldTypeNode,
8+
structTypeNode,
9+
tupleTypeNode,
10+
} from '@codama/nodes';
211
import { visit } from '@codama/visitors-core';
312
import { expect, test } from 'vitest';
413

@@ -23,10 +32,10 @@ test('it validates program nodes', () => {
2332

2433
// Then we expect the following validation errors.
2534
expect(items).toEqual([
26-
validationItem('error', 'Program has no name.', node, []),
27-
validationItem('error', 'Program has no public key.', node, []),
28-
validationItem('warn', 'Program has no version.', node, []),
29-
validationItem('info', 'Program has no origin.', node, []),
35+
validationItem('error', 'Program has no name.', node, [node]),
36+
validationItem('error', 'Program has no public key.', node, [node]),
37+
validationItem('warn', 'Program has no version.', node, [node]),
38+
validationItem('info', 'Program has no origin.', node, [node]),
3039
]);
3140
});
3241

@@ -47,7 +56,65 @@ test('it validates nested nodes', () => {
4756
const tupleNode = node.items[0];
4857
const structNode = node.items[1];
4958
expect(items).toEqual([
50-
validationItem('warn', 'Tuple has no items.', tupleNode, [node]),
51-
validationItem('error', 'Struct field name "owner" is not unique.', structNode.fields[0], [node]),
59+
validationItem('warn', 'Tuple has no items.', tupleNode, [node, tupleNode]),
60+
validationItem('error', 'Struct field name "owner" is not unique.', structNode.fields[0], [node, structNode]),
61+
]);
62+
});
63+
64+
test('it validates a defined type link nested within a struct field', () => {
65+
// Given a program whose defined type links to another through a struct field.
66+
const node = programNode({
67+
accounts: [],
68+
definedTypes: [
69+
definedTypeNode({
70+
name: 'foo',
71+
type: structTypeNode([structFieldTypeNode({ name: 'bar', type: definedTypeLinkNode('baz') })]),
72+
}),
73+
definedTypeNode({ name: 'baz', type: numberTypeNode('u64') }),
74+
],
75+
errors: [],
76+
instructions: [],
77+
name: 'test',
78+
origin: 'anchor',
79+
publicKey: '11111111111111111111111111111111',
80+
version: '1.0.0',
81+
});
82+
83+
// When we get the validation items using a visitor.
84+
const items = visit(node, getValidationItemsVisitor());
85+
86+
// Then we expect no validation errors.
87+
expect(items).toEqual([]);
88+
});
89+
90+
test('it reports a nested defined type link that points at a missing type', () => {
91+
// Given a program whose defined type field links to a type that does not exist.
92+
const link = definedTypeLinkNode('missing');
93+
const field = structFieldTypeNode({ name: 'bar', type: link });
94+
const struct = structTypeNode([field]);
95+
const foo = definedTypeNode({ name: 'foo', type: struct });
96+
const node = programNode({
97+
accounts: [],
98+
definedTypes: [foo],
99+
errors: [],
100+
instructions: [],
101+
name: 'test',
102+
origin: 'anchor',
103+
publicKey: '11111111111111111111111111111111',
104+
version: '1.0.0',
105+
});
106+
107+
// When we get the validation items using a visitor.
108+
const items = visit(node, getValidationItemsVisitor());
109+
110+
// Then the missing link is reported as an error.
111+
expect(items).toEqual([
112+
validationItem('error', 'Pointing to a missing defined type named "missing"', link, [
113+
node,
114+
foo,
115+
struct,
116+
field,
117+
link,
118+
]),
52119
]);
53120
});

0 commit comments

Comments
 (0)