Babel Traverse - Part 2 - @babel/traverse's traverseFast
Nero - Feb 12 2023
Last time we left off at how traversal actually works for babel, today we'll do just that. We'll analyze the traverseFast
function.
Let's check its implementation
export default function traverseFast(
node: t.Node | null | undefined,
enter: (node: t.Node, opts?: any) => void,
opts?: any,
): void {
if (!node) return;
const keys = VISITOR_KEYS[node.type];
if (!keys) return;
opts = opts || {};
enter(node, opts);
for (const key of keys) {
const subNode = node[key];
if (Array.isArray(subNode)) {
for (const node of subNode) {
traverseFast(node, enter, opts);
}
} else {
traverseFast(subNode, enter, opts);
}
}
}
As we can see, the traverseFast
function takes 3 arguments:
- Node - which is the main node to be traversed and modified
- Enter - a function we pass that shows how the nodes are going to be modified
- Opts - an object containing anything we'd pass it, and that gets passed down recursively. For example, we could keep states here.
Enter function
- this is what we are most interested in, this function shows how the nodes are going to be modified
- this function, in turn, gets passed 2 arguments:
node
, which is the current node we are atopts
, which is our opts that we passed (or an empty object if we didn't pass any)
Going back to the traverseFast
function, let's quickly summarize what is happening:
- if the passed node isn't there, we'll just return (e.g. the function was called with no arguments)
- get the subnodes' keys that need to be checked (
VISITOR_KEYS[node.type]
) - in case the
node.type
is not valid, return - modify the current node using the passed
enter
function - traverse subNodes recursively and go to step 1.
Remarks
- There's no way of removing safely a said node, only by detaching it while we are still at its parent; (we could still replace its type to the noop type)
- There's no way of going to the parent node from a said node, we can only go one way, and that is down the tree
- There's only one time transformations are applied: when we enter the node. This is mostly the case, but sometimes you'd want to do transformations when exiting the node. Let's see where:
"abc" + "def" + "ghi" + "jkl"
This code, when parsed, gives us the next tree view:
ExpressionStatement {
expression: BinaryExpression {
left: BinaryExpression {
left: BinaryExpression {
left: StringLiteral {
value: "abc"
}
operator: "+"
right: StringLiteral {
value: "def"
}
}
operator: "+"
right: StringLiteral {
value: "ghi"
}
}
operator: "+"
right: StringLiteral {
value: "jkl"
}
}
}
As we can see, we got nested BinaryExpressions that, in the end, mean concatenation of strings Doing transformations when we enter a BinaryExpression node would look something like:
// visitor
traverseFast(node, node => {
if(node.type !== "BinaryExpression")return;
const left = node.left;
const right = node.right;
if(left.type !== "StringLiteral" || right.type !== "StringLiteral")return;
node.type = "StringLiteral"
node.value = left.value + right.value;
delete node.left;
delete node.right;
delete node.operator;
})
// result
"abcdef" + "ghi" + "jkl"
Of course, we could rewrite or enter-transform visitor to make it work, but the easiest way is to do the transformation before exiting the node. Example:
// visitor
// I'll put just the before-exit visitor when using the normal `traverse` function here
BinaryExpression: {
exit(path) {
const {node} = path;
const left = node.left;
const right = node.right;
if(left.type !== "StringLiteral" || right.type !== "StringLiteral")return;
path.replaceWith(t.StringLiteral(left.value + right.value));
}
}
// result
"abcdefghijkl"