Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/util/references.util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -245,7 +245,7 @@ const MAX_REFERENCE_DEPTH = 7;
* traversal. The DFS itself is guarded by its own seen set, so it terminates on
* cycles that do not lead back to the start type.
*/
const isRecursiveType = (
export const isRecursiveType = (
type: ts.Type,
checker: ts.TypeChecker,
cache: Map<ts.Type, boolean>
Expand Down
39 changes: 34 additions & 5 deletions src/util/schema.util.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import ts, {FunctionDeclaration} from "typescript";
import {getValues} from "./values.util";
import {getReferences} from "./references.util";
import {getReferences, isRecursiveType} from "./references.util";
import {getNodes} from "./nodes.util";
import {
FunctionDefinition,
Expand Down Expand Up @@ -94,6 +94,15 @@ export type Schema =
| GenericInput;


/**
* Maximum object nesting depth for schema generation through recursive data
* types. Same policy as reference path extraction: the visited set keeps each
* individual branch finite, but a cluster of mutually recursive types still
* allows combinatorially many simple paths, so those are additionally
* depth-capped. Non-recursive nesting is expanded exhaustively.
*/
const MAX_SCHEMA_DEPTH = 7;

/**
* Generates a schema definition for a given TypeScript type.
*
Expand Down Expand Up @@ -126,14 +135,16 @@ export const getSchema = (
functions: FunctionDefinition[],
suggestions: boolean = true,
suggestionType?: ts.Type,
visited: Set<ts.Type> = new Set(),
recursionCache: Map<ts.Type, boolean> = new Map(),
): Schema => {

if ((parameterType.flags & ts.TypeFlags.TypeParameter) !== 0) {
const decl = parameterType.symbol?.declarations?.[0]
if (decl && ts.isTypeParameterDeclaration(decl) && decl.constraint) {
// getTypeFromTypeNode statt getBaseConstraintOfType → aliasSymbol bleibt erhalten
const constraintType = checker.getTypeFromTypeNode(decl.constraint)
return getSchema(checker, node, constraintType, functionDeclarations, functions, suggestions, suggestionType)
return getSchema(checker, node, constraintType, functionDeclarations, functions, suggestions, suggestionType, visited, recursionCache)
}
}

Expand Down Expand Up @@ -176,7 +187,7 @@ export const getSchema = (
(t) => (t.flags & (ts.TypeFlags.Undefined | ts.TypeFlags.Null)) === 0
)
if (nonNullish.length === 1) {
const baseSchema = getSchema(checker, node, nonNullish[0], functionDeclarations, functions, false)
const baseSchema = getSchema(checker, node, nonNullish[0], functionDeclarations, functions, false, undefined, visited, recursionCache)
return {...baseSchema, ...combinedSuggestions}
}
}
Expand Down Expand Up @@ -215,7 +226,7 @@ export const getSchema = (
const itemSchemas = itemTypes.flatMap(itemType => {
const itemTypes = itemType.isUnion() ? itemType.types : [itemType];
return itemTypes.map((itemType) =>
getSchema(checker, node, itemType, functionDeclarations, functions, suggestions)
getSchema(checker, node, itemType, functionDeclarations, functions, suggestions, undefined, visited, recursionCache)
)
})

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

// Handle complex object types with properties
if ((parameterType.flags & ts.TypeFlags.Object) !== 0) {
// Recursive data types (e.g. Order.deliveries[].order) are cut off via
// the visited set — the checker caches type identities, so a cycle
// revisits the same ts.Type object. The set only tracks the current
// branch (backtracked below) so the same type may still be expanded on
// sibling paths. The depth cap only applies to types that are part of a
// reference cycle (checked last, it's the expensive test); purely nested
// non-recursive objects are expanded to arbitrary depth.
if (
visited.has(parameterType) ||
(visited.size >= MAX_SCHEMA_DEPTH &&
isRecursiveType(parameterType, checker, recursionCache))
) {
return {input: "data", ...combinedSuggestions};
}
visited.add(parameterType);

const properties: Record<string, Schema | Schema[]> = {};
const required: string[] = [];

Expand Down Expand Up @@ -261,7 +288,7 @@ export const getSchema = (

// Recursively generate schemas for property types
const propertySchemas = propertyTypes.map((type) =>
getSchema(checker, node, type, functionDeclarations, functions, suggestions)
getSchema(checker, node, type, functionDeclarations, functions, suggestions, undefined, visited, recursionCache)
);

properties[property.name] =
Expand All @@ -273,6 +300,8 @@ export const getSchema = (
}
}

visited.delete(parameterType);

return {
input: "data",
properties,
Expand Down