diff --git a/.changeset/fix-validator-nested-defined-type-link.md b/.changeset/fix-validator-nested-defined-type-link.md new file mode 100644 index 000000000..b46938284 --- /dev/null +++ b/.changeset/fix-validator-nested-defined-type-link.md @@ -0,0 +1,5 @@ +--- +'@codama/validators': patch +--- + +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. diff --git a/packages/validators/src/getValidationItemsVisitor.ts b/packages/validators/src/getValidationItemsVisitor.ts index 753493244..c909af8a8 100644 --- a/packages/validators/src/getValidationItemsVisitor.ts +++ b/packages/validators/src/getValidationItemsVisitor.ts @@ -23,8 +23,6 @@ export function getValidationItemsVisitor(): Visitor () => [] as readonly ValidationItem[], (_, items) => items.flat(), ), - v => recordLinkablesOnFirstVisitVisitor(v, linkables), - v => recordNodeStackVisitor(v, stack), v => extendVisitor(v, { visitAccount(node, { next }) { @@ -243,5 +241,11 @@ export function getValidationItemsVisitor(): Visitor return [...items, ...next(node)]; }, }), + // Pipe stages run outermost-first, i.e. in reverse of their listing here: + // pipe(init, g, h, i) => i(h(g(init))) -- i is the outer layer, runs first + // so these record the node (onto the stack, and into linkables) BEFORE the + // extendVisitor logic above runs and reads that state to validate the node. + v => recordNodeStackVisitor(v, stack), + v => recordLinkablesOnFirstVisitVisitor(v, linkables), ); } diff --git a/packages/validators/test/getValidationItemsVisitor.test.ts b/packages/validators/test/getValidationItemsVisitor.test.ts index c57d9866a..cad024b05 100644 --- a/packages/validators/test/getValidationItemsVisitor.test.ts +++ b/packages/validators/test/getValidationItemsVisitor.test.ts @@ -1,4 +1,13 @@ -import { programNode, publicKeyTypeNode, structFieldTypeNode, structTypeNode, tupleTypeNode } from '@codama/nodes'; +import { + definedTypeLinkNode, + definedTypeNode, + numberTypeNode, + programNode, + publicKeyTypeNode, + structFieldTypeNode, + structTypeNode, + tupleTypeNode, +} from '@codama/nodes'; import { visit } from '@codama/visitors-core'; import { expect, test } from 'vitest'; @@ -23,10 +32,10 @@ test('it validates program nodes', () => { // Then we expect the following validation errors. expect(items).toEqual([ - validationItem('error', 'Program has no name.', node, []), - validationItem('error', 'Program has no public key.', node, []), - validationItem('warn', 'Program has no version.', node, []), - validationItem('info', 'Program has no origin.', node, []), + validationItem('error', 'Program has no name.', node, [node]), + validationItem('error', 'Program has no public key.', node, [node]), + validationItem('warn', 'Program has no version.', node, [node]), + validationItem('info', 'Program has no origin.', node, [node]), ]); }); @@ -47,7 +56,65 @@ test('it validates nested nodes', () => { const tupleNode = node.items[0]; const structNode = node.items[1]; expect(items).toEqual([ - validationItem('warn', 'Tuple has no items.', tupleNode, [node]), - validationItem('error', 'Struct field name "owner" is not unique.', structNode.fields[0], [node]), + validationItem('warn', 'Tuple has no items.', tupleNode, [node, tupleNode]), + validationItem('error', 'Struct field name "owner" is not unique.', structNode.fields[0], [node, structNode]), + ]); +}); + +test('it validates a defined type link nested within a struct field', () => { + // Given a program whose defined type links to another through a struct field. + const node = programNode({ + accounts: [], + definedTypes: [ + definedTypeNode({ + name: 'foo', + type: structTypeNode([structFieldTypeNode({ name: 'bar', type: definedTypeLinkNode('baz') })]), + }), + definedTypeNode({ name: 'baz', type: numberTypeNode('u64') }), + ], + errors: [], + instructions: [], + name: 'test', + origin: 'anchor', + publicKey: '11111111111111111111111111111111', + version: '1.0.0', + }); + + // When we get the validation items using a visitor. + const items = visit(node, getValidationItemsVisitor()); + + // Then we expect no validation errors. + expect(items).toEqual([]); +}); + +test('it reports a nested defined type link that points at a missing type', () => { + // Given a program whose defined type field links to a type that does not exist. + const link = definedTypeLinkNode('missing'); + const field = structFieldTypeNode({ name: 'bar', type: link }); + const struct = structTypeNode([field]); + const foo = definedTypeNode({ name: 'foo', type: struct }); + const node = programNode({ + accounts: [], + definedTypes: [foo], + errors: [], + instructions: [], + name: 'test', + origin: 'anchor', + publicKey: '11111111111111111111111111111111', + version: '1.0.0', + }); + + // When we get the validation items using a visitor. + const items = visit(node, getValidationItemsVisitor()); + + // Then the missing link is reported as an error. + expect(items).toEqual([ + validationItem('error', 'Pointing to a missing defined type named "missing"', link, [ + node, + foo, + struct, + field, + link, + ]), ]); });