Skip to content

Commit

Permalink
fix: Typescript zod (and effect) language uses block scoped variables…
Browse files Browse the repository at this point in the history
… before they've been declared (#2419)

* Fixing code generation for zod, ensuring that referenced types are defined before they are referenced

* Apply suggestions from code review

adding a warning to Typescript Zod generator on (unlikely) exceeeding max num passes when determining output order + correcting a comment typo

---------

Co-authored-by: David Siegel <[email protected]>
  • Loading branch information
kriswest and dvdsgl authored Feb 14, 2024
1 parent 1f89a97 commit cab3d94
Showing 1 changed file with 131 additions and 27 deletions.
158 changes: 131 additions & 27 deletions packages/quicktype-core/src/language/TypeScriptZod.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,17 @@ import { RenderContext } from "../Renderer";
import { BooleanOption, Option, OptionValues, getOptionValues } from "../RendererOptions";
import { Sourcelike } from "../Source";
import { TargetLanguage } from "../TargetLanguage";
import { ClassProperty, EnumType, ObjectType, PrimitiveStringTypeKind, TransformedStringTypeKind, Type } from "../Type";
import {
ArrayType,
ClassProperty,
ClassType,
EnumType,
ObjectType,
PrimitiveStringTypeKind,
SetOperationType,
TransformedStringTypeKind,
Type
} from "../Type";
import { matchType } from "../TypeUtils";
import { AcronymStyleOptions, acronymStyle } from "../support/Acronyms";
import {
Expand Down Expand Up @@ -187,49 +197,143 @@ export class TypeScriptZodRenderer extends ConvenienceRenderer {
}
}

/** Static function that extracts underlying type refs for types that form part of the
* definition of the passed type - used to ensure that these appear in generated source
* before types that reference them.
*
* Primitive types don't need defining and enums are output before other types, hence,
* these are ignored.
*/
static extractUnderlyingTyperefs(type: Type): number[] {
let typeRefs: number[] = [];
//Ignore enums and primitives
if (!type.isPrimitive() && type.kind != "enum") {
//need to extract constituent types for unions and intersections (which both extend SetOperationType)
//and can ignore the union/intersection itself
if (type instanceof SetOperationType) {
(type as SetOperationType).members.forEach(member => {
//recurse as the underlying type could itself be a union, instersection or array etc.
typeRefs.push(...TypeScriptZodRenderer.extractUnderlyingTyperefs(member));
});
}

//need to extract additional properties for object, class and map types (which all extend ObjectType)
if (type instanceof ObjectType) {
const addType = (type as ObjectType).getAdditionalProperties();
if (addType) {
//recurse as the underlying type could itself be a union, instersection or array etc.
typeRefs.push(...TypeScriptZodRenderer.extractUnderlyingTyperefs(addType));
}
}

//need to extract items types for ArrayType
if (type instanceof ArrayType) {
const itemsType = (type as ArrayType).items;
if (itemsType) {
//recurse as the underlying type could itself be a union, instersection or array etc.
typeRefs.push(...TypeScriptZodRenderer.extractUnderlyingTyperefs(itemsType));
}
}

//Finally return the reference to a class as that will need to be defined (where objects, maps, unions, intersections and arrays do not)
if (type instanceof ClassType) {
typeRefs.push(type.typeRef);
}
}
return typeRefs;
}

protected emitSchemas(): void {
this.ensureBlankLine();

this.forEachEnum("leading-and-interposing", (u: EnumType, enumName: Name) => {
this.emitEnum(u, enumName);
});

// All children must be defined before this type to avoid forward references in generated code
// Build a model that will tell us if a referenced type has been defined then make multiple
// passes over the defined objects to put them into the correct order for output in the
// generated sourcecode

const order: number[] = [];
const mapKey: Name[] = [];
const mapValue: Sourcelike[][] = [];
this.forEachObject("none", (type: ObjectType, name: Name) => {
mapKey.push(name);
mapValue.push(this.gatherSource(() => this.emitObject(name, type)));
});
const mapType: ObjectType[] = [];
const mapTypeRef: number[] = [];
const mapName: Name[] = [];
const mapChildTypeRefs: number[][] = [];

mapKey.forEach((_, index) => {
// assume first
let ordinal = 0;
this.forEachObject("none", (type: ObjectType, name: Name) => {
mapType.push(type);
mapTypeRef.push(type.typeRef);
mapName.push(name);

// pull out all names
const source = mapValue[index];
const names = source.filter(value => value as Name);
const children = type.getChildren();
let childTypeRefs: number[] = [];

// must be behind all these names
for (let i = 0; i < names.length; i++) {
const depName = names[i];
children.forEach(child => {
childTypeRefs = childTypeRefs.concat(TypeScriptZodRenderer.extractUnderlyingTyperefs(child));
});
mapChildTypeRefs.push(childTypeRefs);
});

// find this name's ordinal, if it has already been added
for (let j = 0; j < order.length; j++) {
const depIndex = order[j];
if (mapKey[depIndex] === depName) {
// this is the index of the dependency, so make sure we come after it
ordinal = Math.max(ordinal, depIndex + 1);
//Items to process on this pass
let indices: number[] = [];
mapType.forEach((_, index) => {
indices.push(index);
});
//items to process on the next pass
let deferredIndices: number[] = [];

//defensive: make sure we don't loop forever, even complex sets shouldn't require many passes
const MAX_PASSES = 999;
let passNum = 0;
do {
indices.forEach(index => {
// must be behind all these children
const childTypeRefs = mapChildTypeRefs[index];
let foundAllChildren = true;

childTypeRefs.forEach(childRef => {
//defensive: first check if there is a definition for the referenced type (there should be)
if (mapTypeRef.indexOf(childRef) > -1) {
let found = false;
// find this childs's ordinal, if it has already been added
//faster to go through what we've defined so far than all definitions
for (let j = 0; j < order.length; j++) {
const childIndex = order[j];
if (mapTypeRef[childIndex] === childRef) {
found = true;
break;
}
}
foundAllChildren = foundAllChildren && found;
} else {
console.error(
"A child type reference was not found amongst all Object definitions! TypeRef: " + childRef
);
}
});

if (foundAllChildren) {
// insert index into order as we are safe to define this type
order.push(index);
} else {
//defer to a subsequent pass as we need to define other types
deferredIndices.push(index);
}
});
indices = deferredIndices;
deferredIndices = [];
passNum++;

if (passNum > MAX_PASSES) {
//giving up
order.push(...deferredIndices);
console.warn("Exceeded maximum number of passes when determining output order, output may contain forward references");
}

// insert index
order.splice(ordinal, 0, index);
});
} while (indices.length > 0 && passNum <= MAX_PASSES);

// now emit ordered source
order.forEach(i => this.emitGatheredSource(mapValue[i]));
order.forEach(i => this.emitGatheredSource(this.gatherSource(() => this.emitObject(mapName[i], mapType[i]))));
}

protected emitSourceStructure(): void {
Expand Down

0 comments on commit cab3d94

Please sign in to comment.