@speclynx/apidom-traverse 4.0.2 → 4.0.3
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/CHANGELOG.md +6 -0
- package/package.json +7 -8
- package/src/Path.cjs +342 -0
- package/src/Path.mjs +338 -0
- package/src/Path.ts +368 -0
- package/src/index.cjs +43 -0
- package/src/index.mjs +19 -0
- package/src/index.ts +44 -0
- package/src/operations/filter.cjs +21 -0
- package/src/operations/filter.mjs +17 -0
- package/src/operations/filter.ts +27 -0
- package/src/operations/find-at-offset.cjs +45 -0
- package/src/operations/find-at-offset.mjs +40 -0
- package/src/operations/find-at-offset.ts +61 -0
- package/src/operations/find.cjs +22 -0
- package/src/operations/find.mjs +18 -0
- package/src/operations/find.ts +28 -0
- package/src/operations/for-each.cjs +36 -0
- package/src/operations/for-each.mjs +30 -0
- package/src/operations/for-each.ts +44 -0
- package/src/operations/parents.cjs +21 -0
- package/src/operations/parents.mjs +17 -0
- package/src/operations/parents.ts +24 -0
- package/src/operations/reject.cjs +14 -0
- package/src/operations/reject.mjs +9 -0
- package/src/operations/reject.ts +17 -0
- package/src/operations/some.cjs +14 -0
- package/src/operations/some.mjs +9 -0
- package/src/operations/some.ts +17 -0
- package/src/traversal.cjs +313 -0
- package/src/traversal.mjs +305 -0
- package/src/traversal.ts +391 -0
- package/src/visitors.cjs +420 -0
- package/src/visitors.mjs +407 -0
- package/src/visitors.ts +490 -0
package/src/visitors.ts
ADDED
|
@@ -0,0 +1,490 @@
|
|
|
1
|
+
import { ApiDOMStructuredError } from '@speclynx/apidom-error';
|
|
2
|
+
import {
|
|
3
|
+
isElement,
|
|
4
|
+
isMemberElement,
|
|
5
|
+
isArrayElement,
|
|
6
|
+
isObjectElement,
|
|
7
|
+
cloneShallow,
|
|
8
|
+
type Element,
|
|
9
|
+
} from '@speclynx/apidom-datamodel';
|
|
10
|
+
import { isPromise } from 'ramda-adjunct';
|
|
11
|
+
|
|
12
|
+
import { Path } from './Path.ts';
|
|
13
|
+
import type { VisitorFn, VisitorResult } from './Path.ts';
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Enter/leave visitor structure for a specific node type.
|
|
17
|
+
* @public
|
|
18
|
+
*/
|
|
19
|
+
export interface NodeVisitor<TNode, TVisitor = unknown> {
|
|
20
|
+
enter?: VisitorFn<TNode, TVisitor>;
|
|
21
|
+
leave?: VisitorFn<TNode, TVisitor>;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// =============================================================================
|
|
25
|
+
// Default implementations for ApiDOM
|
|
26
|
+
// =============================================================================
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Default node type getter - reads the `element` property and converts to Element class name.
|
|
30
|
+
* E.g., "string" -\> "StringElement", "openApi3_1" -\> "OpenApi3_1Element"
|
|
31
|
+
* @public
|
|
32
|
+
*/
|
|
33
|
+
export const getNodeType = <TNode>(node: TNode): string => {
|
|
34
|
+
const type = (node as Element)?.element;
|
|
35
|
+
if (type === undefined || type === 'element') return 'Element';
|
|
36
|
+
return `${type.charAt(0).toUpperCase()}${type.slice(1)}Element`;
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Alternative node type getter using primitive type.
|
|
41
|
+
* Returns the base element class name based on the node's primitive type.
|
|
42
|
+
* E.g., ContactElement (primitive='object') -\> "ObjectElement"
|
|
43
|
+
*
|
|
44
|
+
* Use this with `nodeTypeGetter` option when you want polymorphic behavior
|
|
45
|
+
* where specific elements fall back to their primitive type handlers.
|
|
46
|
+
* @public
|
|
47
|
+
*/
|
|
48
|
+
export const getNodePrimitiveType = <TNode>(node: TNode): string => {
|
|
49
|
+
const type = (node as Element).primitive();
|
|
50
|
+
if (type === undefined || type === 'element') return 'Element';
|
|
51
|
+
return `${type.charAt(0).toUpperCase()}${type.slice(1)}Element`;
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Default node predicate - checks if value is an ApiDOM Element.
|
|
56
|
+
* @public
|
|
57
|
+
*/
|
|
58
|
+
export const isNode = <TNode>(value: unknown): value is TNode => isElement(value);
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Default node clone function - creates a shallow clone of ApiDOM Elements.
|
|
62
|
+
* Uses cloneShallow from apidom-datamodel for proper handling of meta/attributes.
|
|
63
|
+
* @public
|
|
64
|
+
*/
|
|
65
|
+
export const cloneNode = <TNode>(node: TNode): TNode => cloneShallow(node as Element) as TNode;
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Default mutation function that handles ApiDOM structures.
|
|
69
|
+
* - MemberElement: sets parent.value
|
|
70
|
+
* - Arrays: sets parent[key]
|
|
71
|
+
* - Objects: sets parent[key] or deletes if null
|
|
72
|
+
* @public
|
|
73
|
+
*/
|
|
74
|
+
export const mutateNode = <TNode>(parent: TNode, key: PropertyKey, value: TNode | null): void => {
|
|
75
|
+
if (isMemberElement(parent)) {
|
|
76
|
+
// MemberElement stores value in .value property
|
|
77
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
78
|
+
(parent as any).value = value;
|
|
79
|
+
} else if (Array.isArray(parent)) {
|
|
80
|
+
if (value === null) {
|
|
81
|
+
// For arrays, set to undefined (caller handles cleanup)
|
|
82
|
+
parent[key as number] = undefined as unknown as TNode;
|
|
83
|
+
} else {
|
|
84
|
+
parent[key as number] = value;
|
|
85
|
+
}
|
|
86
|
+
} else if (value === null) {
|
|
87
|
+
delete (parent as Record<PropertyKey, unknown>)[key];
|
|
88
|
+
} else {
|
|
89
|
+
(parent as Record<PropertyKey, unknown>)[key] = value;
|
|
90
|
+
}
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Default function to get traversable keys for a node.
|
|
95
|
+
* Uses predicates to handle all ApiDOM element types automatically.
|
|
96
|
+
* @public
|
|
97
|
+
*/
|
|
98
|
+
export const getNodeKeys = <TNode>(node: TNode): readonly string[] => {
|
|
99
|
+
if (isMemberElement(node)) return ['key', 'value'];
|
|
100
|
+
if (isArrayElement(node) || isObjectElement(node)) return ['content'];
|
|
101
|
+
return [];
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
// =============================================================================
|
|
105
|
+
// Visitor function resolution
|
|
106
|
+
// =============================================================================
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Lookup by type with pipe-separated key support ("TypeA|TypeB").
|
|
110
|
+
* Optimized: no array allocations, uses indexOf + boundary checks.
|
|
111
|
+
*/
|
|
112
|
+
const lookup = (record: Record<string, unknown>, type: string): unknown => {
|
|
113
|
+
// Fast path: exact match
|
|
114
|
+
if (record[type] !== undefined) return record[type];
|
|
115
|
+
|
|
116
|
+
// Slow path: check pipe-separated keys
|
|
117
|
+
const len = type.length;
|
|
118
|
+
for (const key in record) {
|
|
119
|
+
// Skip keys without pipe (most common case)
|
|
120
|
+
if (!key.includes('|')) continue;
|
|
121
|
+
|
|
122
|
+
const idx = key.indexOf(type);
|
|
123
|
+
if (idx === -1) continue;
|
|
124
|
+
|
|
125
|
+
// Verify it's a complete segment (bounded by | or string edges)
|
|
126
|
+
const before = idx === 0 || key[idx - 1] === '|';
|
|
127
|
+
const after = idx + len === key.length || key[idx + len] === '|';
|
|
128
|
+
if (before && after) return record[key];
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
return undefined;
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Gets the appropriate visitor function for a node type and phase.
|
|
136
|
+
* Supports pipe-separated type keys like "TypeA|TypeB".
|
|
137
|
+
* @public
|
|
138
|
+
*/
|
|
139
|
+
export const getVisitFn = <TNode>(
|
|
140
|
+
visitor: object,
|
|
141
|
+
type: string | undefined,
|
|
142
|
+
isLeaving: boolean,
|
|
143
|
+
): VisitorFn<TNode> | null => {
|
|
144
|
+
if (type === undefined) return null;
|
|
145
|
+
|
|
146
|
+
const visitorRecord = visitor as Record<string, unknown>;
|
|
147
|
+
const phase = isLeaving ? 'leave' : 'enter';
|
|
148
|
+
|
|
149
|
+
// Pattern 1: { Type() {} } - shorthand for enter only
|
|
150
|
+
const typeVisitor = lookup(visitorRecord, type);
|
|
151
|
+
if (!isLeaving && typeof typeVisitor === 'function') {
|
|
152
|
+
return typeVisitor as VisitorFn<TNode>;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Pattern 2: { Type: { enter, leave } }
|
|
156
|
+
if (typeVisitor != null) {
|
|
157
|
+
const phaseVisitor = (typeVisitor as NodeVisitor<TNode>)[phase];
|
|
158
|
+
if (typeof phaseVisitor === 'function') {
|
|
159
|
+
return phaseVisitor;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Pattern 3: { enter() {}, leave() {} }
|
|
164
|
+
const genericVisitor = visitorRecord[phase];
|
|
165
|
+
if (typeof genericVisitor === 'function') {
|
|
166
|
+
return genericVisitor as VisitorFn<TNode>;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Pattern 4: { enter: { Type() {} } }
|
|
170
|
+
if (genericVisitor != null) {
|
|
171
|
+
const typeInPhase = lookup(genericVisitor as Record<string, unknown>, type);
|
|
172
|
+
if (typeof typeInPhase === 'function') {
|
|
173
|
+
return typeInPhase as VisitorFn<TNode>;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
return null;
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
// =============================================================================
|
|
181
|
+
// Visitor merging
|
|
182
|
+
// =============================================================================
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Options for mergeVisitors.
|
|
186
|
+
* @public
|
|
187
|
+
*/
|
|
188
|
+
export interface MergeVisitorsOptions<TNode> {
|
|
189
|
+
visitFnGetter?: typeof getVisitFn;
|
|
190
|
+
nodeTypeGetter?: (node: TNode) => string | undefined;
|
|
191
|
+
exposeEdits?: boolean;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Sync version of merged visitor.
|
|
196
|
+
* @public
|
|
197
|
+
*/
|
|
198
|
+
export interface MergedVisitor<TNode> {
|
|
199
|
+
enter?: VisitorFn<TNode>;
|
|
200
|
+
leave?: VisitorFn<TNode>;
|
|
201
|
+
[key: string]: VisitorFn<TNode> | NodeVisitor<TNode> | undefined;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Async version of merged visitor.
|
|
206
|
+
* @public
|
|
207
|
+
*/
|
|
208
|
+
export interface MergedVisitorAsync<TNode> {
|
|
209
|
+
enter?: VisitorFn<TNode>;
|
|
210
|
+
leave?: VisitorFn<TNode>;
|
|
211
|
+
[key: string]: VisitorFn<TNode> | NodeVisitor<TNode> | undefined;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Creates a new visitor instance which delegates to many visitors to run in
|
|
216
|
+
* parallel. Each visitor will be visited for each node before moving on.
|
|
217
|
+
*
|
|
218
|
+
* If a prior visitor edits a node, no following visitors will see that node.
|
|
219
|
+
* `exposeEdits=true` can be used to expose the edited node from the previous visitors.
|
|
220
|
+
* @public
|
|
221
|
+
*/
|
|
222
|
+
export const mergeVisitors = <TNode>(
|
|
223
|
+
visitors: object[],
|
|
224
|
+
options: MergeVisitorsOptions<TNode> = {},
|
|
225
|
+
): MergedVisitor<TNode> => {
|
|
226
|
+
const {
|
|
227
|
+
visitFnGetter = getVisitFn,
|
|
228
|
+
nodeTypeGetter = getNodeType as (node: TNode) => string | undefined,
|
|
229
|
+
exposeEdits = false,
|
|
230
|
+
} = options;
|
|
231
|
+
|
|
232
|
+
// Internal symbols for tracking visitor state
|
|
233
|
+
const internalSkipSymbol = Symbol('internal-skip');
|
|
234
|
+
const breakSymbol = Symbol('break');
|
|
235
|
+
// Tracks which visitors should be skipped for current subtree or permanently stopped
|
|
236
|
+
const skipping: (symbol | TNode)[] = new Array(visitors.length).fill(internalSkipSymbol);
|
|
237
|
+
|
|
238
|
+
return {
|
|
239
|
+
enter(path: Path<TNode>): VisitorResult<TNode> {
|
|
240
|
+
let currentNode = path.node;
|
|
241
|
+
let hasChanged = false;
|
|
242
|
+
|
|
243
|
+
for (let i = 0; i < visitors.length; i += 1) {
|
|
244
|
+
if (skipping[i] === internalSkipSymbol) {
|
|
245
|
+
const visitFn = visitFnGetter<TNode>(visitors[i], nodeTypeGetter(currentNode), false);
|
|
246
|
+
|
|
247
|
+
if (typeof visitFn === 'function') {
|
|
248
|
+
// Create a proxy path that tracks changes per-visitor
|
|
249
|
+
const proxyPath = createPathProxy(path, currentNode);
|
|
250
|
+
const result = visitFn.call(visitors[i], proxyPath);
|
|
251
|
+
|
|
252
|
+
// Check if the visitor is async
|
|
253
|
+
if (isPromise(result)) {
|
|
254
|
+
throw new ApiDOMStructuredError('Async visitor not supported in sync mode', {
|
|
255
|
+
visitor: visitors[i],
|
|
256
|
+
visitFn,
|
|
257
|
+
});
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// Handle path-based control flow
|
|
261
|
+
if (proxyPath.shouldStop) {
|
|
262
|
+
skipping[i] = breakSymbol;
|
|
263
|
+
break;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
if (proxyPath.shouldSkip) {
|
|
267
|
+
skipping[i] = currentNode;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
if (proxyPath.removed) {
|
|
271
|
+
path.remove();
|
|
272
|
+
return undefined;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
if (proxyPath._wasReplaced()) {
|
|
276
|
+
const replacement = proxyPath._getReplacementNode()!;
|
|
277
|
+
if (exposeEdits) {
|
|
278
|
+
currentNode = replacement;
|
|
279
|
+
hasChanged = true;
|
|
280
|
+
} else {
|
|
281
|
+
path.replaceWith(replacement);
|
|
282
|
+
return replacement;
|
|
283
|
+
}
|
|
284
|
+
} else if (result !== undefined) {
|
|
285
|
+
// Support return value replacement for backwards compatibility
|
|
286
|
+
if (exposeEdits) {
|
|
287
|
+
currentNode = result as TNode;
|
|
288
|
+
hasChanged = true;
|
|
289
|
+
} else {
|
|
290
|
+
path.replaceWith(result as TNode);
|
|
291
|
+
return result;
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
if (hasChanged) {
|
|
299
|
+
path.replaceWith(currentNode);
|
|
300
|
+
return currentNode;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
return undefined;
|
|
304
|
+
},
|
|
305
|
+
|
|
306
|
+
leave(path: Path<TNode>): VisitorResult<TNode> {
|
|
307
|
+
const currentNode = path.node;
|
|
308
|
+
|
|
309
|
+
for (let i = 0; i < visitors.length; i += 1) {
|
|
310
|
+
if (skipping[i] === internalSkipSymbol) {
|
|
311
|
+
const visitFn = visitFnGetter<TNode>(visitors[i], nodeTypeGetter(currentNode), true);
|
|
312
|
+
|
|
313
|
+
if (typeof visitFn === 'function') {
|
|
314
|
+
// Create a proxy path for leave phase
|
|
315
|
+
const proxyPath = createPathProxy(path, currentNode);
|
|
316
|
+
const result = visitFn.call(visitors[i], proxyPath);
|
|
317
|
+
|
|
318
|
+
// Check if the visitor is async
|
|
319
|
+
if (isPromise(result)) {
|
|
320
|
+
throw new ApiDOMStructuredError('Async visitor not supported in sync mode', {
|
|
321
|
+
visitor: visitors[i],
|
|
322
|
+
visitFn,
|
|
323
|
+
});
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
// Handle path-based control flow
|
|
327
|
+
if (proxyPath.shouldStop) {
|
|
328
|
+
skipping[i] = breakSymbol;
|
|
329
|
+
break;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
if (proxyPath.removed) {
|
|
333
|
+
path.remove();
|
|
334
|
+
return undefined;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
if (proxyPath._wasReplaced()) {
|
|
338
|
+
const replacement = proxyPath._getReplacementNode()!;
|
|
339
|
+
path.replaceWith(replacement);
|
|
340
|
+
return replacement;
|
|
341
|
+
} else if (result !== undefined) {
|
|
342
|
+
path.replaceWith(result as TNode);
|
|
343
|
+
return result;
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
} else if (skipping[i] === currentNode) {
|
|
347
|
+
// Reset skip state when leaving the node that was skipped
|
|
348
|
+
skipping[i] = internalSkipSymbol;
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
return undefined;
|
|
353
|
+
},
|
|
354
|
+
};
|
|
355
|
+
};
|
|
356
|
+
|
|
357
|
+
/**
|
|
358
|
+
* Async version of mergeVisitors.
|
|
359
|
+
* @public
|
|
360
|
+
*/
|
|
361
|
+
export const mergeVisitorsAsync = <TNode>(
|
|
362
|
+
visitors: object[],
|
|
363
|
+
options: MergeVisitorsOptions<TNode> = {},
|
|
364
|
+
): MergedVisitorAsync<TNode> => {
|
|
365
|
+
const {
|
|
366
|
+
visitFnGetter = getVisitFn,
|
|
367
|
+
nodeTypeGetter = getNodeType as (node: TNode) => string | undefined,
|
|
368
|
+
exposeEdits = false,
|
|
369
|
+
} = options;
|
|
370
|
+
|
|
371
|
+
const internalSkipSymbol = Symbol('internal-skip');
|
|
372
|
+
const breakSymbol = Symbol('break');
|
|
373
|
+
const skipping: (symbol | TNode)[] = new Array(visitors.length).fill(internalSkipSymbol);
|
|
374
|
+
|
|
375
|
+
return {
|
|
376
|
+
async enter(path: Path<TNode>): Promise<void | TNode | undefined> {
|
|
377
|
+
let currentNode = path.node;
|
|
378
|
+
let hasChanged = false;
|
|
379
|
+
|
|
380
|
+
for (let i = 0; i < visitors.length; i += 1) {
|
|
381
|
+
if (skipping[i] === internalSkipSymbol) {
|
|
382
|
+
const visitFn = visitFnGetter<TNode>(visitors[i], nodeTypeGetter(currentNode), false);
|
|
383
|
+
|
|
384
|
+
if (typeof visitFn === 'function') {
|
|
385
|
+
const proxyPath = createPathProxy(path, currentNode);
|
|
386
|
+
const result = await visitFn.call(visitors[i], proxyPath);
|
|
387
|
+
|
|
388
|
+
if (proxyPath.shouldStop) {
|
|
389
|
+
skipping[i] = breakSymbol;
|
|
390
|
+
break;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
if (proxyPath.shouldSkip) {
|
|
394
|
+
skipping[i] = currentNode;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
if (proxyPath.removed) {
|
|
398
|
+
path.remove();
|
|
399
|
+
return undefined;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
if (proxyPath._wasReplaced()) {
|
|
403
|
+
const replacement = proxyPath._getReplacementNode()!;
|
|
404
|
+
if (exposeEdits) {
|
|
405
|
+
currentNode = replacement;
|
|
406
|
+
hasChanged = true;
|
|
407
|
+
} else {
|
|
408
|
+
path.replaceWith(replacement);
|
|
409
|
+
return replacement;
|
|
410
|
+
}
|
|
411
|
+
} else if (result !== undefined) {
|
|
412
|
+
if (exposeEdits) {
|
|
413
|
+
currentNode = result as TNode;
|
|
414
|
+
hasChanged = true;
|
|
415
|
+
} else {
|
|
416
|
+
path.replaceWith(result as TNode);
|
|
417
|
+
return result;
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
if (hasChanged) {
|
|
425
|
+
path.replaceWith(currentNode);
|
|
426
|
+
return currentNode;
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
return undefined;
|
|
430
|
+
},
|
|
431
|
+
|
|
432
|
+
async leave(path: Path<TNode>): Promise<void | TNode | undefined> {
|
|
433
|
+
const currentNode = path.node;
|
|
434
|
+
|
|
435
|
+
for (let i = 0; i < visitors.length; i += 1) {
|
|
436
|
+
if (skipping[i] === internalSkipSymbol) {
|
|
437
|
+
const visitFn = visitFnGetter<TNode>(visitors[i], nodeTypeGetter(currentNode), true);
|
|
438
|
+
|
|
439
|
+
if (typeof visitFn === 'function') {
|
|
440
|
+
const proxyPath = createPathProxy(path, currentNode);
|
|
441
|
+
|
|
442
|
+
const result = await visitFn.call(visitors[i], proxyPath);
|
|
443
|
+
|
|
444
|
+
if (proxyPath.shouldStop) {
|
|
445
|
+
skipping[i] = breakSymbol;
|
|
446
|
+
break;
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
if (proxyPath.removed) {
|
|
450
|
+
path.remove();
|
|
451
|
+
return undefined;
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
if (proxyPath._wasReplaced()) {
|
|
455
|
+
const replacement = proxyPath._getReplacementNode()!;
|
|
456
|
+
path.replaceWith(replacement);
|
|
457
|
+
return replacement;
|
|
458
|
+
} else if (result !== undefined) {
|
|
459
|
+
path.replaceWith(result as TNode);
|
|
460
|
+
return result;
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
} else if (skipping[i] === currentNode) {
|
|
464
|
+
skipping[i] = internalSkipSymbol;
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
return undefined;
|
|
469
|
+
},
|
|
470
|
+
};
|
|
471
|
+
};
|
|
472
|
+
|
|
473
|
+
// Attach async version for promisify compatibility
|
|
474
|
+
(mergeVisitors as unknown as Record<symbol, unknown>)[Symbol.for('nodejs.util.promisify.custom')] =
|
|
475
|
+
mergeVisitorsAsync;
|
|
476
|
+
|
|
477
|
+
/**
|
|
478
|
+
* Creates a proxy Path that allows individual visitors to track their own
|
|
479
|
+
* control flow state without affecting the original Path.
|
|
480
|
+
* @internal
|
|
481
|
+
*/
|
|
482
|
+
function createPathProxy<TNode>(originalPath: Path<TNode>, currentNode: TNode): Path<TNode> {
|
|
483
|
+
return new Path<TNode>(
|
|
484
|
+
currentNode,
|
|
485
|
+
originalPath.parent,
|
|
486
|
+
originalPath.parentPath,
|
|
487
|
+
originalPath.key,
|
|
488
|
+
originalPath.inList,
|
|
489
|
+
);
|
|
490
|
+
}
|