@codama/visitors-core
v1.1.0
Published
Core visitors for the Codama framework
Downloads
1,494
Maintainers
Readme
Codama ➤ Visitors ➤ Core
This package provides core interfaces and utilities for creating visitors for Codama IDLs.
Installation
pnpm install @codama/visitors-core
[!NOTE] This package is included in the
@codama/visitors
package and in the maincodama
library. Meaning, you already have access to its content if you are installing Codama in one of these ways.pnpm install @codama/visitors pnpm install codama
Getting started with visitors
The Visitor
type
The type Visitor<T>
is the core interface for defining Codama visitors. The type parameter T
is used to determine the return type of the visitor. For instance, here's the definition of a visitor that goes through the nodes and returns a number.
let myNumberVisitor: Visitor<number>;
The Visitor
type accepts a second type parameter which defines the scope of nodes accepted by the visitor. By default, the visitor accepts all nodes. However, you can restrict the visitor to a specific set of nodes by providing a union of node kinds.
let myVisitorForProgramNodesOnly: Visitor<number, 'programNode'>;
let myVisitorForTypeNodesOnly: Visitor<number, TypeNode['kind']>;
The definition of the Visitor
type is an object such that, for each supported node kind, a function that accepts a node of that kind and returns a value of type T
is defined. The name of the function must be camel-cased, start with visit
and finish with the name of the node kind without the Node
suffix. For instance, the function for the programNode
kind is named visitProgram
.
Writing your own visitor
To write a custom visitor, you may simply define an object with the appropriate functions. For instance, here's a visitor that only visits ProgramNodes
and returns the number of accounts in the program.
const accountCounterVisitor: Visitor<number, 'programNode'> = {
visitProgram: (node: ProgramNode) => node.accounts.length,
};
Note that it is recommended to return visitors from functions so we can easily reuse them and parameterize them. Additionally, this allows our code to be tree-shaken by the bundler. As we will see in this documentation, all provided visitors are returned from functions even if they don't take any parameter.
Here's our previous example updated to accept a multiplier
parameter.
const accountCounterVisitor = (multiplier = 1): Visitor<number, 'programNode'> => ({
visitProgram: (node: ProgramNode) => node.accounts.length * multiplier,
});
In practice, writing a visitor manually can be cumbersome as a function must be provided for each supported node kind. Therefore, it is recommended to compose visitors from a set of core visitors provided by this package and extend them to suit your needs. We will see how to do this in the next sections.
Visiting nodes
Once we have a visitor, we can visit any node it supports by calling the visit
function. This function accepts a node and a visitor of type Visitor<T>
and returns a type T
.
const counter: number = visit(programNode, accountCounterVisitor());
The visitOrElse
function can also be used to gracefully handle the case where the node is not supported by the visitor. In this case, a fallback logic — provided as a third argument — is used to compute the result.
const counter: number = visit(stringTypeNode, accountCounterVisitor(), () => 0);
Also note that, if you are using the Codama
interface — which is a simple wrapper around a RootNode
— you may visit that root node using the provided helpers:
// Runs the visitor and returns the result.
const result: number = codama.accept(myNumberVisitor());
// Runs the visitor and updates the wrapped `RootNode` with the result.
codama.update(myTransformerVisitor());
Core visitors
As mentioned in the previous section, creating visitors is much easier when we start from a set of core visitors and extend them to suit our needs.
Therefore, let's start by exploring the core visitors provided by this package.
Filtering node kinds
Before we list each available core visitor, it is important to know that each of these functions optionally accepts a node kind or an array of node kinds as a keys
attribute in their options
. This allows us to restrict the visitor to a specific set of nodes and will return a Visitor<T, U>
instance where U
is the union of the provided node kinds.
Here are some examples:
// This visitor only accepts `ProgramNodes`.
const visitor: Visitor<number, 'programNode'> = voidVisitor({ keys: 'programNode' });
// This visitor accepts both `NumberTypeNodes` and `StringTypeNodes`.
const visitor: Visitor<number, 'numberTypeNode' | 'stringTypeNode'> = voidVisitor({
keys: ['numberTypeNode', 'stringTypeNode'],
});
// This visitor accepts all type nodes.
const visitor: Visitor<number, TypeNode['kind']> = voidVisitor({ keys: TYPE_NODES });
// This visitor accepts all nodes.
const visitor: Visitor<number> = voidVisitor();
In the following sections describing the core visitors, this exact pattern can be used to restrict the visitors to specific node kinds. We won't cover this for each visitor but know that you can achieve this via the keys
option of each function.
voidVisitor
The voidVisitor
traverses all the nodes and ends up returning undefined
, regardless of the node kind.
visit(node, voidVisitor());
// ^ undefined
Visiting a node with this visitor does nothing and causes no side effects. However, it can be a great starting point for creating new visitors by extending certain visiting functions of the voidVisitor
.
staticVisitor
The staticVisitor
accepts a function that is used for every node. The provided function is called with the node being visited.
const visitor: Visitor<string> = staticVisitor(node => `Visiting ${node.kind}`);
const kind = visit(numberTypeNode('u32'), visitor);
// ^ "Visiting numberTypeNode"
This visitor can be used to create simple visitors where each node shares a similar logic or to provide a starting point for more complex visitors.
identityVisitor
The identityVisitor
traverses the nodes and returns a deep copy of the visited node.
const node = visit(numberTypeNode('u32'), identityVisitor());
// ^ A different instance of numberTypeNode('u32')
Note that the returned visitor is of type Visitor<Node | null>
meaning this visitor allows for nodes to be deleted — i.e. marked as null
. The identityVisitor
can resolve nested null
references depending on the node kind. For instance, if a tupleTypeNode
contains two items and the first one is null
— after having visited its children — then, the tupleTypeNode
will only contain the second item. It is also possible for a nested null
reference to bubble up if it cannot be resolved.
Here are some examples of this behaviour by overriding the visitPublicKeyType
function to return null
.
const visitor = identityVisitor();
visitor.visitPublicKeyType = () => null;
const node = visit(tupleTypeNode([numberTypeNode('u32'), publicKeyTypeNode()]), visitor);
// ^ tupleTypeNode([numberTypeNode('u32')])
const node = visit(definedTypeNode({ name: 'address', type: publicKeyTypeNode() }), visitor);
// ^ null
Also note that, because the visitor is of type Node | null
, it is technically possible to extend it such that a node of a different kind is returned.
const visitor = identityVisitor();
visitor.visitPublicKeyType = () => fixedSizeTypeNode(stringTypeNode('base58'), 32);
nonNullableIdentityVisitor
The nonNullableIdentityVisitor
works the same way as the identityVisitor
but it does not allow nodes to be deleted. That is, its return type must be a Node
and not Node | null
.
const node = visit(numberTypeNode('u32'), nonNullableIdentityVisitor());
// ^ A different instance of numberTypeNode('u32')
mergeVisitor
The mergeVisitor
returns a Visitor<T>
by accepting two functions such that:
- The first function is used on the leaves of the Codama IDL and returns a type
T
. - The second function is used to merge the values
T[]
of the children of a node and aggregate them into a typeT
.
For instance, here is how we can use the mergeVisitor
to create a nested string representation of node kinds.
const visitor = mergeVisitor(
(node): string => node.kind,
(node, values: string[]): string => `${node.kind}(${values.join(',')})`,
);
const result = visit(tupleTypeNode([numberTypeNode('u32'), publicKeyTypeNode()]), visitor);
// ^ "tupleTypeNode(numberTypeNode,publicKeyTypeNode)"
Here's another example, counting the number of traversed nodes.
const visitor = mergeVisitor(
() => 1,
(, values) => values.reduce((a, b) => a + b, 1),
);
const result = visit(tupleTypeNode([numberTypeNode('u32'), publicKeyTypeNode()]), visitor);
// ^ 3
The mergeVisitor
is a powerful starting point to create aggregating visitors.
Composing visitors
The following visitor functions accept an existing visitor and return a new visitor that extends or modifies the behaviour of the provided visitor. These primitives can be used to create complex visitors by composing simpler ones.
extendVisitor
The extendVisitor
function accepts a base visitor and a set of function wrappers that are used to extend the behaviour of the base visitor.
Each function wrapper is given the node
being visited and an object composed of two elements:
next
: A function that can be called to delegate to the base visitor — e.g.next(node)
.self
: The visitor itself, allowing for recursive calls.
To illustrate this, consider the following base visitor that counts the number of nodes.
const baseVisitor = mergeVisitor(
() => 1,
(_, values) => values.reduce((a, b) => a + b, 1),
);
We can extend this visitor to increment the count by 10 when visiting a PublicKeyTypeNode
like so:
const visitor = extendVisitor(baseVisitor, {
visitPublicKeyType: (node, { next }) => next(node) + 10,
});
const result = visit(tupleTypeNode([publicKeyTypeNode(), numberTypeNode('u32')]), visitor);
// ^ 13
Notice how next(node)
can be used to access the underlying visitor meaning we can extend both the input and the output of the base visitor.
Another example is to make use of the self
property to recursively call the extended visitor. For instance, the following code only visits the first item of tuple types.
const visitor = extendVisitor(baseVisitor, {
visitTupleType: (node, { self }) => visit(node.items[0], self) + 1,
});
const result = visit(tupleTypeNode([tupleTypeNode([publicKeyTypeNode()]), numberTypeNode('u32')]), visitor);
// ^ 3
interceptVisitor
The interceptVisitor
allows us to wrap every visiting function of a provided visitor into a given function. This function has access to the node being visited and a next
function that can be called to delegate to the base visitor.
For instance, the following visitor intercepts a voidVisitor
and captures events when visiting nodes.
const events: string[] = [];
const visitor = interceptVisitor(voidVisitor(), (node, next) => {
events.push(`down:${node.kind}`);
next(node);
events.push(`up:${node.kind}`);
});
visit(tupleTypeNode([numberTypeNode('u32'), publicKeyTypeNode()]), visitor);
// events === [
// 'down:tupleTypeNode',
// 'down:numberTypeNode',
// 'up:numberTypeNode',
// 'down:publicKeyTypeNode',
// 'up:publicKeyTypeNode',
// 'up:tupleTypeNode',
// ]
interceptFirstVisitVisitor
The interceptFirstVisitVisitor
works the same way as the interceptVisitor
but only intercepts the first visit of a node. This means that the provided function is called when visiting the specific node provided but not when visiting its children. The parameters are the same as for the interceptVisitor
.
For instance, the following visitor intercepts a voidVisitor
and captures events only during the first visit.
const events: string[] = [];
const visitor = interceptFirstVisitVisitor(voidVisitor(), (node, next) => {
events.push(`down:${node.kind}`);
next(node);
events.push(`up:${node.kind}`);
});
visit(tupleTypeNode([numberTypeNode('u32'), publicKeyTypeNode()]), visitor);
// events === [
// 'down:tupleTypeNode',
// 'up:tupleTypeNode',
// ]
tapVisitor
The tapVisitor
function allows us to tap into the visiting functions of a provided visitor without modifying its behaviour. This means the returned visitor will behave exactly like the base visitor except that the provided function will be called for the specified node kind.
Note that the provided function must not return a value as it is only used for side effects.
let numberOfNumberNodes = 0;
const visitor = tapVisitor(voidVisitor(), 'numberTypeNode', node => {
numberOfNumberNodes++;
});
visit(tupleTypeNode([numberTypeNode('u32'), publicKeyTypeNode()]), visitor);
// numberOfNumberNodes === 1
mapVisitor
The mapVisitor
function accepts a base visitor of type Visitor<T>
and a function of type (value: T) => U
; and returns a new visitor of type Visitor<U>
.
// Gets a nested string representation of node kinds.
const baseVisitor = mergeVisitor(
node => node.kind as string,
(node, values) => `${node.kind}(${values.join(',')})`,
);
// Counts the number of characters in the string representation.
const visitor = mapVisitor(baseVisitor, (value: string): number => value.length);
const result = visit(tupleTypeNode([numberTypeNode('u32'), publicKeyTypeNode()]), visitor);
// ^ 47
pipe
The pipe
helper function allows us to compose visitors in a more readable way. It accepts a base visitor and a set of visitor functions that are used to extend the behaviour of the previous visitor at each step.
const visitor = pipe(
baseVisitor,
v => extendVisitor(v /** ... */),
v => interceptVisitor(v /** ... */),
v => mapVisitor(v /** ... */),
v => tapVisitor(v /** ... */),
);
For instance, here's an example using the pipe
function to transform an identityVisitor
into a visitor that:
- Transforms all number types into
u64
numbers. - Logs the amount of items in tuple types.
- Wraps the visited node in a
DefinedTypeNode
labelled "gift".
const visitor = pipe(
// Starts with the identity visitor.
identityVisitor(),
v =>
// Extends the visitor to make all number types u64.
extendVisitor(v, {
visitNumberType: node => numberTypeNode('u64'),
}),
v =>
// Log the amount of items in tuple types.
tapVisitor(v, 'tupleTypeNode', node => {
console.log(node.items.length);
}),
v =>
// Wrap the visited node in a `DefinedTypeNode` labelled "gift".
interceptVisitor(v, node => (node, next) => {
return definedTypeNode({ name: 'gift', type: next(node) });
}),
);
singleNodeVisitor
The singleNodeVisitor
function is a simple primitive that creates a Visitor
that only visits a single node kind. It accepts a node kind and a function that is used to visit that node kind. Any other node kind will not be supported by the visitor.
const visitor = singleNodeVisitor('tupleTypeNode', node => node.items.length);
const result = visit(tupleTypeNode([numberTypeNode('u32'), publicKeyTypeNode()]), visitor);
// ^ 2
Additionally, a rootNodeVisitor
shortcut is provided to create a visitor that only visits RootNodes
. This can be useful to design top-level visitors using custom logic.
For instance, we can create a visitor that takes a RootNode
and updates it through a series of other visitors before returning the updated RootNode
.
const visitor = rootNodeVisitor((root: RootNode) => {
let newRoot = root;
newRoot = visit(newRoot, visitorA);
newRoot = visit(newRoot, visitorB);
newRoot = visit(newRoot, visitorC);
return newRoot;
});
Recording node paths
NodePath
The NodePath
type defines an immutable array of Nodes
that represents any path of nodes in the Codama IDL. It accepts an optional type parameter that tells us the type of the last node in the path. For instance NodePath<NumberTypeNode>
represents a path of node ending with a NumberTypeNode
.
Additionally, there are several utility functions to use with NodePath
instances:
// An example of a node path.
const path: NodePath<AccountNode> = [rootNode, programNode, accountNode];
// Access the last node in the path. I.e. given NodePath<T>, returns T.
const lastNode: AccountNode = getLastNodeFromPath(path);
// Access the first/last node of a specific kind in the path.
const firstProgramNode: ProgramNode | undefined = findFirstNodeFromPath(path, 'programNode');
const lastProgramNode: ProgramNode | undefined = findLastNodeFromPath(path, 'programNode');
// Access the last program/instruction node in the path (sugar for `findLastNodeFromPath`).
const programNode: ProgramNode | undefined = findProgramNodeFromPath(path);
const instructionNode: InstructionNode | undefined = findInstructionNodeFromPath(path);
// Get the subpath of a path from the beginning to the last node matching a specific kind.
const subpath: NodePath = getNodePathUntilLastNode(path, 'programNode');
// ^ [rootNode, programNode]
// Check that a path is not empty.
if (isFilledNodePath(path as NodePath)) {
path satisfies NodePath<Node>;
}
// Check that a path finishes with a node matching the provided kind or kinds.
if (isNodePath(path as NodePath, ['AccountNode', 'InstructionNode'])) {
path satisfies NodePath<AccountNode | InstructionNode>;
}
// Assert that a path finishes with a node matching the provided kind or kinds.
assertIsNodePath(path as NodePath, ['AccountNode', 'InstructionNode']);
path satisfies NodePath<AccountNode | InstructionNode>;
// Display paths as strings or arrays of strings.
nodePathToStringArray(path); // -> ['[rootNode]', '[programNode]token', '[accountNode]mint']
nodePathToString(path); // -> "[rootNode] > [programNode]token > [accountNode]mint"
NodeStack
The NodeStack
class is a utility that allows us to record the path of nodes that led to the node being currently visited. It is essentially a mutable version of NodePath
that pushes and pops Nodes
as we go down and up the tree of nodes.
For instance, consider the following node:
const node = definedTypeNode({
name: 'myType',
type: tupleTypeNode([numberTypeNode('u32'), publicKeyTypeNode()]),
});
In this example, the numberTypeNode
can be reached using the following stack:
const stack = new NodeStack()
.push(node) // -> definedTypeNode.
.push(node.type) // -> tupleTypeNode.
.push(node.type.items[0]); // -> numberTypeNode.
Once you have access to a NodeStack
instance, you may use the following methods:
// Push a node to the stack.
nodeStack.push(node);
// Pop the last node out of the stack.
const lastNode = nodeStack.pop();
// Peek at the last node in the stack.
const lastNode = nodeStack.peek();
// Get all the nodes in the stack as an immutable `NodePath` array.
const path: NodePath = nodeStack.getPath();
// Get a `NodePath` whilst asserting on the last node kind.
const path: NodePath<AccountNode> = nodeStack.getPath('accountNode');
// Check if the stack is empty.
const isEmpty = nodeStack.isEmpty();
// Clone the stack.
const clonedStack = nodeStack.clone();
Additionally, it is possible to save and restore multiple node paths within a NodeStack
by using the pushPath
and popPath
methods. This is for more advanced uses cases where you need to jump from one part of the tree, to a different part of the tree, and back — without loosing the context of the original path. An application of this is when we need to follow a node from a LinkNode
(see "Resolving link nodes" below for more details).
// Save the current path and push a new path.
stack.pushPath([rootNode, programNode, linkableNode]);
// Pop the current path and restore the previous path.
const previousPath = stack.popPath();
recordNodeStackVisitor
The recordNodeStackVisitor
function gives us a convenient way to record the stack of each node currently being visited. It accepts a base visitor and an empty NodeStack
instance that will automatically be pushed and popped as the visitor traverses the nodes. This means that we can inject the NodeStack
instance into another extension of the visitor to access the stack whilst visiting the nodes.
Note that the recordNodeStackVisitor
should be the last visitor in the pipe to ensure that the stack is correctly recorded and that the current node visited is part of the stack.
For instance, here's how we can log the NodeStack
of any base visitor as we visit the nodes.
const stack = new NodeStack();
const visitor = pipe(
baseVisitor,
v =>
interceptVisitor(v, (node, next) => {
console.log(nodePathToString(stack.getPath()));
return next(node);
}),
v => recordNodeStackVisitor(v, stack),
);
Also note that some core visitors such as the bottomUpTransformerVisitor
or the getByteSizeVisitor
use a NodeStack
internally to keep track of the current path. If you use these visitor within another visitor, you may wish to provide your own NodeStack
instance as an option so that the same NodeStack
is used across all visitors throughout the traversal.
const stack = new NodeStack();
const byteSizeVisitor = getByteSizeVisitor(..., { stack });
const visitor = pipe(
voidVisitor(),
v => tapVisitor(v, 'definedTypeNode', node => {
const byteSize = visit(node, byteSizeVisitor);
console.log(`The byte size of ${node.name} is ${byteSize}`);
}),
v => recordNodeStackVisitor(v, stack),
);
Selecting nodes
When visiting a tree of nodes, it is often useful to be explicit about the paths we want to select. For instance, I may want to delete all accounts from a program node named "token".
To that end, the NodeSelector
type represents a node selection that can take two forms:
- A
NodeSelectorFunction
of type(path: NodePath) => boolean
. In this case, the provided function is used to determine if the last node in the providedNodePath
should be selected. - A
NodeSelectorPath
of typestring
. In this case, the provided string uses a simple syntax to select nodes.
The NodeSelectorPath
syntax is as follows:
- Plain text is used to match the name of a node, if any. For instance,
token
will match any node named "token". - Square brackets
[]
are used to match the kind of a node. For instance,[programNode]
will match anyProgramNode
. - Plain text and square brackets can be combined to match both the name and the kind of a node. For instance,
[programNode]token
will match anyProgramNode
named "token". - Plain texts and/or square brackets can be chained using dots
.
to match several nodes in the currentNodeStack
. - Dot-separated paths must follow the provided order but do not need to be contiguous or exhaustive. This means that
a.b.c
will match aNodeStack
that looks likex.a.y.b.z.c
but notb.a.c
. - The last item of a dot-separated path must match the last node of the
NodeStack
. For instance,a.b
will not matcha.b.x
. - The wildcard
*
can be used at the end of the path to match any node within the matching path. For instance,a.b.*
will matcha.b.x
.
Here are some examples:
'[accountNode]';
// Matches any `AccountNode`.
'mint';
// Matches any node named "mint".
'[accountNode]mint';
// Matches any `AccountNode` named "mint".
'[programNode]token.[accountNode]mint';
// Matches any `AccountNode` named "mint" within a `ProgramNode` named "token".
'[programNode]token.*';
// Matches any node within a `ProgramNode` named "token" (excluding the program node itself).
'token.[structTypeNode].amount';
// Matches any node named "amount" within a `StructTypeNode` within any node named "token".
The NodeSelector
type is used by various visitors such as the bottomUpTransformerVisitor
or the deleteNodesVisitor
to help us select the nodes we want to transform or delete.
Transforming nodes
This package offers several visitors to facilitate the transformation and/or deletion of nodes. These visitors are designed to be used in conjunction with the NodeSelector
type to select the nodes we want to transform/delete.
bottomUpTransformerVisitor
The bottomUpTransformerVisitor
traverses the nodes and intercepts them on the way back up. This means that when we reach a node, we have already visited all its children.
This visitor accepts an array of transformers
where each transformer is an object with the following properties:
select
: ANodeSelector
or an array ofNodeSelectors
used to select the nodes we want to transform. When multiple selectors are provided, they must all match for the node to be selected.transform
: A function that accepts the selected node and itsNodeStack
; and returns a new node ornull
to delete the node.
Here are a few examples:
const visitor = bottomUpTransformerVisitor([
{
// Transform all numbers into u64 numbers.
select: '[numberTypeNode]',
transform: () => numberTypeNode('u64'),
},
{
// Delete all account nodes that start with "m".
select: ['[accountNode]', node => 'name' in node && node.name.startsWith('m')],
transform: () => null,
},
{
// Prefix all fields inside a defined type with "super".
select: '[definedTypeNode]metadata.[structFieldTypeNode]',
transform: node => structFieldTypeNode({ ...node, name: `super${pascalCase(node.name)}` }),
},
]);
Additionally, transformers
can be provided directly as functions. In this case, the function is used to transform all the nodes and further filtering may be needed inside the function.
const visitor = bottomUpTransformerVisitor([
(node, stack) => {
if (!isNode(node, numberTypeNode)) {
return node;
}
const swappedEndian = node.endian === 'be' ? 'le' : 'be';
return numberTypeNode(node.format, swappedEndian);
},
]);
By default, this visitor will keep track of its own NodeStack
but you may provide your own via the stack
option in order to share the same NodeStack
across multiple visitors.
topDownTransformerVisitor
The topDownTransformerVisitor
works the same way as the bottomUpTransformerVisitor
but intercepts the nodes on the way down. This means that when we reach a node, we have not yet visited its children.
const visitor = topDownTransformerVisitor([
{
// Half the amount of all accounts and instructions in programs.
// The other half won't be visited on the way down.
select: '[programNode]',
transform: node =>
programNode({
...node,
accounts: node.accounts.slice(0, Math.floor(node.accounts.length / 2)),
instructions: node.instructions.slice(0, Math.floor(node.instructions.length / 2)),
}),
},
]);
Here as well, you may use the stack
option to provide your own NodeStack
instance.
deleteNodesVisitor
The deleteNodesVisitor
accepts an array of NodeSelectors
and deletes all the nodes that match any of the provided selectors. Therefore, it is equivalent to using a transformer visitor such that the transform
function returns null
for the selected nodes.
// Deletes all account nodes named "mint" and all number type nodes.
const visitor = deleteNodesVisitor(['[accountNode]mint', '[numberTypeNode]']);
Here as well, you may use the stack
option to provide your own NodeStack
instance.
String representations
This package also offers visitors that help render nodes as strings. These visitors can be useful for debugging purposes as well as getting a unique hash string representation of a node.
getDebugStringVisitor
The getDebugStringVisitor
provides a string representation of the nodes that can be used for debugging purposes. By default, it inlines the content of the nodes and does not include any indentation.
const visitor = getDebugStringVisitor();
const result = visit(tupleTypeNode([numberTypeNode('u32'), publicKeyTypeNode()]), visitor);
// ^ "tupleTypeNode(numberTypeNode[u32], publicKeyTypeNode)"
However, you can provide the indent
option to get a more readable string representation.
const visitor = getDebugStringVisitor({ indent: true });
const result = visit(tupleTypeNode([numberTypeNode('u32'), publicKeyTypeNode()]), visitor);
// tupleTypeNode
// | numberTypeNode [u32]
// | publicKeyTypeNode
Note that this string does not always include every piece of information a node has to offer. Therefore, it cannot be used as a unique identifier for the content of a node. For that purpose, see the getUniqueHashStringVisitor
below.
getUniqueHashStringVisitor
The getUniqueHashStringVisitor
provides a unique string representation of the node that can be used to get a unique hash for that node. In other words, if two different nodes have the exact same content, they will output the same string.
const visitor = getUniqueHashStringVisitor();
const result = visit(tupleTypeNode([numberTypeNode('u32'), publicKeyTypeNode()]), visitor);
// ^ '{"items":[{"endian":"le","format":"u32","kind":"numberTypeNode"},{"kind":"publicKeyTypeNode"}],"kind":"tupleTypeNode"}'
consoleLogVisitor
The consoleLogVisitor
accepts any Visitor<string>
and transforms it into a Visitor<void>
such that the provided string
is logged to the console.
// Outputs the indented debug string to the console.
const visitor = consoleLogVisitor(getDebugStringVisitor({ indent: true }));
Resolving link nodes
LinkableDictionary
The LinkableDictionary
type is a utility that allows us to store and access linkable nodes — such as ProgramNodes
, AccountNodes
or PdaNodes
— from their respective link nodes.
It offers the following API:
const linkables = new LinkableDictionary();
// Record linkable nodes via their full path.
linkables.recordPath([rootNode, programNode, accountNode]);
// Get a linkable node using the full path of a link node, or return undefined if it is not found.
const programNode: ProgramNode | undefined = linkables.get([...somePath, programLinkNode]);
// Get a linkable node using the full path of a link node, or throw an error if it is not found.
const programNode: ProgramNode = linkables.getOrThrow([...somePath, programLinkNode]);
// Get the path of a linkable node using the full path of a link node, or return undefined if it is not found.
const accountPath: NodePath<AccountNode> | undefined = linkables.getPath([...somePath, accountLinkNode]);
// Get the path of a linkable node using the full path of a link node, or throw an error if it is not found.
const accountPath: NodePath<AccountNode> = linkables.getPathOrThrow([...somePath, accountLinkNode]);
Note that:
- The path of the recorded node must be provided when recording a linkable node.
- The path of the link node must be provided when getting a linkable node (or its path) from it.
This API may be used with the recordLinkablesOnFirstVisitVisitor
to record the linkable nodes before the first node visit; as well as the recordNodeStackVisitor
to keep track of the current node path when accessing the linkable nodes.
getRecordLinkablesVisitor
This visitor accepts a LinkableDictionary
instance and records all linkable nodes it encounters when visiting the nodes.
const linkables = new LinkableDictionary();
visit(rootNode, getRecordLinkablesVisitor(linkables));
// Now, all linkable nodes are recorded in the `linkables` dictionary.
recordLinkablesOnFirstVisitVisitor
This visitor is a utility that combines interceptFirstVisitVisitor
and getRecordLinkablesVisitor
to record all linkable nodes before the first visit of any node.
It accepts a base visitor and a LinkableDictionary
instance; and returns a new visitor that records all linkable nodes it encounters before the very first visit of the provided base visitor. This means that we can inject the LinkableDictionary
instance into other extensions of the base visitor to resolve any link node we encounter.
Note that the recordLinkablesOnFirstVisitVisitor
should be the last visitor in the pipe to ensure that all linkable nodes are recorded before being used.
Here's an example that records a LinkableDictionary
and uses it to log the amount of seeds in each linked PDA node.
const linkables = new LinkableDictionary();
const stack = new NodeStack();
const visitor = pipe(
baseVisitor,
v =>
tapVisitor(v, 'pdaLinkNode', node => {
const pdaNode = linkables.getOrThrow(stack.getPath(node.kind));
console.log(`${pdaNode.seeds.length} seeds`);
}),
v => recordNodeStackVisitor(v, stack),
v => recordLinkablesOnFirstVisitVisitor(v, linkables),
);
Other useful visitors
This package provides a few other visitors that may help build more complex visitors.
getByteSizeVisitor
The getByteSizeVisitor
calculates the byte size of a given TypeNode
. It returns a number
if the byte size is fixed or null
if it is variable. It requires a LinkableDictionary
instance to resolve any link nodes it encounters.
const visitor = getByteSizeVisitor(linkables);
const size = visit(tupleTypeNode([numberTypeNode('u32'), publicKeyTypeNode()]), visitor);
// ^ 36 (4 bytes for the u32 number and 32 bytes for the public key)
By default, this visitor will keep track of its own NodeStack
but you may provide your own via the stack
option in order to share the same NodeStack
across multiple visitors.
getResolvedInstructionInputsVisitor
The getResolvedInstructionInputsVisitor
visits InstructionNodes
only and returns an array of instruction accounts and arguments in the order they should be rendered for their default values to be resolved.
For instance, say we have an instruction with two accounts and one argument such that argumentA
defaults to accountB
and accountA
is a PDA that uses argumentA
as a seed. Therefore, the visitor will return an array in the order [accountB, argumentA, accountA]
.
This is mostly useful when rendering client code for instructions.
removeDocsVisitor
The removeDocsVisitor
goes through all nodes that have a docs
property and clears its content.
const node = definedTypeNode({
name: 'authority',
type: publicKeyTypeNode(),
docs: ['The authority of the account'],
});
const updatedNode = visit(node, removeDocsVisitor());
// ^ definedTypeNode({ name: 'authority', type: publicKeyTypeNode() })
This is used internally by the getUniqueHashStringVisitor
to get a unique identifier for a node regardless of its documentation.