From 2d7d0861f5ad5da67a003f4a230ab1814201a924 Mon Sep 17 00:00:00 2001 From: Alex Date: Sun, 6 Jul 2025 19:23:33 +0100 Subject: [PATCH] feat: inline unused generic aliases --- src/SchemaGenerator.ts | 7 +- src/Utils/inlineSingleUseDefs.ts | 165 +++++++++++++++++++++++++++++++ 2 files changed, 170 insertions(+), 2 deletions(-) create mode 100644 src/Utils/inlineSingleUseDefs.ts diff --git a/src/SchemaGenerator.ts b/src/SchemaGenerator.ts index 4d9665f1d..4defd5f00 100644 --- a/src/SchemaGenerator.ts +++ b/src/SchemaGenerator.ts @@ -11,6 +11,7 @@ import type { StringMap } from "./Utils/StringMap.js"; import { hasJsDocTag } from "./Utils/hasJsDocTag.js"; import { removeUnreachable } from "./Utils/removeUnreachable.js"; import { symbolAtNode } from "./Utils/symbolAtNode.js"; +import { inlineSingleUseDefs } from "./Utils/inlineSingleUseDefs.js"; export class SchemaGenerator { public constructor( @@ -52,11 +53,13 @@ export class SchemaGenerator { {}, ); + const inlined = inlineSingleUseDefs(rootTypeDefinition, reachableDefinitions); + return { ...(this.config?.schemaId ? { $id: this.config.schemaId } : {}), $schema: "http://json-schema.org/draft-07/schema#", - ...(rootTypeDefinition ?? {}), - definitions: reachableDefinitions, + ...(inlined.rootDef ?? {}), + definitions: inlined.definitions, }; } diff --git a/src/Utils/inlineSingleUseDefs.ts b/src/Utils/inlineSingleUseDefs.ts new file mode 100644 index 000000000..89dbbebcf --- /dev/null +++ b/src/Utils/inlineSingleUseDefs.ts @@ -0,0 +1,165 @@ +import type { JSONSchema7Definition } from "json-schema"; +import type { Definition } from "../Schema/Definition.js"; +import type { StringMap } from "./StringMap.js"; + +const DEFINITION_OFFSET = "#/definitions/".length; + +function isLocalRef(ref: string): boolean { + return ref.charAt(0) === "#"; +} + +function isGenerated(name: string): boolean { + return ( + /^alias-/i.test(name) || + /^structure-/i.test(name) || + /^object-/i.test(name) || + /^indexed-type-/i.test(name) || + name.includes("<") + ); +} + +function collectRefs( + def: Definition | JSONSchema7Definition, + counts: Map, +): void { + if (typeof def === "boolean") { + return; + } + if (def.$ref && isLocalRef(def.$ref)) { + const name = decodeURIComponent(def.$ref.slice(DEFINITION_OFFSET)); + counts.set(name, (counts.get(name) || 0) + 1); + return; + } + + if (def.anyOf) { + for (const d of def.anyOf) { + collectRefs(d, counts); + } + } + if (def.allOf) { + for (const d of def.allOf) { + collectRefs(d, counts); + } + } + if (def.oneOf) { + for (const d of def.oneOf) { + collectRefs(d, counts); + } + } + if (def.not) { + collectRefs(def.not, counts); + } + if (def.then) { + collectRefs(def.then, counts); + } + if (Array.isArray(def.type) ? def.type.includes("object") : def.type === "object") { + if (def.properties) { + for (const p of Object.values(def.properties)) { + collectRefs(p, counts); + } + } + if (def.additionalProperties && typeof def.additionalProperties === "object") { + collectRefs(def.additionalProperties, counts); + } + } + if (Array.isArray(def.type) ? def.type.includes("array") : def.type === "array") { + if (Array.isArray(def.items)) { + for (const it of def.items) { + collectRefs(it, counts); + } + } else if (def.items) { + collectRefs(def.items, counts); + } + } +} + +function replaceRefs( + def: Definition | JSONSchema7Definition, + defs: StringMap, + inline: Set, +): Definition | JSONSchema7Definition { + if (typeof def === "boolean") { + return def; + } + if (def.$ref && isLocalRef(def.$ref)) { + const name = decodeURIComponent(def.$ref.slice(DEFINITION_OFFSET)); + if (inline.has(name)) { + const cloned = structuredClone(defs[name]); + return replaceRefs(cloned, defs, inline); + } + return def; + } + + if (def.anyOf) { + def.anyOf = def.anyOf.map((d) => replaceRefs(d, defs, inline)); + } + if (def.allOf) { + def.allOf = def.allOf.map((d) => replaceRefs(d, defs, inline)); + } + if (def.oneOf) { + def.oneOf = def.oneOf.map((d) => replaceRefs(d, defs, inline)); + } + if (def.not) { + def.not = replaceRefs(def.not, defs, inline); + } + if (def.then) { + def.then = replaceRefs(def.then, defs, inline); + } + if (Array.isArray(def.type) ? def.type.includes("object") : def.type === "object") { + if (def.properties) { + for (const key of Object.keys(def.properties)) { + def.properties[key] = replaceRefs(def.properties[key], defs, inline); + } + } + if (def.additionalProperties && typeof def.additionalProperties === "object") { + def.additionalProperties = replaceRefs(def.additionalProperties, defs, inline); + } + } + if (Array.isArray(def.type) ? def.type.includes("array") : def.type === "array") { + if (Array.isArray(def.items)) { + def.items = def.items.map((i) => replaceRefs(i, defs, inline)); + } else if (def.items) { + def.items = replaceRefs(def.items, defs, inline); + } + } + return def; +} + +export function inlineSingleUseDefs( + rootDef: Definition | undefined, + defs: StringMap, +): { rootDef: Definition | undefined; definitions: StringMap } { + let changed = true; + while (changed) { + changed = false; + const counts = new Map(); + if (rootDef) { + collectRefs(rootDef, counts); + } + for (const def of Object.values(defs)) { + collectRefs(def, counts); + } + const toInline = new Set(); + for (const [name, count] of counts) { + if (count === 1 && defs[name] && isGenerated(name)) { + toInline.add(name); + } + } + if (toInline.size === 0) { + break; + } + if (rootDef) { + rootDef = replaceRefs(rootDef, defs, toInline) as Definition; + } + for (const key of Object.keys(defs)) { + defs[key] = replaceRefs(defs[key], defs, toInline) as Definition; + } + for (const name of toInline) { + delete defs[name]; + } + changed = true; + } + + return { rootDef, definitions: defs }; +} +