@speclynx/apidom-traverse 4.0.3 → 4.0.4

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/src/traversal.ts DELETED
@@ -1,391 +0,0 @@
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
-
10
- import { Path } from './Path.ts';
11
- import type { VisitorFn, VisitorResult } from './Path.ts';
12
- import { getNodeType, isNode, cloneNode, mutateNode, getVisitFn, getNodeKeys } from './visitors.ts';
13
-
14
- /**
15
- * Options for the traverse function.
16
- * @public
17
- */
18
- export interface TraverseOptions<TNode> {
19
- /**
20
- * Map of node types to their traversable keys, or a function that returns keys for a node.
21
- * Defaults to predicate-based detection for ApiDOM elements.
22
- */
23
- keyMap?: Record<string, readonly string[]> | ((node: TNode) => readonly string[]) | null;
24
- /**
25
- * State object to assign to visitor during traversal.
26
- */
27
- state?: Record<string, unknown>;
28
- /**
29
- * Function to get the type of a node. Defaults to `node.type`.
30
- */
31
- nodeTypeGetter?: (node: TNode) => string | undefined;
32
- /**
33
- * Predicate to check if a value is a valid node.
34
- */
35
- nodePredicate?: (value: unknown) => value is TNode;
36
- /**
37
- * Function to clone a node. Used when edits are made in immutable mode.
38
- */
39
- nodeCloneFn?: (node: TNode) => TNode;
40
- /**
41
- * Whether to detect and skip cycles. Defaults to true.
42
- */
43
- detectCycles?: boolean;
44
- /**
45
- * If true, edits modify the original tree in place.
46
- * If false (default), creates a new tree with changes applied.
47
- */
48
- mutable?: boolean;
49
- /**
50
- * Custom function for applying mutations in mutable mode.
51
- * Handles ApiDOM structures (MemberElement, arrays) by default.
52
- */
53
- mutationFn?: (parent: TNode, key: PropertyKey, value: TNode | null) => void;
54
- }
55
-
56
- // =============================================================================
57
- // Internal types for generator
58
- // =============================================================================
59
-
60
- interface VisitorCall<TNode> {
61
- visitFn: VisitorFn<TNode>;
62
- path: Path<TNode>;
63
- isLeaving: boolean;
64
- }
65
-
66
- interface TraversalState<TNode> {
67
- inArray: boolean;
68
- index: number;
69
- keys: readonly PropertyKey[] | TNode[];
70
- edits: Array<[PropertyKey, TNode | null]>;
71
- parentPath: Path<TNode> | null;
72
- prev: TraversalState<TNode> | undefined;
73
- }
74
-
75
- // =============================================================================
76
- // Core generator
77
- // =============================================================================
78
-
79
- function* traverseGenerator<TNode>(
80
- root: TNode,
81
- visitor: object,
82
- options: Required<TraverseOptions<TNode>>,
83
- ): Generator<VisitorCall<TNode>, TNode, VisitorResult<TNode>> {
84
- const {
85
- keyMap,
86
- state,
87
- nodeTypeGetter,
88
- nodePredicate,
89
- nodeCloneFn,
90
- detectCycles,
91
- mutable,
92
- mutationFn,
93
- } = options;
94
- const keyMapIsFunction = typeof keyMap === 'function';
95
-
96
- let stack: TraversalState<TNode> | undefined;
97
- let inArray = Array.isArray(root);
98
- let keys: readonly PropertyKey[] | TNode[] = [root];
99
- let index = -1;
100
- let parent: TNode | undefined;
101
- let edits: Array<[PropertyKey, TNode | null]> = [];
102
- let node: TNode = root;
103
- let currentPath: Path<TNode> | null = null;
104
- let parentPath: Path<TNode> | null = null;
105
- const ancestors: TNode[] = [];
106
-
107
- do {
108
- index += 1;
109
- const isLeaving = index === keys.length;
110
- let key: PropertyKey | undefined;
111
- const isEdited = isLeaving && edits.length !== 0;
112
-
113
- if (isLeaving) {
114
- key = ancestors.length === 0 ? undefined : (currentPath as Path<TNode> | null)?.key;
115
- node = parent as TNode;
116
- parent = ancestors.pop();
117
- parentPath = (currentPath as Path<TNode> | null)?.parentPath?.parentPath ?? null;
118
-
119
- if (isEdited) {
120
- if (mutable) {
121
- // Mutable mode: modify in place using mutationFn
122
- for (const [editKey, editValue] of edits) {
123
- mutationFn(node, editKey, editValue);
124
- }
125
- } else {
126
- // Immutable mode: clone then modify
127
- if (inArray) {
128
- node = (node as unknown as TNode[]).slice() as unknown as TNode;
129
- let editOffset = 0;
130
- for (const [editKey, editValue] of edits) {
131
- const arrayKey = (editKey as number) - editOffset;
132
- if (editValue === null) {
133
- (node as unknown as TNode[]).splice(arrayKey, 1);
134
- editOffset += 1;
135
- } else {
136
- (node as unknown as TNode[])[arrayKey] = editValue;
137
- }
138
- }
139
- } else {
140
- node = nodeCloneFn(node);
141
- for (const [editKey, editValue] of edits) {
142
- (node as Record<PropertyKey, unknown>)[editKey] = editValue;
143
- }
144
- }
145
- }
146
- }
147
-
148
- if (stack !== undefined) {
149
- // Restore parent's state
150
- index = stack.index;
151
- keys = stack.keys;
152
- edits = stack.edits;
153
- const parentInArray = stack.inArray;
154
- parentPath = stack.parentPath;
155
- stack = stack.prev;
156
-
157
- // Push the edited node to parent's edits for propagation up the tree
158
- // Use the restored index to get the correct key for this node in its parent
159
- // Only needed in immutable mode - mutable mode modifies in place
160
- if (isEdited && !mutable) {
161
- const editKey = parentInArray ? index : (keys[index] as PropertyKey);
162
- edits.push([editKey, node]);
163
- }
164
- inArray = parentInArray;
165
- }
166
- } else if (parent !== undefined) {
167
- key = inArray ? index : (keys[index] as PropertyKey);
168
- node = (parent as Record<PropertyKey, TNode>)[key];
169
- if (node === undefined) {
170
- continue;
171
- }
172
- }
173
-
174
- if (!Array.isArray(node)) {
175
- if (!nodePredicate(node)) {
176
- throw new ApiDOMStructuredError(`Invalid AST Node: ${String(node)}`, { node });
177
- }
178
-
179
- // Cycle detection
180
- if (detectCycles && ancestors.includes(node)) {
181
- continue;
182
- }
183
-
184
- // Always create Path for the current node (needed for parentPath chain)
185
- currentPath = new Path<TNode>(node, parent, parentPath, key, inArray);
186
-
187
- const visitFn = getVisitFn<TNode>(visitor, nodeTypeGetter(node), isLeaving);
188
-
189
- if (visitFn) {
190
- // Assign state to visitor
191
- for (const [stateKey, stateValue] of Object.entries(state)) {
192
- (visitor as Record<string, unknown>)[stateKey] = stateValue;
193
- }
194
-
195
- // Yield to caller to execute visitor
196
- const result: VisitorResult<TNode> = yield {
197
- visitFn,
198
- path: currentPath,
199
- isLeaving,
200
- };
201
-
202
- // Handle path-based control flow
203
- if (currentPath.shouldStop) {
204
- break;
205
- }
206
-
207
- if (currentPath.shouldSkip) {
208
- if (!isLeaving) {
209
- continue;
210
- }
211
- }
212
-
213
- if (currentPath.removed) {
214
- edits.push([key!, null]);
215
- if (!isLeaving) {
216
- continue;
217
- }
218
- } else if (currentPath._wasReplaced()) {
219
- const replacement = currentPath._getReplacementNode()!;
220
- edits.push([key!, replacement]);
221
- if (!isLeaving) {
222
- if (nodePredicate(replacement)) {
223
- node = replacement;
224
- } else {
225
- continue;
226
- }
227
- }
228
- } else if (result !== undefined) {
229
- // Support return value replacement for backwards compatibility
230
- edits.push([key!, result as TNode]);
231
- if (!isLeaving) {
232
- if (nodePredicate(result)) {
233
- node = result as TNode;
234
- } else {
235
- continue;
236
- }
237
- }
238
- }
239
-
240
- // Mark path as stale after processing - warns if used from child visitors
241
- currentPath._markStale();
242
- }
243
- }
244
-
245
- if (!isLeaving) {
246
- stack = { inArray, index, keys, edits, parentPath, prev: stack };
247
- inArray = Array.isArray(node);
248
- if (inArray) {
249
- keys = node as unknown as TNode[];
250
- } else if (keyMapIsFunction) {
251
- keys = keyMap(node);
252
- } else {
253
- const nodeType = nodeTypeGetter(node);
254
- keys =
255
- nodeType !== undefined
256
- ? ((keyMap as Record<string, readonly string[]>)[nodeType] ?? [])
257
- : [];
258
- }
259
- index = -1;
260
- edits = [];
261
- if (parent !== undefined) {
262
- ancestors.push(parent);
263
- }
264
- parent = node;
265
- parentPath = currentPath;
266
- }
267
- } while (stack !== undefined);
268
-
269
- if (edits.length !== 0) {
270
- return edits.at(-1)![1] as TNode;
271
- }
272
-
273
- return root;
274
- }
275
-
276
- // =============================================================================
277
- // Public API
278
- // =============================================================================
279
-
280
- /**
281
- * traverse() walks through a tree using preorder depth-first traversal, calling
282
- * the visitor's enter function at each node, and calling leave after visiting
283
- * that node and all its children.
284
- *
285
- * Visitors receive a Path object with:
286
- * - `path.node` - the current node
287
- * - `path.parent` - the parent node
288
- * - `path.key` - key in parent
289
- * - `path.parentPath` - parent Path (linked list structure)
290
- * - `path.replaceWith(node)` - replace current node
291
- * - `path.remove()` - remove current node
292
- * - `path.skip()` - skip children (enter only)
293
- * - `path.stop()` - stop all traversal
294
- *
295
- * When editing, the original tree is not modified. A new version with changes
296
- * applied is returned.
297
- *
298
- * @example
299
- * ```typescript
300
- * const edited = traverse(ast, {
301
- * enter(path) {
302
- * console.log(path.node);
303
- * if (shouldSkip) path.skip();
304
- * if (shouldReplace) path.replaceWith(newNode);
305
- * },
306
- * leave(path) {
307
- * if (shouldRemove) path.remove();
308
- * }
309
- * });
310
- * ```
311
- *
312
- * Visitor patterns supported:
313
- * 1. `{ Kind(path) {} }` - enter specific node type
314
- * 2. `{ Kind: { enter(path) {}, leave(path) {} } }` - enter/leave specific type
315
- * 3. `{ enter(path) {}, leave(path) {} }` - enter/leave any node
316
- * 4. `{ enter: { Kind(path) {} }, leave: { Kind(path) {} } }` - parallel style
317
- *
318
- * @public
319
- */
320
- export const traverse = <TNode>(
321
- root: TNode,
322
- visitor: object,
323
- options: TraverseOptions<TNode> = {},
324
- ): TNode => {
325
- const resolvedOptions: Required<TraverseOptions<TNode>> = {
326
- keyMap: options.keyMap ?? (getNodeKeys as (node: TNode) => readonly string[]),
327
- state: options.state ?? {},
328
- nodeTypeGetter: options.nodeTypeGetter ?? (getNodeType as (node: TNode) => string | undefined),
329
- nodePredicate: options.nodePredicate ?? (isNode as (value: unknown) => value is TNode),
330
- nodeCloneFn: options.nodeCloneFn ?? (cloneNode as (node: TNode) => TNode),
331
- detectCycles: options.detectCycles ?? true,
332
- mutable: options.mutable ?? false,
333
- mutationFn: options.mutationFn ?? mutateNode,
334
- };
335
-
336
- const generator = traverseGenerator(root, visitor, resolvedOptions);
337
- let step = generator.next();
338
-
339
- while (!step.done) {
340
- const call = step.value;
341
- const result = call.visitFn.call(visitor, call.path);
342
-
343
- if (isPromise(result)) {
344
- throw new ApiDOMStructuredError('Async visitor not supported in sync mode', {
345
- visitor,
346
- visitFn: call.visitFn,
347
- });
348
- }
349
-
350
- step = generator.next(result);
351
- }
352
-
353
- return step.value;
354
- };
355
-
356
- /**
357
- * Async version of traverse().
358
- * @public
359
- */
360
- export const traverseAsync = async <TNode>(
361
- root: TNode,
362
- visitor: object,
363
- options: TraverseOptions<TNode> = {},
364
- ): Promise<TNode> => {
365
- const resolvedOptions: Required<TraverseOptions<TNode>> = {
366
- keyMap: options.keyMap ?? (getNodeKeys as (node: TNode) => readonly string[]),
367
- state: options.state ?? {},
368
- nodeTypeGetter: options.nodeTypeGetter ?? (getNodeType as (node: TNode) => string | undefined),
369
- nodePredicate: options.nodePredicate ?? (isNode as (value: unknown) => value is TNode),
370
- nodeCloneFn: options.nodeCloneFn ?? (cloneNode as (node: TNode) => TNode),
371
- detectCycles: options.detectCycles ?? true,
372
- mutable: options.mutable ?? false,
373
- mutationFn: options.mutationFn ?? mutateNode,
374
- };
375
-
376
- const generator = traverseGenerator(root, visitor, resolvedOptions);
377
- let step = generator.next();
378
-
379
- while (!step.done) {
380
- const call = step.value;
381
- const result = await call.visitFn.call(visitor, call.path);
382
-
383
- step = generator.next(result);
384
- }
385
-
386
- return step.value;
387
- };
388
-
389
- // Attach async version for promisify compatibility
390
- (traverse as unknown as Record<symbol, unknown>)[Symbol.for('nodejs.util.promisify.custom')] =
391
- traverseAsync;