Skip to content

Commit 86485ca

Browse files
committed
Report defined types without a finite value in validators
1 parent 6510101 commit 86485ca

3 files changed

Lines changed: 194 additions & 6 deletions

File tree

‎packages/validators/src/ValidationItem.ts‎

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,11 @@ export type LogLevel = (typeof LOG_LEVELS)[number];
99
* A diagnostic reported when validating a Codama IDL.
1010
*
1111
* The `path` leads to the node the diagnostic is about, which is its last
12-
* node. When the diagnostic stems from a `CodamaError` thrown while
13-
* validating, e.g. a cyclic dependency between instruction inputs, that
14-
* error is kept as its `cause`, so consumers can match its code and context
15-
* rather than its message.
12+
* node. When the diagnostic corresponds to a `CodamaError`, that error is
13+
* kept as its `cause`, so consumers can match its code and context rather
14+
* than its message. It may be thrown while validating, e.g. a cyclic
15+
* dependency between instruction inputs, or be the error that creating a
16+
* codec would throw.
1617
*/
1718
export type ValidationItem<TNode extends Node = Node> = {
1819
cause?: CodamaError;
@@ -28,7 +29,7 @@ export type ValidationItem<TNode extends Node = Node> = {
2829
* @param message - A human-readable description of the diagnostic.
2930
* @param path - The path to the node the diagnostic is about, or the stack
3031
* whose current path leads to it.
31-
* @param cause - The `CodamaError` the diagnostic stems from, if any.
32+
* @param cause - The `CodamaError` the diagnostic matches, if any.
3233
* @throws `CODAMA_ERROR__UNEXPECTED_NODE_KIND` when given a stack with an
3334
* empty path, since a diagnostic must be about a node.
3435
*

‎packages/validators/src/getValidationItemsVisitor.ts‎

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
1-
import { isCodamaError } from '@codama/errors';
1+
import { CODAMA_ERROR__DEFINED_TYPE_HAS_NO_FINITE_VALUE, CodamaError, isCodamaError } from '@codama/errors';
22
import { isNode, REGISTERED_NODE_KINDS } from '@codama/nodes';
33
import {
44
extendVisitor,
5+
findProgramNodeFromPath,
56
getResolvedInstructionInputsVisitor,
7+
hasDefinedTypeFiniteValue,
8+
isDefinedTypeCyclic,
69
LinkableDictionary,
710
mergeVisitor,
811
NodeStack,
@@ -44,6 +47,24 @@ export function getValidationItemsVisitor(): Visitor<readonly ValidationItem[]>
4447
if (!node.identifier) {
4548
items.push(validationItem('error', 'Defined type has no identifier.', stack));
4649
}
50+
51+
// A cyclic type whose every value nests another one cannot be encoded or decoded. The
52+
// cyclic check matches the codecs, which only throw for links back to their own type:
53+
// an enum without variants alone is reported by its own warning. Links can only be
54+
// resolved within a program, so types outside one are skipped.
55+
const path = stack.getPath('definedTypeNode');
56+
if (
57+
findProgramNodeFromPath(path) &&
58+
isDefinedTypeCyclic(path, linkables) &&
59+
!hasDefinedTypeFiniteValue(path, linkables)
60+
) {
61+
// Keep the error creating its codec would throw, so both report it the same way.
62+
const cause = new CodamaError(CODAMA_ERROR__DEFINED_TYPE_HAS_NO_FINITE_VALUE, {
63+
name: node.identifier,
64+
path,
65+
});
66+
items.push(validationItem('error', cause.message, stack, cause));
67+
}
4768
return [...items, ...next(node)];
4869
},
4970

‎packages/validators/test/getValidationItemsVisitor.test.ts‎

Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import {
2+
CODAMA_ERROR__DEFINED_TYPE_HAS_NO_FINITE_VALUE,
23
CODAMA_ERROR__UNEXPECTED_NODE_KIND,
34
CODAMA_ERROR__VISITORS__CYCLIC_DEPENDENCY_DETECTED_WHEN_RESOLVING_INSTRUCTION_DEFAULT_VALUES,
45
CodamaError,
@@ -19,7 +20,9 @@ import {
1920
instructionStatusNode,
2021
integerTypeNode,
2122
integerValueNode,
23+
optionTypeNode,
2224
pluginNode,
25+
programLinkNode,
2326
programNode,
2427
ProgramNode,
2528
providedNode,
@@ -426,3 +429,166 @@ test('it reports text nodes without plugins', () => {
426429
]);
427430
expect(translatedItems).toEqual([]);
428431
});
432+
433+
test('it reports cyclic defined types without a finite value', () => {
434+
// Given a loop whose every value nests another one.
435+
const loop = definedTypeNode({
436+
identifier: 'loop',
437+
type: structTypeNode([structFieldTypeNode({ identifier: 'next', type: definedTypeLinkNode('loop') })]),
438+
});
439+
const program = programNode({ definedTypes: [loop], identifier: 'myProgram', publicKey: '1111', version: '1.0.0' });
440+
const root = rootNode(program);
441+
442+
const loopCause = new CodamaError(CODAMA_ERROR__DEFINED_TYPE_HAS_NO_FINITE_VALUE, {
443+
name: loop.identifier,
444+
path: [root, program, loop],
445+
});
446+
447+
// When we validate it.
448+
const items = visit(root, getValidationItemsVisitor());
449+
450+
// Then the loop is reported.
451+
expect(items).toStrictEqual([validationItem('error', loopCause.message, [root, program, loop], loopCause)]);
452+
});
453+
454+
test('it reports defined types aliasing themselves', () => {
455+
// Given a type aliasing itself.
456+
const alias = definedTypeNode({ identifier: 'alias', type: definedTypeLinkNode('alias') });
457+
const program = programNode({
458+
definedTypes: [alias],
459+
identifier: 'myProgram',
460+
publicKey: '1111',
461+
version: '1.0.0',
462+
});
463+
const root = rootNode(program);
464+
465+
const aliasCause = new CodamaError(CODAMA_ERROR__DEFINED_TYPE_HAS_NO_FINITE_VALUE, {
466+
name: alias.identifier,
467+
path: [root, program, alias],
468+
});
469+
470+
// When we validate it.
471+
const items = visit(root, getValidationItemsVisitor());
472+
473+
// Then the alias is reported.
474+
expect(items).toStrictEqual([validationItem('error', aliasCause.message, [root, program, alias], aliasCause)]);
475+
});
476+
477+
test('it accepts cyclic defined types with a finite value', () => {
478+
// Given a linked list that may end with `None`.
479+
const list = definedTypeNode({
480+
identifier: 'list',
481+
type: structTypeNode([
482+
structFieldTypeNode({
483+
identifier: 'next',
484+
type: optionTypeNode(definedTypeLinkNode('list'), { prefix: integerTypeNode('u8') }),
485+
}),
486+
]),
487+
});
488+
const program = programNode({ definedTypes: [list], identifier: 'myProgram', publicKey: '1111', version: '1.0.0' });
489+
490+
// When we validate it, then nothing is reported.
491+
expect(visit(rootNode(program), getValidationItemsVisitor())).toStrictEqual([]);
492+
});
493+
494+
test('it only reports the cyclic defined type when another one links to it', () => {
495+
// Given a type linking to a loop.
496+
const loop = definedTypeNode({
497+
identifier: 'loop',
498+
type: structTypeNode([structFieldTypeNode({ identifier: 'next', type: definedTypeLinkNode('loop') })]),
499+
});
500+
const usesLoop = definedTypeNode({ identifier: 'usesLoop', type: definedTypeLinkNode('loop') });
501+
const program = programNode({
502+
definedTypes: [loop, usesLoop],
503+
identifier: 'myProgram',
504+
publicKey: '1111',
505+
version: '1.0.0',
506+
});
507+
const root = rootNode(program);
508+
509+
const loopCause = new CodamaError(CODAMA_ERROR__DEFINED_TYPE_HAS_NO_FINITE_VALUE, {
510+
name: loop.identifier,
511+
path: [root, program, loop],
512+
});
513+
514+
// When we validate it.
515+
const items = visit(root, getValidationItemsVisitor());
516+
517+
// Then only the loop is reported.
518+
expect(items).toStrictEqual([validationItem('error', loopCause.message, [root, program, loop], loopCause)]);
519+
});
520+
521+
test('it reports every defined type of a cycle without a finite value', () => {
522+
// Given two types nesting each other.
523+
const a = definedTypeNode({
524+
identifier: 'a',
525+
type: structTypeNode([structFieldTypeNode({ identifier: 'b', type: definedTypeLinkNode('b') })]),
526+
});
527+
const b = definedTypeNode({
528+
identifier: 'b',
529+
type: structTypeNode([structFieldTypeNode({ identifier: 'a', type: definedTypeLinkNode('a') })]),
530+
});
531+
const program = programNode({ definedTypes: [a, b], identifier: 'myProgram', publicKey: '1111', version: '1.0.0' });
532+
const root = rootNode(program);
533+
534+
const aCause = new CodamaError(CODAMA_ERROR__DEFINED_TYPE_HAS_NO_FINITE_VALUE, {
535+
name: a.identifier,
536+
path: [root, program, a],
537+
});
538+
539+
const bCause = new CodamaError(CODAMA_ERROR__DEFINED_TYPE_HAS_NO_FINITE_VALUE, {
540+
name: b.identifier,
541+
path: [root, program, b],
542+
});
543+
544+
// When we validate them.
545+
const items = visit(root, getValidationItemsVisitor());
546+
547+
// Then both are reported.
548+
expect(items).toStrictEqual([
549+
validationItem('error', aCause.message, [root, program, a], aCause),
550+
validationItem('error', bCause.message, [root, program, b], bCause),
551+
]);
552+
});
553+
554+
test('it does not mistake links to same-named defined types of other programs for cycles', () => {
555+
// Given a type of program A linking to a same-named type of program B.
556+
const typeA = definedTypeNode({
557+
identifier: 'config',
558+
type: definedTypeLinkNode('config', { program: programLinkNode('programB') }),
559+
});
560+
const typeB = definedTypeNode({ identifier: 'config', type: integerTypeNode('u32') });
561+
const programA = programNode({
562+
definedTypes: [typeA],
563+
identifier: 'programA',
564+
publicKey: '1111',
565+
version: '1.0.0',
566+
});
567+
const programB = programNode({
568+
definedTypes: [typeB],
569+
identifier: 'programB',
570+
publicKey: '2222',
571+
version: '1.0.0',
572+
});
573+
574+
// When we validate them, then nothing is reported.
575+
expect(visit(rootNode(programA, { additionalPrograms: [programB] }), getValidationItemsVisitor())).toStrictEqual(
576+
[],
577+
);
578+
});
579+
580+
test('it does not check the values of defined types outside a program', () => {
581+
// Given a loop validated on its own.
582+
const link = definedTypeLinkNode('loop');
583+
const field = structFieldTypeNode({ identifier: 'next', type: link });
584+
const struct = structTypeNode([field]);
585+
const loop = definedTypeNode({ identifier: 'loop', type: struct });
586+
587+
// When we validate it.
588+
const items = visit(loop, getValidationItemsVisitor());
589+
590+
// Then it does not throw, and only reports the link it cannot resolve without a program.
591+
expect(items).toStrictEqual([
592+
validationItem('error', 'Pointing to a missing defined type named "loop"', [loop, struct, field, link]),
593+
]);
594+
});

0 commit comments

Comments
 (0)