@speclynx/apidom-traverse 1.12.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSES/AFL-3.0.txt +182 -0
- package/LICENSES/Apache-2.0.txt +202 -0
- package/LICENSES/BSD-3-Clause.txt +26 -0
- package/LICENSES/MIT.txt +9 -0
- package/NOTICE +74 -0
- package/README.md +238 -0
- package/dist/apidom-traverse.browser.js +6900 -0
- package/dist/apidom-traverse.browser.min.js +1 -0
- package/package.json +59 -0
- package/src/Path.cjs +276 -0
- package/src/Path.mjs +271 -0
- package/src/index.cjs +43 -0
- package/src/index.mjs +19 -0
- package/src/operations/filter.cjs +21 -0
- package/src/operations/filter.mjs +17 -0
- package/src/operations/find-at-offset.cjs +45 -0
- package/src/operations/find-at-offset.mjs +40 -0
- package/src/operations/find.cjs +22 -0
- package/src/operations/find.mjs +18 -0
- package/src/operations/for-each.cjs +37 -0
- package/src/operations/for-each.mjs +31 -0
- package/src/operations/parents.cjs +21 -0
- package/src/operations/parents.mjs +17 -0
- package/src/operations/reject.cjs +14 -0
- package/src/operations/reject.mjs +9 -0
- package/src/operations/some.cjs +14 -0
- package/src/operations/some.mjs +9 -0
- package/src/traversal.cjs +313 -0
- package/src/traversal.mjs +305 -0
- package/src/visitors.cjs +420 -0
- package/src/visitors.mjs +407 -0
- package/types/apidom-traverse.d.ts +403 -0
|
@@ -0,0 +1,313 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
exports.__esModule = true;
|
|
4
|
+
exports.traverseAsync = exports.traverse = void 0;
|
|
5
|
+
var _apidomError = require("@speclynx/apidom-error");
|
|
6
|
+
var _ramdaAdjunct = require("ramda-adjunct");
|
|
7
|
+
var _Path = require("./Path.cjs");
|
|
8
|
+
var _visitors = require("./visitors.cjs");
|
|
9
|
+
/**
|
|
10
|
+
* SPDX-FileCopyrightText: Copyright (c) GraphQL Contributors
|
|
11
|
+
*
|
|
12
|
+
* SPDX-License-Identifier: MIT
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Options for the traverse function.
|
|
17
|
+
* @public
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
// =============================================================================
|
|
21
|
+
// Internal types for generator
|
|
22
|
+
// =============================================================================
|
|
23
|
+
|
|
24
|
+
// =============================================================================
|
|
25
|
+
// Core generator
|
|
26
|
+
// =============================================================================
|
|
27
|
+
|
|
28
|
+
function* traverseGenerator(root, visitor, options) {
|
|
29
|
+
const {
|
|
30
|
+
keyMap,
|
|
31
|
+
state,
|
|
32
|
+
nodeTypeGetter,
|
|
33
|
+
nodePredicate,
|
|
34
|
+
nodeCloneFn,
|
|
35
|
+
detectCycles,
|
|
36
|
+
mutable,
|
|
37
|
+
mutationFn
|
|
38
|
+
} = options;
|
|
39
|
+
const keyMapIsFunction = typeof keyMap === 'function';
|
|
40
|
+
let stack;
|
|
41
|
+
let inArray = Array.isArray(root);
|
|
42
|
+
let keys = [root];
|
|
43
|
+
let index = -1;
|
|
44
|
+
let parent;
|
|
45
|
+
let edits = [];
|
|
46
|
+
let node = root;
|
|
47
|
+
let currentPath = null;
|
|
48
|
+
let parentPath = null;
|
|
49
|
+
const ancestors = [];
|
|
50
|
+
do {
|
|
51
|
+
index += 1;
|
|
52
|
+
const isLeaving = index === keys.length;
|
|
53
|
+
let key;
|
|
54
|
+
const isEdited = isLeaving && edits.length !== 0;
|
|
55
|
+
if (isLeaving) {
|
|
56
|
+
key = ancestors.length === 0 ? undefined : currentPath?.key;
|
|
57
|
+
node = parent;
|
|
58
|
+
parent = ancestors.pop();
|
|
59
|
+
parentPath = currentPath?.parentPath?.parentPath ?? null;
|
|
60
|
+
if (isEdited) {
|
|
61
|
+
if (mutable) {
|
|
62
|
+
// Mutable mode: modify in place using mutationFn
|
|
63
|
+
for (const [editKey, editValue] of edits) {
|
|
64
|
+
mutationFn(node, editKey, editValue);
|
|
65
|
+
}
|
|
66
|
+
} else {
|
|
67
|
+
// Immutable mode: clone then modify
|
|
68
|
+
if (inArray) {
|
|
69
|
+
node = node.slice();
|
|
70
|
+
let editOffset = 0;
|
|
71
|
+
for (const [editKey, editValue] of edits) {
|
|
72
|
+
const arrayKey = editKey - editOffset;
|
|
73
|
+
if (editValue === null) {
|
|
74
|
+
node.splice(arrayKey, 1);
|
|
75
|
+
editOffset += 1;
|
|
76
|
+
} else {
|
|
77
|
+
node[arrayKey] = editValue;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
} else {
|
|
81
|
+
node = nodeCloneFn(node);
|
|
82
|
+
for (const [editKey, editValue] of edits) {
|
|
83
|
+
node[editKey] = editValue;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
if (stack !== undefined) {
|
|
89
|
+
// Restore parent's state
|
|
90
|
+
index = stack.index;
|
|
91
|
+
keys = stack.keys;
|
|
92
|
+
edits = stack.edits;
|
|
93
|
+
const parentInArray = stack.inArray;
|
|
94
|
+
parentPath = stack.parentPath;
|
|
95
|
+
stack = stack.prev;
|
|
96
|
+
|
|
97
|
+
// Push the edited node to parent's edits for propagation up the tree
|
|
98
|
+
// Use the restored index to get the correct key for this node in its parent
|
|
99
|
+
// Only needed in immutable mode - mutable mode modifies in place
|
|
100
|
+
if (isEdited && !mutable) {
|
|
101
|
+
const editKey = parentInArray ? index : keys[index];
|
|
102
|
+
edits.push([editKey, node]);
|
|
103
|
+
}
|
|
104
|
+
inArray = parentInArray;
|
|
105
|
+
}
|
|
106
|
+
} else if (parent !== undefined) {
|
|
107
|
+
key = inArray ? index : keys[index];
|
|
108
|
+
node = parent[key];
|
|
109
|
+
if (node === undefined) {
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
if (!Array.isArray(node)) {
|
|
114
|
+
if (!nodePredicate(node)) {
|
|
115
|
+
throw new _apidomError.ApiDOMStructuredError(`Invalid AST Node: ${String(node)}`, {
|
|
116
|
+
node
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Cycle detection
|
|
121
|
+
if (detectCycles && ancestors.includes(node)) {
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Always create Path for the current node (needed for parentPath chain)
|
|
126
|
+
currentPath = new _Path.Path(node, parent, parentPath, key, inArray);
|
|
127
|
+
const visitFn = (0, _visitors.getVisitFn)(visitor, nodeTypeGetter(node), isLeaving);
|
|
128
|
+
if (visitFn) {
|
|
129
|
+
// Assign state to visitor
|
|
130
|
+
for (const [stateKey, stateValue] of Object.entries(state)) {
|
|
131
|
+
visitor[stateKey] = stateValue;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Yield to caller to execute visitor
|
|
135
|
+
const result = yield {
|
|
136
|
+
visitFn,
|
|
137
|
+
path: currentPath,
|
|
138
|
+
isLeaving
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
// Handle path-based control flow
|
|
142
|
+
if (currentPath.shouldStop) {
|
|
143
|
+
break;
|
|
144
|
+
}
|
|
145
|
+
if (currentPath.shouldSkip) {
|
|
146
|
+
if (!isLeaving) {
|
|
147
|
+
continue;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
if (currentPath.removed) {
|
|
151
|
+
edits.push([key, null]);
|
|
152
|
+
if (!isLeaving) {
|
|
153
|
+
continue;
|
|
154
|
+
}
|
|
155
|
+
} else if (currentPath._wasReplaced()) {
|
|
156
|
+
const replacement = currentPath._getReplacementNode();
|
|
157
|
+
edits.push([key, replacement]);
|
|
158
|
+
if (!isLeaving) {
|
|
159
|
+
if (nodePredicate(replacement)) {
|
|
160
|
+
node = replacement;
|
|
161
|
+
} else {
|
|
162
|
+
continue;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
} else if (result !== undefined) {
|
|
166
|
+
// Support return value replacement for backwards compatibility
|
|
167
|
+
edits.push([key, result]);
|
|
168
|
+
if (!isLeaving) {
|
|
169
|
+
if (nodePredicate(result)) {
|
|
170
|
+
node = result;
|
|
171
|
+
} else {
|
|
172
|
+
continue;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// Mark path as stale after processing - warns if used from child visitors
|
|
178
|
+
currentPath._markStale();
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
if (!isLeaving) {
|
|
182
|
+
stack = {
|
|
183
|
+
inArray,
|
|
184
|
+
index,
|
|
185
|
+
keys,
|
|
186
|
+
edits,
|
|
187
|
+
parentPath,
|
|
188
|
+
prev: stack
|
|
189
|
+
};
|
|
190
|
+
inArray = Array.isArray(node);
|
|
191
|
+
if (inArray) {
|
|
192
|
+
keys = node;
|
|
193
|
+
} else if (keyMapIsFunction) {
|
|
194
|
+
keys = keyMap(node);
|
|
195
|
+
} else {
|
|
196
|
+
const nodeType = nodeTypeGetter(node);
|
|
197
|
+
keys = nodeType !== undefined ? keyMap[nodeType] ?? [] : [];
|
|
198
|
+
}
|
|
199
|
+
index = -1;
|
|
200
|
+
edits = [];
|
|
201
|
+
if (parent !== undefined) {
|
|
202
|
+
ancestors.push(parent);
|
|
203
|
+
}
|
|
204
|
+
parent = node;
|
|
205
|
+
parentPath = currentPath;
|
|
206
|
+
}
|
|
207
|
+
} while (stack !== undefined);
|
|
208
|
+
if (edits.length !== 0) {
|
|
209
|
+
return edits.at(-1)[1];
|
|
210
|
+
}
|
|
211
|
+
return root;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// =============================================================================
|
|
215
|
+
// Public API
|
|
216
|
+
// =============================================================================
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* traverse() walks through a tree using preorder depth-first traversal, calling
|
|
220
|
+
* the visitor's enter function at each node, and calling leave after visiting
|
|
221
|
+
* that node and all its children.
|
|
222
|
+
*
|
|
223
|
+
* Visitors receive a Path object with:
|
|
224
|
+
* - `path.node` - the current node
|
|
225
|
+
* - `path.parent` - the parent node
|
|
226
|
+
* - `path.key` - key in parent
|
|
227
|
+
* - `path.parentPath` - parent Path (linked list structure)
|
|
228
|
+
* - `path.replaceWith(node)` - replace current node
|
|
229
|
+
* - `path.remove()` - remove current node
|
|
230
|
+
* - `path.skip()` - skip children (enter only)
|
|
231
|
+
* - `path.stop()` - stop all traversal
|
|
232
|
+
*
|
|
233
|
+
* When editing, the original tree is not modified. A new version with changes
|
|
234
|
+
* applied is returned.
|
|
235
|
+
*
|
|
236
|
+
* @example
|
|
237
|
+
* ```typescript
|
|
238
|
+
* const edited = traverse(ast, {
|
|
239
|
+
* enter(path) {
|
|
240
|
+
* console.log(path.node);
|
|
241
|
+
* if (shouldSkip) path.skip();
|
|
242
|
+
* if (shouldReplace) path.replaceWith(newNode);
|
|
243
|
+
* },
|
|
244
|
+
* leave(path) {
|
|
245
|
+
* if (shouldRemove) path.remove();
|
|
246
|
+
* }
|
|
247
|
+
* });
|
|
248
|
+
* ```
|
|
249
|
+
*
|
|
250
|
+
* Visitor patterns supported:
|
|
251
|
+
* 1. `{ Kind(path) {} }` - enter specific node type
|
|
252
|
+
* 2. `{ Kind: { enter(path) {}, leave(path) {} } }` - enter/leave specific type
|
|
253
|
+
* 3. `{ enter(path) {}, leave(path) {} }` - enter/leave any node
|
|
254
|
+
* 4. `{ enter: { Kind(path) {} }, leave: { Kind(path) {} } }` - parallel style
|
|
255
|
+
*
|
|
256
|
+
* @public
|
|
257
|
+
*/
|
|
258
|
+
const traverse = (root, visitor, options = {}) => {
|
|
259
|
+
const resolvedOptions = {
|
|
260
|
+
keyMap: options.keyMap ?? _visitors.getNodeKeys,
|
|
261
|
+
state: options.state ?? {},
|
|
262
|
+
nodeTypeGetter: options.nodeTypeGetter ?? _visitors.getNodeType,
|
|
263
|
+
nodePredicate: options.nodePredicate ?? _visitors.isNode,
|
|
264
|
+
nodeCloneFn: options.nodeCloneFn ?? _visitors.cloneNode,
|
|
265
|
+
detectCycles: options.detectCycles ?? true,
|
|
266
|
+
mutable: options.mutable ?? false,
|
|
267
|
+
mutationFn: options.mutationFn ?? _visitors.mutateNode
|
|
268
|
+
};
|
|
269
|
+
const generator = traverseGenerator(root, visitor, resolvedOptions);
|
|
270
|
+
let step = generator.next();
|
|
271
|
+
while (!step.done) {
|
|
272
|
+
const call = step.value;
|
|
273
|
+
const result = call.visitFn.call(visitor, call.path);
|
|
274
|
+
if ((0, _ramdaAdjunct.isPromise)(result)) {
|
|
275
|
+
throw new _apidomError.ApiDOMStructuredError('Async visitor not supported in sync mode', {
|
|
276
|
+
visitor,
|
|
277
|
+
visitFn: call.visitFn
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
step = generator.next(result);
|
|
281
|
+
}
|
|
282
|
+
return step.value;
|
|
283
|
+
};
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* Async version of traverse().
|
|
287
|
+
* @public
|
|
288
|
+
*/
|
|
289
|
+
exports.traverse = traverse;
|
|
290
|
+
const traverseAsync = async (root, visitor, options = {}) => {
|
|
291
|
+
const resolvedOptions = {
|
|
292
|
+
keyMap: options.keyMap ?? _visitors.getNodeKeys,
|
|
293
|
+
state: options.state ?? {},
|
|
294
|
+
nodeTypeGetter: options.nodeTypeGetter ?? _visitors.getNodeType,
|
|
295
|
+
nodePredicate: options.nodePredicate ?? _visitors.isNode,
|
|
296
|
+
nodeCloneFn: options.nodeCloneFn ?? _visitors.cloneNode,
|
|
297
|
+
detectCycles: options.detectCycles ?? true,
|
|
298
|
+
mutable: options.mutable ?? false,
|
|
299
|
+
mutationFn: options.mutationFn ?? _visitors.mutateNode
|
|
300
|
+
};
|
|
301
|
+
const generator = traverseGenerator(root, visitor, resolvedOptions);
|
|
302
|
+
let step = generator.next();
|
|
303
|
+
while (!step.done) {
|
|
304
|
+
const call = step.value;
|
|
305
|
+
const result = await call.visitFn.call(visitor, call.path);
|
|
306
|
+
step = generator.next(result);
|
|
307
|
+
}
|
|
308
|
+
return step.value;
|
|
309
|
+
};
|
|
310
|
+
|
|
311
|
+
// Attach async version for promisify compatibility
|
|
312
|
+
exports.traverseAsync = traverseAsync;
|
|
313
|
+
traverse[Symbol.for('nodejs.util.promisify.custom')] = traverseAsync;
|
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SPDX-FileCopyrightText: Copyright (c) GraphQL Contributors
|
|
3
|
+
*
|
|
4
|
+
* SPDX-License-Identifier: MIT
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { ApiDOMStructuredError } from '@speclynx/apidom-error';
|
|
8
|
+
import { isPromise } from 'ramda-adjunct';
|
|
9
|
+
import { Path } from "./Path.mjs";
|
|
10
|
+
import { getNodeType, isNode, cloneNode, mutateNode, getVisitFn, getNodeKeys } from "./visitors.mjs";
|
|
11
|
+
/**
|
|
12
|
+
* Options for the traverse function.
|
|
13
|
+
* @public
|
|
14
|
+
*/
|
|
15
|
+
// =============================================================================
|
|
16
|
+
// Internal types for generator
|
|
17
|
+
// =============================================================================
|
|
18
|
+
// =============================================================================
|
|
19
|
+
// Core generator
|
|
20
|
+
// =============================================================================
|
|
21
|
+
|
|
22
|
+
function* traverseGenerator(root, visitor, options) {
|
|
23
|
+
const {
|
|
24
|
+
keyMap,
|
|
25
|
+
state,
|
|
26
|
+
nodeTypeGetter,
|
|
27
|
+
nodePredicate,
|
|
28
|
+
nodeCloneFn,
|
|
29
|
+
detectCycles,
|
|
30
|
+
mutable,
|
|
31
|
+
mutationFn
|
|
32
|
+
} = options;
|
|
33
|
+
const keyMapIsFunction = typeof keyMap === 'function';
|
|
34
|
+
let stack;
|
|
35
|
+
let inArray = Array.isArray(root);
|
|
36
|
+
let keys = [root];
|
|
37
|
+
let index = -1;
|
|
38
|
+
let parent;
|
|
39
|
+
let edits = [];
|
|
40
|
+
let node = root;
|
|
41
|
+
let currentPath = null;
|
|
42
|
+
let parentPath = null;
|
|
43
|
+
const ancestors = [];
|
|
44
|
+
do {
|
|
45
|
+
index += 1;
|
|
46
|
+
const isLeaving = index === keys.length;
|
|
47
|
+
let key;
|
|
48
|
+
const isEdited = isLeaving && edits.length !== 0;
|
|
49
|
+
if (isLeaving) {
|
|
50
|
+
key = ancestors.length === 0 ? undefined : currentPath?.key;
|
|
51
|
+
node = parent;
|
|
52
|
+
parent = ancestors.pop();
|
|
53
|
+
parentPath = currentPath?.parentPath?.parentPath ?? null;
|
|
54
|
+
if (isEdited) {
|
|
55
|
+
if (mutable) {
|
|
56
|
+
// Mutable mode: modify in place using mutationFn
|
|
57
|
+
for (const [editKey, editValue] of edits) {
|
|
58
|
+
mutationFn(node, editKey, editValue);
|
|
59
|
+
}
|
|
60
|
+
} else {
|
|
61
|
+
// Immutable mode: clone then modify
|
|
62
|
+
if (inArray) {
|
|
63
|
+
node = node.slice();
|
|
64
|
+
let editOffset = 0;
|
|
65
|
+
for (const [editKey, editValue] of edits) {
|
|
66
|
+
const arrayKey = editKey - editOffset;
|
|
67
|
+
if (editValue === null) {
|
|
68
|
+
node.splice(arrayKey, 1);
|
|
69
|
+
editOffset += 1;
|
|
70
|
+
} else {
|
|
71
|
+
node[arrayKey] = editValue;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
} else {
|
|
75
|
+
node = nodeCloneFn(node);
|
|
76
|
+
for (const [editKey, editValue] of edits) {
|
|
77
|
+
node[editKey] = editValue;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
if (stack !== undefined) {
|
|
83
|
+
// Restore parent's state
|
|
84
|
+
index = stack.index;
|
|
85
|
+
keys = stack.keys;
|
|
86
|
+
edits = stack.edits;
|
|
87
|
+
const parentInArray = stack.inArray;
|
|
88
|
+
parentPath = stack.parentPath;
|
|
89
|
+
stack = stack.prev;
|
|
90
|
+
|
|
91
|
+
// Push the edited node to parent's edits for propagation up the tree
|
|
92
|
+
// Use the restored index to get the correct key for this node in its parent
|
|
93
|
+
// Only needed in immutable mode - mutable mode modifies in place
|
|
94
|
+
if (isEdited && !mutable) {
|
|
95
|
+
const editKey = parentInArray ? index : keys[index];
|
|
96
|
+
edits.push([editKey, node]);
|
|
97
|
+
}
|
|
98
|
+
inArray = parentInArray;
|
|
99
|
+
}
|
|
100
|
+
} else if (parent !== undefined) {
|
|
101
|
+
key = inArray ? index : keys[index];
|
|
102
|
+
node = parent[key];
|
|
103
|
+
if (node === undefined) {
|
|
104
|
+
continue;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
if (!Array.isArray(node)) {
|
|
108
|
+
if (!nodePredicate(node)) {
|
|
109
|
+
throw new ApiDOMStructuredError(`Invalid AST Node: ${String(node)}`, {
|
|
110
|
+
node
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Cycle detection
|
|
115
|
+
if (detectCycles && ancestors.includes(node)) {
|
|
116
|
+
continue;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Always create Path for the current node (needed for parentPath chain)
|
|
120
|
+
currentPath = new Path(node, parent, parentPath, key, inArray);
|
|
121
|
+
const visitFn = getVisitFn(visitor, nodeTypeGetter(node), isLeaving);
|
|
122
|
+
if (visitFn) {
|
|
123
|
+
// Assign state to visitor
|
|
124
|
+
for (const [stateKey, stateValue] of Object.entries(state)) {
|
|
125
|
+
visitor[stateKey] = stateValue;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Yield to caller to execute visitor
|
|
129
|
+
const result = yield {
|
|
130
|
+
visitFn,
|
|
131
|
+
path: currentPath,
|
|
132
|
+
isLeaving
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
// Handle path-based control flow
|
|
136
|
+
if (currentPath.shouldStop) {
|
|
137
|
+
break;
|
|
138
|
+
}
|
|
139
|
+
if (currentPath.shouldSkip) {
|
|
140
|
+
if (!isLeaving) {
|
|
141
|
+
continue;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
if (currentPath.removed) {
|
|
145
|
+
edits.push([key, null]);
|
|
146
|
+
if (!isLeaving) {
|
|
147
|
+
continue;
|
|
148
|
+
}
|
|
149
|
+
} else if (currentPath._wasReplaced()) {
|
|
150
|
+
const replacement = currentPath._getReplacementNode();
|
|
151
|
+
edits.push([key, replacement]);
|
|
152
|
+
if (!isLeaving) {
|
|
153
|
+
if (nodePredicate(replacement)) {
|
|
154
|
+
node = replacement;
|
|
155
|
+
} else {
|
|
156
|
+
continue;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
} else if (result !== undefined) {
|
|
160
|
+
// Support return value replacement for backwards compatibility
|
|
161
|
+
edits.push([key, result]);
|
|
162
|
+
if (!isLeaving) {
|
|
163
|
+
if (nodePredicate(result)) {
|
|
164
|
+
node = result;
|
|
165
|
+
} else {
|
|
166
|
+
continue;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// Mark path as stale after processing - warns if used from child visitors
|
|
172
|
+
currentPath._markStale();
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
if (!isLeaving) {
|
|
176
|
+
stack = {
|
|
177
|
+
inArray,
|
|
178
|
+
index,
|
|
179
|
+
keys,
|
|
180
|
+
edits,
|
|
181
|
+
parentPath,
|
|
182
|
+
prev: stack
|
|
183
|
+
};
|
|
184
|
+
inArray = Array.isArray(node);
|
|
185
|
+
if (inArray) {
|
|
186
|
+
keys = node;
|
|
187
|
+
} else if (keyMapIsFunction) {
|
|
188
|
+
keys = keyMap(node);
|
|
189
|
+
} else {
|
|
190
|
+
const nodeType = nodeTypeGetter(node);
|
|
191
|
+
keys = nodeType !== undefined ? keyMap[nodeType] ?? [] : [];
|
|
192
|
+
}
|
|
193
|
+
index = -1;
|
|
194
|
+
edits = [];
|
|
195
|
+
if (parent !== undefined) {
|
|
196
|
+
ancestors.push(parent);
|
|
197
|
+
}
|
|
198
|
+
parent = node;
|
|
199
|
+
parentPath = currentPath;
|
|
200
|
+
}
|
|
201
|
+
} while (stack !== undefined);
|
|
202
|
+
if (edits.length !== 0) {
|
|
203
|
+
return edits.at(-1)[1];
|
|
204
|
+
}
|
|
205
|
+
return root;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// =============================================================================
|
|
209
|
+
// Public API
|
|
210
|
+
// =============================================================================
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* traverse() walks through a tree using preorder depth-first traversal, calling
|
|
214
|
+
* the visitor's enter function at each node, and calling leave after visiting
|
|
215
|
+
* that node and all its children.
|
|
216
|
+
*
|
|
217
|
+
* Visitors receive a Path object with:
|
|
218
|
+
* - `path.node` - the current node
|
|
219
|
+
* - `path.parent` - the parent node
|
|
220
|
+
* - `path.key` - key in parent
|
|
221
|
+
* - `path.parentPath` - parent Path (linked list structure)
|
|
222
|
+
* - `path.replaceWith(node)` - replace current node
|
|
223
|
+
* - `path.remove()` - remove current node
|
|
224
|
+
* - `path.skip()` - skip children (enter only)
|
|
225
|
+
* - `path.stop()` - stop all traversal
|
|
226
|
+
*
|
|
227
|
+
* When editing, the original tree is not modified. A new version with changes
|
|
228
|
+
* applied is returned.
|
|
229
|
+
*
|
|
230
|
+
* @example
|
|
231
|
+
* ```typescript
|
|
232
|
+
* const edited = traverse(ast, {
|
|
233
|
+
* enter(path) {
|
|
234
|
+
* console.log(path.node);
|
|
235
|
+
* if (shouldSkip) path.skip();
|
|
236
|
+
* if (shouldReplace) path.replaceWith(newNode);
|
|
237
|
+
* },
|
|
238
|
+
* leave(path) {
|
|
239
|
+
* if (shouldRemove) path.remove();
|
|
240
|
+
* }
|
|
241
|
+
* });
|
|
242
|
+
* ```
|
|
243
|
+
*
|
|
244
|
+
* Visitor patterns supported:
|
|
245
|
+
* 1. `{ Kind(path) {} }` - enter specific node type
|
|
246
|
+
* 2. `{ Kind: { enter(path) {}, leave(path) {} } }` - enter/leave specific type
|
|
247
|
+
* 3. `{ enter(path) {}, leave(path) {} }` - enter/leave any node
|
|
248
|
+
* 4. `{ enter: { Kind(path) {} }, leave: { Kind(path) {} } }` - parallel style
|
|
249
|
+
*
|
|
250
|
+
* @public
|
|
251
|
+
*/
|
|
252
|
+
export const traverse = (root, visitor, options = {}) => {
|
|
253
|
+
const resolvedOptions = {
|
|
254
|
+
keyMap: options.keyMap ?? getNodeKeys,
|
|
255
|
+
state: options.state ?? {},
|
|
256
|
+
nodeTypeGetter: options.nodeTypeGetter ?? getNodeType,
|
|
257
|
+
nodePredicate: options.nodePredicate ?? isNode,
|
|
258
|
+
nodeCloneFn: options.nodeCloneFn ?? cloneNode,
|
|
259
|
+
detectCycles: options.detectCycles ?? true,
|
|
260
|
+
mutable: options.mutable ?? false,
|
|
261
|
+
mutationFn: options.mutationFn ?? mutateNode
|
|
262
|
+
};
|
|
263
|
+
const generator = traverseGenerator(root, visitor, resolvedOptions);
|
|
264
|
+
let step = generator.next();
|
|
265
|
+
while (!step.done) {
|
|
266
|
+
const call = step.value;
|
|
267
|
+
const result = call.visitFn.call(visitor, call.path);
|
|
268
|
+
if (isPromise(result)) {
|
|
269
|
+
throw new ApiDOMStructuredError('Async visitor not supported in sync mode', {
|
|
270
|
+
visitor,
|
|
271
|
+
visitFn: call.visitFn
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
step = generator.next(result);
|
|
275
|
+
}
|
|
276
|
+
return step.value;
|
|
277
|
+
};
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* Async version of traverse().
|
|
281
|
+
* @public
|
|
282
|
+
*/
|
|
283
|
+
export const traverseAsync = async (root, visitor, options = {}) => {
|
|
284
|
+
const resolvedOptions = {
|
|
285
|
+
keyMap: options.keyMap ?? getNodeKeys,
|
|
286
|
+
state: options.state ?? {},
|
|
287
|
+
nodeTypeGetter: options.nodeTypeGetter ?? getNodeType,
|
|
288
|
+
nodePredicate: options.nodePredicate ?? isNode,
|
|
289
|
+
nodeCloneFn: options.nodeCloneFn ?? cloneNode,
|
|
290
|
+
detectCycles: options.detectCycles ?? true,
|
|
291
|
+
mutable: options.mutable ?? false,
|
|
292
|
+
mutationFn: options.mutationFn ?? mutateNode
|
|
293
|
+
};
|
|
294
|
+
const generator = traverseGenerator(root, visitor, resolvedOptions);
|
|
295
|
+
let step = generator.next();
|
|
296
|
+
while (!step.done) {
|
|
297
|
+
const call = step.value;
|
|
298
|
+
const result = await call.visitFn.call(visitor, call.path);
|
|
299
|
+
step = generator.next(result);
|
|
300
|
+
}
|
|
301
|
+
return step.value;
|
|
302
|
+
};
|
|
303
|
+
|
|
304
|
+
// Attach async version for promisify compatibility
|
|
305
|
+
traverse[Symbol.for('nodejs.util.promisify.custom')] = traverseAsync;
|