diff --git a/.changeset/gold-eyes-battle.md b/.changeset/gold-eyes-battle.md new file mode 100644 index 000000000..17a55842f --- /dev/null +++ b/.changeset/gold-eyes-battle.md @@ -0,0 +1,6 @@ +--- +'@codama/visitors-core': minor +'@codama/visitors': minor +--- + +Use `NodePaths` in `NodeSelectors` diff --git a/packages/visitors-core/src/NodePath.ts b/packages/visitors-core/src/NodePath.ts index 73e40e85d..a5f0bcc78 100644 --- a/packages/visitors-core/src/NodePath.ts +++ b/packages/visitors-core/src/NodePath.ts @@ -2,7 +2,7 @@ import { assertIsNode, GetNodeFromKind, InstructionNode, isNode, Node, NodeKind, export type NodePath = TNode extends undefined ? readonly Node[] - : readonly [...Node[], TNode]; + : readonly [...(readonly Node[]), TNode]; export function getLastNodeFromPath(path: NodePath): TNode { return path[path.length - 1] as TNode; @@ -49,7 +49,7 @@ export function getNodePathUntilLastNode( return path.slice(0, lastIndex + 1) as unknown as NodePath>; } -function isNotEmptyNodePath(path: NodePath | null | undefined): path is NodePath { +export function isFilledNodePath(path: NodePath | null | undefined): path is NodePath { return !!path && path.length > 0; } @@ -57,14 +57,14 @@ export function isNodePath( path: NodePath | null | undefined, kind: TKind | TKind[], ): path is NodePath> { - return isNode(isNotEmptyNodePath(path) ? getLastNodeFromPath(path) : null, kind); + return isNode(isFilledNodePath(path) ? getLastNodeFromPath(path) : null, kind); } export function assertIsNodePath( path: NodePath | null | undefined, kind: TKind | TKind[], ): asserts path is NodePath> { - assertIsNode(isNotEmptyNodePath(path) ? getLastNodeFromPath(path) : null, kind); + assertIsNode(isFilledNodePath(path) ? getLastNodeFromPath(path) : null, kind); } export function nodePathToStringArray(path: NodePath): string[] { diff --git a/packages/visitors-core/src/NodeSelector.ts b/packages/visitors-core/src/NodeSelector.ts index ba40a98aa..22f78e986 100644 --- a/packages/visitors-core/src/NodeSelector.ts +++ b/packages/visitors-core/src/NodeSelector.ts @@ -1,6 +1,6 @@ import { camelCase, CamelCaseString, Node } from '@codama/nodes'; -import type { NodeStack } from './NodeStack'; +import { NodePath } from './NodePath'; export type NodeSelector = NodeSelectorFunction | NodeSelectorPath; @@ -11,11 +11,11 @@ export type NodeSelector = NodeSelectorFunction | NodeSelectorPath; * - `[someNode]` matches a node of the given kind. * - `[someNode|someOtherNode]` matches a node with any of the given kind. * - `[someNode]someText` matches both the kind and the name of a node. - * - `a.b.c` matches a node `c` such that its parent stack contains `a` and `b` in order (but not necessarily subsequent). + * - `a.b.c` matches a node `c` such that its ancestors contains `a` and `b` in order (but not necessarily subsequent). */ export type NodeSelectorPath = string; -export type NodeSelectorFunction = (node: Node, stack: NodeStack) => boolean; +export type NodeSelectorFunction = (path: NodePath) => boolean; export const getNodeSelectorFunction = (selector: NodeSelector): NodeSelectorFunction => { if (typeof selector === 'function') return selector; @@ -40,25 +40,29 @@ export const getNodeSelectorFunction = (selector: NodeSelector): NodeSelectorFun return true; }; - const checkStack = (nodeStack: Node[], nodeSelectors: string[]): boolean => { + const checkPath = (path: Node[], nodeSelectors: string[]): boolean => { if (nodeSelectors.length === 0) return true; - if (nodeStack.length === 0) return false; - const lastNode = nodeStack.pop() as Node; + if (path.length === 0) return false; + const lastNode = path.pop() as Node; const lastNodeSelector = nodeSelectors.pop() as string; return checkNode(lastNode, lastNodeSelector) - ? checkStack(nodeStack, nodeSelectors) - : checkStack(nodeStack, [...nodeSelectors, lastNodeSelector]); + ? checkPath(path, nodeSelectors) + : checkPath(path, [...nodeSelectors, lastNodeSelector]); }; - const nodeSelectors = selector.split('.'); - const lastNodeSelector = nodeSelectors.pop() as string; + const checkInitialPath = (path: Node[], nodeSelectors: string[]): boolean => { + if (nodeSelectors.length === 0 || path.length === 0) return false; + const lastNode = path.pop() as Node; + const lastNodeSelector = nodeSelectors.pop() as string; + return checkNode(lastNode, lastNodeSelector) && checkPath(path, nodeSelectors); + }; - return (node, stack) => - checkNode(node, lastNodeSelector) && checkStack(stack.getPath() as Node[], [...nodeSelectors]); + const nodeSelectors = selector.split('.'); + return path => checkInitialPath([...path], [...nodeSelectors]); }; export const getConjunctiveNodeSelectorFunction = (selector: NodeSelector | NodeSelector[]): NodeSelectorFunction => { const selectors = Array.isArray(selector) ? selector : [selector]; const selectorFunctions = selectors.map(getNodeSelectorFunction); - return (node, stack) => selectorFunctions.every(fn => fn(node, stack)); + return path => selectorFunctions.every(fn => fn(path)); }; diff --git a/packages/visitors-core/src/bottomUpTransformerVisitor.ts b/packages/visitors-core/src/bottomUpTransformerVisitor.ts index e123ae093..74f81773c 100644 --- a/packages/visitors-core/src/bottomUpTransformerVisitor.ts +++ b/packages/visitors-core/src/bottomUpTransformerVisitor.ts @@ -8,11 +8,11 @@ import { pipe } from './pipe'; import { recordNodeStackVisitor } from './recordNodeStackVisitor'; import { Visitor } from './visitor'; -export type BottomUpNodeTransformer = (node: TNode, stack: NodeStack) => Node | null; +export type BottomUpNodeTransformer = (node: Node, stack: NodeStack) => Node | null; -export type BottomUpNodeTransformerWithSelector = { +export type BottomUpNodeTransformerWithSelector = { select: NodeSelector | NodeSelector[]; - transform: BottomUpNodeTransformer; + transform: BottomUpNodeTransformer; }; export function bottomUpTransformerVisitor( @@ -22,7 +22,7 @@ export function bottomUpTransformerVisitor { if (typeof transformer === 'function') return transformer; return (node, stack) => - getConjunctiveNodeSelectorFunction(transformer.select)(node, stack) + getConjunctiveNodeSelectorFunction(transformer.select)(stack.getPath()) ? transformer.transform(node, stack) : node; }); @@ -30,13 +30,13 @@ export function bottomUpTransformerVisitor recordNodeStackVisitor(v, stack), v => - interceptVisitor(v, (node, next) => - transformerFunctions.reduce( - (acc, transformer) => (acc === null ? null : transformer(acc, stack.clone())), + interceptVisitor(v, (node, next) => { + return transformerFunctions.reduce( + (acc, transformer) => (acc === null ? null : transformer(acc, stack)), next(node), - ), - ), + ); + }), + v => recordNodeStackVisitor(v, stack), ); } diff --git a/packages/visitors-core/src/topDownTransformerVisitor.ts b/packages/visitors-core/src/topDownTransformerVisitor.ts index f31de4db0..ee5ecf20a 100644 --- a/packages/visitors-core/src/topDownTransformerVisitor.ts +++ b/packages/visitors-core/src/topDownTransformerVisitor.ts @@ -8,14 +8,11 @@ import { pipe } from './pipe'; import { recordNodeStackVisitor } from './recordNodeStackVisitor'; import { Visitor } from './visitor'; -export type TopDownNodeTransformer = ( - node: T, - stack: NodeStack, -) => T | null; +export type TopDownNodeTransformer = (node: TNode, stack: NodeStack) => TNode | null; -export type TopDownNodeTransformerWithSelector = { +export type TopDownNodeTransformerWithSelector = { select: NodeSelector | NodeSelector[]; - transform: TopDownNodeTransformer; + transform: TopDownNodeTransformer; }; export function topDownTransformerVisitor( @@ -25,7 +22,7 @@ export function topDownTransformerVisitor const transformerFunctions = transformers.map((transformer): TopDownNodeTransformer => { if (typeof transformer === 'function') return transformer; return (node, stack) => - getConjunctiveNodeSelectorFunction(transformer.select)(node, stack) + getConjunctiveNodeSelectorFunction(transformer.select)(stack.getPath()) ? transformer.transform(node, stack) : node; }); @@ -33,15 +30,15 @@ export function topDownTransformerVisitor const stack = new NodeStack(); return pipe( identityVisitor(nodeKeys), - v => recordNodeStackVisitor(v, stack), v => interceptVisitor(v, (node, next) => { const appliedNode = transformerFunctions.reduce( - (acc, transformer) => (acc === null ? null : transformer(acc, stack.clone())), + (acc, transformer) => (acc === null ? null : transformer(acc, stack)), node as Parameters[0] | null, ); if (appliedNode === null) return null; return next(appliedNode); }), + v => recordNodeStackVisitor(v, stack), ); } diff --git a/packages/visitors-core/test/NodeSelector.test.ts b/packages/visitors-core/test/NodeSelector.test.ts index 8ad164411..9bef7c7a6 100644 --- a/packages/visitors-core/test/NodeSelector.test.ts +++ b/packages/visitors-core/test/NodeSelector.test.ts @@ -10,7 +10,6 @@ import { instructionAccountNode, instructionArgumentNode, instructionNode, - isNode, Node, numberTypeNode, optionTypeNode, @@ -23,9 +22,11 @@ import { import { expect, test } from 'vitest'; import { + getLastNodeFromPath, getNodeSelectorFunction, identityVisitor, interceptVisitor, + isNodePath, NodeSelector, NodeStack, pipe, @@ -196,12 +197,12 @@ const macro = (selector: NodeSelector, expectedSelected: Node[]) => { const selected = [] as Node[]; const visitor = pipe( identityVisitor(), - v => recordNodeStackVisitor(v, stack), v => interceptVisitor(v, (node, next) => { - if (selectorFunction(node, stack.clone())) selected.push(node); + if (selectorFunction(stack.getPath())) selected.push(node); return next(node); }), + v => recordNodeStackVisitor(v, stack), ); // When we visit the tree. @@ -329,4 +330,7 @@ macro('[accountNode]gift.[publicKeyTypeNode|booleanTypeNode]', [ ]); // Select using functions. -macro(node => isNode(node, 'numberTypeNode') && node.format === 'u32', [tokenDelegatedAmountOption.prefix]); +macro( + path => isNodePath(path, 'numberTypeNode') && getLastNodeFromPath(path).format === 'u32', + [tokenDelegatedAmountOption.prefix], +); diff --git a/packages/visitors-core/test/bottomUpTransformerVisitor.test.ts b/packages/visitors-core/test/bottomUpTransformerVisitor.test.ts index a6ddb00ba..dcf3feeef 100644 --- a/packages/visitors-core/test/bottomUpTransformerVisitor.test.ts +++ b/packages/visitors-core/test/bottomUpTransformerVisitor.test.ts @@ -94,7 +94,7 @@ test('it can transform nodes using multiple node selectors', () => { // - the second one selects all nodes with more than one ancestor. const visitor = bottomUpTransformerVisitor([ { - select: ['[numberTypeNode]', (_, nodeStack) => nodeStack.getPath().length > 1], + select: ['[numberTypeNode]', path => path.length > 2], transform: () => stringTypeNode('utf8'), }, ]); diff --git a/packages/visitors-core/test/topDownTransformerVisitor.test.ts b/packages/visitors-core/test/topDownTransformerVisitor.test.ts index 8e5b6c382..0a03f5139 100644 --- a/packages/visitors-core/test/topDownTransformerVisitor.test.ts +++ b/packages/visitors-core/test/topDownTransformerVisitor.test.ts @@ -102,7 +102,7 @@ test('it can transform nodes using multiple node selectors', () => { // - the second one selects all nodes with more than one ancestor. const visitor = topDownTransformerVisitor([ { - select: ['[numberTypeNode]', (_, nodeStack) => nodeStack.getPath().length > 1], + select: ['[numberTypeNode]', path => path.length > 2], transform: node => numberTypeNode('u64') as typeof node, }, ]); diff --git a/packages/visitors/src/setFixedAccountSizesVisitor.ts b/packages/visitors/src/setFixedAccountSizesVisitor.ts index e808b4d27..95cf5d4f1 100644 --- a/packages/visitors/src/setFixedAccountSizesVisitor.ts +++ b/packages/visitors/src/setFixedAccountSizesVisitor.ts @@ -1,6 +1,8 @@ -import { accountNode, assertIsNode, isNode } from '@codama/nodes'; +import { accountNode, assertIsNode } from '@codama/nodes'; import { getByteSizeVisitor, + getLastNodeFromPath, + isNodePath, LinkableDictionary, NodeStack, pipe, @@ -18,7 +20,7 @@ export function setFixedAccountSizesVisitor() { const visitor = topDownTransformerVisitor( [ { - select: node => isNode(node, 'accountNode') && node.size === undefined, + select: path => isNodePath(path, 'accountNode') && getLastNodeFromPath(path).size === undefined, transform: node => { assertIsNode(node, 'accountNode'); const size = visit(node.data, byteSizeVisitor); diff --git a/packages/visitors/src/unwrapTupleEnumWithSingleStructVisitor.ts b/packages/visitors/src/unwrapTupleEnumWithSingleStructVisitor.ts index 107b405db..655c47d27 100644 --- a/packages/visitors/src/unwrapTupleEnumWithSingleStructVisitor.ts +++ b/packages/visitors/src/unwrapTupleEnumWithSingleStructVisitor.ts @@ -3,7 +3,6 @@ import { CamelCaseString, DefinedTypeNode, enumStructVariantTypeNode, - EnumTupleVariantTypeNode, getAllDefinedTypes, isNode, resolveNestedTypeNode, @@ -28,8 +27,7 @@ export function unwrapTupleEnumWithSingleStructVisitor(enumsOrVariantsToUnwrap: ? [() => true] : enumsOrVariantsToUnwrap.map(selector => getNodeSelectorFunction(selector)); - const shouldUnwrap = (node: EnumTupleVariantTypeNode, stack: NodeStack): boolean => - selectorFunctions.some(selector => selector(node, stack)); + const shouldUnwrap = (stack: NodeStack): boolean => selectorFunctions.some(selector => selector(stack.getPath())); return rootNodeVisitor(root => { const typesToPotentiallyUnwrap: string[] = []; @@ -44,7 +42,7 @@ export function unwrapTupleEnumWithSingleStructVisitor(enumsOrVariantsToUnwrap: select: '[enumTupleVariantTypeNode]', transform: (node, stack) => { assertIsNode(node, 'enumTupleVariantTypeNode'); - if (!shouldUnwrap(node, stack)) return node; + if (!shouldUnwrap(stack)) return node; const tupleNode = resolveNestedTypeNode(node.tuple); if (tupleNode.items.length !== 1) return node; let item = tupleNode.items[0];