Skip to content

Commit 04b3b0d

Browse files
authored
Merge pull request #138 from code0-tech/feat/recursion-data-types
Maximum nesting depth for schema
2 parents 7888b7d + 776a5b9 commit 04b3b0d

2 files changed

Lines changed: 35 additions & 6 deletions

File tree

src/util/references.util.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -245,7 +245,7 @@ const MAX_REFERENCE_DEPTH = 7;
245245
* traversal. The DFS itself is guarded by its own seen set, so it terminates on
246246
* cycles that do not lead back to the start type.
247247
*/
248-
const isRecursiveType = (
248+
export const isRecursiveType = (
249249
type: ts.Type,
250250
checker: ts.TypeChecker,
251251
cache: Map<ts.Type, boolean>

src/util/schema.util.ts

Lines changed: 34 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import ts, {FunctionDeclaration} from "typescript";
22
import {getValues} from "./values.util";
3-
import {getReferences} from "./references.util";
3+
import {getReferences, isRecursiveType} from "./references.util";
44
import {getNodes} from "./nodes.util";
55
import {
66
FunctionDefinition,
@@ -94,6 +94,15 @@ export type Schema =
9494
| GenericInput;
9595

9696

97+
/**
98+
* Maximum object nesting depth for schema generation through recursive data
99+
* types. Same policy as reference path extraction: the visited set keeps each
100+
* individual branch finite, but a cluster of mutually recursive types still
101+
* allows combinatorially many simple paths, so those are additionally
102+
* depth-capped. Non-recursive nesting is expanded exhaustively.
103+
*/
104+
const MAX_SCHEMA_DEPTH = 7;
105+
97106
/**
98107
* Generates a schema definition for a given TypeScript type.
99108
*
@@ -126,14 +135,16 @@ export const getSchema = (
126135
functions: FunctionDefinition[],
127136
suggestions: boolean = true,
128137
suggestionType?: ts.Type,
138+
visited: Set<ts.Type> = new Set(),
139+
recursionCache: Map<ts.Type, boolean> = new Map(),
129140
): Schema => {
130141

131142
if ((parameterType.flags & ts.TypeFlags.TypeParameter) !== 0) {
132143
const decl = parameterType.symbol?.declarations?.[0]
133144
if (decl && ts.isTypeParameterDeclaration(decl) && decl.constraint) {
134145
// getTypeFromTypeNode statt getBaseConstraintOfType → aliasSymbol bleibt erhalten
135146
const constraintType = checker.getTypeFromTypeNode(decl.constraint)
136-
return getSchema(checker, node, constraintType, functionDeclarations, functions, suggestions, suggestionType)
147+
return getSchema(checker, node, constraintType, functionDeclarations, functions, suggestions, suggestionType, visited, recursionCache)
137148
}
138149
}
139150

@@ -176,7 +187,7 @@ export const getSchema = (
176187
(t) => (t.flags & (ts.TypeFlags.Undefined | ts.TypeFlags.Null)) === 0
177188
)
178189
if (nonNullish.length === 1) {
179-
const baseSchema = getSchema(checker, node, nonNullish[0], functionDeclarations, functions, false)
190+
const baseSchema = getSchema(checker, node, nonNullish[0], functionDeclarations, functions, false, undefined, visited, recursionCache)
180191
return {...baseSchema, ...combinedSuggestions}
181192
}
182193
}
@@ -215,7 +226,7 @@ export const getSchema = (
215226
const itemSchemas = itemTypes.flatMap(itemType => {
216227
const itemTypes = itemType.isUnion() ? itemType.types : [itemType];
217228
return itemTypes.map((itemType) =>
218-
getSchema(checker, node, itemType, functionDeclarations, functions, suggestions)
229+
getSchema(checker, node, itemType, functionDeclarations, functions, suggestions, undefined, visited, recursionCache)
219230
)
220231
})
221232

@@ -229,6 +240,22 @@ export const getSchema = (
229240

230241
// Handle complex object types with properties
231242
if ((parameterType.flags & ts.TypeFlags.Object) !== 0) {
243+
// Recursive data types (e.g. Order.deliveries[].order) are cut off via
244+
// the visited set — the checker caches type identities, so a cycle
245+
// revisits the same ts.Type object. The set only tracks the current
246+
// branch (backtracked below) so the same type may still be expanded on
247+
// sibling paths. The depth cap only applies to types that are part of a
248+
// reference cycle (checked last, it's the expensive test); purely nested
249+
// non-recursive objects are expanded to arbitrary depth.
250+
if (
251+
visited.has(parameterType) ||
252+
(visited.size >= MAX_SCHEMA_DEPTH &&
253+
isRecursiveType(parameterType, checker, recursionCache))
254+
) {
255+
return {input: "data", ...combinedSuggestions};
256+
}
257+
visited.add(parameterType);
258+
232259
const properties: Record<string, Schema | Schema[]> = {};
233260
const required: string[] = [];
234261

@@ -261,7 +288,7 @@ export const getSchema = (
261288

262289
// Recursively generate schemas for property types
263290
const propertySchemas = propertyTypes.map((type) =>
264-
getSchema(checker, node, type, functionDeclarations, functions, suggestions)
291+
getSchema(checker, node, type, functionDeclarations, functions, suggestions, undefined, visited, recursionCache)
265292
);
266293

267294
properties[property.name] =
@@ -273,6 +300,8 @@ export const getSchema = (
273300
}
274301
}
275302

303+
visited.delete(parameterType);
304+
276305
return {
277306
input: "data",
278307
properties,

0 commit comments

Comments
 (0)