diff --git a/packages/quicktype-core/src/language/TypeScriptFlow/TypeScriptFlowBaseRenderer.ts b/packages/quicktype-core/src/language/TypeScriptFlow/TypeScriptFlowBaseRenderer.ts index 6ce623d1b..9635a1efb 100644 --- a/packages/quicktype-core/src/language/TypeScriptFlow/TypeScriptFlowBaseRenderer.ts +++ b/packages/quicktype-core/src/language/TypeScriptFlow/TypeScriptFlowBaseRenderer.ts @@ -20,7 +20,11 @@ import { type Type, UnionType, } from "../../Type/index.js"; -import { matchType, nullableFromUnion } from "../../Type/TypeUtils.js"; +import { + directlyReachableSingleNamedType, + matchType, + nullableFromUnion, +} from "../../Type/TypeUtils.js"; import { JavaScriptRenderer, type JavaScriptTypeAnnotations, @@ -237,15 +241,21 @@ export abstract class TypeScriptFlowBaseRenderer extends JavaScriptRenderer { } protected emitTypes(): void { - // emit primitive top levels + // Emit top levels whose name isn't used by forEachNamedType below. this.forEachTopLevel("none", (t, name) => { - if (!t.isPrimitive()) { + if (directlyReachableSingleNamedType(t) !== undefined) { return; } this.ensureBlankLine(); this.emitDescription(this.descriptionForType(t)); - this.emitLine("type ", name, " = ", this.sourceFor(t).source, ";"); + this.emitLine( + "export type ", + name, + " = ", + this.sourceFor(t).source, + ";", + ); }); this.forEachNamedType( diff --git a/test/inputs/schema/empty-object.1.fail.json b/test/inputs/schema/empty-object.1.fail.json new file mode 100644 index 000000000..b5d8bb58d --- /dev/null +++ b/test/inputs/schema/empty-object.1.fail.json @@ -0,0 +1 @@ +[1, 2, 3] diff --git a/test/inputs/schema/empty-object.1.json b/test/inputs/schema/empty-object.1.json new file mode 100644 index 000000000..0967ef424 --- /dev/null +++ b/test/inputs/schema/empty-object.1.json @@ -0,0 +1 @@ +{} diff --git a/test/inputs/schema/empty-object.schema b/test/inputs/schema/empty-object.schema new file mode 100644 index 000000000..af61a2a0d --- /dev/null +++ b/test/inputs/schema/empty-object.schema @@ -0,0 +1,5 @@ +{ + "$schema": "http://json-schema.org/draft-06/schema#", + "type": "object", + "properties": {} +} diff --git a/test/languages.ts b/test/languages.ts index 120f3170b..68c498df1 100644 --- a/test/languages.ts +++ b/test/languages.ts @@ -1199,7 +1199,11 @@ export const JavaScriptPropTypesLanguage: Language = { "spotify-album.json", // renderer does not support recursion "76ae1.json", // renderer does not support recursion ], - skipSchema: ["integer-before-number.schema"], // Python-specific union-order regression. + skipSchema: [ + // The renderer does not support a bare top-level map. + "empty-object.schema", + "integer-before-number.schema", // Python-specific union-order regression. + ], skipMiscJSON: false, rendererOptions: { "module-system": "es6" }, quickTestRendererOptions: [{ converters: "top-level" }], @@ -1372,7 +1376,11 @@ I havea no idea how to encode these tests correctly. "php-mixed-union.json", "nst-test-suite.json", ], - skipSchema: ["integer-before-number.schema"], // Python-specific union-order regression. + skipSchema: [ + // The renderer does not support a bare top-level map. + "empty-object.schema", + "integer-before-number.schema", // Python-specific union-order regression. + ], skipMiscJSON: false, rendererOptions: { "just-types": "true" }, quickTestRendererOptions: [], @@ -1907,6 +1915,7 @@ export const HaskellLanguage: Language = { // (A top-level `[Int]` correctly fails to decode `[1, 2, "three"]`, // but the driver still exits 0.) "boolean-subschema.schema", + "empty-object.schema", "issue2680-top-level-array.schema", "nested-intersection-union.schema", "prefix-items.schema", @@ -1963,6 +1972,8 @@ export const PHPLanguage: Language = { ], skipMiscJSON: true, skipSchema: [ + // The renderer does not support a bare top-level map. + "empty-object.schema", "integer-before-number.schema", // Python-specific union-order regression. // PHP class names are case-insensitive, but the namer dedups // case-sensitively, so this declares classes that collide (same @@ -2070,6 +2081,8 @@ export const TypeScriptZodLanguage: Language = { ], skipMiscJSON: false, skipSchema: [ + // The renderer does not support a bare top-level map. + "empty-object.schema", "integer-before-number.schema", // Python-specific union-order regression. "any.schema", ...skipsUntypedUnions, @@ -2195,6 +2208,8 @@ export const TypeScriptEffectSchemaLanguage: Language = { ], skipMiscJSON: false, skipSchema: [ + // The renderer does not support a bare top-level map. + "empty-object.schema", "integer-before-number.schema", // Python-specific union-order regression. "any.schema", ...skipsUntypedUnions, @@ -2265,7 +2280,10 @@ export const ElixirLanguage: Language = { ], skipMiscJSON: false, skipSchema: [ + // The renderer does not support a bare top-level map. + "empty-object.schema", "integer-before-number.schema", // Python-specific union-order regression. + // The error occurs because a guard clause that references TopLevel is compiled before TopLevel itself. To fix this, put // TopLevel before Bar, but this doesn't address the actual problem if for example a pattern match to Bar was in TopLevel. "mutually-recursive.schema", diff --git a/test/unit/typescript-flow-top-level-alias.test.ts b/test/unit/typescript-flow-top-level-alias.test.ts new file mode 100644 index 000000000..13173588f --- /dev/null +++ b/test/unit/typescript-flow-top-level-alias.test.ts @@ -0,0 +1,79 @@ +// Top-level array/map schema fixtures exercise their generated converters, but +// those converters inline the collection type, so they still compile when the +// public top-level alias is missing. Assert the declaration itself here to +// prevent that regression. + +import { + InputData, + JSONSchemaInput, + type LanguageName, + quicktype, +} from "quicktype-core"; +import { describe, expect, test } from "vitest"; + +async function renderSchema( + lang: LanguageName, + name: string, + schema: object, +): Promise { + const schemaInput = new JSONSchemaInput(undefined); + await schemaInput.addSource({ name, schema: JSON.stringify(schema) }); + const inputData = new InputData(); + inputData.addInput(schemaInput); + + const result = await quicktype({ + inputData, + lang, + rendererOptions: { "just-types": true }, + }); + return result.lines.join("\n"); +} + +describe("TypeScript/Flow unnamed top-level aliases", () => { + test.each([ + ["typescript", "unknown"], + ["flow", "mixed"], + ] as const)("%s emits a top-level map alias", async (lang, anyType) => { + const output = await renderSchema(lang, "Values", { + type: "object", + additionalProperties: {}, + }); + + expect(output).toContain( + `export type Values = { [key: string]: ${anyType} };`, + ); + }); + + test.each([ + "typescript", + "flow", + ] as const)("%s emits a top-level array alias", async (lang) => { + const output = await renderSchema(lang, "Values", { + type: "array", + items: { type: "number" }, + }); + + expect(output).toContain("export type Values = number[];"); + }); + + test.each([ + "typescript", + "flow", + ] as const)("%s does not alias a map whose value type claims the top-level name", async (lang) => { + const output = await renderSchema(lang, "TopLevel", { + type: "object", + additionalProperties: { + type: "object", + properties: { + one: { type: "integer" }, + two: { type: "boolean" }, + }, + required: ["one", "two"], + }, + }); + const declarations = + output.match(/export (?:type|interface) TopLevel\b/g) ?? []; + + expect(declarations).toHaveLength(1); + }); +});