UNPKG

@codama/validators

Version:

Validator visitors for the Codama framework

260 lines (257 loc) 9.14 kB
// src/getValidationItemsVisitor.ts import { camelCase, getAllInstructionArguments, isNode } from "@codama/nodes"; import { extendVisitor, getResolvedInstructionInputsVisitor, LinkableDictionary, mergeVisitor, NodeStack, pipe, recordLinkablesOnFirstVisitVisitor, recordNodeStackVisitor, visit } from "@codama/visitors-core"; // src/ValidationItem.ts var LOG_LEVELS = ["debug", "trace", "info", "warn", "error"]; function validationItem(level, message, node, path) { return { level, message, node, path: Array.isArray(path) ? path : path.getPath() }; } var getLevelIndex = (level) => LOG_LEVELS.indexOf(level); // src/getValidationItemsVisitor.ts function getValidationItemsVisitor() { const linkables = new LinkableDictionary(); const stack = new NodeStack(); return pipe( mergeVisitor( () => [], (_, items) => items.flat() ), (v) => recordLinkablesOnFirstVisitVisitor(v, linkables), (v) => recordNodeStackVisitor(v, stack), (v) => extendVisitor(v, { visitAccount(node, { next }) { const items = []; if (!node.name) { items.push(validationItem("error", "Account has no name.", node, stack)); } return [...items, ...next(node)]; }, visitDefinedType(node, { next }) { const items = []; if (!node.name) { items.push(validationItem("error", "Defined type has no name.", node, stack)); } return [...items, ...next(node)]; }, visitDefinedTypeLink(node, { next }) { const items = []; if (!node.name) { items.push(validationItem("error", "Pointing to a defined type with no name.", node, stack)); } else if (!linkables.has(stack.getPath(node.kind))) { items.push( validationItem( "error", `Pointing to a missing defined type named "${node.name}"`, node, stack ) ); } return [...items, ...next(node)]; }, visitEnumEmptyVariantType(node, { next }) { const items = []; if (!node.name) { items.push(validationItem("error", "Enum variant has no name.", node, stack)); } return [...items, ...next(node)]; }, visitEnumStructVariantType(node, { next }) { const items = []; if (!node.name) { items.push(validationItem("error", "Enum variant has no name.", node, stack)); } return [...items, ...next(node)]; }, visitEnumTupleVariantType(node, { next }) { const items = []; if (!node.name) { items.push(validationItem("error", "Enum variant has no name.", node, stack)); } return [...items, ...next(node)]; }, visitEnumType(node, { next }) { const items = []; 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 = []; 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 = []; if (!node.name) { items.push(validationItem("error", "Instruction has no name.", node, stack)); } const accountNameHistogram = /* @__PURE__ */ new Map(); 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); if (count === 2) { items.push( validationItem( "error", `Account name "${account.name}" is not unique in instruction "${node.name}".`, node, stack ) ); } }); const cyclicCheckVisitor = getResolvedInstructionInputsVisitor(); try { visit(node, cyclicCheckVisitor); } catch (error) { items.push(validationItem("error", error.message, node, stack)); } 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 ) ); } 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 = []; 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 = []; if (!node.name) { items.push(validationItem("error", "Struct field has no name.", node, stack)); } return [...items, ...next(node)]; }, visitStructType(node, { next }) { const items = []; const fieldNameHistogram = /* @__PURE__ */ new Map(); node.fields.forEach((field) => { if (!field.name) return; const count = (fieldNameHistogram.get(field.name) ?? 0) + 1; fieldNameHistogram.set(field.name, count); 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 = []; if (node.items.length === 0) { items.push(validationItem("warn", "Tuple has no items.", node, stack)); } return [...items, ...next(node)]; } }) ); } // src/throwValidatorItemsVisitor.ts import { CODAMA_ERROR__VISITORS__FAILED_TO_VALIDATE_NODE, CodamaError } from "@codama/errors"; import { mapVisitor } from "@codama/visitors-core"; function throwValidatorItemsVisitor(visitor, throwLevel = "error") { 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; }, {} ); const maxLevel = Object.keys(levelHistogram).map((level) => getLevelIndex(level)).sort((a, b) => b - a)[0]; if (maxLevel >= getLevelIndex(throwLevel)) { const formattedHistogram = Object.keys(levelHistogram).map((level) => `${level}s: ${levelHistogram[level]}`).join(", "); throw new CodamaError(CODAMA_ERROR__VISITORS__FAILED_TO_VALIDATE_NODE, { formattedHistogram, validationItems }); } }); } export { LOG_LEVELS, getLevelIndex, getValidationItemsVisitor, throwValidatorItemsVisitor, validationItem }; //# sourceMappingURL=index.browser.mjs.map