Forráskód Böngészése

Add validators package (#7)

Loris Leiva 1 éve
szülő
commit
586b3e6fb0

+ 1 - 0
packages/errors/src/context.ts

@@ -96,6 +96,7 @@ export type KinobiErrorContext = DefaultUnspecifiedErrorContextToUndefined<{
         instructionName: CamelCaseString;
     };
     [KINOBI_ERROR__VISITORS__FAILED_TO_VALIDATE_NODE]: {
+        formattedHistogram: string;
         validationItems: ValidationItem[];
     };
     [KINOBI_ERROR__VISITORS__INSTRUCTION_ENUM_ARGUMENT_NOT_FOUND]: {

+ 1 - 1
packages/errors/src/messages.ts

@@ -46,7 +46,7 @@ export const KinobiErrorMessages: Readonly<{
         'Cannot use optional account [$seedValueName] as the [$seedName] PDA seed for the [$instructionAccountName] account of the [$instruction] instruction.',
     [KINOBI_ERROR__VISITORS__CYCLIC_DEPENDENCY_DETECTED_WHEN_RESOLVING_INSTRUCTION_DEFAULT_VALUES]:
         "Circular dependency detected when resolving the accounts and arguments' default values of the [$instructionName] instruction. Got the following dependency cycle [$formattedCycle].",
-    [KINOBI_ERROR__VISITORS__FAILED_TO_VALIDATE_NODE]: 'Failed to validate the given node.',
+    [KINOBI_ERROR__VISITORS__FAILED_TO_VALIDATE_NODE]: 'Failed to validate the given node [$formattedHistogram].',
     [KINOBI_ERROR__VISITORS__INSTRUCTION_ENUM_ARGUMENT_NOT_FOUND]:
         'Could not find an enum argument named [$argumentName] for instruction [$instructionName].',
     [KINOBI_ERROR__VISITORS__INVALID_INSTRUCTION_DEFAULT_VALUE_DEPENDENCY]:

+ 1 - 0
packages/validators/.gitignore

@@ -0,0 +1 @@
+dist/

+ 1 - 0
packages/validators/.prettierignore

@@ -0,0 +1 @@
+dist/

+ 23 - 0
packages/validators/LICENSE

@@ -0,0 +1,23 @@
+MIT License
+
+Copyright (c) 2024 Kinobi
+Copyright (c) 2024 Metaplex Foundation
+
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of this software and associated documentation files (the
+"Software"), to deal in the Software without restriction, including
+without limitation the rights to use, copy, modify, merge, publish,
+distribute, sublicense, and/or sell copies of the Software, and to
+permit persons to whom the Software is furnished to do so, subject to
+the following conditions:
+
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

+ 3 - 0
packages/validators/README.md

@@ -0,0 +1,3 @@
+# Kinobi ➤ Validators
+
+TODO

+ 69 - 0
packages/validators/package.json

@@ -0,0 +1,69 @@
+{
+    "name": "@kinobi-so/validators",
+    "version": "0.20.0",
+    "description": "Validator visitors for the Kinobi framework",
+    "exports": {
+        "react-native": "./dist/index.react-native.js",
+        "browser": {
+            "import": "./dist/index.browser.js",
+            "require": "./dist/index.browser.cjs"
+        },
+        "node": {
+            "import": "./dist/index.node.js",
+            "require": "./dist/index.node.cjs"
+        },
+        "types": "./dist/types/index.d.ts"
+    },
+    "browser": {
+        "./dist/index.node.cjs": "./dist/index.browser.cjs",
+        "./dist/index.node.js": "./dist/index.browser.js"
+    },
+    "main": "./dist/index.node.cjs",
+    "module": "./dist/index.node.js",
+    "react-native": "./dist/index.react-native.js",
+    "types": "./dist/types/index.d.ts",
+    "type": "module",
+    "files": [
+        "./dist/types",
+        "./dist/index.*"
+    ],
+    "sideEffects": false,
+    "keywords": [
+        "solana",
+        "framework",
+        "standard",
+        "visitors"
+    ],
+    "scripts": {
+        "build": "rimraf dist && pnpm build:src && pnpm build:types",
+        "build:src": "zx ../../node_modules/@kinobi-so/internals/scripts/build-src.mjs package",
+        "build:types": "zx ../../node_modules/@kinobi-so/internals/scripts/build-types.mjs",
+        "dev": "zx ../../node_modules/@kinobi-so/internals/scripts/test-unit.mjs browser --watch",
+        "lint": "zx ../../node_modules/@kinobi-so/internals/scripts/lint.mjs",
+        "lint:fix": "zx ../../node_modules/@kinobi-so/internals/scripts/lint.mjs --fix",
+        "prepublishOnly": "pnpm build",
+        "test": "pnpm test:types && pnpm test:treeshakability && pnpm test:browser && pnpm test:node && pnpm test:react-native",
+        "test:browser": "zx ../../node_modules/@kinobi-so/internals/scripts/test-unit.mjs browser",
+        "test:node": "zx ../../node_modules/@kinobi-so/internals/scripts/test-unit.mjs node",
+        "test:react-native": "zx ../../node_modules/@kinobi-so/internals/scripts/test-unit.mjs react-native",
+        "test:treeshakability": "zx ../../node_modules/@kinobi-so/internals/scripts/test-treeshakability.mjs",
+        "test:types": "zx ../../node_modules/@kinobi-so/internals/scripts/test-types.mjs"
+    },
+    "dependencies": {
+        "@kinobi-so/errors": "workspace:*",
+        "@kinobi-so/nodes": "workspace:*",
+        "@kinobi-so/visitors-core": "workspace:*"
+    },
+    "license": "MIT",
+    "repository": {
+        "type": "git",
+        "url": "https://github.com/kinobi-so/kinobi"
+    },
+    "bugs": {
+        "url": "http://github.com/kinobi-so/kinobi/issues"
+    },
+    "browserslist": [
+        "supports bigint and not dead",
+        "maintained node versions"
+    ]
+}

+ 28 - 0
packages/validators/src/ValidationItem.ts

@@ -0,0 +1,28 @@
+import { Node } from '@kinobi-so/nodes';
+import { NodeStack } from '@kinobi-so/visitors-core';
+
+export const LOG_LEVELS = ['debug', 'trace', 'info', 'warn', 'error'] as const;
+export type LogLevel = (typeof LOG_LEVELS)[number];
+
+export type ValidationItem = {
+    level: LogLevel;
+    message: string;
+    node: Node;
+    stack: readonly Node[];
+};
+
+export function validationItem(
+    level: LogLevel,
+    message: string,
+    node: Node,
+    stack: Node[] | NodeStack,
+): ValidationItem {
+    return {
+        level,
+        message,
+        node,
+        stack: Array.isArray(stack) ? [...stack] : stack.all(),
+    };
+}
+
+export const getLevelIndex = (level: LogLevel): number => LOG_LEVELS.indexOf(level);

+ 247 - 0
packages/validators/src/getValidationItemsVisitor.ts

@@ -0,0 +1,247 @@
+import { camelCase, getAllInstructionArguments, isNode } from '@kinobi-so/nodes';
+import {
+    extendVisitor,
+    getResolvedInstructionInputsVisitor,
+    LinkableDictionary,
+    mergeVisitor,
+    NodeStack,
+    pipe,
+    recordLinkablesVisitor,
+    recordNodeStackVisitor,
+    visit,
+    Visitor,
+} from '@kinobi-so/visitors-core';
+
+import { ValidationItem, validationItem } from './ValidationItem';
+
+export function getValidationItemsVisitor(): Visitor<readonly ValidationItem[]> {
+    const linkables = new LinkableDictionary();
+    const stack = new NodeStack();
+
+    return pipe(
+        mergeVisitor(
+            () => [] as readonly ValidationItem[],
+            (_, items) => items.flat(),
+        ),
+        v => recordLinkablesVisitor(v, linkables),
+        v => recordNodeStackVisitor(v, stack),
+        v =>
+            extendVisitor(v, {
+                visitAccount(node, { next }) {
+                    const items = [] as ValidationItem[];
+                    if (!node.name) {
+                        items.push(validationItem('error', 'Account has no name.', node, stack));
+                    }
+                    return [...items, ...next(node)];
+                },
+
+                visitDefinedType(node, { next }) {
+                    const items = [] as ValidationItem[];
+                    if (!node.name) {
+                        items.push(validationItem('error', 'Defined type has no name.', node, stack));
+                    }
+                    return [...items, ...next(node)];
+                },
+
+                visitDefinedTypeLink(node, { next }) {
+                    const items = [] as ValidationItem[];
+                    if (!node.name) {
+                        items.push(validationItem('error', 'Pointing to a defined type with no name.', node, stack));
+                    } else if (!node.importFrom && !linkables.has(node)) {
+                        items.push(
+                            validationItem(
+                                'error',
+                                `Pointing to a missing defined type named "${node.name}"`,
+                                node,
+                                stack,
+                            ),
+                        );
+                    }
+                    return [...items, ...next(node)];
+                },
+
+                visitEnumEmptyVariantType(node, { next }) {
+                    const items = [] as ValidationItem[];
+                    if (!node.name) {
+                        items.push(validationItem('error', 'Enum variant has no name.', node, stack));
+                    }
+                    return [...items, ...next(node)];
+                },
+
+                visitEnumStructVariantType(node, { next }) {
+                    const items = [] as ValidationItem[];
+                    if (!node.name) {
+                        items.push(validationItem('error', 'Enum variant has no name.', node, stack));
+                    }
+                    return [...items, ...next(node)];
+                },
+
+                visitEnumTupleVariantType(node, { next }) {
+                    const items = [] as ValidationItem[];
+                    if (!node.name) {
+                        items.push(validationItem('error', 'Enum variant has no name.', node, stack));
+                    }
+                    return [...items, ...next(node)];
+                },
+
+                visitEnumType(node, { next }) {
+                    const items = [] as ValidationItem[];
+                    if (node.variants.length === 0) {
+                        items.push(validationItem('warn', 'Enum has no variants.', node, stack));
+                    }
+                    node.variants.forEach(variant => {
+                        if (!variant.name) {
+                            items.push(validationItem('error', 'Enum variant has no name.', node, stack));
+                        }
+                    });
+                    return [...items, ...next(node)];
+                },
+
+                visitError(node, { next }) {
+                    const items = [] as ValidationItem[];
+                    if (!node.name) {
+                        items.push(validationItem('error', 'Error has no name.', node, stack));
+                    }
+                    if (typeof node.code !== 'number') {
+                        items.push(validationItem('error', 'Error has no code.', node, stack));
+                    }
+                    if (!node.message) {
+                        items.push(validationItem('warn', 'Error has no message.', node, stack));
+                    }
+                    return [...items, ...next(node)];
+                },
+
+                visitInstruction(node, { next }) {
+                    const items = [] as ValidationItem[];
+                    if (!node.name) {
+                        items.push(validationItem('error', 'Instruction has no name.', node, stack));
+                    }
+
+                    // Check for duplicate account names.
+                    const accountNameHistogram = new Map<string, number>();
+                    node.accounts.forEach(account => {
+                        if (!account.name) {
+                            items.push(validationItem('error', 'Instruction account has no name.', node, stack));
+                            return;
+                        }
+                        const count = (accountNameHistogram.get(account.name) ?? 0) + 1;
+                        accountNameHistogram.set(account.name, count);
+                        // Only throw an error once per duplicated names.
+                        if (count === 2) {
+                            items.push(
+                                validationItem(
+                                    'error',
+                                    `Account name "${account.name}" is not unique in instruction "${node.name}".`,
+                                    node,
+                                    stack,
+                                ),
+                            );
+                        }
+                    });
+
+                    // Check for cyclic dependencies in account defaults.
+                    const cyclicCheckVisitor = getResolvedInstructionInputsVisitor();
+                    try {
+                        visit(node, cyclicCheckVisitor);
+                    } catch (error) {
+                        items.push(validationItem('error', (error as Error).message, node, stack));
+                    }
+
+                    // Check args.
+                    const names = getAllInstructionArguments(node).map(({ name }) => camelCase(name));
+                    const duplicates = names.filter((e, i, a) => a.indexOf(e) !== i);
+                    const uniqueDuplicates = [...new Set(duplicates)];
+                    const hasConflictingNames = uniqueDuplicates.length > 0;
+                    if (hasConflictingNames) {
+                        items.push(
+                            validationItem(
+                                'error',
+                                `The names of the following instruction arguments are conflicting: ` +
+                                    `[${uniqueDuplicates.join(', ')}].`,
+                                node,
+                                stack,
+                            ),
+                        );
+                    }
+
+                    // Check arg defaults.
+                    getAllInstructionArguments(node).forEach(argument => {
+                        const { defaultValue } = argument;
+                        if (isNode(defaultValue, 'accountBumpValueNode')) {
+                            const defaultAccount = node.accounts.find(account => account.name === defaultValue.name);
+                            if (defaultAccount && defaultAccount.isSigner !== false) {
+                                items.push(
+                                    validationItem(
+                                        'error',
+                                        `Argument ${argument.name} cannot default to the bump attribute of ` +
+                                            `the [${defaultValue.name}] account as it may be a Signer.`,
+                                        node,
+                                        stack,
+                                    ),
+                                );
+                            }
+                        }
+                    });
+
+                    return [...items, ...next(node)];
+                },
+
+                visitProgram(node, { next }) {
+                    const items = [] as ValidationItem[];
+                    if (!node.name) {
+                        items.push(validationItem('error', 'Program has no name.', node, stack));
+                    }
+                    if (!node.publicKey) {
+                        items.push(validationItem('error', 'Program has no public key.', node, stack));
+                    }
+                    if (!node.version) {
+                        items.push(validationItem('warn', 'Program has no version.', node, stack));
+                    }
+                    if (!node.origin) {
+                        items.push(validationItem('info', 'Program has no origin.', node, stack));
+                    }
+                    return [...items, ...next(node)];
+                },
+
+                visitStructFieldType(node, { next }) {
+                    const items = [] as ValidationItem[];
+                    if (!node.name) {
+                        items.push(validationItem('error', 'Struct field has no name.', node, stack));
+                    }
+                    return [...items, ...next(node)];
+                },
+
+                visitStructType(node, { next }) {
+                    const items = [] as ValidationItem[];
+
+                    // Check for duplicate field names.
+                    const fieldNameHistogram = new Map<string, number>();
+                    node.fields.forEach(field => {
+                        if (!field.name) return; // Handled by TypeStructField
+                        const count = (fieldNameHistogram.get(field.name) ?? 0) + 1;
+                        fieldNameHistogram.set(field.name, count);
+                        // Only throw an error once per duplicated names.
+                        if (count === 2) {
+                            items.push(
+                                validationItem(
+                                    'error',
+                                    `Struct field name "${field.name}" is not unique.`,
+                                    field,
+                                    stack,
+                                ),
+                            );
+                        }
+                    });
+                    return [...items, ...next(node)];
+                },
+
+                visitTupleType(node, { next }) {
+                    const items = [] as ValidationItem[];
+                    if (node.items.length === 0) {
+                        items.push(validationItem('warn', 'Tuple has no items.', node, stack));
+                    }
+                    return [...items, ...next(node)];
+                },
+            }),
+    );
+}

+ 2 - 0
packages/validators/src/index.ts

@@ -0,0 +1,2 @@
+export * from './getValidationItemsVisitor';
+export * from './ValidationItem';

+ 35 - 0
packages/validators/src/throwValidatorItemsVisitor.ts

@@ -0,0 +1,35 @@
+import { KINOBI_ERROR__VISITORS__FAILED_TO_VALIDATE_NODE, KinobiError } from '@kinobi-so/errors';
+import { NodeKind } from '@kinobi-so/nodes';
+import { mapVisitor, Visitor } from '@kinobi-so/visitors-core';
+
+import { getLevelIndex, LogLevel, ValidationItem } from './ValidationItem';
+
+export function throwValidatorItemsVisitor<TNodeKind extends NodeKind = NodeKind>(
+    visitor: Visitor<readonly ValidationItem[], TNodeKind>,
+    throwLevel: LogLevel = 'error',
+): Visitor<void, TNodeKind> {
+    return mapVisitor(visitor, validationItems => {
+        const levelHistogram = [...validationItems]
+            .sort((a, b) => getLevelIndex(b.level) - getLevelIndex(a.level))
+            .reduce(
+                (acc, item) => {
+                    acc[item.level] = (acc[item.level] ?? 0) + 1;
+                    return acc;
+                },
+                {} as Record<LogLevel, number>,
+            );
+        const maxLevel = Object.keys(levelHistogram)
+            .map(level => getLevelIndex(level as LogLevel))
+            .sort((a, b) => b - a)[0];
+
+        if (maxLevel >= getLevelIndex(throwLevel)) {
+            const formattedHistogram = Object.keys(levelHistogram)
+                .map(level => `${level}s: ${levelHistogram[level as LogLevel]}`)
+                .join(', ');
+            throw new KinobiError(KINOBI_ERROR__VISITORS__FAILED_TO_VALIDATE_NODE, {
+                formattedHistogram,
+                validationItems,
+            });
+        }
+    });
+}

+ 53 - 0
packages/validators/test/getValidationItemsVisitor.test.ts

@@ -0,0 +1,53 @@
+import { programNode, publicKeyTypeNode, structFieldTypeNode, structTypeNode, tupleTypeNode } from '@kinobi-so/nodes';
+import { visit } from '@kinobi-so/visitors-core';
+import test from 'ava';
+
+import { getValidationItemsVisitor, validationItem } from '../src/index.js';
+
+test('it validates program nodes', t => {
+    // Given the following program node with empty strings.
+    const node = programNode({
+        accounts: [],
+        definedTypes: [],
+        errors: [],
+        instructions: [],
+        name: '',
+        origin: undefined,
+        publicKey: '',
+        // @ts-expect-error Empty string does not match ProgramVersion.
+        version: '',
+    });
+
+    // When we get the validation items using a visitor.
+    const items = visit(node, getValidationItemsVisitor());
+
+    // Then we expect the following validation errors.
+    t.deepEqual(items, [
+        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, []),
+    ]);
+});
+
+test('it validates nested nodes', t => {
+    // Given the following tuple with nested issues.
+    const node = tupleTypeNode([
+        tupleTypeNode([]),
+        structTypeNode([
+            structFieldTypeNode({ name: 'owner', type: publicKeyTypeNode() }),
+            structFieldTypeNode({ name: 'owner', type: publicKeyTypeNode() }),
+        ]),
+    ]);
+
+    // When we get the validation items using a visitor.
+    const items = visit(node, getValidationItemsVisitor());
+
+    // Then we expect the following validation errors.
+    const tupleNode = node.items[0];
+    const structNode = node.items[1];
+    t.deepEqual(items, [
+        validationItem('warn', 'Tuple has no items.', tupleNode, [node]),
+        validationItem('error', 'Struct field name "owner" is not unique.', structNode.fields[0], [node]),
+    ]);
+});

+ 10 - 0
packages/validators/tsconfig.declarations.json

@@ -0,0 +1,10 @@
+{
+    "compilerOptions": {
+        "declaration": true,
+        "declarationMap": true,
+        "emitDeclarationOnly": true,
+        "outDir": "./dist/types"
+    },
+    "extends": "./tsconfig.json",
+    "include": ["src/index.ts", "src/types"]
+}

+ 7 - 0
packages/validators/tsconfig.json

@@ -0,0 +1,7 @@
+{
+    "$schema": "https://json.schemastore.org/tsconfig",
+    "compilerOptions": { "lib": [] },
+    "display": "@kinobi-so/validators",
+    "extends": "../internals/tsconfig.base.json",
+    "include": ["src", "test"]
+}

+ 1 - 1
packages/visitors-core/src/NodeSelector.ts

@@ -53,7 +53,7 @@ export const getNodeSelectorFunction = (selector: NodeSelector): NodeSelectorFun
     const nodeSelectors = selector.split('.');
     const lastNodeSelector = nodeSelectors.pop() as string;
 
-    return (node, stack) => checkNode(node, lastNodeSelector) && checkStack(stack.all(), [...nodeSelectors]);
+    return (node, stack) => checkNode(node, lastNodeSelector) && checkStack(stack.all() as Node[], [...nodeSelectors]);
 };
 
 export const getConjunctiveNodeSelectorFunction = (selector: NodeSelector | NodeSelector[]): NodeSelectorFunction => {

+ 2 - 14
packages/visitors-core/src/NodeStack.ts

@@ -1,4 +1,4 @@
-import { camelCase, isNodeFilter, Node, ProgramNode } from '@kinobi-so/nodes';
+import { isNodeFilter, Node, ProgramNode } from '@kinobi-so/nodes';
 
 export class NodeStack {
     private readonly stack: Node[];
@@ -23,7 +23,7 @@ export class NodeStack {
         return this.stack.find(isNodeFilter('programNode'));
     }
 
-    public all(): Node[] {
+    public all(): readonly Node[] {
         return [...this.stack];
     }
 
@@ -44,16 +44,4 @@ export class NodeStack {
             return 'name' in node ? `[${node.kind}]${node.name}` : `[${node.kind}]`;
         });
     }
-
-    public matchesWithNames(names: string[]): boolean {
-        const remainingNames = [...names].map(camelCase);
-        this.stack.forEach(node => {
-            const nodeName = (node as { name?: string }).name;
-            if (nodeName && remainingNames.length > 0 && remainingNames[0] === camelCase(nodeName)) {
-                remainingNames.shift();
-            }
-        });
-
-        return remainingNames.length === 0;
-    }
 }

+ 3 - 1
packages/visitors/src/getResolvedInstructionInputsVisitor.ts → packages/visitors-core/src/getResolvedInstructionInputsVisitor.ts

@@ -18,7 +18,9 @@ import {
     isNode,
     VALUE_NODES,
 } from '@kinobi-so/nodes';
-import { singleNodeVisitor, Visitor } from '@kinobi-so/visitors-core';
+
+import { singleNodeVisitor } from './singleNodeVisitor';
+import { Visitor } from './visitor';
 
 export type ResolvedInstructionInput = ResolvedInstructionAccount | ResolvedInstructionArgument;
 export type ResolvedInstructionAccount = InstructionAccountNode & {

+ 1 - 0
packages/visitors-core/src/index.ts

@@ -6,6 +6,7 @@ export * from './consoleLogVisitor';
 export * from './deleteNodesVisitor';
 export * from './extendVisitor';
 export * from './getDebugStringVisitor';
+export * from './getResolvedInstructionInputsVisitor';
 export * from './getUniqueHashStringVisitor';
 export * from './identityVisitor';
 export * from './interceptVisitor';

+ 12 - 9
packages/visitors-core/test/extendVisitor.test.ts

@@ -1,3 +1,4 @@
+import { KINOBI_ERROR__VISITORS__CANNOT_EXTEND_MISSING_VISIT_FUNCTION, KinobiError } from '@kinobi-so/errors';
 import { numberTypeNode, publicKeyTypeNode, tupleTypeNode } from '@kinobi-so/nodes';
 import test from 'ava';
 
@@ -52,14 +53,16 @@ test('it cannot extends nodes that are not supported by the base visitor', t =>
     const baseVisitor = voidVisitor(['tupleTypeNode']);
 
     // Then we expect an error when we try to extend other nodes for that visitor.
-    t.throws(
-        () =>
-            extendVisitor(baseVisitor, {
-                // @ts-expect-error NumberTypeNode is not part of the base visitor.
-                visitNumberType: () => undefined,
-            }),
-        {
-            message: 'Cannot extend visitor with function "visitNumberType" as the base visitor does not support it.',
-        },
+    const error = t.throws(() =>
+        extendVisitor(baseVisitor, {
+            // @ts-expect-error NumberTypeNode is not part of the base visitor.
+            visitNumberType: () => undefined,
+        }),
+    );
+    t.deepEqual(
+        error,
+        new KinobiError(KINOBI_ERROR__VISITORS__CANNOT_EXTEND_MISSING_VISIT_FUNCTION, {
+            visitFunction: 'visitNumberType',
+        }),
     );
 });

+ 1 - 2
packages/visitors/test/getResolvedInstructionInputsVisitor.test.ts → packages/visitors-core/test/getResolvedInstructionInputsVisitor.test.ts

@@ -6,10 +6,9 @@ import {
     numberTypeNode,
     publicKeyTypeNode,
 } from '@kinobi-so/nodes';
-import { visit } from '@kinobi-so/visitors-core';
 import test from 'ava';
 
-import { getResolvedInstructionInputsVisitor } from '../src/index.js';
+import { getResolvedInstructionInputsVisitor, visit } from '../src/index.js';
 
 test('it returns all instruction accounts in order of resolution', t => {
     // Given the following instruction node with an account that defaults to another account.

+ 0 - 1
packages/visitors/src/index.ts

@@ -9,7 +9,6 @@ export * from './flattenInstructionDataArgumentsVisitor';
 export * from './flattenStructVisitor';
 export * from './getByteSizeVisitor';
 export * from './getDefinedTypeHistogramVisitor';
-export * from './getResolvedInstructionInputsVisitor';
 export * from './setAccountDiscriminatorFromFieldVisitor';
 export * from './setFixedAccountSizesVisitor';
 export * from './setInstructionAccountDefaultValuesVisitor';

+ 12 - 0
pnpm-lock.yaml

@@ -113,6 +113,18 @@ importers:
         specifier: workspace:*
         version: link:../node-types
 
+  packages/validators:
+    dependencies:
+      '@kinobi-so/errors':
+        specifier: workspace:*
+        version: link:../errors
+      '@kinobi-so/nodes':
+        specifier: workspace:*
+        version: link:../nodes
+      '@kinobi-so/visitors-core':
+        specifier: workspace:*
+        version: link:../visitors-core
+
   packages/visitors:
     dependencies:
       '@kinobi-so/errors':