Skip to content

Commit 9f01ea0

Browse files
Merge pull request #28 from ShaderFrog/path-stop
Adding path.stop()
2 parents f042dc0 + 5ce6e49 commit 9f01ea0

File tree

4 files changed

+119
-27
lines changed

4 files changed

+119
-27
lines changed

README.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -355,6 +355,8 @@ node itself. The path object:
355355

356356
// Don't visit any children of this node
357357
skip: () => void;
358+
// Stop traversal entirely
359+
stop: () => void;
358360
// Remove this node from the AST
359361
remove: () => void;
360362
// Replace this node with another AST node. See replaceWith() documentation.
@@ -401,6 +403,13 @@ const ast = parser.parse(`float a = 1.0;`);
401403
visitPreprocessedAst(ast, visitors);
402404
```
403405
406+
### Stopping traversal
407+
408+
To skip all children of a node, call `path.skip()`.
409+
410+
To stop traversal entirely, call `path.stop()` in either `enter()` or `exit()`.
411+
No future `enter()` nor `exit()` callbacks will fire.
412+
404413
### Visitor `.replaceWith()` Behavior
405414
406415
When you visit a node and call `path.replaceWith(otherNode)` inside the visitor's `enter()` method:

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
"engines": {
44
"node": ">=16"
55
},
6-
"version": "4.0.0",
6+
"version": "4.1.0",
77
"type": "module",
88
"description": "A GLSL ES 1.0 and 3.0 parser and preprocessor that can preserve whitespace and comments",
99
"scripts": {

src/ast/ast.test.ts

Lines changed: 81 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,16 @@ import {
44
IdentifierNode,
55
LiteralNode,
66
} from './ast-types.js';
7-
import { visit } from './visit.js';
7+
import { Path, visit } from './visit.js';
8+
9+
const visitLogger = () => {
10+
const visitLog: Array<['enter' | 'exit', AstNode['type']]> = [];
11+
const track = (type: 'enter' | 'exit') => (path: Path<any>) =>
12+
visitLog.push([type, path.node.type]);
13+
const enter = track('enter');
14+
const exit = track('exit');
15+
return [visitLog, enter, exit, track] as const;
16+
};
817

918
const literal = <T>(literal: T): LiteralNode<T> => ({
1019
type: 'literal',
@@ -69,7 +78,7 @@ test('visit()', () => {
6978
});
7079

7180
test('visit with replace', () => {
72-
const visitLog: Array<['enter' | 'exit', AstNode['type']]> = [];
81+
const [visitLog, enter, exit] = visitLogger();
7382

7483
const tree: BinaryNode = {
7584
type: 'binary',
@@ -94,42 +103,30 @@ test('visit with replace', () => {
94103
visit(tree, {
95104
group: {
96105
enter: (path) => {
97-
visitLog.push(['enter', path.node.type]);
106+
enter(path);
98107
path.replaceWith(identifier('baz'));
99108
},
100-
exit: (path) => {
101-
visitLog.push(['exit', path.node.type]);
102-
},
109+
exit,
103110
},
104111
binary: {
105-
enter: (path) => {
106-
visitLog.push(['enter', path.node.type]);
107-
},
108-
exit: (path) => {
109-
visitLog.push(['exit', path.node.type]);
110-
},
112+
enter,
113+
exit,
111114
},
112115
literal: {
113-
enter: (path) => {
114-
visitLog.push(['enter', path.node.type]);
115-
},
116-
exit: (path) => {
117-
visitLog.push(['exit', path.node.type]);
118-
},
116+
enter,
117+
exit,
119118
},
120119
identifier: {
121120
enter: (path) => {
122-
visitLog.push(['enter', path.node.type]);
121+
enter(path);
123122
if (path.node.identifier === 'baz') {
124123
sawBaz = true;
125124
}
126125
if (path.node.identifier === 'bar') {
127126
sawBar = true;
128127
}
129128
},
130-
exit: (path) => {
131-
visitLog.push(['exit', path.node.type]);
132-
},
129+
exit,
133130
},
134131
});
135132

@@ -160,4 +157,65 @@ test('visit with replace', () => {
160157

161158
// The children of the new replacement node should be visited
162159
expect(sawBaz).toBeTruthy();
163-
})
160+
});
161+
162+
test('visit stop()', () => {
163+
const [visitLog, enter, exit] = visitLogger();
164+
165+
const tree: BinaryNode = {
166+
type: 'binary',
167+
operator: literal('-'),
168+
left: {
169+
type: 'binary',
170+
operator: literal('+'),
171+
left: identifier('foo'),
172+
right: identifier('bar'),
173+
},
174+
right: {
175+
type: 'group',
176+
lp: literal('('),
177+
rp: literal(')'),
178+
expression: identifier('baz'),
179+
},
180+
};
181+
182+
visit(tree, {
183+
group: {
184+
enter,
185+
exit,
186+
},
187+
binary: {
188+
enter,
189+
exit,
190+
},
191+
literal: {
192+
enter,
193+
exit,
194+
},
195+
identifier: {
196+
enter: (path) => {
197+
enter(path);
198+
if (path.node.identifier === 'foo') {
199+
path.stop();
200+
}
201+
},
202+
exit,
203+
},
204+
});
205+
206+
expect(visitLog).toEqual([
207+
['enter', 'binary'],
208+
209+
// tree.operator
210+
['enter', 'literal'],
211+
['exit', 'literal'],
212+
213+
// tree.left
214+
['enter', 'binary'],
215+
['enter', 'literal'],
216+
['exit', 'literal'],
217+
218+
// stop on first identifier!
219+
['enter', 'identifier'],
220+
]);
221+
});

src/ast/visit.ts

Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,13 @@ export type Path<NodeType> = {
99
parentPath: Path<any> | undefined;
1010
key: string | undefined;
1111
index: number | undefined;
12+
stop: () => void;
1213
skip: () => void;
1314
remove: () => void;
1415
replaceWith: (replacer: AstNode) => void;
1516
findParent: (test: (p: Path<any>) => boolean) => Path<any> | undefined;
1617

18+
stopped?: boolean;
1719
skipped?: boolean;
1820
removed?: boolean;
1921
replaced?: any;
@@ -31,6 +33,9 @@ const makePath = <NodeType>(
3133
parentPath,
3234
key,
3335
index,
36+
stop: function () {
37+
this.stopped = true;
38+
},
3439
skip: function () {
3540
this.skipped = true;
3641
},
@@ -72,13 +77,20 @@ export type NodeVisitors = {
7277
* Apply the visitor pattern to an AST that conforms to this compiler's spec
7378
*/
7479
export const visit = (ast: Program | AstNode, visitors: NodeVisitors) => {
80+
let stopped = false;
81+
7582
const visitNode = (
7683
node: AstNode | Program,
7784
parent?: AstNode | Program,
7885
parentPath?: Path<any>,
7986
key?: string,
8087
index?: number
8188
) => {
89+
// Handle case where stop happened at exit
90+
if (stopped) {
91+
return;
92+
}
93+
8294
const visitor = visitors[node.type];
8395
const path = makePath(node, parent, parentPath, key, index);
8496
const parentNode = parent as any;
@@ -115,6 +127,11 @@ export const visit = (ast: Program | AstNode, visitors: NodeVisitors) => {
115127
}
116128
}
117129

130+
if (path.stopped) {
131+
stopped = true;
132+
return;
133+
}
134+
118135
if (path.replaced) {
119136
const replacedNode = path.replaced as AstNode;
120137
visitNode(replacedNode, parent, parentPath, key, index);
@@ -123,19 +140,27 @@ export const visit = (ast: Program | AstNode, visitors: NodeVisitors) => {
123140
.filter(([_, nodeValue]) => isTraversable(nodeValue))
124141
.forEach(([nodeKey, nodeValue]) => {
125142
if (Array.isArray(nodeValue)) {
126-
for (let i = 0, offset = 0; i - offset < nodeValue.length; i++) {
143+
for (
144+
let i = 0, offset = 0;
145+
i - offset < nodeValue.length && !stopped;
146+
i++
147+
) {
127148
const child = nodeValue[i - offset];
128149
const res = visitNode(child, node, path, nodeKey, i - offset);
129150
if (res?.removed) {
130151
offset += 1;
131152
}
132153
}
133154
} else {
134-
visitNode(nodeValue, node, path, nodeKey);
155+
if (!stopped) {
156+
visitNode(nodeValue, node, path, nodeKey);
157+
}
135158
}
136159
});
137160

138-
visitor?.exit?.(path as any);
161+
if (!stopped) {
162+
visitor?.exit?.(path as any);
163+
}
139164
}
140165
};
141166

0 commit comments

Comments
 (0)