From f19ab36118ee2316be1e596976590a0f598cb937 Mon Sep 17 00:00:00 2001 From: cds-amal Date: Wed, 1 Jul 2026 19:30:50 -0400 Subject: [PATCH 1/3] 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. --- .../test/getValidationItemsVisitor.test.ts | 63 ++++++++++++++++++- 1 file changed, 62 insertions(+), 1 deletion(-) diff --git a/packages/validators/test/getValidationItemsVisitor.test.ts b/packages/validators/test/getValidationItemsVisitor.test.ts index c57d9866a..dc77bdf58 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'; @@ -51,3 +60,55 @@ test('it validates nested nodes', () => { validationItem('error', 'Struct field name "owner" is not unique.', structNode.fields[0], [node]), ]); }); + +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]), + ]); +}); From d88b551975721988ead36bbb6a26cd2e4d65d9fc Mon Sep 17 00:00:00 2001 From: cds-amal Date: Wed, 1 Jul 2026 19:31:30 -0400 Subject: [PATCH 2/3] 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. --- .changeset/fix-validator-nested-defined-type-link.md | 5 +++++ packages/validators/src/getValidationItemsVisitor.ts | 8 ++++++-- .../test/getValidationItemsVisitor.test.ts | 12 ++++++------ 3 files changed, 17 insertions(+), 8 deletions(-) create mode 100644 .changeset/fix-validator-nested-defined-type-link.md 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 dc77bdf58..6fec7501a 100644 --- a/packages/validators/test/getValidationItemsVisitor.test.ts +++ b/packages/validators/test/getValidationItemsVisitor.test.ts @@ -32,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]), ]); }); @@ -56,8 +56,8 @@ 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]), ]); }); From 0ce540c5651cd3faf948b546f7079ad21ad81c33 Mon Sep 17 00:00:00 2001 From: cds-amal Date: Mon, 13 Jul 2026 09:21:23 -0400 Subject: [PATCH 3/3] prettier(validators): fix write :) --- .../validators/test/getValidationItemsVisitor.test.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/packages/validators/test/getValidationItemsVisitor.test.ts b/packages/validators/test/getValidationItemsVisitor.test.ts index 6fec7501a..cad024b05 100644 --- a/packages/validators/test/getValidationItemsVisitor.test.ts +++ b/packages/validators/test/getValidationItemsVisitor.test.ts @@ -109,6 +109,12 @@ test('it reports a nested defined type link that points at a missing type', () = // 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]), + validationItem('error', 'Pointing to a missing defined type named "missing"', link, [ + node, + foo, + struct, + field, + link, + ]), ]); });