From 7191c585cf3015f8943667b83cd65c5278bd00da Mon Sep 17 00:00:00 2001 From: "Marc J. Schmidt" Date: Fri, 2 Dec 2022 12:50:35 +0100 Subject: [PATCH] fix(type): allow using (self) function type reference fixes #354 --- packages/type-compiler/src/compiler.ts | 20 +++++++--- .../type-compiler/tests/transpile.spec.ts | 24 +++++++++++ packages/type-spec/src/type.ts | 2 + packages/type/src/reflection/processor.ts | 15 ++++--- packages/type/tests/type.spec.ts | 40 +++++++++++++++++++ 5 files changed, 90 insertions(+), 11 deletions(-) diff --git a/packages/type-compiler/src/compiler.ts b/packages/type-compiler/src/compiler.ts index 2327f0621..84506d861 100644 --- a/packages/type-compiler/src/compiler.ts +++ b/packages/type-compiler/src/compiler.ts @@ -118,7 +118,7 @@ import { TypeReferenceNode, UnionTypeNode, visitEachChild, - visitNode + visitNode, isVariableDeclaration, isTypeNode } from 'typescript'; import { ensureImportIsEmitted, @@ -1693,11 +1693,11 @@ export class ReflectionTransformer implements CustomTransformer { * This is a custom resolver based on populated `locals` from the binder. It uses a custom resolution algorithm since * we have no access to the binder/TypeChecker directly and instantiating a TypeChecker per file/transformer is incredible slow. */ - protected resolveDeclaration(typeName: EntityName): { declaration: Declaration, importDeclaration?: ImportDeclaration, typeOnly?: boolean } | void { + protected resolveDeclaration(typeName: EntityName): { declaration: Node, importDeclaration?: ImportDeclaration, typeOnly?: boolean } | void { let current: Node = typeName.parent; if (typeName.kind === SyntaxKind.QualifiedName) return; //namespace access not supported yet, e.g. type a = Namespace.X; - let declaration: Declaration | undefined = undefined; + let declaration: Node | undefined = undefined; while (current) { if (isNodeWithLocals(current) && current.locals) { @@ -1878,7 +1878,15 @@ export class ReflectionTransformer implements CustomTransformer { return; } - const declaration = resolved.declaration; + let declaration: Node = resolved.declaration; + if (isVariableDeclaration(declaration)) { + if (declaration.type) { + declaration = declaration.type; + } else if (declaration.initializer) { + declaration = declaration.initializer; + } + } + if (isModuleDeclaration(declaration) && resolved.importDeclaration) { if (isIdentifier(typeName)) ensureImportIsEmitted(resolved.importDeclaration, typeName); @@ -2023,7 +2031,7 @@ export class ReflectionTransformer implements CustomTransformer { // //{[Property in keyof Type]: boolean;}; // this.extractPackStructOfType(declaration, program); // return; - } else if (isClassDeclaration(declaration)) { + } else if (isClassDeclaration(declaration) || isFunctionDeclaration(declaration) || isFunctionExpression(declaration) || isArrowFunction(declaration)) { //if explicit `import {type T}`, we do not emit an import and instead push any if (resolved.typeOnly) { program.pushOp(ReflectionOp.any); @@ -2039,7 +2047,7 @@ export class ReflectionTransformer implements CustomTransformer { } const body = isIdentifier(typeName) ? typeName : this.createAccessorForEntityName(typeName); const index = program.pushStack(this.f.createArrowFunction(undefined, undefined, [], undefined, undefined, body)); - program.pushOp(ReflectionOp.classReference, index); + program.pushOp(isClassDeclaration(declaration) ? ReflectionOp.classReference : ReflectionOp.functionReference, index); program.popFrameImplicit(); } else if (isTypeParameterDeclaration(declaration)) { this.resolveTypeParameter(declaration, type, program); diff --git a/packages/type-compiler/tests/transpile.spec.ts b/packages/type-compiler/tests/transpile.spec.ts index 3573d31d1..e3df3b44d 100644 --- a/packages/type-compiler/tests/transpile.spec.ts +++ b/packages/type-compiler/tests/transpile.spec.ts @@ -369,6 +369,30 @@ test('es2021', () => { expect(res.app).toContain(`const type = typeOf([], [() => __ΩReadUser, 'n!'])`); }); +test('Return function ref', () => { + //see GitHub issue #354 + const res = transpile({ + 'app': ` + function Option(val: T): Option { + }; + ` + }); + console.log(res); + expect(res.app).toContain(`() => Option,`); +}); + +test('Return arrow function ref', () => { + //see GitHub issue #354 + const res = transpile({ + 'app': ` + const Option = (val: T): Option => { + }; + ` + }); + console.log(res); + expect(res.app).toContain(`() => Option,`); +}); + //currently knownLibFilesForCompilerOptions from @typescript/vfs doesn't return correct lib files for ES2022 // test('es2022', () => { // const res = transpile({ diff --git a/packages/type-spec/src/type.ts b/packages/type-spec/src/type.ts index 981bb7d0d..552517314 100644 --- a/packages/type-spec/src/type.ts +++ b/packages/type-spec/src/type.ts @@ -208,4 +208,6 @@ export enum ReflectionOp { static, mappedType2, //same as mappedType2 but the given functionPointer returns a tuple [type, name] + + functionReference, //Same as classReference but for functions } diff --git a/packages/type/src/reflection/processor.ts b/packages/type/src/reflection/processor.ts index d0394d1e1..7c78de377 100644 --- a/packages/type/src/reflection/processor.ts +++ b/packages/type/src/reflection/processor.ts @@ -629,20 +629,25 @@ export class Processor { this.pushType(t); break; } + case ReflectionOp.functionReference: case ReflectionOp.classReference: { const ref = this.eatParameter() as number; - const classType = resolveFunction(program.stack[ref] as Function, program.object); + const classOrFunction = resolveFunction(program.stack[ref] as Function, program.object); const inputs = this.popFrame() as Type[]; - if (!classType) throw new Error('No class reference given in ' + String(program.stack[ref])); + if (!classOrFunction) throw new Error('No class/function reference given in ' + String(program.stack[ref])); - if (!classType.__type) { - this.pushType({ kind: ReflectionKind.class, classType, typeArguments: inputs, types: [] }); + if (!classOrFunction.__type) { + if (op === ReflectionOp.classReference) { + this.pushType({ kind: ReflectionKind.class, classType: classOrFunction, typeArguments: inputs, types: [] }); + } else if (op === ReflectionOp.functionReference) { + this.pushType({ kind: ReflectionKind.function, function: classOrFunction, parameters: [], return: { kind: ReflectionKind.unknown } }); + } } else { //when it's just a simple reference resolution like typeOf() then enable cache re-use (so always the same type is returned) const reuseCached = !!(this.isEnded() && program.previous && program.previous.end === 0); - const result = this.reflect(classType, inputs, { reuseCached }); + const result = this.reflect(classOrFunction, inputs, { reuseCached }); this.push(result, program); if (isWithAnnotations(result) && inputs.length) { diff --git a/packages/type/tests/type.spec.ts b/packages/type/tests/type.spec.ts index b07783f0b..d5cf0bde7 100644 --- a/packages/type/tests/type.spec.ts +++ b/packages/type/tests/type.spec.ts @@ -1254,6 +1254,46 @@ test('ignore constructor in keyof', () => { expect(type.types.map(v => v.kind === ReflectionKind.literal ? v.literal : 'unknown')).toEqual(['foo', 'id']); }); +test('function returns self reference', () => { + type Option = OptionType; + + class OptionType { + constructor(val: T, some: boolean) { + } + } + + function Option(val: T): Option { + return new OptionType(val, true); + } + + const type = reflect(Option); + assertType(type, ReflectionKind.function); + expect(type.function).toBe(Option); + assertType(type.return, ReflectionKind.function); + expect(type.return.function).toBe(Option); +}); + +test('arrow function returns self reference', () => { + type Option = OptionType; + + class OptionType { + constructor(val: T, some: boolean) { + } + } + + const Option = (val: T): Option => { + return new OptionType(val, true); + } + + const type = reflect(Option); + assertType(type, ReflectionKind.function); + expect(type.function).toBe(Option); + + //we need to find out why TS does resolve Option in arrow function to the class and not the variable + assertType(type.return, ReflectionKind.class); + expect(type.return.classType).toBe(OptionType); +}); + class User { a!: string; }