Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/fix-validator-nested-defined-type-link.md
Original file line number Diff line number Diff line change
@@ -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.
8 changes: 6 additions & 2 deletions packages/validators/src/getValidationItemsVisitor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,6 @@ export function getValidationItemsVisitor(): Visitor<readonly ValidationItem[]>
() => [] as readonly ValidationItem[],
(_, items) => items.flat(),
),
v => recordLinkablesOnFirstVisitVisitor(v, linkables),
v => recordNodeStackVisitor(v, stack),
v =>
extendVisitor(v, {
visitAccount(node, { next }) {
Expand Down Expand Up @@ -243,5 +241,11 @@ export function getValidationItemsVisitor(): Visitor<readonly ValidationItem[]>
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),
);
}
75 changes: 68 additions & 7 deletions packages/validators/test/getValidationItemsVisitor.test.ts
Original file line number Diff line number Diff line change
@@ -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';

Expand All @@ -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]),
]);
});

Expand All @@ -47,7 +56,59 @@ 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]),
]);
});