@kubb/ast 5.0.0-alpha.12 → 5.0.0-alpha.14

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,5 +1,6 @@
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
 
5
6
  /**
@@ -35,27 +36,47 @@ function createLimit(concurrency: number) {
35
36
  type LimitFn = ReturnType<typeof createLimit>
36
37
 
37
38
  /**
38
- * Shared options for `walk`, `transform`, and `collect`.
39
+ * Single source of truth: ordered list of `[NodeType, ParentType]` pairs
40
+ * describing which node types can be the parent of a given node in the AST.
41
+ *
42
+ * `ParentOf` walks this tuple and returns the parent type of the first matching entry.
39
43
  */
40
- export type VisitorOptions = {
41
- depth?: VisitorDepth
42
- /**
43
- * Maximum number of sibling nodes visited concurrently inside `walk`.
44
- * @default 30
45
- */
46
- concurrency?: number
44
+ type ParentNodeMap = [
45
+ [RootNode, undefined],
46
+ [OperationNode, RootNode],
47
+ [SchemaNode, RootNode | OperationNode | SchemaNode | PropertyNode | ParameterNode | ResponseNode],
48
+ [PropertyNode, SchemaNode],
49
+ [ParameterNode, OperationNode],
50
+ [ResponseNode, OperationNode],
51
+ ]
52
+
53
+ export type ParentOf<T extends Node, TEntries extends ReadonlyArray<[Node, unknown]> = ParentNodeMap> = TEntries extends [
54
+ infer TEntry extends [Node, unknown],
55
+ ...infer TRest extends ReadonlyArray<[Node, unknown]>,
56
+ ]
57
+ ? T extends TEntry[0]
58
+ ? TEntry[1]
59
+ : ParentOf<T, TRest>
60
+ : Node
61
+
62
+ /**
63
+ * Traversal context passed as the second argument to every visitor callback.
64
+ * The `parent` field is narrowed based on the node type being visited.
65
+ */
66
+ export type VisitorContext<T extends Node = Node> = {
67
+ parent?: ParentOf<T>
47
68
  }
48
69
 
49
70
  /**
50
71
  * Synchronous visitor for `transform` and `walk`.
51
72
  */
52
73
  export type Visitor = {
53
- root?(node: RootNode): void | RootNode
54
- operation?(node: OperationNode): void | OperationNode
55
- schema?(node: SchemaNode): void | SchemaNode
56
- property?(node: PropertyNode): void | PropertyNode
57
- parameter?(node: ParameterNode): void | ParameterNode
58
- response?(node: ResponseNode): void | ResponseNode
74
+ root?(node: RootNode, context: VisitorContext<RootNode>): void | RootNode
75
+ operation?(node: OperationNode, context: VisitorContext<OperationNode>): void | OperationNode
76
+ schema?(node: SchemaNode, context: VisitorContext<SchemaNode>): void | SchemaNode
77
+ property?(node: PropertyNode, context: VisitorContext<PropertyNode>): void | PropertyNode
78
+ parameter?(node: ParameterNode, context: VisitorContext<ParameterNode>): void | ParameterNode
79
+ response?(node: ResponseNode, context: VisitorContext<ResponseNode>): void | ResponseNode
59
80
  }
60
81
 
61
82
  type MaybePromise<T> = T | Promise<T>
@@ -64,24 +85,52 @@ type MaybePromise<T> = T | Promise<T>
64
85
  * Async visitor for `walk`. Synchronous `Visitor` objects are compatible.
65
86
  */
66
87
  export type AsyncVisitor = {
67
- root?(node: RootNode): MaybePromise<void | RootNode>
68
- operation?(node: OperationNode): MaybePromise<void | OperationNode>
69
- schema?(node: SchemaNode): MaybePromise<void | SchemaNode>
70
- property?(node: PropertyNode): MaybePromise<void | PropertyNode>
71
- parameter?(node: ParameterNode): MaybePromise<void | ParameterNode>
72
- response?(node: ResponseNode): MaybePromise<void | ResponseNode>
88
+ root?(node: RootNode, context: VisitorContext<RootNode>): MaybePromise<void | RootNode>
89
+ operation?(node: OperationNode, context: VisitorContext<OperationNode>): MaybePromise<void | OperationNode>
90
+ schema?(node: SchemaNode, context: VisitorContext<SchemaNode>): MaybePromise<void | SchemaNode>
91
+ property?(node: PropertyNode, context: VisitorContext<PropertyNode>): MaybePromise<void | PropertyNode>
92
+ parameter?(node: ParameterNode, context: VisitorContext<ParameterNode>): MaybePromise<void | ParameterNode>
93
+ response?(node: ResponseNode, context: VisitorContext<ResponseNode>): MaybePromise<void | ResponseNode>
73
94
  }
74
95
 
75
96
  /**
76
97
  * Visitor for `collect`.
77
98
  */
78
99
  export type CollectVisitor<T> = {
79
- root?(node: RootNode): T | undefined
80
- operation?(node: OperationNode): T | undefined
81
- schema?(node: SchemaNode): T | undefined
82
- property?(node: PropertyNode): T | undefined
83
- parameter?(node: ParameterNode): T | undefined
84
- response?(node: ResponseNode): T | undefined
100
+ root?(node: RootNode, context: VisitorContext<RootNode>): T | undefined
101
+ operation?(node: OperationNode, context: VisitorContext<OperationNode>): T | undefined
102
+ schema?(node: SchemaNode, context: VisitorContext<SchemaNode>): T | undefined
103
+ property?(node: PropertyNode, context: VisitorContext<PropertyNode>): T | undefined
104
+ parameter?(node: ParameterNode, context: VisitorContext<ParameterNode>): T | undefined
105
+ response?(node: ResponseNode, context: VisitorContext<ResponseNode>): T | undefined
106
+ }
107
+
108
+ /**
109
+ * Options for `transform` and `collect`. Extends `Visitor` with traversal settings.
110
+ */
111
+ export type TransformOptions = Visitor & {
112
+ depth?: VisitorDepth
113
+ parent?: Node
114
+ }
115
+
116
+ /**
117
+ * Options for `walk`. Extends `AsyncVisitor` with traversal settings.
118
+ */
119
+ export type WalkOptions = AsyncVisitor & {
120
+ depth?: VisitorDepth
121
+ /**
122
+ * Maximum number of sibling nodes visited concurrently.
123
+ * @default 30
124
+ */
125
+ concurrency?: number
126
+ }
127
+
128
+ /**
129
+ * Options for `collect`. Extends `CollectVisitor` with traversal settings.
130
+ */
131
+ export type CollectOptions<T> = CollectVisitor<T> & {
132
+ depth?: VisitorDepth
133
+ parent?: Node
85
134
  }
86
135
 
87
136
  /**
@@ -125,34 +174,31 @@ function getChildren(node: Node, recurse: boolean): Array<Node> {
125
174
  * Depth-first traversal for side effects. Visitor return values are ignored.
126
175
  * Sibling nodes at each level are visited concurrently up to `options.concurrency` (default: 30).
127
176
  */
128
- export async function walk(node: Node, visitor: AsyncVisitor, options: VisitorOptions = {}): Promise<void> {
177
+ export async function walk(node: Node, options: WalkOptions): Promise<void> {
129
178
  const recurse = (options.depth ?? visitorDepths.deep) === visitorDepths.deep
130
179
  const limit = createLimit(options.concurrency ?? WALK_CONCURRENCY)
131
- return _walk(node, visitor, recurse, limit)
180
+ return _walk(node, options, recurse, limit, undefined)
132
181
  }
133
182
 
134
- /**
135
- * Internal recursive walk implementation — calls visitor then recurses into children.
136
- */
137
- async function _walk(node: Node, visitor: AsyncVisitor, recurse: boolean, limit: LimitFn): Promise<void> {
183
+ async function _walk(node: Node, visitor: AsyncVisitor, recurse: boolean, limit: LimitFn, parent: Node | undefined): Promise<void> {
138
184
  switch (node.kind) {
139
185
  case 'Root':
140
- await limit(() => visitor.root?.(node))
186
+ await limit(() => visitor.root?.(node, { parent: parent as ParentOf<RootNode> }))
141
187
  break
142
188
  case 'Operation':
143
- await limit(() => visitor.operation?.(node))
189
+ await limit(() => visitor.operation?.(node, { parent: parent as ParentOf<OperationNode> }))
144
190
  break
145
191
  case 'Schema':
146
- await limit(() => visitor.schema?.(node))
192
+ await limit(() => visitor.schema?.(node, { parent: parent as ParentOf<SchemaNode> }))
147
193
  break
148
194
  case 'Property':
149
- await limit(() => visitor.property?.(node))
195
+ await limit(() => visitor.property?.(node, { parent: parent as ParentOf<PropertyNode> }))
150
196
  break
151
197
  case 'Parameter':
152
- await limit(() => visitor.parameter?.(node))
198
+ await limit(() => visitor.parameter?.(node, { parent: parent as ParentOf<ParameterNode> }))
153
199
  break
154
200
  case 'Response':
155
- await limit(() => visitor.response?.(node))
201
+ await limit(() => visitor.response?.(node, { parent: parent as ParentOf<ResponseNode> }))
156
202
  break
157
203
  case 'FunctionParameter':
158
204
  case 'ObjectBindingParameter':
@@ -161,91 +207,94 @@ async function _walk(node: Node, visitor: AsyncVisitor, recurse: boolean, limit:
161
207
  }
162
208
 
163
209
  const children = getChildren(node, recurse)
164
- await Promise.all(children.map((child) => _walk(child, visitor, recurse, limit)))
210
+ await Promise.all(children.map((child) => _walk(child, visitor, recurse, limit, node)))
165
211
  }
166
212
 
167
213
  /**
168
214
  * Depth-first immutable transformation. Visitor return values replace nodes; `undefined` keeps the original.
169
215
  */
170
- export function transform(node: RootNode, visitor: Visitor, options?: VisitorOptions): RootNode
171
- export function transform(node: OperationNode, visitor: Visitor, options?: VisitorOptions): OperationNode
172
- export function transform(node: SchemaNode, visitor: Visitor, options?: VisitorOptions): SchemaNode
173
- export function transform(node: PropertyNode, visitor: Visitor, options?: VisitorOptions): PropertyNode
174
- export function transform(node: ParameterNode, visitor: Visitor, options?: VisitorOptions): ParameterNode
175
- export function transform(node: ResponseNode, visitor: Visitor, options?: VisitorOptions): ResponseNode
176
- export function transform(node: Node, visitor: Visitor, options?: VisitorOptions): Node
177
- export function transform(node: Node, visitor: Visitor, options: VisitorOptions = {}): Node {
178
- const recurse = (options.depth ?? visitorDepths.deep) === visitorDepths.deep
216
+ export function transform(node: RootNode, options: TransformOptions): RootNode
217
+ export function transform(node: OperationNode, options: TransformOptions): OperationNode
218
+ export function transform(node: SchemaNode, options: TransformOptions): SchemaNode
219
+ export function transform(node: PropertyNode, options: TransformOptions): PropertyNode
220
+ export function transform(node: ParameterNode, options: TransformOptions): ParameterNode
221
+ export function transform(node: ResponseNode, options: TransformOptions): ResponseNode
222
+ export function transform(node: Node, options: TransformOptions): Node
223
+ export function transform(node: Node, options: TransformOptions): Node {
224
+ const { depth, parent, ...visitor } = options
225
+ const recurse = (depth ?? visitorDepths.deep) === visitorDepths.deep
179
226
 
180
227
  switch (node.kind) {
181
228
  case 'Root': {
182
229
  let root = node
183
- const replaced = visitor.root?.(root)
230
+ const replaced = visitor.root?.(root, { parent: parent as ParentOf<RootNode> })
184
231
  if (replaced) root = replaced
185
232
 
186
233
  return {
187
234
  ...root,
188
- schemas: root.schemas.map((s) => transform(s, visitor, options)),
189
- operations: root.operations.map((op) => transform(op, visitor, options)),
235
+ schemas: root.schemas.map((s) => transform(s, { ...options, parent: root })),
236
+ operations: root.operations.map((op) => transform(op, { ...options, parent: root })),
190
237
  }
191
238
  }
192
239
  case 'Operation': {
193
240
  let op = node
194
- const replaced = visitor.operation?.(op)
241
+ const replaced = visitor.operation?.(op, { parent: parent as ParentOf<OperationNode> })
195
242
  if (replaced) op = replaced
196
243
 
197
244
  return {
198
245
  ...op,
199
- parameters: op.parameters.map((p) => transform(p, visitor, options)),
246
+ parameters: op.parameters.map((p) => transform(p, { ...options, parent: op })),
200
247
  requestBody: op.requestBody
201
- ? { ...op.requestBody, schema: op.requestBody.schema ? transform(op.requestBody.schema, visitor, options) : undefined }
248
+ ? { ...op.requestBody, schema: op.requestBody.schema ? transform(op.requestBody.schema, { ...options, parent: op }) : undefined }
202
249
  : undefined,
203
- responses: op.responses.map((r) => transform(r, visitor, options)),
250
+ responses: op.responses.map((r) => transform(r, { ...options, parent: op })),
204
251
  }
205
252
  }
206
253
  case 'Schema': {
207
254
  let schema = node
208
- const replaced = visitor.schema?.(schema)
255
+ const replaced = visitor.schema?.(schema, { parent: parent as ParentOf<SchemaNode> })
209
256
  if (replaced) schema = replaced
210
257
 
258
+ const childOptions = { ...options, parent: schema }
259
+
211
260
  return {
212
261
  ...schema,
213
- ...('properties' in schema && recurse ? { properties: schema.properties.map((p) => transform(p, visitor, options)) } : {}),
214
- ...('items' in schema && recurse ? { items: schema.items?.map((i) => transform(i, visitor, options)) } : {}),
215
- ...('members' in schema && recurse ? { members: schema.members?.map((m) => transform(m, visitor, options)) } : {}),
262
+ ...('properties' in schema && recurse ? { properties: schema.properties.map((p) => transform(p, childOptions)) } : {}),
263
+ ...('items' in schema && recurse ? { items: schema.items?.map((i) => transform(i, childOptions)) } : {}),
264
+ ...('members' in schema && recurse ? { members: schema.members?.map((m) => transform(m, childOptions)) } : {}),
216
265
  ...('additionalProperties' in schema && recurse && schema.additionalProperties && schema.additionalProperties !== true
217
- ? { additionalProperties: transform(schema.additionalProperties, visitor, options) }
266
+ ? { additionalProperties: transform(schema.additionalProperties, childOptions) }
218
267
  : {}),
219
- }
268
+ } as SchemaNode
220
269
  }
221
270
  case 'Property': {
222
271
  let prop = node
223
- const replaced = visitor.property?.(prop)
272
+ const replaced = visitor.property?.(prop, { parent: parent as ParentOf<PropertyNode> })
224
273
  if (replaced) prop = replaced
225
274
 
226
- return {
275
+ return createProperty({
227
276
  ...prop,
228
- schema: transform(prop.schema, visitor, options),
229
- }
277
+ schema: transform(prop.schema, { ...options, parent: prop }),
278
+ })
230
279
  }
231
280
  case 'Parameter': {
232
281
  let param = node
233
- const replaced = visitor.parameter?.(param)
282
+ const replaced = visitor.parameter?.(param, { parent: parent as ParentOf<ParameterNode> })
234
283
  if (replaced) param = replaced
235
284
 
236
- return {
285
+ return createParameter({
237
286
  ...param,
238
- schema: transform(param.schema, visitor, options),
239
- }
287
+ schema: transform(param.schema, { ...options, parent: param }),
288
+ })
240
289
  }
241
290
  case 'Response': {
242
291
  let response = node
243
- const replaced = visitor.response?.(response)
292
+ const replaced = visitor.response?.(response, { parent: parent as ParentOf<ResponseNode> })
244
293
  if (replaced) response = replaced
245
294
 
246
295
  return {
247
296
  ...response,
248
- schema: transform(response.schema, visitor, options),
297
+ schema: transform(response.schema, { ...options, parent: response }),
249
298
  }
250
299
  }
251
300
  case 'FunctionParameter':
@@ -255,32 +304,60 @@ export function transform(node: Node, visitor: Visitor, options: VisitorOptions
255
304
  }
256
305
  }
257
306
 
307
+ /**
308
+ * Combines multiple visitors into a single visitor that applies them sequentially (left to right).
309
+ * For each node kind, the output of one visitor becomes the input of the next.
310
+ */
311
+ export function composeTransformers(...visitors: Array<Visitor>): Visitor {
312
+ return {
313
+ root(node, context) {
314
+ return visitors.reduce<RootNode>((acc, v) => v.root?.(acc, context) ?? acc, node)
315
+ },
316
+ operation(node, context) {
317
+ return visitors.reduce<OperationNode>((acc, v) => v.operation?.(acc, context) ?? acc, node)
318
+ },
319
+ schema(node, context) {
320
+ return visitors.reduce<SchemaNode>((acc, v) => v.schema?.(acc, context) ?? acc, node)
321
+ },
322
+ property(node, context) {
323
+ return visitors.reduce<PropertyNode>((acc, v) => v.property?.(acc, context) ?? acc, node)
324
+ },
325
+ parameter(node, context) {
326
+ return visitors.reduce<ParameterNode>((acc, v) => v.parameter?.(acc, context) ?? acc, node)
327
+ },
328
+ response(node, context) {
329
+ return visitors.reduce<ResponseNode>((acc, v) => v.response?.(acc, context) ?? acc, node)
330
+ },
331
+ }
332
+ }
333
+
258
334
  /**
259
335
  * Depth-first synchronous reduction. Collects non-`undefined` visitor return values into an array.
260
336
  */
261
- export function collect<T>(node: Node, visitor: CollectVisitor<T>, options: VisitorOptions = {}): Array<T> {
262
- const recurse = (options.depth ?? visitorDepths.deep) === visitorDepths.deep
337
+ export function collect<T>(node: Node, options: CollectOptions<T>): Array<T> {
338
+ const { depth, parent, ...visitor } = options
339
+ const recurse = (depth ?? visitorDepths.deep) === visitorDepths.deep
263
340
  const results: Array<T> = []
264
341
 
265
342
  let v: T | undefined
266
343
  switch (node.kind) {
267
344
  case 'Root':
268
- v = visitor.root?.(node)
345
+ v = visitor.root?.(node, { parent: parent as ParentOf<RootNode> })
269
346
  break
270
347
  case 'Operation':
271
- v = visitor.operation?.(node)
348
+ v = visitor.operation?.(node, { parent: parent as ParentOf<OperationNode> })
272
349
  break
273
350
  case 'Schema':
274
- v = visitor.schema?.(node)
351
+ v = visitor.schema?.(node, { parent: parent as ParentOf<SchemaNode> })
275
352
  break
276
353
  case 'Property':
277
- v = visitor.property?.(node)
354
+ v = visitor.property?.(node, { parent: parent as ParentOf<PropertyNode> })
278
355
  break
279
356
  case 'Parameter':
280
- v = visitor.parameter?.(node)
357
+ v = visitor.parameter?.(node, { parent: parent as ParentOf<ParameterNode> })
281
358
  break
282
359
  case 'Response':
283
- v = visitor.response?.(node)
360
+ v = visitor.response?.(node, { parent: parent as ParentOf<ResponseNode> })
284
361
  break
285
362
  case 'FunctionParameter':
286
363
  case 'ObjectBindingParameter':
@@ -290,7 +367,7 @@ export function collect<T>(node: Node, visitor: CollectVisitor<T>, options: Visi
290
367
  if (v !== undefined) results.push(v)
291
368
 
292
369
  for (const child of getChildren(node, recurse)) {
293
- for (const item of collect(child, visitor, options)) {
370
+ for (const item of collect(child, { ...options, parent: node })) {
294
371
  results.push(item)
295
372
  }
296
373
  }