From 4f0229295d6b10ce25bc4879619ed0c337159a7d Mon Sep 17 00:00:00 2001 From: sinclair Date: Thu, 5 Dec 2024 13:35:21 +0900 Subject: [PATCH 1/6] Fix Compile For Deep Referential Module Types --- src/compiler/compiler.ts | 38 +++++++++++++++-------------- test/runtime/compiler-ajv/module.ts | 35 ++++++++++++++++++++++++++ test/runtime/compiler/module.ts | 35 ++++++++++++++++++++++++++ test/runtime/value/check/module.ts | 35 ++++++++++++++++++++++++++ 4 files changed, 125 insertions(+), 18 deletions(-) diff --git a/src/compiler/compiler.ts b/src/compiler/compiler.ts index 7450c8aa..dfdb5b85 100644 --- a/src/compiler/compiler.ts +++ b/src/compiler/compiler.ts @@ -97,14 +97,6 @@ export class TypeCheck { public Code(): string { return this.code } - /** Returns the schema type used to validate */ - public Schema(): T { - return this.schema - } - /** Returns reference types used to validate */ - public References(): TSchema[] { - return this.references - } /** Returns an iterator for each error in this value. */ public Errors(value: unknown): ValueErrorIterator { return Errors(this.schema, this.references, value) @@ -113,13 +105,21 @@ export class TypeCheck { public Check(value: unknown): value is Static { return this.checkFunc(value) } + /** Returns the schema type used to validate */ + public Schema(): T { + return this.schema + } + /** Returns reference types used to validate */ + public References(): TSchema[] { + return this.references + } /** Decodes a value or throws if error */ public Decode, Result extends Static = Static>(value: unknown): Result { if (!this.checkFunc(value)) throw new TransformDecodeCheckError(this.schema, value, this.Errors(value).First()!) return (this.hasTransform ? TransformDecode(this.schema, this.references, value) : value) as never } /** Encodes a value or throws if error */ - public Encode, Result extends Static = Static>(value: unknown): Result { + public Encode, Result extends Static = Static>(value: unknown): Result { const encoded = this.hasTransform ? TransformEncode(this.schema, this.references, value) : value if (!this.checkFunc(encoded)) throw new TransformEncodeCheckError(this.schema, value, this.Errors(value).First()!) return encoded as never @@ -298,9 +298,11 @@ export namespace TypeCompiler { yield `(typeof ${value} === 'function')` } function* FromImport(schema: TImport, references: TSchema[], value: string): IterableIterator { - const definitions = globalThis.Object.values(schema.$defs) as TSchema[] - const target = schema.$defs[schema.$ref] as TSchema - yield* Visit(target, [...references, ...definitions], value) + const interior = globalThis.Object.getOwnPropertyNames(schema.$defs).reduce((result, key) => { + return [...result, schema.$defs[key as never] as TSchema] + }, [] as TSchema[]) + const target = { [Kind]: 'Ref', $ref: schema.$ref } as never + yield* Visit(target, [...references, ...interior], value) } function* FromInteger(schema: TInteger, references: TSchema[], value: string): IterableIterator { yield `Number.isInteger(${value})` @@ -399,12 +401,8 @@ export namespace TypeCompiler { function* FromRef(schema: TRef, references: TSchema[], value: string): IterableIterator { const target = Deref(schema, references) // Reference: If we have seen this reference before we can just yield and return the function call. - // If this isn't the case we defer to visit to generate and set the _recursion_end_for_ for subsequent - // passes. This operation is very awkward as we are using the functions state to store values to - // enable self referential types to terminate. This needs to be refactored. - const recursiveEnd = `_recursion_end_for_${schema.$ref}` - if (state.functions.has(recursiveEnd)) return yield `${CreateFunctionName(schema.$ref)}(${value})` - state.functions.set(recursiveEnd, '') // terminate recursion here by setting the name. + // If this isn't the case we defer to visit to generate and set the function for subsequent passes. + if (state.functions.has(schema.$ref)) return yield `${CreateFunctionName(schema.$ref)}(${value})` yield* Visit(target, references, value) } function* FromRegExp(schema: TRegExp, references: TSchema[], value: string): IterableIterator { @@ -481,6 +479,10 @@ export namespace TypeCompiler { if (state.functions.has(functionName)) { return yield `${functionName}(${value})` } else { + // Note: In the case of cyclic types, we need to create a 'functions' record + // to prevent infinitely re-visiting the CreateFunction. Subsequent attempts + // to visit will be caught by the above condition. + state.functions.set(functionName, '') const functionCode = CreateFunction(functionName, schema, references, 'value', false) state.functions.set(functionName, functionCode) return yield `${functionName}(${value})` diff --git a/test/runtime/compiler-ajv/module.ts b/test/runtime/compiler-ajv/module.ts index 756c09f3..0797dd45 100644 --- a/test/runtime/compiler-ajv/module.ts +++ b/test/runtime/compiler-ajv/module.ts @@ -106,4 +106,39 @@ describe('compiler-ajv/Module', () => { Ok(T, { y: [null], w: [null] }) Fail(T, { x: [1], y: [null], w: [null] }) }) + // ---------------------------------------------------------------- + // https://github.com/sinclairzx81/typebox/issues/1109 + // ---------------------------------------------------------------- + it('Should validate deep referential 1', () => { + const Module = Type.Module({ + A: Type.Union([Type.Literal('Foo'), Type.Literal('Bar')]), + B: Type.Ref('A'), + C: Type.Object({ ref: Type.Ref('B') }), + D: Type.Union([Type.Ref('B'), Type.Ref('C')]), + }) + Ok(Module.Import('A') as never, 'Foo') + Ok(Module.Import('A') as never, 'Bar') + Ok(Module.Import('B') as never, 'Foo') + Ok(Module.Import('B') as never, 'Bar') + Ok(Module.Import('C') as never, { ref: 'Foo' }) + Ok(Module.Import('C') as never, { ref: 'Bar' }) + Ok(Module.Import('D') as never, 'Foo') + Ok(Module.Import('D') as never, 'Bar') + Ok(Module.Import('D') as never, { ref: 'Foo' }) + Ok(Module.Import('D') as never, { ref: 'Bar' }) + }) + it('Should validate deep referential 2', () => { + const Module = Type.Module({ + A: Type.Literal('Foo'), + B: Type.Ref('A'), + C: Type.Ref('B'), + D: Type.Ref('C'), + E: Type.Ref('D'), + }) + Ok(Module.Import('A'), 'Foo') + Ok(Module.Import('B'), 'Foo') + Ok(Module.Import('C'), 'Foo') + Ok(Module.Import('D'), 'Foo') + Ok(Module.Import('E'), 'Foo') + }) }) diff --git a/test/runtime/compiler/module.ts b/test/runtime/compiler/module.ts index 32b45560..2121a9d5 100644 --- a/test/runtime/compiler/module.ts +++ b/test/runtime/compiler/module.ts @@ -106,4 +106,39 @@ describe('compiler/Module', () => { Ok(T, { y: [null], w: [null] }) Fail(T, { x: [1], y: [null], w: [null] }) }) + // ---------------------------------------------------------------- + // https://github.com/sinclairzx81/typebox/issues/1109 + // ---------------------------------------------------------------- + it('Should validate deep referential 1', () => { + const Module = Type.Module({ + A: Type.Union([Type.Literal('Foo'), Type.Literal('Bar')]), + B: Type.Ref('A'), + C: Type.Object({ ref: Type.Ref('B') }), + D: Type.Union([Type.Ref('B'), Type.Ref('C')]), + }) + Ok(Module.Import('A') as never, 'Foo') + Ok(Module.Import('A') as never, 'Bar') + Ok(Module.Import('B') as never, 'Foo') + Ok(Module.Import('B') as never, 'Bar') + Ok(Module.Import('C') as never, { ref: 'Foo' }) + Ok(Module.Import('C') as never, { ref: 'Bar' }) + Ok(Module.Import('D') as never, 'Foo') + Ok(Module.Import('D') as never, 'Bar') + Ok(Module.Import('D') as never, { ref: 'Foo' }) + Ok(Module.Import('D') as never, { ref: 'Bar' }) + }) + it('Should validate deep referential 2', () => { + const Module = Type.Module({ + A: Type.Literal('Foo'), + B: Type.Ref('A'), + C: Type.Ref('B'), + D: Type.Ref('C'), + E: Type.Ref('D'), + }) + Ok(Module.Import('A'), 'Foo') + Ok(Module.Import('B'), 'Foo') + Ok(Module.Import('C'), 'Foo') + Ok(Module.Import('D'), 'Foo') + Ok(Module.Import('E'), 'Foo') + }) }) diff --git a/test/runtime/value/check/module.ts b/test/runtime/value/check/module.ts index 371b302d..af80b0e6 100644 --- a/test/runtime/value/check/module.ts +++ b/test/runtime/value/check/module.ts @@ -108,4 +108,39 @@ describe('value/check/Module', () => { Assert.IsTrue(Value.Check(T, { y: [null], w: [null] })) Assert.IsFalse(Value.Check(T, { x: [1], y: [null], w: [null] })) }) + // ---------------------------------------------------------------- + // https://github.com/sinclairzx81/typebox/issues/1109 + // ---------------------------------------------------------------- + it('Should validate deep referential 1', () => { + const Module = Type.Module({ + A: Type.Union([Type.Literal('Foo'), Type.Literal('Bar')]), + B: Type.Ref('A'), + C: Type.Object({ ref: Type.Ref('B') }), + D: Type.Union([Type.Ref('B'), Type.Ref('C')]), + }) + Assert.IsTrue(Value.Check(Module.Import('A') as never, 'Foo')) + Assert.IsTrue(Value.Check(Module.Import('A') as never, 'Bar')) + Assert.IsTrue(Value.Check(Module.Import('B') as never, 'Foo')) + Assert.IsTrue(Value.Check(Module.Import('B') as never, 'Bar')) + Assert.IsTrue(Value.Check(Module.Import('C') as never, { ref: 'Foo' })) + Assert.IsTrue(Value.Check(Module.Import('C') as never, { ref: 'Bar' })) + Assert.IsTrue(Value.Check(Module.Import('D') as never, 'Foo')) + Assert.IsTrue(Value.Check(Module.Import('D') as never, 'Bar')) + Assert.IsTrue(Value.Check(Module.Import('D') as never, { ref: 'Foo' })) + Assert.IsTrue(Value.Check(Module.Import('D') as never, { ref: 'Bar' })) + }) + it('Should validate deep referential 2', () => { + const Module = Type.Module({ + A: Type.Literal('Foo'), + B: Type.Ref('A'), + C: Type.Ref('B'), + D: Type.Ref('C'), + E: Type.Ref('D'), + }) + Assert.IsTrue(Value.Check(Module.Import('A'), 'Foo')) + Assert.IsTrue(Value.Check(Module.Import('B'), 'Foo')) + Assert.IsTrue(Value.Check(Module.Import('C'), 'Foo')) + Assert.IsTrue(Value.Check(Module.Import('D'), 'Foo')) + Assert.IsTrue(Value.Check(Module.Import('E'), 'Foo')) + }) }) From c9d707424c293ddf037869dc24b26be18c444e5b Mon Sep 17 00:00:00 2001 From: sinclair Date: Thu, 5 Dec 2024 14:09:45 +0900 Subject: [PATCH 2/6] Version --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index acfa4550..37145ec0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@sinclair/typebox", - "version": "0.34.10", + "version": "0.34.11", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@sinclair/typebox", - "version": "0.34.10", + "version": "0.34.11", "license": "MIT", "devDependencies": { "@arethetypeswrong/cli": "^0.13.2", diff --git a/package.json b/package.json index 8b857c36..2734e73e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@sinclair/typebox", - "version": "0.34.10", + "version": "0.34.11", "description": "Json Schema Type Builder with Static Type Resolution for TypeScript", "keywords": [ "typescript", From 942940e222a01e8dc7e16a566e525f476c004559 Mon Sep 17 00:00:00 2001 From: sinclair Date: Thu, 5 Dec 2024 14:13:29 +0900 Subject: [PATCH 3/6] ChangeLog --- changelog/0.34.0.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/changelog/0.34.0.md b/changelog/0.34.0.md index 0b821e45..b7774bfe 100644 --- a/changelog/0.34.0.md +++ b/changelog/0.34.0.md @@ -1,4 +1,6 @@ ### 0.34.0 +- [Revision 0.34.11](https://github.com/sinclairzx81/typebox/pull/1110) + - Fix Compiler Emit for Deeply Referential Module Types - [Revision 0.34.10](https://github.com/sinclairzx81/typebox/pull/1107) - Fix Declaration Emit for Index and Mapped Types - Fix Record Inference Presentation when Embedded in Modules From f0e7aa74ea078e8f4917a3f08334526db0c87dcb Mon Sep 17 00:00:00 2001 From: sinclair Date: Thu, 5 Dec 2024 14:18:05 +0900 Subject: [PATCH 4/6] Update Compiler FromImport Implementation --- src/compiler/compiler.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/compiler/compiler.ts b/src/compiler/compiler.ts index dfdb5b85..24a1cc4a 100644 --- a/src/compiler/compiler.ts +++ b/src/compiler/compiler.ts @@ -59,7 +59,7 @@ import type { TNumber } from '../type/number/index' import type { TObject } from '../type/object/index' import type { TPromise } from '../type/promise/index' import type { TRecord } from '../type/record/index' -import type { TRef } from '../type/ref/index' +import { Ref, type TRef } from '../type/ref/index' import type { TRegExp } from '../type/regexp/index' import type { TTemplateLiteral } from '../type/template-literal/index' import type { TThis } from '../type/recursive/index' @@ -298,11 +298,11 @@ export namespace TypeCompiler { yield `(typeof ${value} === 'function')` } function* FromImport(schema: TImport, references: TSchema[], value: string): IterableIterator { - const interior = globalThis.Object.getOwnPropertyNames(schema.$defs).reduce((result, key) => { + const members = globalThis.Object.getOwnPropertyNames(schema.$defs).reduce((result, key) => { return [...result, schema.$defs[key as never] as TSchema] }, [] as TSchema[]) - const target = { [Kind]: 'Ref', $ref: schema.$ref } as never - yield* Visit(target, [...references, ...interior], value) + const ref = Ref(schema.$ref) + yield* Visit(ref, [...references, ...members], value) } function* FromInteger(schema: TInteger, references: TSchema[], value: string): IterableIterator { yield `Number.isInteger(${value})` From d4e95a489d648ffd6f1a69c80cdbf54d06e0fcdd Mon Sep 17 00:00:00 2001 From: sinclair Date: Thu, 5 Dec 2024 14:20:03 +0900 Subject: [PATCH 5/6] TypeCheck Encode Inference and Schema and Reference Definition Order --- src/compiler/compiler.ts | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/compiler/compiler.ts b/src/compiler/compiler.ts index 24a1cc4a..37726471 100644 --- a/src/compiler/compiler.ts +++ b/src/compiler/compiler.ts @@ -97,14 +97,6 @@ export class TypeCheck { public Code(): string { return this.code } - /** Returns an iterator for each error in this value. */ - public Errors(value: unknown): ValueErrorIterator { - return Errors(this.schema, this.references, value) - } - /** Returns true if the value matches the compiled type. */ - public Check(value: unknown): value is Static { - return this.checkFunc(value) - } /** Returns the schema type used to validate */ public Schema(): T { return this.schema @@ -113,13 +105,21 @@ export class TypeCheck { public References(): TSchema[] { return this.references } + /** Returns an iterator for each error in this value. */ + public Errors(value: unknown): ValueErrorIterator { + return Errors(this.schema, this.references, value) + } + /** Returns true if the value matches the compiled type. */ + public Check(value: unknown): value is Static { + return this.checkFunc(value) + } /** Decodes a value or throws if error */ public Decode, Result extends Static = Static>(value: unknown): Result { if (!this.checkFunc(value)) throw new TransformDecodeCheckError(this.schema, value, this.Errors(value).First()!) return (this.hasTransform ? TransformDecode(this.schema, this.references, value) : value) as never } /** Encodes a value or throws if error */ - public Encode, Result extends Static = Static>(value: unknown): Result { + public Encode, Result extends Static = Static>(value: unknown): Result { const encoded = this.hasTransform ? TransformEncode(this.schema, this.references, value) : value if (!this.checkFunc(encoded)) throw new TransformEncodeCheckError(this.schema, value, this.Errors(value).First()!) return encoded as never From 06c7e19cc0100aad7cddb2604d757cfe4a993462 Mon Sep 17 00:00:00 2001 From: sinclair Date: Thu, 5 Dec 2024 14:21:25 +0900 Subject: [PATCH 6/6] Inline Ref on FromImport --- src/compiler/compiler.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/compiler/compiler.ts b/src/compiler/compiler.ts index 37726471..5edf8fd7 100644 --- a/src/compiler/compiler.ts +++ b/src/compiler/compiler.ts @@ -301,8 +301,7 @@ export namespace TypeCompiler { const members = globalThis.Object.getOwnPropertyNames(schema.$defs).reduce((result, key) => { return [...result, schema.$defs[key as never] as TSchema] }, [] as TSchema[]) - const ref = Ref(schema.$ref) - yield* Visit(ref, [...references, ...members], value) + yield* Visit(Ref(schema.$ref), [...references, ...members], value) } function* FromInteger(schema: TInteger, references: TSchema[], value: string): IterableIterator { yield `Number.isInteger(${value})`