Skip to content

Commit

Permalink
fix(type): handle Function correctly in type comparisons
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
marcj committed Mar 4, 2024
1 parent 58bb3c8 commit 16f0c1d
Show file tree
Hide file tree
Showing 9 changed files with 105 additions and 42 deletions.
7 changes: 4 additions & 3 deletions packages/type-compiler/src/compiler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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]) {
Expand Down
9 changes: 9 additions & 0 deletions packages/type-compiler/tests/transpile.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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': `
Expand Down
56 changes: 34 additions & 22 deletions packages/type/src/reflection/extends.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@
import {
addType,
emptyObject,
flatten, getTypeJitContainer,
flatten,
getTypeJitContainer,
indexAccess,
isMember,
isOptional,
Expand All @@ -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';
Expand Down Expand Up @@ -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;

Expand Down Expand Up @@ -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)) {
Expand Down
6 changes: 5 additions & 1 deletion packages/type/src/reflection/processor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)[],
Expand Down Expand Up @@ -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,
Expand Down
14 changes: 14 additions & 0 deletions packages/type/tests/integration4.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -143,3 +144,16 @@ test('union loosely', () => {
expect(cast<a>({ id: 2 })).toEqual({ id: 2 });
expect(cast<a>({ 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<Function>());
console.log(typeOf<{ a: Function } >());
equalType<t1, true>();
equalType<t2, true>();
equalType<t3, true>();
equalType<t4, false>();
});
5 changes: 5 additions & 0 deletions packages/type/tests/processor.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand Down
15 changes: 2 additions & 13 deletions packages/type/tests/standard-functions.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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, B>(a?: ReceiveType<A>, b?: ReceiveType<B>) {
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<Exclude<'a' | 'b' | 'c', 'b'>, 'a' | 'c'>();
Expand Down
9 changes: 9 additions & 0 deletions packages/type/tests/typeguard.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<Function>((a: any): number => 2)).toEqual(true);
});

test('class', () => {
class A {
a!: string;
Expand Down
26 changes: 23 additions & 3 deletions packages/type/tests/utils.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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) {
Expand Down Expand Up @@ -73,7 +81,7 @@ export function visitWithParent(type: Type, visitor: (type: Type, path: string,

export function expectType<E extends ParentLessType>(
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);

Expand All @@ -88,10 +96,22 @@ export function expectType<E extends ParentLessType>(
}
}

export function equalType<A, B>(a?: ReceiveType<A>, b?: ReceiveType<B>) {
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)) {
Expand Down

0 comments on commit 16f0c1d

Please sign in to comment.