@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.
@@ -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;