@kubb/ast 5.0.0-alpha.66 → 5.0.0-alpha.69

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kubb/ast",
3
- "version": "5.0.0-alpha.66",
3
+ "version": "5.0.0-alpha.69",
4
4
  "description": "Spec-agnostic AST layer for Kubb. Defines nodes, visitor pattern, and factory functions used across codegen plugins.",
5
5
  "keywords": [
6
6
  "ast",
package/src/index.ts CHANGED
@@ -34,5 +34,16 @@ export { childName, collectImports, enumPropName, findDiscriminator } from './re
34
34
  export { mergeAdjacentObjects, setDiscriminatorEnum, setEnumName, simplifyUnion } from './transformers.ts'
35
35
  export type * from './types.ts'
36
36
  export type { OperationParamsResolver } from './utils.ts'
37
- export { caseParams, createDiscriminantNode, createOperationParams, extractStringsFromNodes, isStringType, syncSchemaRef } from './utils.ts'
37
+ export {
38
+ caseParams,
39
+ collectReferencedSchemaNames,
40
+ containsCircularRef,
41
+ createDiscriminantNode,
42
+ createOperationParams,
43
+ extractStringsFromNodes,
44
+ findCircularSchemas,
45
+ isStringType,
46
+ resolveRefName,
47
+ syncSchemaRef,
48
+ } from './utils.ts'
38
49
  export { collect, transform, walk } from './visitor.ts'
package/src/utils.ts CHANGED
@@ -16,6 +16,8 @@ import type {
16
16
  SourceNode,
17
17
  } from './nodes/index.ts'
18
18
  import type { SchemaType } from './nodes/schema.ts'
19
+ import { extractRefName } from './refs.ts'
20
+ import { collect } from './visitor.ts'
19
21
 
20
22
  const plainStringTypes = new Set<SchemaType>(['string', 'uuid', 'email', 'url', 'datetime'] as const)
21
23
 
@@ -739,3 +741,133 @@ export function extractStringsFromNodes(nodes: Array<CodeNode> | undefined): str
739
741
  .filter(Boolean)
740
742
  .join('\n')
741
743
  }
744
+
745
+ /**
746
+ * Resolves the referenced schema name of a `ref` node, falling back through
747
+ * `ref` → `name` → nested `schema.name`. Returns `undefined` for non-ref
748
+ * nodes or when no name can be resolved.
749
+ *
750
+ * @example
751
+ * ```ts
752
+ * resolveRefName({ kind: 'Schema', type: 'ref', ref: '#/components/schemas/Pet' })
753
+ * // => 'Pet'
754
+ * ```
755
+ */
756
+ export function resolveRefName(node: SchemaNode | undefined): string | undefined {
757
+ if (!node || node.type !== 'ref') return undefined
758
+ if (node.ref) return extractRefName(node.ref) ?? node.name ?? node.schema?.name ?? undefined
759
+
760
+ return node.name ?? node.schema?.name ?? undefined
761
+ }
762
+
763
+ /**
764
+ * Recursively collects every named schema referenced (transitively) from
765
+ * `node` via `ref` edges. Refs are followed by name only — the resolved
766
+ * `node.schema` of a ref is not traversed inline.
767
+ *
768
+ * @example
769
+ * ```ts
770
+ * const refs = collectReferencedSchemaNames(petSchema)
771
+ * // => Set { 'Cat', 'Dog' }
772
+ * ```
773
+ */
774
+ export function collectReferencedSchemaNames(node: SchemaNode | undefined, out: Set<string> = new Set()): Set<string> {
775
+ if (!node) return out
776
+ collect<void>(node, {
777
+ schema(child) {
778
+ if (child.type === 'ref') {
779
+ const name = resolveRefName(child)
780
+
781
+ if (name) out.add(name)
782
+ }
783
+ return undefined
784
+ },
785
+ })
786
+ return out
787
+ }
788
+
789
+ /**
790
+ * Identifies every named schema that participates in a circular dependency
791
+ * chain — including direct self-loops (e.g. `TreeNode → TreeNode`) and indirect
792
+ * cycles spanning multiple schemas (e.g. `Pet → Cat → Pet`).
793
+ *
794
+ * The returned set contains schema names. Plugins that translate schemas into
795
+ * a host language can use this to wrap recursive positions in a deferred
796
+ * construct (lazy getter, `z.lazy(() => …)`, etc.) and avoid runtime stack
797
+ * overflows when the generated code is executed.
798
+ *
799
+ * Refs are followed by name only — `node.schema` (the resolved referent) is
800
+ * not traversed inline, which keeps the algorithm linear in the size of the
801
+ * schema graph.
802
+ *
803
+ * @example
804
+ * ```ts
805
+ * const circular = findCircularSchemas(inputNode.schemas)
806
+ * if (circular.has('Pet')) {
807
+ * // emit lazy wrapper for any property whose schema references Pet
808
+ * }
809
+ * ```
810
+ */
811
+ export function findCircularSchemas(schemas: ReadonlyArray<SchemaNode>): Set<string> {
812
+ const graph = new Map<string, Set<string>>()
813
+
814
+ for (const schema of schemas) {
815
+ if (!schema.name) continue
816
+ graph.set(schema.name, collectReferencedSchemaNames(schema))
817
+ }
818
+
819
+ const circular = new Set<string>()
820
+ for (const start of graph.keys()) {
821
+ const visited = new Set<string>()
822
+ const stack: string[] = [...(graph.get(start) ?? [])]
823
+ while (stack.length > 0) {
824
+ const node = stack.pop()!
825
+ if (node === start) {
826
+ circular.add(start)
827
+ break
828
+ }
829
+ if (visited.has(node)) continue
830
+ visited.add(node)
831
+
832
+ const next = graph.get(node)
833
+ if (next) for (const r of next) stack.push(r)
834
+ }
835
+ }
836
+
837
+ return circular
838
+ }
839
+
840
+ /**
841
+ * Returns true when `node` (or anything nested within it) carries a `ref`
842
+ * whose resolved name belongs to `circularSchemas`.
843
+ *
844
+ * When `excludeName` is provided, refs to that name are ignored — useful
845
+ * when self-references are already handled separately from cross-schema
846
+ * cycles (e.g. the faker plugin emits `undefined as any` for direct
847
+ * self-recursion but a lazy getter for indirect cycles).
848
+ *
849
+ * @example
850
+ * ```ts
851
+ * const circular = findCircularSchemas(schemas)
852
+ * if (containsCircularRef(property.schema, { circularSchemas: circular, excludeName: 'Pet' })) {
853
+ * // emit `get foo() { return fakeCat() }` instead of eager call
854
+ * }
855
+ * ```
856
+ */
857
+ export function containsCircularRef(
858
+ node: SchemaNode | undefined,
859
+ { circularSchemas, excludeName }: { circularSchemas: ReadonlySet<string>; excludeName?: string },
860
+ ): boolean {
861
+ if (!node || circularSchemas.size === 0) return false
862
+
863
+ const matches = collect<true>(node, {
864
+ schema(child) {
865
+ if (child.type !== 'ref') return undefined
866
+ const name = resolveRefName(child)
867
+
868
+ return name && name !== excludeName && circularSchemas.has(name) ? true : undefined
869
+ },
870
+ })
871
+
872
+ return matches.length > 0
873
+ }