From 16f0c1da4b6e2ce216c45681e1574bfe68c9044f Mon Sep 17 00:00:00 2001 From: "Marc J. Schmidt" Date: Mon, 4 Mar 2024 14:27:56 +0100 Subject: [PATCH] fix(type): handle `Function` correctly in type comparisons Previously `Function` was not unique represented in the runtime type system and hence it was not possible to compare other types with it. This is now fixed so that `Function` is in runtime types `{kind: Kind.Function, function: Function}` which can be detected via the global Function symbol `type.function === Function`. Adjusts `extend` check accordingly and makes sure more function comparisons are supported. --- packages/type-compiler/src/compiler.ts | 7 ++- .../type-compiler/tests/transpile.spec.ts | 9 +++ packages/type/src/reflection/extends.ts | 56 +++++++++++-------- packages/type/src/reflection/processor.ts | 6 +- packages/type/tests/integration4.spec.ts | 14 +++++ packages/type/tests/processor.spec.ts | 5 ++ .../type/tests/standard-functions.spec.ts | 15 +---- packages/type/tests/typeguard.spec.ts | 9 +++ packages/type/tests/utils.ts | 26 ++++++++- 9 files changed, 105 insertions(+), 42 deletions(-) diff --git a/packages/type-compiler/src/compiler.ts b/packages/type-compiler/src/compiler.ts index 34c42f713..eaa6e505a 100644 --- a/packages/type-compiler/src/compiler.ts +++ b/packages/type-compiler/src/compiler.ts @@ -2096,9 +2096,10 @@ export class ReflectionTransformer implements CustomTransformer { program.pushOp(ReflectionOp.array); return; } else if (name === 'Function') { - program.pushOp(ReflectionOp.frame); - program.pushOp(ReflectionOp.any); - program.pushOp(ReflectionOp.function, program.pushStack('')); + program.pushFrame(); + const index = program.pushStack(this.f.createArrowFunction(undefined, undefined, [], undefined, undefined, this.f.createIdentifier('Function'))); + program.pushOp(ReflectionOp.functionReference, index); + program.popFrameImplicit(); return; } else if (name === 'Set') { if (type.typeArguments && type.typeArguments[0]) { diff --git a/packages/type-compiler/tests/transpile.spec.ts b/packages/type-compiler/tests/transpile.spec.ts index 817a228eb..44501722c 100644 --- a/packages/type-compiler/tests/transpile.spec.ts +++ b/packages/type-compiler/tests/transpile.spec.ts @@ -462,6 +462,15 @@ test('keep "use x" at top', () => { expect(res.app.startsWith('"use client";')).toBe(true); }); +test('Function', () => { + const res = transpile({ + 'app': ` + type a = Function; + ` + }); + expect(res.app).toContain(`[() => Function, `); +}); + test('inline type definitions should compile', () => { const res = transpile({ 'app': ` diff --git a/packages/type/src/reflection/extends.ts b/packages/type/src/reflection/extends.ts index 18264a3a1..b0818cfd5 100644 --- a/packages/type/src/reflection/extends.ts +++ b/packages/type/src/reflection/extends.ts @@ -11,7 +11,8 @@ import { addType, emptyObject, - flatten, getTypeJitContainer, + flatten, + getTypeJitContainer, indexAccess, isMember, isOptional, @@ -23,17 +24,20 @@ import { stringifyType, Type, TypeAny, + TypeCallSignature, + TypeFunction, TypeInfer, TypeLiteral, TypeMethod, TypeMethodSignature, TypeNumber, TypeObjectLiteral, - TypeParameter, TypePromise, + TypeParameter, + TypePromise, TypeString, TypeTemplateLiteral, TypeTuple, - TypeUnion + TypeUnion, } from './type.js'; import { isPrototypeOfBase } from '@deepkit/core'; import { typeInfer } from './processor.js'; @@ -75,6 +79,25 @@ export function isExtendable(leftValue: AssignableType, rightValue: AssignableTy return valid; } +function isFunctionLike(type: Type) { + return type.kind === ReflectionKind.function || type.kind === ReflectionKind.method || type.kind === ReflectionKind.callSignature + || type.kind === ReflectionKind.methodSignature || type.kind === ReflectionKind.objectLiteral + || ((type.kind === ReflectionKind.property || type.kind === ReflectionKind.propertySignature) && type.type.kind === ReflectionKind.function); +} + +function getFunctionLikeType(type: Type): TypeMethod | TypeMethodSignature | TypeFunction | TypeCallSignature | undefined { + if (type.kind === ReflectionKind.function || type.kind === ReflectionKind.method || type.kind === ReflectionKind.methodSignature) return type; + if (type.kind === ReflectionKind.objectLiteral) { + for (const member of resolveTypeMembers(type)) { + if (member.kind === ReflectionKind.callSignature) return member; + } + } + if (type.kind === ReflectionKind.property || type.kind === ReflectionKind.propertySignature) { + return type.type.kind === ReflectionKind.function ? getFunctionLikeType(type.type) : undefined; + } + return; +} + export function _isExtendable(left: Type, right: Type, extendStack: StackEntry[] = []): boolean { if (hasStack(extendStack, left, right)) return true; @@ -194,29 +217,18 @@ export function _isExtendable(left: Type, right: Type, extendStack: StackEntry[] } } - if (left.kind === ReflectionKind.function && right.kind === ReflectionKind.function && left.function && left.function === right.function) return true; + if (isFunctionLike(left) && isFunctionLike(right)) { + const leftType = getFunctionLikeType(left); + const rightType = getFunctionLikeType(right); + if (leftType && rightType) { + if (rightType.kind === ReflectionKind.function && rightType.function === Function) return true; + if (leftType.kind === ReflectionKind.function && rightType.kind === ReflectionKind.function && leftType.function && leftType.function === rightType.function) return true; - if ((left.kind === ReflectionKind.function || left.kind === ReflectionKind.method || left.kind === ReflectionKind.methodSignature) && - (right.kind === ReflectionKind.function || right.kind === ReflectionKind.method || right.kind === ReflectionKind.methodSignature || right.kind === ReflectionKind.objectLiteral) - ) { - if (right.kind === ReflectionKind.objectLiteral) { - for (const type of resolveTypeMembers(right)) { - if (type.kind === ReflectionKind.callSignature) { - if (_isExtendable(left, type, extendStack)) return true; - } - } - - return false; - } - - if (right.kind === ReflectionKind.function || right.kind === ReflectionKind.methodSignature || right.kind === ReflectionKind.method) { - const returnValid = _isExtendable(left.return, right.return, extendStack); + const returnValid = _isExtendable(leftType.return, rightType.return, extendStack); if (!returnValid) return false; - return isFunctionParameterExtendable(left, right, extendStack); + return isFunctionParameterExtendable(leftType, rightType, extendStack); } - - return false; } if ((left.kind === ReflectionKind.propertySignature || left.kind === ReflectionKind.property) && (right.kind === ReflectionKind.propertySignature || right.kind === ReflectionKind.property)) { diff --git a/packages/type/src/reflection/processor.ts b/packages/type/src/reflection/processor.ts index 908610450..9bb1bef53 100644 --- a/packages/type/src/reflection/processor.ts +++ b/packages/type/src/reflection/processor.ts @@ -1865,6 +1865,10 @@ function applyPropertyDecorator(type: Type, data: TData) { } } +function collapseFunctionToMethod(member: TypePropertySignature | TypeMethodSignature): member is TypePropertySignature & { type: TypeMethodSignature } { + return member.kind === ReflectionKind.propertySignature && member.type.kind === ReflectionKind.function && member.type.function !== Function; +} + function pushObjectLiteralTypes( type: TypeObjectLiteral, types: (TypeIndexSignature | TypePropertySignature | TypeMethodSignature | TypeObjectLiteral | TypeCallSignature)[], @@ -1904,7 +1908,7 @@ function pushObjectLiteralTypes( //note: is it possible to overwrite an index signature? type.types.push(member); } else if (member.kind === ReflectionKind.propertySignature || member.kind === ReflectionKind.methodSignature) { - const toAdd = member.kind === ReflectionKind.propertySignature && member.type.kind === ReflectionKind.function ? { + const toAdd = collapseFunctionToMethod(member) ? { kind: ReflectionKind.methodSignature, name: member.name, optional: member.optional, diff --git a/packages/type/tests/integration4.spec.ts b/packages/type/tests/integration4.spec.ts index f8eb70413..099a3b001 100644 --- a/packages/type/tests/integration4.spec.ts +++ b/packages/type/tests/integration4.spec.ts @@ -12,6 +12,7 @@ import { expect, test } from '@jest/globals'; import { assertType, AutoIncrement, Group, groupAnnotation, PrimaryKey, ReflectionKind } from '../src/reflection/type.js'; import { typeOf } from '../src/reflection/reflection.js'; import { cast } from '../src/serializer-facade.js'; +import { equalType } from './utils.js'; test('group from enum', () => { enum Groups { @@ -143,3 +144,16 @@ test('union loosely', () => { expect(cast({ id: 2 })).toEqual({ id: 2 }); expect(cast({ id: '3' })).toEqual({ id: 3 }); }); + +test('function conditions', () => { + type t1 = (() => any) extends Function ? true : false; + type t2 = ((a: string) => void) extends Function ? true : false; + type t3 = { a(a: string): void } extends { a: Function } ? true : false; + type t4 = { a(a: string): void } extends { a(): void } ? true : false; + console.log(typeOf()); + console.log(typeOf<{ a: Function } >()); + equalType(); + equalType(); + equalType(); + equalType(); +}); diff --git a/packages/type/tests/processor.spec.ts b/packages/type/tests/processor.spec.ts index 8c5eb3add..c8043020d 100644 --- a/packages/type/tests/processor.spec.ts +++ b/packages/type/tests/processor.spec.ts @@ -103,6 +103,11 @@ test('extends fn', () => { { kind: ReflectionKind.function, return: { kind: ReflectionKind.literal, literal: true }, parameters: [] }, { kind: ReflectionKind.function, return: { kind: ReflectionKind.boolean }, parameters: [] } )).toBe(true); + + expect(isExtendable( + { kind: ReflectionKind.function, return: { kind: ReflectionKind.literal, literal: true }, parameters: [] }, + { kind: ReflectionKind.function, function: Function, return: { kind: ReflectionKind.unknown }, parameters: [] } + )).toBe(true); }); test('arg', () => { diff --git a/packages/type/tests/standard-functions.spec.ts b/packages/type/tests/standard-functions.spec.ts index facfc6d86..5a38ffdf4 100644 --- a/packages/type/tests/standard-functions.spec.ts +++ b/packages/type/tests/standard-functions.spec.ts @@ -8,19 +8,8 @@ * You should have received a copy of the MIT License along with this program. */ -import { test, expect } from '@jest/globals'; -import { ReceiveType, removeTypeName, resolveReceiveType, typeOf } from '../src/reflection/reflection.js'; -import { expectEqualType } from './utils.js'; -import { stringifyResolvedType, stringifyType } from '../src/reflection/type.js'; -import { createPromiseObjectLiteral } from '../src/reflection/extends.js'; -import { serializeType } from '../src/type-serialization.js'; - -function equalType(a?: ReceiveType, b?: ReceiveType) { - const aType = removeTypeName(resolveReceiveType(a)); - const bType = removeTypeName(resolveReceiveType(b)); - expect(stringifyResolvedType(aType)).toBe(stringifyResolvedType(bType)); - expectEqualType(aType, bType as any); -} +import { test } from '@jest/globals'; +import { equalType } from './utils.js'; test('Exclude', () => { equalType, 'a' | 'c'>(); diff --git a/packages/type/tests/typeguard.spec.ts b/packages/type/tests/typeguard.spec.ts index 83fb0541c..130edcedf 100644 --- a/packages/type/tests/typeguard.spec.ts +++ b/packages/type/tests/typeguard.spec.ts @@ -229,6 +229,15 @@ test('object literal', () => { expect(is<{ a: string, b: number }>({ a: 'a', b: 'asd' })).toEqual(false); }); +test('function', () => { + expect(is<(a: string) => void>((a: string): void => undefined)).toEqual(true); + expect(is<(a: string) => void>((a: string): string => 'asd')).toEqual(false); + expect(is<(a: string) => void>((a: string): number => 2)).toEqual(false); + expect(is<(a: string) => void>((a: string): any => 2)).toEqual(true); + expect(is<(a: string) => void>((a: any): number => 2)).toEqual(false); + expect(is((a: any): number => 2)).toEqual(true); +}); + test('class', () => { class A { a!: string; diff --git a/packages/type/tests/utils.ts b/packages/type/tests/utils.ts index c15969cb0..584796f87 100644 --- a/packages/type/tests/utils.ts +++ b/packages/type/tests/utils.ts @@ -1,5 +1,12 @@ -import { getTypeJitContainer, ParentLessType, ReflectionKind, Type } from '../src/reflection/type.js'; +import { + getTypeJitContainer, + ParentLessType, + ReflectionKind, + stringifyResolvedType, + Type, +} from '../src/reflection/type.js'; import { Processor, RuntimeStackEntry } from '../src/reflection/processor.js'; +import { ReceiveType, removeTypeName, resolveReceiveType } from '../src/reflection/reflection.js'; import { expect } from '@jest/globals'; import { ReflectionOp } from '@deepkit/type-spec'; import { isArray, isObject } from '@deepkit/core'; @@ -18,6 +25,7 @@ function reflectionName(kind: ReflectionKind): string { } let visitStackId: number = 0; + export function visitWithParent(type: Type, visitor: (type: Type, path: string, parent?: Type) => false | void, onCircular?: () => void, stack: number = visitStackId++, path: string = '', parent?: Type): void { const jit = getTypeJitContainer(type); if (jit.visitId === visitStackId) { @@ -73,7 +81,7 @@ export function visitWithParent(type: Type, visitor: (type: Type, path: string, export function expectType( pack: ReflectionOp[] | { ops: ReflectionOp[], stack: RuntimeStackEntry[], inputs?: RuntimeStackEntry[] }, - expectObject: E | number | string | boolean + expectObject: E | number | string | boolean, ): void { const type = Processor.get().run(isArray(pack) ? pack : pack.ops, isArray(pack) ? [] : pack.stack, isArray(pack) ? [] : pack.inputs); @@ -88,10 +96,22 @@ export function expectType( } } +export function equalType(a?: ReceiveType, b?: ReceiveType) { + const aType = removeTypeName(resolveReceiveType(a)); + const bType = removeTypeName(resolveReceiveType(b)); + expect(stringifyResolvedType(aType)).toBe(stringifyResolvedType(bType)); + expectEqualType(aType, bType as any); +} + /** * Types can not be compared via toEqual since they contain circular references (.parent) and other stuff can not be easily assigned. */ -export function expectEqualType(actual: any, expected: any, options: { noTypeNames?: true, noOrigin?: true, excludes?: string[], stack?: any[] } = {}, path: string = ''): void { +export function expectEqualType(actual: any, expected: any, options: { + noTypeNames?: true, + noOrigin?: true, + excludes?: string[], + stack?: any[] +} = {}, path: string = ''): void { if (!options.stack) options.stack = []; if (options.stack.includes(expected)) {