Skip to content

Commit

Permalink
Visit replaced nodes
Browse files Browse the repository at this point in the history
  • Loading branch information
davepagurek committed May 30, 2024
1 parent 9c1c4a9 commit b400952
Show file tree
Hide file tree
Showing 2 changed files with 113 additions and 18 deletions.
94 changes: 94 additions & 0 deletions src/ast/ast.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,3 +67,97 @@ test('visit()', () => {
expect(parent?.type).toBe('binary');
expect(unfound).not.toBeDefined();
});

test('visit with replace', () => {
const visitLog: Array<['enter' | 'exit', AstNode['type']]> = [];

const tree: BinaryNode = {
type: 'binary',
operator: literal('-'),
// mock location data
location: {
start: { line: 0, column: 0, offset: 0 },
end: { line: 0, column: 0, offset: 0 },
},
left: identifier('foo'),
right: {
type: 'group',
lp: literal('('),
rp: literal(')'),
expression: identifier('bar'),
},
};

let sawBar = false;
let sawBaz = false;

visit(tree, {
group: {
enter: (path) => {
visitLog.push(['enter', path.node.type]);
path.replaceWith(identifier('baz'));
},
exit: (path) => {
visitLog.push(['exit', path.node.type]);
},
},
binary: {
enter: (path) => {
visitLog.push(['enter', path.node.type]);
},
exit: (path) => {
visitLog.push(['exit', path.node.type]);
},
},
literal: {
enter: (path) => {
visitLog.push(['enter', path.node.type]);
},
exit: (path) => {
visitLog.push(['exit', path.node.type]);
},
},
identifier: {
enter: (path) => {
visitLog.push(['enter', path.node.type]);
if (path.node.identifier === 'baz') {
sawBaz = true;
}
if (path.node.identifier === 'bar') {
sawBar = true;
}
},
exit: (path) => {
visitLog.push(['exit', path.node.type]);
},
},
});

expect(visitLog).toEqual([
['enter', 'binary'],

// tree.operator
['enter', 'literal'],
['exit', 'literal'],

// tree.left
['enter', 'identifier'],
['exit', 'identifier'],

// tree.right
['enter', 'group'],
// No exit because it got replaced

// Replaced tree.right
['enter', 'identifier'],
['exit', 'identifier'],

['exit', 'binary'],
]);

// The children of the node that got replaced should not be visited
expect(sawBar).toBeFalsy();

// The children of the new replacement node should be visited
expect(sawBaz).toBeTruthy();
})
37 changes: 19 additions & 18 deletions src/ast/visit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -115,27 +115,28 @@ export const visit = (ast: Program | AstNode, visitors: NodeVisitors) => {
}
}

const toTraverse = (path.replaced as AstNode | undefined) ?? node;
const newPath = path.replaced
? makePath(toTraverse, parent, parentPath, key, index)
: path;
Object.entries(toTraverse)
.filter(([_, nodeValue]) => isTraversable(nodeValue))
.forEach(([nodeKey, nodeValue]) => {
if (Array.isArray(nodeValue)) {
for (let i = 0, offset = 0; i - offset < nodeValue.length; i++) {
const child = nodeValue[i - offset];
const res = visitNode(child, toTraverse, newPath, nodeKey, i - offset);
if (res?.removed) {
offset += 1;
if (path.replaced) {
const replacedNode = path.replaced as AstNode;
visitNode(replacedNode, parent, parentPath, key, index);
} else {
Object.entries(node)
.filter(([_, nodeValue]) => isTraversable(nodeValue))
.forEach(([nodeKey, nodeValue]) => {
if (Array.isArray(nodeValue)) {
for (let i = 0, offset = 0; i - offset < nodeValue.length; i++) {
const child = nodeValue[i - offset];
const res = visitNode(child, node, path, nodeKey, i - offset);
if (res?.removed) {
offset += 1;
}
}
} else {
visitNode(nodeValue, node, path, nodeKey);
}
} else {
visitNode(nodeValue, toTraverse, newPath, nodeKey);
}
});
});

visitor?.exit?.(path as any);
visitor?.exit?.(path as any);
}
};

visitNode(ast);
Expand Down

0 comments on commit b400952

Please sign in to comment.