@kubb/ast 5.0.0-alpha.3 → 5.0.0-alpha.31

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/visitor.ts CHANGED
@@ -1,7 +1,22 @@
1
1
  import type { VisitorDepth } from './constants.ts'
2
2
  import { visitorDepths, WALK_CONCURRENCY } from './constants.ts'
3
+ import { createParameter, createProperty } from './factory.ts'
3
4
  import type { Node, OperationNode, ParameterNode, PropertyNode, ResponseNode, RootNode, SchemaNode } from './nodes/index.ts'
4
5
 
6
+ /**
7
+ * Creates a small async concurrency limiter.
8
+ *
9
+ * At most `concurrency` tasks are in flight at once. Extra tasks are queued.
10
+ *
11
+ * @example
12
+ * ```ts
13
+ * const limit = createLimit(2)
14
+ * for (const task of [taskA, taskB, taskC]) {
15
+ * await limit(() => task())
16
+ * }
17
+ * // only 2 tasks run at the same time
18
+ * ```
19
+ */
5
20
  function createLimit(concurrency: number) {
6
21
  let active = 0
7
22
  const queue: Array<() => void> = []
@@ -31,64 +46,227 @@ function createLimit(concurrency: number) {
31
46
  type LimitFn = ReturnType<typeof createLimit>
32
47
 
33
48
  /**
34
- * Shared options for `walk`, `transform`, and `collect`.
49
+ * Ordered mapping of `[NodeType, ParentType]` pairs.
50
+ *
51
+ * `ParentOf` uses this map to find parent types.
35
52
  */
36
- export type VisitorOptions = {
37
- depth?: VisitorDepth
53
+ type ParentNodeMap = [
54
+ [RootNode, undefined],
55
+ [OperationNode, RootNode],
56
+ [SchemaNode, RootNode | OperationNode | SchemaNode | PropertyNode | ParameterNode | ResponseNode],
57
+ [PropertyNode, SchemaNode],
58
+ [ParameterNode, OperationNode],
59
+ [ResponseNode, OperationNode],
60
+ ]
61
+
62
+ /**
63
+ * Resolves the parent node type for a given AST node type.
64
+ *
65
+ * This is used by visitor context so `ctx.parent` is correctly typed
66
+ * for each callback.
67
+ *
68
+ * @example
69
+ * ```ts
70
+ * type RootParent = ParentOf<RootNode>
71
+ * // undefined
72
+ * ```
73
+ *
74
+ * @example
75
+ * ```ts
76
+ * type PropertyParent = ParentOf<PropertyNode>
77
+ * // SchemaNode
78
+ * ```
79
+ *
80
+ * @example
81
+ * ```ts
82
+ * type SchemaParent = ParentOf<SchemaNode>
83
+ * // RootNode | OperationNode | SchemaNode | PropertyNode | ParameterNode | ResponseNode
84
+ * ```
85
+ */
86
+ export type ParentOf<T extends Node, TEntries extends ReadonlyArray<[Node, unknown]> = ParentNodeMap> = TEntries extends [
87
+ infer TEntry extends [Node, unknown],
88
+ ...infer TRest extends ReadonlyArray<[Node, unknown]>,
89
+ ]
90
+ ? T extends TEntry[0]
91
+ ? TEntry[1]
92
+ : ParentOf<T, TRest>
93
+ : Node
94
+
95
+ /**
96
+ * Traversal context passed as the second argument to every visitor callback.
97
+ * `parent` is typed from the current node type.
98
+ *
99
+ * @example
100
+ * ```ts
101
+ * const visitor: Visitor = {
102
+ * schema(node, { parent }) {
103
+ * // parent type is narrowed by node kind
104
+ * },
105
+ * }
106
+ * ```
107
+ */
108
+ export type VisitorContext<T extends Node = Node> = {
38
109
  /**
39
- * Maximum number of sibling nodes visited concurrently inside `walk`.
40
- * @default 30
110
+ * Parent node of the currently visited node.
111
+ * For `RootNode`, this is `undefined`.
41
112
  */
42
- concurrency?: number
113
+ parent?: ParentOf<T>
43
114
  }
44
115
 
45
116
  /**
46
- * Synchronous visitor for `transform` and `walk`.
117
+ * Synchronous visitor used by `transform`.
118
+ *
119
+ * @example
120
+ * ```ts
121
+ * const visitor: Visitor = {
122
+ * operation(node) {
123
+ * return { ...node, operationId: `x_${node.operationId}` }
124
+ * },
125
+ * }
126
+ * ```
47
127
  */
48
128
  export type Visitor = {
49
- root?(node: RootNode): void | RootNode
50
- operation?(node: OperationNode): void | OperationNode
51
- schema?(node: SchemaNode): void | SchemaNode
52
- property?(node: PropertyNode): void | PropertyNode
53
- parameter?(node: ParameterNode): void | ParameterNode
54
- response?(node: ResponseNode): void | ResponseNode
129
+ root?(node: RootNode, context: VisitorContext<RootNode>): void | RootNode
130
+ operation?(node: OperationNode, context: VisitorContext<OperationNode>): void | OperationNode
131
+ schema?(node: SchemaNode, context: VisitorContext<SchemaNode>): void | SchemaNode
132
+ property?(node: PropertyNode, context: VisitorContext<PropertyNode>): void | PropertyNode
133
+ parameter?(node: ParameterNode, context: VisitorContext<ParameterNode>): void | ParameterNode
134
+ response?(node: ResponseNode, context: VisitorContext<ResponseNode>): void | ResponseNode
55
135
  }
56
136
 
137
+ /**
138
+ * Utility type for values that can be returned directly or asynchronously.
139
+ */
57
140
  type MaybePromise<T> = T | Promise<T>
58
141
 
59
142
  /**
60
143
  * Async visitor for `walk`. Synchronous `Visitor` objects are compatible.
144
+ *
145
+ * @example
146
+ * ```ts
147
+ * const visitor: AsyncVisitor = {
148
+ * async operation(node) {
149
+ * await Promise.resolve(node.operationId)
150
+ * },
151
+ * }
152
+ * ```
61
153
  */
62
154
  export type AsyncVisitor = {
63
- root?(node: RootNode): MaybePromise<void | RootNode>
64
- operation?(node: OperationNode): MaybePromise<void | OperationNode>
65
- schema?(node: SchemaNode): MaybePromise<void | SchemaNode>
66
- property?(node: PropertyNode): MaybePromise<void | PropertyNode>
67
- parameter?(node: ParameterNode): MaybePromise<void | ParameterNode>
68
- response?(node: ResponseNode): MaybePromise<void | ResponseNode>
155
+ root?(node: RootNode, context: VisitorContext<RootNode>): MaybePromise<void | RootNode>
156
+ operation?(node: OperationNode, context: VisitorContext<OperationNode>): MaybePromise<void | OperationNode>
157
+ schema?(node: SchemaNode, context: VisitorContext<SchemaNode>): MaybePromise<void | SchemaNode>
158
+ property?(node: PropertyNode, context: VisitorContext<PropertyNode>): MaybePromise<void | PropertyNode>
159
+ parameter?(node: ParameterNode, context: VisitorContext<ParameterNode>): MaybePromise<void | ParameterNode>
160
+ response?(node: ResponseNode, context: VisitorContext<ResponseNode>): MaybePromise<void | ResponseNode>
69
161
  }
70
162
 
71
163
  /**
72
- * Visitor for `collect`.
164
+ * Visitor used by `collect`.
165
+ *
166
+ * @example
167
+ * ```ts
168
+ * const visitor: CollectVisitor<string> = {
169
+ * operation(node) {
170
+ * return node.operationId
171
+ * },
172
+ * }
173
+ * ```
73
174
  */
74
175
  export type CollectVisitor<T> = {
75
- root?(node: RootNode): T | undefined
76
- operation?(node: OperationNode): T | undefined
77
- schema?(node: SchemaNode): T | undefined
78
- property?(node: PropertyNode): T | undefined
79
- parameter?(node: ParameterNode): T | undefined
80
- response?(node: ResponseNode): T | undefined
176
+ root?(node: RootNode, context: VisitorContext<RootNode>): T | undefined
177
+ operation?(node: OperationNode, context: VisitorContext<OperationNode>): T | undefined
178
+ schema?(node: SchemaNode, context: VisitorContext<SchemaNode>): T | undefined
179
+ property?(node: PropertyNode, context: VisitorContext<PropertyNode>): T | undefined
180
+ parameter?(node: ParameterNode, context: VisitorContext<ParameterNode>): T | undefined
181
+ response?(node: ResponseNode, context: VisitorContext<ResponseNode>): T | undefined
81
182
  }
82
183
 
83
184
  /**
84
- * Traversable children of `node`, respecting `recurse` for schema nodes.
185
+ * Options for `transform`.
186
+ *
187
+ * @example
188
+ * ```ts
189
+ * const options: TransformOptions = { depth: 'deep', schema: (node) => node }
190
+ * ```
191
+ *
192
+ * @example
193
+ * ```ts
194
+ * // Only transform the current node, not nested children
195
+ * const options: TransformOptions = { depth: 'shallow', schema: (node) => node }
196
+ * ```
197
+ */
198
+ export type TransformOptions = Visitor & {
199
+ /**
200
+ * Traversal depth (`'deep'` by default).
201
+ * @default 'deep'
202
+ */
203
+ depth?: VisitorDepth
204
+ /**
205
+ * Internal parent override used during recursion.
206
+ */
207
+ parent?: Node
208
+ }
209
+
210
+ /**
211
+ * Options for `walk`.
212
+ *
213
+ * @example
214
+ * ```ts
215
+ * const options: WalkOptions = { depth: 'deep', concurrency: 10, root: () => {} }
216
+ * ```
217
+ */
218
+ export type WalkOptions = AsyncVisitor & {
219
+ /**
220
+ * Traversal depth (`'deep'` by default).
221
+ * @default 'deep'
222
+ */
223
+ depth?: VisitorDepth
224
+ /**
225
+ * Maximum number of sibling nodes visited concurrently.
226
+ * @default 30
227
+ */
228
+ concurrency?: number
229
+ }
230
+
231
+ /**
232
+ * Options for `collect`.
233
+ *
234
+ * @example
235
+ * ```ts
236
+ * const options: CollectOptions<string> = { depth: 'shallow', schema: () => undefined }
237
+ * ```
238
+ */
239
+ export type CollectOptions<T> = CollectVisitor<T> & {
240
+ /**
241
+ * Traversal depth (`'deep'` by default).
242
+ * @default 'deep'
243
+ */
244
+ depth?: VisitorDepth
245
+ /**
246
+ * Internal parent override used during recursion.
247
+ */
248
+ parent?: Node
249
+ }
250
+
251
+ /**
252
+ * Returns the immediate traversable children of `node`.
253
+ *
254
+ * For `Schema` nodes, children (`properties`, `items`, `members`, and non-boolean
255
+ * `additionalProperties`) are only included
256
+ * when `recurse` is `true`; shallow mode skips them.
257
+ *
258
+ * @example
259
+ * ```ts
260
+ * const children = getChildren(operationNode, true)
261
+ * // returns parameters, requestBody schema (if present), and responses
262
+ * ```
85
263
  */
86
264
  function getChildren(node: Node, recurse: boolean): Array<Node> {
87
265
  switch (node.kind) {
88
266
  case 'Root':
89
267
  return [...node.schemas, ...node.operations]
90
268
  case 'Operation':
91
- return [...node.parameters, ...(node.requestBody ? [node.requestBody] : []), ...node.responses]
269
+ return [...node.parameters, ...(node.requestBody?.schema ? [node.requestBody.schema] : []), ...node.responses]
92
270
  case 'Schema': {
93
271
  const children: Array<Node> = []
94
272
 
@@ -97,6 +275,7 @@ function getChildren(node: Node, recurse: boolean): Array<Node> {
97
275
  if ('properties' in node && node.properties.length > 0) children.push(...node.properties)
98
276
  if ('items' in node && node.items) children.push(...node.items)
99
277
  if ('members' in node && node.members) children.push(...node.members)
278
+ if ('additionalProperties' in node && node.additionalProperties && node.additionalProperties !== true) children.push(node.additionalProperties)
100
279
 
101
280
  return children
102
281
  }
@@ -106,159 +285,277 @@ function getChildren(node: Node, recurse: boolean): Array<Node> {
106
285
  return [node.schema]
107
286
  case 'Response':
108
287
  return node.schema ? [node.schema] : []
288
+ case 'FunctionParameter':
289
+ case 'ParameterGroup':
290
+ case 'FunctionParameters':
291
+ case 'Type':
292
+ return []
109
293
  }
110
294
  }
111
295
 
112
296
  /**
113
297
  * Depth-first traversal for side effects. Visitor return values are ignored.
114
- * Sibling nodes at each level are visited concurrently up to `options.concurrency` (default: 30).
298
+ * Sibling nodes at each level are visited concurrently up to `options.concurrency`
299
+ * (default: `WALK_CONCURRENCY`).
300
+ *
301
+ * @example
302
+ * ```ts
303
+ * await walk(root, {
304
+ * operation(node) {
305
+ * console.log(node.operationId)
306
+ * },
307
+ * })
308
+ * ```
309
+ *
310
+ * @example
311
+ * ```ts
312
+ * // Visit only the current node
313
+ * await walk(root, { depth: 'shallow', root: () => {} })
314
+ * ```
115
315
  */
116
- export async function walk(node: Node, visitor: AsyncVisitor, options: VisitorOptions = {}): Promise<void> {
316
+ export async function walk(node: Node, options: WalkOptions): Promise<void> {
117
317
  const recurse = (options.depth ?? visitorDepths.deep) === visitorDepths.deep
118
318
  const limit = createLimit(options.concurrency ?? WALK_CONCURRENCY)
119
- return _walk(node, visitor, recurse, limit)
319
+
320
+ return _walk(node, options, recurse, limit, undefined)
120
321
  }
121
322
 
122
- async function _walk(node: Node, visitor: AsyncVisitor, recurse: boolean, limit: LimitFn): Promise<void> {
323
+ async function _walk(node: Node, visitor: AsyncVisitor, recurse: boolean, limit: LimitFn, parent: Node | undefined): Promise<void> {
123
324
  switch (node.kind) {
124
325
  case 'Root':
125
- await limit(() => visitor.root?.(node))
326
+ await limit(() => visitor.root?.(node, { parent: parent as ParentOf<RootNode> }))
126
327
  break
127
328
  case 'Operation':
128
- await limit(() => visitor.operation?.(node))
329
+ await limit(() => visitor.operation?.(node, { parent: parent as ParentOf<OperationNode> }))
129
330
  break
130
331
  case 'Schema':
131
- await limit(() => visitor.schema?.(node))
332
+ await limit(() => visitor.schema?.(node, { parent: parent as ParentOf<SchemaNode> }))
132
333
  break
133
334
  case 'Property':
134
- await limit(() => visitor.property?.(node))
335
+ await limit(() => visitor.property?.(node, { parent: parent as ParentOf<PropertyNode> }))
135
336
  break
136
337
  case 'Parameter':
137
- await limit(() => visitor.parameter?.(node))
338
+ await limit(() => visitor.parameter?.(node, { parent: parent as ParentOf<ParameterNode> }))
138
339
  break
139
340
  case 'Response':
140
- await limit(() => visitor.response?.(node))
341
+ await limit(() => visitor.response?.(node, { parent: parent as ParentOf<ResponseNode> }))
342
+ break
343
+ case 'FunctionParameter':
344
+ case 'ParameterGroup':
345
+ case 'FunctionParameters':
141
346
  break
142
347
  }
143
348
 
144
349
  const children = getChildren(node, recurse)
145
- await Promise.all(children.map((child) => _walk(child, visitor, recurse, limit)))
350
+ for (const child of children) {
351
+ await _walk(child, visitor, recurse, limit, node)
352
+ }
146
353
  }
147
354
 
148
355
  /**
149
- * Depth-first immutable transformation. Visitor return values replace nodes; `undefined` keeps the original.
356
+ * Runs a depth-first immutable transform.
357
+ *
358
+ * If a visitor returns a node, it replaces the current node.
359
+ * If it returns `undefined`, the current node stays the same.
360
+ *
361
+ * @example
362
+ * ```ts
363
+ * const next = transform(root, {
364
+ * operation(node) {
365
+ * return { ...node, operationId: `prefixed_${node.operationId}` }
366
+ * },
367
+ * })
368
+ * ```
369
+ *
370
+ * @example
371
+ * ```ts
372
+ * // Shallow mode: only transform the input node
373
+ * const next = transform(root, { depth: 'shallow', root: (node) => node })
374
+ * ```
150
375
  */
151
- export function transform(node: RootNode, visitor: Visitor, options?: VisitorOptions): RootNode
152
- export function transform(node: OperationNode, visitor: Visitor, options?: VisitorOptions): OperationNode
153
- export function transform(node: SchemaNode, visitor: Visitor, options?: VisitorOptions): SchemaNode
154
- export function transform(node: PropertyNode, visitor: Visitor, options?: VisitorOptions): PropertyNode
155
- export function transform(node: ParameterNode, visitor: Visitor, options?: VisitorOptions): ParameterNode
156
- export function transform(node: ResponseNode, visitor: Visitor, options?: VisitorOptions): ResponseNode
157
- export function transform(node: Node, visitor: Visitor, options?: VisitorOptions): Node
158
- export function transform(node: Node, visitor: Visitor, options: VisitorOptions = {}): Node {
159
- const recurse = (options.depth ?? visitorDepths.deep) === visitorDepths.deep
376
+ export function transform(node: RootNode, options: TransformOptions): RootNode
377
+ export function transform(node: OperationNode, options: TransformOptions): OperationNode
378
+ export function transform(node: SchemaNode, options: TransformOptions): SchemaNode
379
+ export function transform(node: PropertyNode, options: TransformOptions): PropertyNode
380
+ export function transform(node: ParameterNode, options: TransformOptions): ParameterNode
381
+ export function transform(node: ResponseNode, options: TransformOptions): ResponseNode
382
+ export function transform(node: Node, options: TransformOptions): Node
383
+ export function transform(node: Node, options: TransformOptions): Node {
384
+ const { depth, parent, ...visitor } = options
385
+ const recurse = (depth ?? visitorDepths.deep) === visitorDepths.deep
160
386
 
161
387
  switch (node.kind) {
162
388
  case 'Root': {
163
389
  let root = node
164
- const replaced = visitor.root?.(root)
390
+ const replaced = visitor.root?.(root, { parent: parent as ParentOf<RootNode> })
165
391
  if (replaced) root = replaced
166
392
 
167
393
  return {
168
394
  ...root,
169
- schemas: root.schemas.map((s) => transform(s, visitor, options)),
170
- operations: root.operations.map((op) => transform(op, visitor, options)),
395
+ schemas: root.schemas.map((s) => transform(s, { ...options, parent: root })),
396
+ operations: root.operations.map((op) => transform(op, { ...options, parent: root })),
171
397
  }
172
398
  }
173
399
  case 'Operation': {
174
400
  let op = node
175
- const replaced = visitor.operation?.(op)
401
+ const replaced = visitor.operation?.(op, { parent: parent as ParentOf<OperationNode> })
176
402
  if (replaced) op = replaced
177
403
 
178
404
  return {
179
405
  ...op,
180
- parameters: op.parameters.map((p) => transform(p, visitor, options)),
181
- requestBody: op.requestBody ? transform(op.requestBody, visitor, options) : undefined,
182
- responses: op.responses.map((r) => transform(r, visitor, options)),
406
+ parameters: op.parameters.map((p) => transform(p, { ...options, parent: op })),
407
+ requestBody: op.requestBody
408
+ ? { ...op.requestBody, schema: op.requestBody.schema ? transform(op.requestBody.schema, { ...options, parent: op }) : undefined }
409
+ : undefined,
410
+ responses: op.responses.map((r) => transform(r, { ...options, parent: op })),
183
411
  }
184
412
  }
185
413
  case 'Schema': {
186
414
  let schema = node
187
- const replaced = visitor.schema?.(schema)
415
+ const replaced = visitor.schema?.(schema, { parent: parent as ParentOf<SchemaNode> })
188
416
  if (replaced) schema = replaced
189
417
 
418
+ const childOptions = { ...options, parent: schema }
419
+
190
420
  return {
191
421
  ...schema,
192
- ...('properties' in schema && recurse ? { properties: schema.properties.map((p) => transform(p, visitor, options)) } : {}),
193
- ...('items' in schema && recurse ? { items: schema.items?.map((i) => transform(i, visitor, options)) } : {}),
194
- ...('members' in schema && recurse ? { members: schema.members?.map((m) => transform(m, visitor, options)) } : {}),
195
- }
422
+ ...('properties' in schema && recurse ? { properties: schema.properties.map((p) => transform(p, childOptions)) } : {}),
423
+ ...('items' in schema && recurse ? { items: schema.items?.map((i) => transform(i, childOptions)) } : {}),
424
+ ...('members' in schema && recurse ? { members: schema.members?.map((m) => transform(m, childOptions)) } : {}),
425
+ ...('additionalProperties' in schema && recurse && schema.additionalProperties && schema.additionalProperties !== true
426
+ ? { additionalProperties: transform(schema.additionalProperties, childOptions) }
427
+ : {}),
428
+ } as SchemaNode
196
429
  }
197
430
  case 'Property': {
198
431
  let prop = node
199
- const replaced = visitor.property?.(prop)
432
+ const replaced = visitor.property?.(prop, { parent: parent as ParentOf<PropertyNode> })
200
433
  if (replaced) prop = replaced
201
434
 
202
- return {
435
+ return createProperty({
203
436
  ...prop,
204
- schema: transform(prop.schema, visitor, options),
205
- }
437
+ schema: transform(prop.schema, { ...options, parent: prop }),
438
+ })
206
439
  }
207
440
  case 'Parameter': {
208
441
  let param = node
209
- const replaced = visitor.parameter?.(param)
442
+ const replaced = visitor.parameter?.(param, { parent: parent as ParentOf<ParameterNode> })
210
443
  if (replaced) param = replaced
211
444
 
212
- return {
445
+ return createParameter({
213
446
  ...param,
214
- schema: transform(param.schema, visitor, options),
215
- }
447
+ schema: transform(param.schema, { ...options, parent: param }),
448
+ })
216
449
  }
217
450
  case 'Response': {
218
451
  let response = node
219
- const replaced = visitor.response?.(response)
452
+ const replaced = visitor.response?.(response, { parent: parent as ParentOf<ResponseNode> })
220
453
  if (replaced) response = replaced
221
454
 
222
455
  return {
223
456
  ...response,
224
- schema: response.schema ? transform(response.schema, visitor, options) : undefined,
457
+ schema: transform(response.schema, { ...options, parent: response }),
225
458
  }
226
459
  }
460
+ case 'FunctionParameter':
461
+ case 'ParameterGroup':
462
+ case 'FunctionParameters':
463
+ case 'Type':
464
+ return node
227
465
  }
228
466
  }
229
467
 
230
468
  /**
231
- * Depth-first synchronous reduction. Collects non-`undefined` visitor return values into an array.
469
+ * Composes multiple visitors into one visitor, applied left to right.
470
+ *
471
+ * For each node kind, output from one visitor is input to the next.
472
+ * If a visitor returns `undefined`, the previous node value is kept.
473
+ *
474
+ * @example
475
+ * ```ts
476
+ * const visitor = composeTransformers(
477
+ * { operation: (node) => ({ ...node, operationId: `a_${node.operationId}` }) },
478
+ * { operation: (node) => ({ ...node, operationId: `b_${node.operationId}` }) },
479
+ * )
480
+ * ```
232
481
  */
233
- export function collect<T>(node: Node, visitor: CollectVisitor<T>, options: VisitorOptions = {}): Array<T> {
234
- const recurse = (options.depth ?? visitorDepths.deep) === visitorDepths.deep
482
+ export function composeTransformers(...visitors: Array<Visitor>): Visitor {
483
+ return {
484
+ root(node, context) {
485
+ return visitors.reduce<RootNode>((acc, v) => v.root?.(acc, context) ?? acc, node)
486
+ },
487
+ operation(node, context) {
488
+ return visitors.reduce<OperationNode>((acc, v) => v.operation?.(acc, context) ?? acc, node)
489
+ },
490
+ schema(node, context) {
491
+ return visitors.reduce<SchemaNode>((acc, v) => v.schema?.(acc, context) ?? acc, node)
492
+ },
493
+ property(node, context) {
494
+ return visitors.reduce<PropertyNode>((acc, v) => v.property?.(acc, context) ?? acc, node)
495
+ },
496
+ parameter(node, context) {
497
+ return visitors.reduce<ParameterNode>((acc, v) => v.parameter?.(acc, context) ?? acc, node)
498
+ },
499
+ response(node, context) {
500
+ return visitors.reduce<ResponseNode>((acc, v) => v.response?.(acc, context) ?? acc, node)
501
+ },
502
+ }
503
+ }
504
+
505
+ /**
506
+ * Runs a depth-first synchronous collection pass.
507
+ *
508
+ * Non-`undefined` values returned by visitor callbacks are appended to the result.
509
+ *
510
+ * @example
511
+ * ```ts
512
+ * const ids = collect(root, {
513
+ * operation(node) {
514
+ * return node.operationId
515
+ * },
516
+ * })
517
+ * ```
518
+ *
519
+ * @example
520
+ * ```ts
521
+ * // Collect from only the current node
522
+ * const values = collect(root, { depth: 'shallow', root: () => 'root' })
523
+ * ```
524
+ */
525
+ export function collect<T>(node: Node, options: CollectOptions<T>): Array<T> {
526
+ const { depth, parent, ...visitor } = options
527
+ const recurse = (depth ?? visitorDepths.deep) === visitorDepths.deep
235
528
  const results: Array<T> = []
236
529
 
237
530
  let v: T | undefined
238
531
  switch (node.kind) {
239
532
  case 'Root':
240
- v = visitor.root?.(node)
533
+ v = visitor.root?.(node, { parent: parent as ParentOf<RootNode> })
241
534
  break
242
535
  case 'Operation':
243
- v = visitor.operation?.(node)
536
+ v = visitor.operation?.(node, { parent: parent as ParentOf<OperationNode> })
244
537
  break
245
538
  case 'Schema':
246
- v = visitor.schema?.(node)
539
+ v = visitor.schema?.(node, { parent: parent as ParentOf<SchemaNode> })
247
540
  break
248
541
  case 'Property':
249
- v = visitor.property?.(node)
542
+ v = visitor.property?.(node, { parent: parent as ParentOf<PropertyNode> })
250
543
  break
251
544
  case 'Parameter':
252
- v = visitor.parameter?.(node)
545
+ v = visitor.parameter?.(node, { parent: parent as ParentOf<ParameterNode> })
253
546
  break
254
547
  case 'Response':
255
- v = visitor.response?.(node)
548
+ v = visitor.response?.(node, { parent: parent as ParentOf<ResponseNode> })
549
+ break
550
+ case 'FunctionParameter':
551
+ case 'ParameterGroup':
552
+ case 'FunctionParameters':
256
553
  break
257
554
  }
258
555
  if (v !== undefined) results.push(v)
259
556
 
260
557
  for (const child of getChildren(node, recurse)) {
261
- for (const item of collect(child, visitor, options)) {
558
+ for (const item of collect(child, { ...options, parent: node })) {
262
559
  results.push(item)
263
560
  }
264
561
  }