From 96e9695d08b2371ed3a74bef20d87a7ecce94165 Mon Sep 17 00:00:00 2001 From: Martin Jesper Low Madsen Date: Sat, 9 May 2020 13:43:57 +0200 Subject: [PATCH 01/26] enhancement(repository): Add hidden __factory property to mocks to differentiate them in conditional typing --- src/repository/repository.ts | 34 ++++++++++++++++++++++++++++++++-- 1 file changed, 32 insertions(+), 2 deletions(-) diff --git a/src/repository/repository.ts b/src/repository/repository.ts index 7cca95d4e..5f3f4ac02 100644 --- a/src/repository/repository.ts +++ b/src/repository/repository.ts @@ -1,4 +1,5 @@ -type Factory = Function; +// eslint-disable-next-line +type Factory = (...args: any[]) => any; export class Repository { private readonly _repository: { [key: string]: Factory }; @@ -15,7 +16,36 @@ export class Repository { } public registerFactory(key: string, factory: Factory): void { - this._repository[key] = factory; + const proxy: Factory = new Proxy( + factory, + { + apply(target: Factory, _this: unknown, args: Parameters): ReturnType { + const mock: ReturnType = target(...args); + + if (typeof mock === 'undefined') { + return; + } + + if (!(mock instanceof Object)) { + return mock; + } + + if (typeof mock.__factory !== 'undefined') { + return mock; + } + + Object.defineProperty(mock, '__factory', { + enumerable: false, + writable: false, + value: key, + }); + + return mock; + }, + }, + ); + + this._repository[key] = proxy; } public getFactory(key: string): Factory { From 2bcf6d27b5fa9ba87bea5111cb3cf744a0d372a0 Mon Sep 17 00:00:00 2001 From: Martin Jesper Low Madsen Date: Sun, 12 Apr 2020 01:39:13 +0200 Subject: [PATCH 02/26] enhancement(transformer): Add declared function overload support --- .../method/provider/functionMethod.ts | 4 +- src/merge/merge.ts | 2 +- src/transformer/descriptor/helper/helper.ts | 16 +++ .../descriptor/method/functionAssignment.ts | 2 +- .../descriptor/method/functionType.ts | 2 +- src/transformer/descriptor/method/method.ts | 106 +++++++++++++++++- .../descriptor/method/methodDeclaration.ts | 28 ++++- .../descriptor/method/methodSignature.ts | 8 +- .../descriptor/typeQuery/typeQuery.test.ts | 22 ++++ .../utils/typeQuery/typeQueryUtils.ts | 36 ++++++ 10 files changed, 208 insertions(+), 18 deletions(-) diff --git a/src/extension/method/provider/functionMethod.ts b/src/extension/method/provider/functionMethod.ts index 9a7b08aa1..f4d2d57ac 100644 --- a/src/extension/method/provider/functionMethod.ts +++ b/src/extension/method/provider/functionMethod.ts @@ -1,5 +1,5 @@ // eslint-disable-next-line @typescript-eslint/no-explicit-any -export function functionMethod(name: string, value: () => any): any { +export function functionMethod(name: string, value: (...args: any[]) => any): any { // eslint-disable-next-line @typescript-eslint/no-explicit-any - return (): any => value(); + return (...args: any[]): any => value(...args); } diff --git a/src/merge/merge.ts b/src/merge/merge.ts index 62182d0bc..feab8e1c8 100644 --- a/src/merge/merge.ts +++ b/src/merge/merge.ts @@ -1,4 +1,4 @@ -import { merge} from 'lodash-es'; +import { merge } from 'lodash-es'; import { DeepPartial } from '../partial/deepPartial'; export class Merge { diff --git a/src/transformer/descriptor/helper/helper.ts b/src/transformer/descriptor/helper/helper.ts index ea86d74b7..e79fa7793 100644 --- a/src/transformer/descriptor/helper/helper.ts +++ b/src/transformer/descriptor/helper/helper.ts @@ -119,6 +119,22 @@ export namespace TypescriptHelper { return !!((symbol.flags & ts.SymbolFlags.Alias) || (symbol.flags & ts.SymbolFlags.AliasExcludes)); } + export interface RuntimeTypeNode extends ts.TypeNode { + kind: ts.SyntaxKind.NumberKeyword | ts.SyntaxKind.ObjectKeyword | ts.SyntaxKind.BooleanKeyword | ts.SyntaxKind.StringKeyword | ts.SyntaxKind.UndefinedKeyword; + } + + export function isLiteralRuntimeTypeNode(typeNode: ts.TypeNode): typeNode is RuntimeTypeNode { + switch (typeNode.kind) { + case ts.SyntaxKind.NumberKeyword: + case ts.SyntaxKind.ObjectKeyword: + case ts.SyntaxKind.BooleanKeyword: + case ts.SyntaxKind.StringKeyword: + return true; + } + + return false; + } + function isImportExportDeclaration(declaration: ts.Declaration): declaration is ImportDeclaration { return ts.isImportEqualsDeclaration(declaration) || ts.isImportOrExportSpecifier(declaration) || ts.isImportClause(declaration); } diff --git a/src/transformer/descriptor/method/functionAssignment.ts b/src/transformer/descriptor/method/functionAssignment.ts index 316da73cd..bd2559306 100644 --- a/src/transformer/descriptor/method/functionAssignment.ts +++ b/src/transformer/descriptor/method/functionAssignment.ts @@ -10,5 +10,5 @@ export function GetFunctionAssignmentDescriptor(node: functionAssignment, scope: const property: ts.PropertyName = PropertySignatureCache.instance.get(); const returnValue: ts.Expression = GetReturnTypeFromBodyDescriptor(node, scope); - return GetMethodDescriptor(property, returnValue); + return GetMethodDescriptor(property, [{ returnValue }]); } diff --git a/src/transformer/descriptor/method/functionType.ts b/src/transformer/descriptor/method/functionType.ts index a044b8463..1a3e4d669 100644 --- a/src/transformer/descriptor/method/functionType.ts +++ b/src/transformer/descriptor/method/functionType.ts @@ -13,5 +13,5 @@ export function GetFunctionTypeDescriptor(node: ts.FunctionTypeNode | ts.CallSig const returnValue: ts.Expression = GetDescriptor(node.type, scope); - return GetMethodDescriptor(property, returnValue); + return GetMethodDescriptor(property, [{ returnValue }]); } diff --git a/src/transformer/descriptor/method/method.ts b/src/transformer/descriptor/method/method.ts index 64db3e66e..0f96bac17 100644 --- a/src/transformer/descriptor/method/method.ts +++ b/src/transformer/descriptor/method/method.ts @@ -1,23 +1,119 @@ -import * as ts from 'typescript'; +import ts from 'typescript'; import { TypescriptCreator } from '../../helper/creator'; import { MockDefiner } from '../../mockDefiner/mockDefiner'; import { ModuleName } from '../../mockDefiner/modules/moduleName'; import { TypescriptHelper } from '../helper/helper'; -export function GetMethodDescriptor(propertyName: ts.PropertyName, returnValue: ts.Expression): ts.Expression { +export interface MethodSignature { + parameters?: ts.ParameterDeclaration[]; + returnValue: ts.Expression; +} + +export function GetMethodDescriptor(propertyName: ts.PropertyName, methodSignatures: MethodSignature[]): ts.Expression { const providerGetMethod: ts.PropertyAccessExpression = CreateProviderGetMethod(); const propertyNameString: string = TypescriptHelper.GetStringPropertyName(propertyName); const propertyNameStringLiteral: ts.StringLiteral = ts.createStringLiteral(propertyNameString); - const propertyValueFunction: ts.ArrowFunction = TypescriptCreator.createArrowFunction(ts.createBlock( - [ts.createReturn(returnValue)], + const [signatureWithMostParameters]: MethodSignature[] = [...methodSignatures].sort( + ( + { parameters: leftParameters = [] }: MethodSignature, + { parameters: rightParameters = [] }: MethodSignature, + ) => rightParameters.length - leftParameters.length, + ); + + const longestParameterList: ts.ParameterDeclaration[] = signatureWithMostParameters.parameters || []; + + const block: ts.Block = ts.createBlock( + [ + ResolveSignatureElseBranch(methodSignatures, longestParameterList), + ], true, - )); + ); + + const propertyValueFunction: ts.ArrowFunction = TypescriptCreator.createArrowFunction( + block, + longestParameterList, + ); return TypescriptCreator.createCall(providerGetMethod, [propertyNameStringLiteral, propertyValueFunction]); } +function CreateTypeEquality(signatureType: ts.TypeNode | undefined, primaryDeclaration: ts.ParameterDeclaration): ts.Expression { + const identifier: ts.Identifier = ts.createIdentifier(primaryDeclaration.name.getText()); + + if (!signatureType) { + return ts.createPrefix( + ts.SyntaxKind.ExclamationToken, + ts.createPrefix( + ts.SyntaxKind.ExclamationToken, + identifier, + ), + ); + } + + if (TypescriptHelper.isLiteralRuntimeTypeNode(signatureType)) { + return ts.createStrictEquality( + ts.createTypeOf(identifier), + signatureType ? ts.createStringLiteral(signatureType.getText()) : ts.createVoidZero(), + ); + } else { + return ts.createBinary(identifier, ts.SyntaxKind.InstanceOfKeyword, ts.createIdentifier(signatureType.getText())); + } +} + +function CreateUnionTypeOfEquality(signatureType: ts.TypeNode | undefined, primaryDeclaration: ts.ParameterDeclaration): ts.Expression { + const typeNodes: ts.TypeNode[] = []; + + if (signatureType) { + if (ts.isUnionTypeNode(signatureType)) { + typeNodes.push(...signatureType.types); + } else { + typeNodes.push(signatureType); + } + } + + const [firstType, ...remainingTypes]: ts.TypeNode[] = typeNodes; + + return remainingTypes.reduce( + (prevStatement: ts.Expression, typeNode: ts.TypeNode) => + ts.createLogicalOr( + prevStatement, + CreateTypeEquality(typeNode, primaryDeclaration), + ), + CreateTypeEquality(firstType, primaryDeclaration), + ); +} + +function ResolveParameterBranch(declarations: ts.ParameterDeclaration[], allDeclarations: ts.ParameterDeclaration[], returnValue: ts.Expression, elseBranch: ts.Statement): ts.Statement { + const [firstDeclaration, ...remainingDeclarations]: ts.ParameterDeclaration[] = declarations; + + const condition: ts.Expression = remainingDeclarations.reduce( + (prevStatement: ts.Expression, declaration: ts.ParameterDeclaration, index: number) => + ts.createLogicalAnd( + prevStatement, + CreateUnionTypeOfEquality(declaration.type, allDeclarations[index + 1]), + ), + CreateUnionTypeOfEquality(firstDeclaration.type, allDeclarations[0]), + ); + + + return ts.createIf(condition, ts.createReturn(returnValue), elseBranch); +} + +function ResolveSignatureElseBranch(signatures: MethodSignature[], longestParameterList: ts.ParameterDeclaration[]): ts.Statement { + const [signature, ...remainingSignatures]: MethodSignature[] = signatures; + + if (remainingSignatures.length) { + const elseBranch: ts.Statement = ResolveSignatureElseBranch(remainingSignatures, longestParameterList); + + const currentParameters: ts.ParameterDeclaration[] = signature.parameters || []; + return ResolveParameterBranch(currentParameters, longestParameterList, signature.returnValue, elseBranch); + } else { + return ts.createReturn(signature.returnValue); + } +} + function CreateProviderGetMethod(): ts.PropertyAccessExpression { return ts.createPropertyAccess( ts.createPropertyAccess( diff --git a/src/transformer/descriptor/method/methodDeclaration.ts b/src/transformer/descriptor/method/methodDeclaration.ts index 528fc4120..dc9704ef9 100644 --- a/src/transformer/descriptor/method/methodDeclaration.ts +++ b/src/transformer/descriptor/method/methodDeclaration.ts @@ -1,12 +1,32 @@ import * as ts from 'typescript'; import { Scope } from '../../scope/scope'; +import { TypeChecker } from '../../typeChecker/typeChecker'; import { GetDescriptor } from '../descriptor'; import { GetFunctionReturnType } from './functionReturnType'; -import { GetMethodDescriptor } from './method'; +import { GetMethodDescriptor, MethodSignature } from './method'; export function GetMethodDeclarationDescriptor(node: ts.MethodDeclaration | ts.FunctionDeclaration, scope: Scope): ts.Expression { - const returnTypeNode: ts.Node = GetFunctionReturnType(node); - const returnType: ts.Expression = GetDescriptor(returnTypeNode, scope); + const declarationType: ts.Type | undefined = TypeChecker().getTypeAtLocation(node); + const methodDeclarations: Array = declarationType.symbol.declarations + .filter( + (declaration: ts.Declaration): declaration is ts.MethodDeclaration | ts.FunctionDeclaration => + ts.isMethodDeclaration(declaration) || ts.isFunctionDeclaration(declaration) + ); + + if (!methodDeclarations.length) { + methodDeclarations.push(node); + } + + const methodSignatures: MethodSignature[] = methodDeclarations.map( + (declaration: ts.MethodDeclaration | ts.FunctionDeclaration) => { + const returnTypeNode: ts.Node = GetFunctionReturnType(declaration); + + return { + parameters: declaration.parameters.map((parameter: ts.ParameterDeclaration) => parameter), + returnValue: GetDescriptor(returnTypeNode, scope), + }; + } + ); if (!node.name) { throw new Error( @@ -14,5 +34,5 @@ export function GetMethodDeclarationDescriptor(node: ts.MethodDeclaration | ts.F ); } - return GetMethodDescriptor(node.name, returnType); + return GetMethodDescriptor(node.name, methodSignatures); } diff --git a/src/transformer/descriptor/method/methodSignature.ts b/src/transformer/descriptor/method/methodSignature.ts index 80993c613..e2400cc4b 100644 --- a/src/transformer/descriptor/method/methodSignature.ts +++ b/src/transformer/descriptor/method/methodSignature.ts @@ -5,13 +5,13 @@ import { GetNullDescriptor } from '../null/null'; import { GetMethodDescriptor } from './method'; export function GetMethodSignatureDescriptor(node: ts.MethodSignature, scope: Scope): ts.Expression { - let returnType: ts.Expression; + let returnValue: ts.Expression; if (node.type) { - returnType = GetDescriptor(node.type, scope); + returnValue = GetDescriptor(node.type, scope); } else { - returnType = GetNullDescriptor(); + returnValue = GetNullDescriptor(); } - return GetMethodDescriptor(node.name, returnType); + return GetMethodDescriptor(node.name, [{ returnValue }]); } diff --git a/test/transformer/descriptor/typeQuery/typeQuery.test.ts b/test/transformer/descriptor/typeQuery/typeQuery.test.ts index 2a61db5e5..92a557e2d 100644 --- a/test/transformer/descriptor/typeQuery/typeQuery.test.ts +++ b/test/transformer/descriptor/typeQuery/typeQuery.test.ts @@ -6,6 +6,7 @@ import { ExportedClass, ExportedDeclaredClass, exportedDeclaredFunction, + exportedDeclaredOverloadedFunction, ExportedEnum, exportedFunction, WrapExportedClass, @@ -41,6 +42,27 @@ describe('typeQuery', () => { expect(functionMock()).toEqual(''); }); + it('should assign the correct function mock for an imported and overloaded function declaration', () => { + const functionMock: typeof exportedDeclaredOverloadedFunction = createMock(); + + // eslint-disable-next-line + const expectations = [ + { args: ['', 0, false], returnValue: '' }, + { args: [false, '', 0], returnValue: false }, + { args: [0, false, ''], returnValue: 0 }, + { args: [false, false, false], returnValue: false }, + { args: [''], returnValue: '' }, + { args: [false], returnValue: false }, + { args: [0], returnValue: 0 }, + ]; + + for (const { args, returnValue } of expectations) { + // eslint-disable-next-line + const [first, second, third] = args; + expect(functionMock(first, second, third)).toEqual(returnValue); + } + }); + it('should assign the function mock for an imported function declaration with body', () => { const functionMock: typeof exportedFunction = createMock(); diff --git a/test/transformer/descriptor/utils/typeQuery/typeQueryUtils.ts b/test/transformer/descriptor/utils/typeQuery/typeQueryUtils.ts index 4bb98ed7f..a170a0f7f 100644 --- a/test/transformer/descriptor/utils/typeQuery/typeQueryUtils.ts +++ b/test/transformer/descriptor/utils/typeQuery/typeQueryUtils.ts @@ -1,5 +1,41 @@ export declare function exportedDeclaredFunction(): string; +export declare function exportedDeclaredOverloadedFunction(a: boolean, b: boolean, c: number): boolean; +export declare function exportedDeclaredOverloadedFunction(a: boolean, b: boolean, c: string): boolean; +export declare function exportedDeclaredOverloadedFunction(a: boolean, b: number, c: number): boolean; +export declare function exportedDeclaredOverloadedFunction(a: boolean, b: number, c: string): boolean; +export declare function exportedDeclaredOverloadedFunction(a: boolean, b: string, c: number): boolean; +export declare function exportedDeclaredOverloadedFunction(a: boolean, b: string, c: string): boolean; +export declare function exportedDeclaredOverloadedFunction(a: number, b: boolean, c: boolean): number; +export declare function exportedDeclaredOverloadedFunction(a: number, b: boolean, c: string): number; +export declare function exportedDeclaredOverloadedFunction(a: number, b: number, c: boolean): number; +export declare function exportedDeclaredOverloadedFunction(a: number, b: number, c: string): number; +export declare function exportedDeclaredOverloadedFunction(a: number, b: string, c: boolean): number; +export declare function exportedDeclaredOverloadedFunction(a: number, b: string, c: string): number; +export declare function exportedDeclaredOverloadedFunction(a: string, b: boolean, c: boolean): string; +export declare function exportedDeclaredOverloadedFunction(a: string, b: boolean, c: number): string; +export declare function exportedDeclaredOverloadedFunction(a: string, b: number, c: boolean): string; +export declare function exportedDeclaredOverloadedFunction(a: string, b: number, c: number): string; +export declare function exportedDeclaredOverloadedFunction(a: string, b: string, c: boolean): string; +export declare function exportedDeclaredOverloadedFunction(a: string, b: string, c: number): string; +export declare function exportedDeclaredOverloadedFunction(a: number, b: number, c: number): number; +export declare function exportedDeclaredOverloadedFunction(a: boolean, b: boolean, c: boolean): boolean; +export declare function exportedDeclaredOverloadedFunction(a: string, b: string, c: string): string; +export declare function exportedDeclaredOverloadedFunction(a: string, b: number, c: string): string; +export declare function exportedDeclaredOverloadedFunction(a: string, b: boolean, c: string): string; +export declare function exportedDeclaredOverloadedFunction(a: number, b: string, c: number): number; +export declare function exportedDeclaredOverloadedFunction(a: number, b: boolean, c: number): number; +export declare function exportedDeclaredOverloadedFunction(a: boolean, b: number, c: boolean): boolean; +export declare function exportedDeclaredOverloadedFunction(a: boolean, b: string, c: boolean): boolean; +export declare function exportedDeclaredOverloadedFunction(a: string | number | boolean, b: string | number | boolean, c: string | number | boolean): string | number | boolean; +// TODO: ExportedClass may need to be mocked and it is not imported as of this +// writing. The transformation does take `a instanceof ExportedClass` into +// consideration though. +// export declare function exportedDeclaredOverloadedFunction(a: ExportedClass): ExportedClass; +export declare function exportedDeclaredOverloadedFunction(a: boolean): boolean; +export declare function exportedDeclaredOverloadedFunction(a: number): number; +export declare function exportedDeclaredOverloadedFunction(a: string): string; + export declare class ExportedDeclaredClass { public prop: string; } From 28bb478ed6d0295e39ae1172b884f3c10d06fb7a Mon Sep 17 00:00:00 2001 From: Martin Jesper Low Madsen Date: Mon, 4 May 2020 20:39:20 +0200 Subject: [PATCH 03/26] enhancement(transformer): Add transformOverloads option --- config/test/webpack.js | 3 ++- src/options/default.ts | 1 + src/options/options.ts | 2 ++ src/options/overload.ts | 7 +++++++ tsconfig.playground.json | 3 ++- 5 files changed, 14 insertions(+), 2 deletions(-) create mode 100644 src/options/overload.ts diff --git a/config/test/webpack.js b/config/test/webpack.js index 5fbd67c24..b8b6de91a 100644 --- a/config/test/webpack.js +++ b/config/test/webpack.js @@ -26,7 +26,8 @@ module.exports = function (debug, disableCache) { getCustomTransformers: (program) => ({ before: [transformer.default(program, { debug: debug ? debug : false, - cacheBetweenTests: disableCache !== 'true' + cacheBetweenTests: disableCache !== 'true', + transformOverloads: true, })] }) } diff --git a/src/options/default.ts b/src/options/default.ts index a204c88ed..8c3f2adbe 100644 --- a/src/options/default.ts +++ b/src/options/default.ts @@ -3,4 +3,5 @@ import { TsAutoMockOptions } from './options'; export const defaultOptions: TsAutoMockOptions = { debug: false, cacheBetweenTests: true, + transformOverloads: false, }; diff --git a/src/options/options.ts b/src/options/options.ts index 460d6d067..6b306e8e9 100644 --- a/src/options/options.ts +++ b/src/options/options.ts @@ -1,10 +1,12 @@ import { TsAutoMockCacheOptions } from './cache'; import { TsAutoMockDebugOptions } from './debug'; +import { TsAutoMockOverloadOptions } from './overload'; import { defaultOptions } from './default'; export interface TsAutoMockOptions { debug: TsAutoMockDebugOptions; cacheBetweenTests: TsAutoMockCacheOptions; + transformOverloads: TsAutoMockOverloadOptions; } let tsAutoMockOptions: TsAutoMockOptions = defaultOptions; diff --git a/src/options/overload.ts b/src/options/overload.ts new file mode 100644 index 000000000..b73c56c6c --- /dev/null +++ b/src/options/overload.ts @@ -0,0 +1,7 @@ +import { GetOptionByKey } from './options'; + +export type TsAutoMockOverloadOptions = boolean; + +export function GetTsAutoMockOverloadOptions(): TsAutoMockOverloadOptions { + return GetOptionByKey('transformOverloads'); +} diff --git a/tsconfig.playground.json b/tsconfig.playground.json index cb21525ec..7291fbba8 100644 --- a/tsconfig.playground.json +++ b/tsconfig.playground.json @@ -5,7 +5,8 @@ "plugins": [ { "transform": "./dist/transformer", - "debug": "console" + "debug": "console", + "transformOverloads": true } ] }, From 02685076345f639bbba8bdda22b21a9fca4c764d Mon Sep 17 00:00:00 2001 From: Martin Jesper Low Madsen Date: Mon, 4 May 2020 20:42:19 +0200 Subject: [PATCH 04/26] enhancement(transformer): Support overloads on interfaces --- .../descriptor/method/functionReturnType.ts | 16 ++++--- src/transformer/descriptor/method/method.ts | 11 +++-- .../descriptor/method/methodDeclaration.ts | 20 ++++---- src/transformer/descriptor/mock/mockCall.ts | 46 +++++++++++-------- .../descriptor/mock/mockProperties.ts | 8 ++-- 5 files changed, 58 insertions(+), 43 deletions(-) diff --git a/src/transformer/descriptor/method/functionReturnType.ts b/src/transformer/descriptor/method/functionReturnType.ts index 8bc4cb634..4a0d34966 100644 --- a/src/transformer/descriptor/method/functionReturnType.ts +++ b/src/transformer/descriptor/method/functionReturnType.ts @@ -1,14 +1,16 @@ import * as ts from 'typescript'; import { GetReturnNodeFromBody } from './bodyReturnType'; -export function GetFunctionReturnType(node: ts.FunctionLikeDeclaration): ts.Node { - let returnType: ts.Node; +export function GetFunctionReturnType(node: ts.SignatureDeclaration): ts.Node { + if (ts.isArrowFunction(node) || ts.isFunctionExpression(node)) { + return GetReturnNodeFromBody(node); + } - if (node.type) { - returnType = node.type; - } else { - returnType = GetReturnNodeFromBody(node); + if (!node.type) { + throw new Error( + `The transformer couldn't determine the type of ${node.getText()}. Did you declare its type?`, + ); } - return returnType; + return node.type; } diff --git a/src/transformer/descriptor/method/method.ts b/src/transformer/descriptor/method/method.ts index 0f96bac17..1990dd248 100644 --- a/src/transformer/descriptor/method/method.ts +++ b/src/transformer/descriptor/method/method.ts @@ -1,4 +1,6 @@ import ts from 'typescript'; +import { GetTsAutoMockOverloadOptions, TsAutoMockOverloadOptions } from '../../../options/overload'; +import { GetClassDeclarationDescriptor } from '../class/classDeclaration'; import { TypescriptCreator } from '../../helper/creator'; import { MockDefiner } from '../../mockDefiner/mockDefiner'; import { ModuleName } from '../../mockDefiner/modules/moduleName'; @@ -9,7 +11,7 @@ export interface MethodSignature { returnValue: ts.Expression; } -export function GetMethodDescriptor(propertyName: ts.PropertyName, methodSignatures: MethodSignature[]): ts.Expression { +export function GetMethodDescriptor(propertyName: ts.PropertyName, methodSignatures: MethodSignature[]): ts.CallExpression { const providerGetMethod: ts.PropertyAccessExpression = CreateProviderGetMethod(); const propertyNameString: string = TypescriptHelper.GetStringPropertyName(propertyName); @@ -97,12 +99,13 @@ function ResolveParameterBranch(declarations: ts.ParameterDeclaration[], allDecl CreateUnionTypeOfEquality(firstDeclaration.type, allDeclarations[0]), ); - return ts.createIf(condition, ts.createReturn(returnValue), elseBranch); } -function ResolveSignatureElseBranch(signatures: MethodSignature[], longestParameterList: ts.ParameterDeclaration[]): ts.Statement { - const [signature, ...remainingSignatures]: MethodSignature[] = signatures; +export function ResolveSignatureElseBranch(signatures: MethodSignature[], longestParameterList: ts.ParameterDeclaration[]): ts.Statement { + const transformOverloadsOption: TsAutoMockOverloadOptions = GetTsAutoMockOverloadOptions(); + + const [signature, ...remainingSignatures]: MethodSignature[] = signatures.filter((_: unknown, i: number) => transformOverloadsOption || i === 0); if (remainingSignatures.length) { const elseBranch: ts.Statement = ResolveSignatureElseBranch(remainingSignatures, longestParameterList); diff --git a/src/transformer/descriptor/method/methodDeclaration.ts b/src/transformer/descriptor/method/methodDeclaration.ts index dc9704ef9..be03a0e34 100644 --- a/src/transformer/descriptor/method/methodDeclaration.ts +++ b/src/transformer/descriptor/method/methodDeclaration.ts @@ -17,16 +17,7 @@ export function GetMethodDeclarationDescriptor(node: ts.MethodDeclaration | ts.F methodDeclarations.push(node); } - const methodSignatures: MethodSignature[] = methodDeclarations.map( - (declaration: ts.MethodDeclaration | ts.FunctionDeclaration) => { - const returnTypeNode: ts.Node = GetFunctionReturnType(declaration); - - return { - parameters: declaration.parameters.map((parameter: ts.ParameterDeclaration) => parameter), - returnValue: GetDescriptor(returnTypeNode, scope), - }; - } - ); + const methodSignatures: MethodSignature[] = methodDeclarations.map((declaration: ts.MethodDeclaration | ts.FunctionDeclaration) => ReshapeCallableDeclaration(declaration, scope)); if (!node.name) { throw new Error( @@ -36,3 +27,12 @@ export function GetMethodDeclarationDescriptor(node: ts.MethodDeclaration | ts.F return GetMethodDescriptor(node.name, methodSignatures); } + +export function ReshapeCallableDeclaration(declaration: ts.SignatureDeclaration, scope: Scope): MethodSignature { + const returnTypeNode: ts.Node = GetFunctionReturnType(declaration); + + return { + parameters: declaration.parameters.map((parameter: ts.ParameterDeclaration) => parameter), + returnValue: GetDescriptor(returnTypeNode, scope), + }; +} diff --git a/src/transformer/descriptor/mock/mockCall.ts b/src/transformer/descriptor/mock/mockCall.ts index 5e1be62ba..e7c590e4b 100644 --- a/src/transformer/descriptor/mock/mockCall.ts +++ b/src/transformer/descriptor/mock/mockCall.ts @@ -1,27 +1,35 @@ import * as ts from 'typescript'; import { TypescriptCreator } from '../../helper/creator'; import { MockIdentifierInternalValues, MockIdentifierObjectReturnValue } from '../../mockIdentifier/mockIdentifier'; +import { GetMethodDescriptor, MethodSignature } from '../method/method'; import { GetMockMarkerProperty, Property } from './mockMarker'; import { PropertyAssignments } from './mockPropertiesAssignments'; -export function GetMockCall(properties: PropertyAssignments, signature: ts.Expression | null): ts.CallExpression { +export function GetMockCall(properties: PropertyAssignments, signatures: MethodSignature[]): ts.CallExpression { const mockObjectReturnValueName: ts.Identifier = MockIdentifierObjectReturnValue; + + if (signatures.length) { + // FIXME: It'd probably be wise to extract the name of the callable + // signature and fallback to `new` or smth if there is none. + return GetMethodDescriptor(ts.createStringLiteral('new'), signatures); + } + const statements: ts.Statement[] = [ TypescriptCreator.createVariableStatement([ TypescriptCreator.createVariableDeclaration(MockIdentifierInternalValues, ts.createObjectLiteral()), - TypescriptCreator.createVariableDeclaration(mockObjectReturnValueName, signature || ts.createObjectLiteral(properties.literals)), + TypescriptCreator.createVariableDeclaration(mockObjectReturnValueName, ts.createObjectLiteral(properties.literals)), ]), ]; - if (signature) { - let literalProperty: ts.PropertyAssignment; - let index: number = 0; + // if (signatures[0]) { + // let literalProperty: ts.PropertyAssignment; + // let index: number = 0; - // tslint:disable-next-line:no-conditional-assignment - while ((literalProperty = properties.literals[index++])) { - statements.push(AssignLiteralPropertyTo(mockObjectReturnValueName, literalProperty)); - } - } + // // tslint:disable-next-line:no-conditional-assignment + // while ((literalProperty = properties.literals[index++])) { + // statements.push(AssignLiteralPropertyTo(mockObjectReturnValueName, literalProperty)); + // } + // } if (properties.lazy.length) { const addPropertiesToUniqueVariable: ts.ExpressionStatement = AssignPropertiesTo(properties.lazy, mockObjectReturnValueName); @@ -30,24 +38,24 @@ export function GetMockCall(properties: PropertyAssignments, signature: ts.Expre const addMockMarkerToUniqueVariable: ts.ExpressionStatement = AssignMockMarkerPropertyTo(mockObjectReturnValueName); statements.push(addMockMarkerToUniqueVariable); - statements.push(ts.createReturn(mockObjectReturnValueName)); const functionBlock: ts.Block = ts.createBlock(statements); const functionExpression: ts.FunctionExpression = TypescriptCreator.createFunctionExpression(functionBlock); const IFFEFunction: ts.ParenthesizedExpression = ts.createParen(functionExpression); + return ts.createCall(IFFEFunction, [], []); } -function AssignVariableTo(variable: ts.Expression, expression: ts.Expression): ts.ExpressionStatement { - const binaryExpression: ts.BinaryExpression = ts.createBinary(variable, ts.SyntaxKind.EqualsToken, expression); - return ts.createExpressionStatement(binaryExpression); -} +// function AssignVariableTo(variable: ts.Expression, expression: ts.Expression): ts.ExpressionStatement { +// const binaryExpression: ts.BinaryExpression = ts.createBinary(variable, ts.SyntaxKind.EqualsToken, expression); +// return ts.createExpressionStatement(binaryExpression); +// } -function AssignLiteralPropertyTo(mockObjectReturnValueName: ts.Identifier, literalProperty: ts.PropertyAssignment): ts.ExpressionStatement { - const propertyAccess: ts.ElementAccessExpression = ts.createElementAccess(mockObjectReturnValueName, literalProperty.name as ts.StringLiteral); - return AssignVariableTo(propertyAccess, literalProperty.initializer); -} +// function AssignLiteralPropertyTo(mockObjectReturnValueName: ts.Identifier, literalProperty: ts.PropertyAssignment): ts.ExpressionStatement { +// const propertyAccess: ts.ElementAccessExpression = ts.createElementAccess(mockObjectReturnValueName, literalProperty.name as ts.StringLiteral); +// return AssignVariableTo(propertyAccess, literalProperty.initializer); +// } function AssignMockMarkerPropertyTo(identifier: ts.Identifier): ts.ExpressionStatement { const mockMarkerProperty: Property = GetMockMarkerProperty(); diff --git a/src/transformer/descriptor/mock/mockProperties.ts b/src/transformer/descriptor/mock/mockProperties.ts index 0fa48afe5..bc33e757f 100644 --- a/src/transformer/descriptor/mock/mockProperties.ts +++ b/src/transformer/descriptor/mock/mockProperties.ts @@ -1,7 +1,8 @@ import * as ts from 'typescript'; import { Scope } from '../../scope/scope'; -import { GetDescriptor } from '../descriptor'; import { IsTypescriptType } from '../tsLibs/typecriptLibs'; +import { MethodSignature } from '../method/method'; +import { ReshapeCallableDeclaration } from '../method/methodDeclaration'; import { GetMockCall } from './mockCall'; import { GetMockPropertiesAssignments, PropertyAssignments } from './mockPropertiesAssignments'; import { PropertyLike } from './propertyLike'; @@ -34,6 +35,7 @@ export function GetMockPropertiesFromDeclarations(list: ReadonlyArray ReshapeCallableDeclaration(declaration, scope)); + + return GetMockCall(accessorDeclaration, methodSignatures); } From 21ec311d4c13c5a3f3dd25cc43d1f2bb7de16e86 Mon Sep 17 00:00:00 2001 From: Martin Jesper Low Madsen Date: Mon, 4 May 2020 20:54:39 +0200 Subject: [PATCH 05/26] enhancement(test): Extend method signature overload test to conditional typing --- test/transformer/descriptor/methods/methods.test.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/test/transformer/descriptor/methods/methods.test.ts b/test/transformer/descriptor/methods/methods.test.ts index da65fe2cc..aa23385cd 100644 --- a/test/transformer/descriptor/methods/methods.test.ts +++ b/test/transformer/descriptor/methods/methods.test.ts @@ -118,10 +118,12 @@ describe('for methods', () => { expect(properties.b).toBe(''); }); - it('should use the first overload if any', () => { + it('should use the overload as requested by input', () => { const properties: InterfaceWithConstructSignatureOverload = createMock(); // eslint-disable-next-line - expect((new properties() as any).a).toBe(0); + expect((new properties(0)).a).toBe(0); + expect((new properties('')).b).toBe(''); + expect((new properties()).c).toBeInstanceOf(Date); }); }); From 00d256f051f253d2d96387abcf68d3815d8f6191 Mon Sep 17 00:00:00 2001 From: Martin Jesper Low Madsen Date: Mon, 4 May 2020 21:15:45 +0200 Subject: [PATCH 06/26] fix(transformer): Revert to terminating early if a function declares its return type --- .../descriptor/method/functionReturnType.ts | 26 ++++++++++++++++--- 1 file changed, 23 insertions(+), 3 deletions(-) diff --git a/src/transformer/descriptor/method/functionReturnType.ts b/src/transformer/descriptor/method/functionReturnType.ts index 4a0d34966..608f72628 100644 --- a/src/transformer/descriptor/method/functionReturnType.ts +++ b/src/transformer/descriptor/method/functionReturnType.ts @@ -1,16 +1,36 @@ import * as ts from 'typescript'; import { GetReturnNodeFromBody } from './bodyReturnType'; +function isFunctionLikeDeclaration(node: ts.SignatureDeclaration): node is ts.FunctionLikeDeclaration { + switch (true) { + case ts.isFunctionDeclaration(node): + case ts.isMethodDeclaration(node): + case ts.isGetAccessorDeclaration(node): + case ts.isSetAccessorDeclaration(node): + case ts.isConstructorDeclaration(node): + case ts.isFunctionExpression(node): + case ts.isArrowFunction(node): + return true; + } + + return false; +} + export function GetFunctionReturnType(node: ts.SignatureDeclaration): ts.Node { - if (ts.isArrowFunction(node) || ts.isFunctionExpression(node)) { + const declaredReturnType: ts.TypeNode | undefined = node.type; + if (declaredReturnType) { + return declaredReturnType; + } + + if (isFunctionLikeDeclaration(node)) { return GetReturnNodeFromBody(node); } - if (!node.type) { + if (!declaredReturnType) { throw new Error( `The transformer couldn't determine the type of ${node.getText()}. Did you declare its type?`, ); } - return node.type; + return declaredReturnType; } From fcf95defd7964c1b1a48f0e2c1524e173e6633a0 Mon Sep 17 00:00:00 2001 From: Martin Jesper Low Madsen Date: Fri, 8 May 2020 15:30:55 +0200 Subject: [PATCH 07/26] enhancement(transformer): Fallback to `instanceof Object' control flow for overloads with non literal inputs --- src/transformer/descriptor/method/method.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/transformer/descriptor/method/method.ts b/src/transformer/descriptor/method/method.ts index 1990dd248..f002f4379 100644 --- a/src/transformer/descriptor/method/method.ts +++ b/src/transformer/descriptor/method/method.ts @@ -1,6 +1,5 @@ import ts from 'typescript'; import { GetTsAutoMockOverloadOptions, TsAutoMockOverloadOptions } from '../../../options/overload'; -import { GetClassDeclarationDescriptor } from '../class/classDeclaration'; import { TypescriptCreator } from '../../helper/creator'; import { MockDefiner } from '../../mockDefiner/mockDefiner'; import { ModuleName } from '../../mockDefiner/modules/moduleName'; @@ -60,7 +59,8 @@ function CreateTypeEquality(signatureType: ts.TypeNode | undefined, primaryDecla signatureType ? ts.createStringLiteral(signatureType.getText()) : ts.createVoidZero(), ); } else { - return ts.createBinary(identifier, ts.SyntaxKind.InstanceOfKeyword, ts.createIdentifier(signatureType.getText())); + // FIXME: Support `instanceof Class`, falls back to Object for now. The fallback causes undefined behavior! + return ts.createBinary(identifier, ts.SyntaxKind.InstanceOfKeyword, ts.createIdentifier('Object')); } } From f0f1d13386201695273cd03ce739a8d3e0f46174 Mon Sep 17 00:00:00 2001 From: Martin Jesper Low Madsen Date: Fri, 8 May 2020 15:33:13 +0200 Subject: [PATCH 08/26] fix(transformer): Properly cover overloads where no signatures declare arguments --- src/transformer/descriptor/method/method.ts | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/src/transformer/descriptor/method/method.ts b/src/transformer/descriptor/method/method.ts index f002f4379..bc892b99b 100644 --- a/src/transformer/descriptor/method/method.ts +++ b/src/transformer/descriptor/method/method.ts @@ -88,7 +88,7 @@ function CreateUnionTypeOfEquality(signatureType: ts.TypeNode | undefined, prima } function ResolveParameterBranch(declarations: ts.ParameterDeclaration[], allDeclarations: ts.ParameterDeclaration[], returnValue: ts.Expression, elseBranch: ts.Statement): ts.Statement { - const [firstDeclaration, ...remainingDeclarations]: ts.ParameterDeclaration[] = declarations; + const [firstDeclaration, ...remainingDeclarations]: Array = declarations; const condition: ts.Expression = remainingDeclarations.reduce( (prevStatement: ts.Expression, declaration: ts.ParameterDeclaration, index: number) => @@ -96,7 +96,7 @@ function ResolveParameterBranch(declarations: ts.ParameterDeclaration[], allDecl prevStatement, CreateUnionTypeOfEquality(declaration.type, allDeclarations[index + 1]), ), - CreateUnionTypeOfEquality(firstDeclaration.type, allDeclarations[0]), + CreateUnionTypeOfEquality(firstDeclaration?.type, allDeclarations[0]), ); return ts.createIf(condition, ts.createReturn(returnValue), elseBranch); @@ -105,16 +105,17 @@ function ResolveParameterBranch(declarations: ts.ParameterDeclaration[], allDecl export function ResolveSignatureElseBranch(signatures: MethodSignature[], longestParameterList: ts.ParameterDeclaration[]): ts.Statement { const transformOverloadsOption: TsAutoMockOverloadOptions = GetTsAutoMockOverloadOptions(); - const [signature, ...remainingSignatures]: MethodSignature[] = signatures.filter((_: unknown, i: number) => transformOverloadsOption || i === 0); + const [signature, ...remainingSignatures]: MethodSignature[] = signatures.filter((_: unknown, notFirst: number) => transformOverloadsOption || !notFirst); - if (remainingSignatures.length) { - const elseBranch: ts.Statement = ResolveSignatureElseBranch(remainingSignatures, longestParameterList); - - const currentParameters: ts.ParameterDeclaration[] = signature.parameters || []; - return ResolveParameterBranch(currentParameters, longestParameterList, signature.returnValue, elseBranch); - } else { + const indistinctSignatures: boolean = signatures.every((sig: MethodSignature) => !sig.parameters?.length); + if (!remainingSignatures.length || indistinctSignatures) { return ts.createReturn(signature.returnValue); } + + const elseBranch: ts.Statement = ResolveSignatureElseBranch(remainingSignatures, longestParameterList); + + const currentParameters: ts.ParameterDeclaration[] = signature.parameters || []; + return ResolveParameterBranch(currentParameters, longestParameterList, signature.returnValue, elseBranch); } function CreateProviderGetMethod(): ts.PropertyAccessExpression { From 001b08789d7fe4ba83a5cb7cb64c5ae8bae43c0a Mon Sep 17 00:00:00 2001 From: Martin Jesper Low Madsen Date: Fri, 8 May 2020 15:34:47 +0200 Subject: [PATCH 09/26] enhancement(transformer): Implement conditional typing for direct mock calls of interfaces with call signatures --- src/transformer/descriptor/mock/mockCall.ts | 59 ++++++++++++--------- 1 file changed, 34 insertions(+), 25 deletions(-) diff --git a/src/transformer/descriptor/mock/mockCall.ts b/src/transformer/descriptor/mock/mockCall.ts index e7c590e4b..a369c4515 100644 --- a/src/transformer/descriptor/mock/mockCall.ts +++ b/src/transformer/descriptor/mock/mockCall.ts @@ -8,29 +8,38 @@ import { PropertyAssignments } from './mockPropertiesAssignments'; export function GetMockCall(properties: PropertyAssignments, signatures: MethodSignature[]): ts.CallExpression { const mockObjectReturnValueName: ts.Identifier = MockIdentifierObjectReturnValue; - if (signatures.length) { + const variableStatements: ts.VariableDeclaration[] = [ + TypescriptCreator.createVariableDeclaration(MockIdentifierInternalValues, ts.createObjectLiteral()), + ]; + const propertyAssignmentStatements: ts.ExpressionStatement[] = []; + + const isCallable: boolean = !!signatures.length; + if (isCallable) { // FIXME: It'd probably be wise to extract the name of the callable - // signature and fallback to `new` or smth if there is none. - return GetMethodDescriptor(ts.createStringLiteral('new'), signatures); + // signature and only fallback to `new` if there is none (or something + // shorter). + const callableEntry: ts.CallExpression = GetMethodDescriptor(ts.createStringLiteral('new'), signatures); + + variableStatements.push( + TypescriptCreator.createVariableDeclaration(mockObjectReturnValueName, callableEntry), + ); + + propertyAssignmentStatements.push( + ...properties.literals.map( + (literalProperty: ts.PropertyAssignment) => AssignLiteralPropertyTo(mockObjectReturnValueName, literalProperty) + ), + ); + } else { + variableStatements.push( + TypescriptCreator.createVariableDeclaration(mockObjectReturnValueName, ts.createObjectLiteral(properties.literals)), + ); } const statements: ts.Statement[] = [ - TypescriptCreator.createVariableStatement([ - TypescriptCreator.createVariableDeclaration(MockIdentifierInternalValues, ts.createObjectLiteral()), - TypescriptCreator.createVariableDeclaration(mockObjectReturnValueName, ts.createObjectLiteral(properties.literals)), - ]), + TypescriptCreator.createVariableStatement(variableStatements), + ...propertyAssignmentStatements, ]; - // if (signatures[0]) { - // let literalProperty: ts.PropertyAssignment; - // let index: number = 0; - - // // tslint:disable-next-line:no-conditional-assignment - // while ((literalProperty = properties.literals[index++])) { - // statements.push(AssignLiteralPropertyTo(mockObjectReturnValueName, literalProperty)); - // } - // } - if (properties.lazy.length) { const addPropertiesToUniqueVariable: ts.ExpressionStatement = AssignPropertiesTo(properties.lazy, mockObjectReturnValueName); statements.push(addPropertiesToUniqueVariable); @@ -47,15 +56,15 @@ export function GetMockCall(properties: PropertyAssignments, signatures: MethodS return ts.createCall(IFFEFunction, [], []); } -// function AssignVariableTo(variable: ts.Expression, expression: ts.Expression): ts.ExpressionStatement { -// const binaryExpression: ts.BinaryExpression = ts.createBinary(variable, ts.SyntaxKind.EqualsToken, expression); -// return ts.createExpressionStatement(binaryExpression); -// } +function AssignVariableTo(variable: ts.Expression, expression: ts.Expression): ts.ExpressionStatement { + const binaryExpression: ts.BinaryExpression = ts.createBinary(variable, ts.SyntaxKind.EqualsToken, expression); + return ts.createExpressionStatement(binaryExpression); +} -// function AssignLiteralPropertyTo(mockObjectReturnValueName: ts.Identifier, literalProperty: ts.PropertyAssignment): ts.ExpressionStatement { -// const propertyAccess: ts.ElementAccessExpression = ts.createElementAccess(mockObjectReturnValueName, literalProperty.name as ts.StringLiteral); -// return AssignVariableTo(propertyAccess, literalProperty.initializer); -// } +function AssignLiteralPropertyTo(mockObjectReturnValueName: ts.Identifier, literalProperty: ts.PropertyAssignment): ts.ExpressionStatement { + const propertyAccess: ts.ElementAccessExpression = ts.createElementAccess(mockObjectReturnValueName, literalProperty.name as ts.StringLiteral); + return AssignVariableTo(propertyAccess, literalProperty.initializer); +} function AssignMockMarkerPropertyTo(identifier: ts.Identifier): ts.ExpressionStatement { const mockMarkerProperty: Property = GetMockMarkerProperty(); From 0f1da326118d0e240a5f13e4a62e06e77af01b0f Mon Sep 17 00:00:00 2001 From: Martin Jesper Low Madsen Date: Fri, 8 May 2020 15:37:40 +0200 Subject: [PATCH 10/26] chore(transformer): Move overload tests into its own file nested under method --- .../descriptor/methods/methods.test.ts | 30 ------- .../descriptor/methods/overloads.test.ts | 79 +++++++++++++++++++ .../descriptor/typeQuery/typeQuery.test.ts | 22 ------ 3 files changed, 79 insertions(+), 52 deletions(-) create mode 100644 test/transformer/descriptor/methods/overloads.test.ts diff --git a/test/transformer/descriptor/methods/methods.test.ts b/test/transformer/descriptor/methods/methods.test.ts index aa23385cd..2a7abcaae 100644 --- a/test/transformer/descriptor/methods/methods.test.ts +++ b/test/transformer/descriptor/methods/methods.test.ts @@ -58,22 +58,6 @@ describe('for methods', () => { }); }); - describe('for interface call signature with overload', () => { - interface InterfaceWithCallSignature { - (a: number): number; - (a: string): string; - b: string; - } - - it('should only consider the first signature declaration', () => { - const properties: InterfaceWithCallSignature = createMock(); - expect(properties(2)).toBe(0); - // @ts-ignore - expect(properties('2')).toBe(0); - expect(properties.b).toBe(''); - }); - }); - describe('for interface call signature with recursive', () => { interface InterfaceWithCallSignature { (a: number): InterfaceWithCallSignature; @@ -99,12 +83,6 @@ describe('for methods', () => { b: string; } - interface InterfaceWithConstructSignatureOverload { - new (a: number): { a: number }; - new (b: string): { b: string }; - new (): { c: Date }; - } - it('should set the constructor and properties', () => { const properties: InterfaceWithConstructSignature = createMock(); expect(new properties(2).a).toBe(0); @@ -117,14 +95,6 @@ describe('for methods', () => { expect(new properties(2).b).toBe(''); expect(properties.b).toBe(''); }); - - it('should use the overload as requested by input', () => { - const properties: InterfaceWithConstructSignatureOverload = createMock(); - // eslint-disable-next-line - expect((new properties(0)).a).toBe(0); - expect((new properties('')).b).toBe(''); - expect((new properties()).c).toBeInstanceOf(Date); - }); }); describe('for interface construct signature', () => { diff --git a/test/transformer/descriptor/methods/overloads.test.ts b/test/transformer/descriptor/methods/overloads.test.ts new file mode 100644 index 000000000..54dce2bb1 --- /dev/null +++ b/test/transformer/descriptor/methods/overloads.test.ts @@ -0,0 +1,79 @@ +import { createMock } from 'ts-auto-mock'; + +import { + exportedDeclaredOverloadedFunction, + // ExportedDeclaredClass, +} from '../utils/typeQuery/typeQueryUtils'; + +describe('for overloads', () => { + + describe('for type query', () => { + + it('should assign the correct function mock for literal inputs', () => { + const functionMock: typeof exportedDeclaredOverloadedFunction = createMock(); + + // eslint-disable-next-line + const expectations = [ + ['', 0, false], + [false, '', 0], + [0, false, ''], + [false, false, false], + [''], + [false], + [0], + ]; + + for (const args of expectations) { + // eslint-disable-next-line + const [first] = args; + + // @ts-ignore + expect(functionMock(...args)).toEqual(first); + } + }); + + // FIXME: Support more than just literals + // it('should assign the correct function mock for mockable inputs', () => { + // const classMock: typeof ExportedDeclaredClass = createMock(); + + // const functionMock: typeof exportedDeclaredOverloadedFunction = createMock(); + + // expect(functionMock(new classMock())).toBeInstanceOf(ExportedDeclaredClass); + // }); + + }); + + describe('for interface', () => { + describe('for construct signature', () => { + interface InterfaceWithConstructSignatureOverload { + new (a: number): { a: number }; + new (b: string): { b: string }; + new (): { c: Date }; + } + + it('should use the correct signature as requested by input', () => { + const properties: InterfaceWithConstructSignatureOverload = createMock(); + expect((new properties(0)).a).toBe(0); + expect((new properties('')).b).toBe(''); + // FIXME: Enable after Date PR + // expect((new properties()).c).toBeInstanceOf(Date); + }); + }); + }); + + describe('call signature', () => { + interface InterfaceWithCallSignature { + (a: number): number; + (a: string): string; + b: string; + } + + it('should consider all signature declarations and properties', () => { + const properties: InterfaceWithCallSignature = createMock(); + expect(properties(2)).toBe(0); + expect(properties('2')).toBe(''); + expect(properties.b).toBe(''); + }); + }); + +}); diff --git a/test/transformer/descriptor/typeQuery/typeQuery.test.ts b/test/transformer/descriptor/typeQuery/typeQuery.test.ts index 92a557e2d..2a61db5e5 100644 --- a/test/transformer/descriptor/typeQuery/typeQuery.test.ts +++ b/test/transformer/descriptor/typeQuery/typeQuery.test.ts @@ -6,7 +6,6 @@ import { ExportedClass, ExportedDeclaredClass, exportedDeclaredFunction, - exportedDeclaredOverloadedFunction, ExportedEnum, exportedFunction, WrapExportedClass, @@ -42,27 +41,6 @@ describe('typeQuery', () => { expect(functionMock()).toEqual(''); }); - it('should assign the correct function mock for an imported and overloaded function declaration', () => { - const functionMock: typeof exportedDeclaredOverloadedFunction = createMock(); - - // eslint-disable-next-line - const expectations = [ - { args: ['', 0, false], returnValue: '' }, - { args: [false, '', 0], returnValue: false }, - { args: [0, false, ''], returnValue: 0 }, - { args: [false, false, false], returnValue: false }, - { args: [''], returnValue: '' }, - { args: [false], returnValue: false }, - { args: [0], returnValue: 0 }, - ]; - - for (const { args, returnValue } of expectations) { - // eslint-disable-next-line - const [first, second, third] = args; - expect(functionMock(first, second, third)).toEqual(returnValue); - } - }); - it('should assign the function mock for an imported function declaration with body', () => { const functionMock: typeof exportedFunction = createMock(); From 9921c9c5e51d272498a0d88281531da83bac4506 Mon Sep 17 00:00:00 2001 From: Martin Jesper Low Madsen Date: Sat, 9 May 2020 13:45:58 +0200 Subject: [PATCH 11/26] chore(transformer): Merge isLiteralRuntimeTypeNode and IsLiteralOrPrimitive --- src/transformer/descriptor/helper/helper.ts | 38 +++++++++------------ src/transformer/descriptor/method/method.ts | 2 +- 2 files changed, 17 insertions(+), 23 deletions(-) diff --git a/src/transformer/descriptor/helper/helper.ts b/src/transformer/descriptor/helper/helper.ts index e79fa7793..f5dd14465 100644 --- a/src/transformer/descriptor/helper/helper.ts +++ b/src/transformer/descriptor/helper/helper.ts @@ -6,12 +6,22 @@ type Declaration = ts.InterfaceDeclaration | ts.ClassDeclaration | ts.TypeAliasD type ImportDeclaration = ts.ImportEqualsDeclaration | ts.ImportOrExportSpecifier | ts.ImportClause; export namespace TypescriptHelper { - export function IsLiteralOrPrimitive(typeNode: ts.Node): boolean { - return ts.isLiteralTypeNode(typeNode) || - typeNode.kind === ts.SyntaxKind.StringKeyword || - typeNode.kind === ts.SyntaxKind.BooleanKeyword || - typeNode.kind === ts.SyntaxKind.NumberKeyword || - typeNode.kind === ts.SyntaxKind.ArrayType; + export interface PrimitiveTypeNode extends ts.TypeNode { + kind: ts.SyntaxKind.LiteralType | ts.SyntaxKind.NumberKeyword | ts.SyntaxKind.ObjectKeyword | ts.SyntaxKind.BooleanKeyword | ts.SyntaxKind.StringKeyword | ts.SyntaxKind.ArrayType; + } + + export function IsLiteralOrPrimitive(typeNode: ts.Node): typeNode is PrimitiveTypeNode { + switch (typeNode.kind) { + case ts.SyntaxKind.LiteralType: + case ts.SyntaxKind.NumberKeyword: + case ts.SyntaxKind.ObjectKeyword: + case ts.SyntaxKind.BooleanKeyword: + case ts.SyntaxKind.StringKeyword: + case ts.SyntaxKind.ArrayType: + return true; + } + + return false; } export function GetDeclarationFromNode(node: ts.Node): ts.Declaration { @@ -119,22 +129,6 @@ export namespace TypescriptHelper { return !!((symbol.flags & ts.SymbolFlags.Alias) || (symbol.flags & ts.SymbolFlags.AliasExcludes)); } - export interface RuntimeTypeNode extends ts.TypeNode { - kind: ts.SyntaxKind.NumberKeyword | ts.SyntaxKind.ObjectKeyword | ts.SyntaxKind.BooleanKeyword | ts.SyntaxKind.StringKeyword | ts.SyntaxKind.UndefinedKeyword; - } - - export function isLiteralRuntimeTypeNode(typeNode: ts.TypeNode): typeNode is RuntimeTypeNode { - switch (typeNode.kind) { - case ts.SyntaxKind.NumberKeyword: - case ts.SyntaxKind.ObjectKeyword: - case ts.SyntaxKind.BooleanKeyword: - case ts.SyntaxKind.StringKeyword: - return true; - } - - return false; - } - function isImportExportDeclaration(declaration: ts.Declaration): declaration is ImportDeclaration { return ts.isImportEqualsDeclaration(declaration) || ts.isImportOrExportSpecifier(declaration) || ts.isImportClause(declaration); } diff --git a/src/transformer/descriptor/method/method.ts b/src/transformer/descriptor/method/method.ts index bc892b99b..d237ba4a2 100644 --- a/src/transformer/descriptor/method/method.ts +++ b/src/transformer/descriptor/method/method.ts @@ -53,7 +53,7 @@ function CreateTypeEquality(signatureType: ts.TypeNode | undefined, primaryDecla ); } - if (TypescriptHelper.isLiteralRuntimeTypeNode(signatureType)) { + if (TypescriptHelper.IsLiteralOrPrimitive(signatureType)) { return ts.createStrictEquality( ts.createTypeOf(identifier), signatureType ? ts.createStringLiteral(signatureType.getText()) : ts.createVoidZero(), From 36deaa650905dc0957787d6b82e0353bb32d3da9 Mon Sep 17 00:00:00 2001 From: Martin Jesper Low Madsen Date: Sat, 9 May 2020 14:07:41 +0200 Subject: [PATCH 12/26] enhancement(transformer): Support non-primitive function arguments --- src/transformer/descriptor/method/method.ts | 126 ++++++++++++++---- .../descriptor/methods/overloads.test.ts | 13 +- .../utils/typeQuery/typeQueryUtils.ts | 7 +- 3 files changed, 108 insertions(+), 38 deletions(-) diff --git a/src/transformer/descriptor/method/method.ts b/src/transformer/descriptor/method/method.ts index d237ba4a2..31151ce26 100644 --- a/src/transformer/descriptor/method/method.ts +++ b/src/transformer/descriptor/method/method.ts @@ -16,21 +16,56 @@ export function GetMethodDescriptor(propertyName: ts.PropertyName, methodSignatu const propertyNameString: string = TypescriptHelper.GetStringPropertyName(propertyName); const propertyNameStringLiteral: ts.StringLiteral = ts.createStringLiteral(propertyNameString); - const [signatureWithMostParameters]: MethodSignature[] = [...methodSignatures].sort( - ( - { parameters: leftParameters = [] }: MethodSignature, - { parameters: rightParameters = [] }: MethodSignature, - ) => rightParameters.length - leftParameters.length, + const signatureWithMostParameters: MethodSignature = methodSignatures.reduce( + (acc: MethodSignature, signature: MethodSignature) => { + const longestParametersLength: number = (acc.parameters || []).length; + const parametersLength: number = (signature.parameters || []).length; + + return parametersLength < longestParametersLength ? acc : signature; + }, ); const longestParameterList: ts.ParameterDeclaration[] = signatureWithMostParameters.parameters || []; - const block: ts.Block = ts.createBlock( - [ - ResolveSignatureElseBranch(methodSignatures, longestParameterList), - ], - true, - ); + const declarationVariableMap: Map = new Map(); + + let i: number = 0; + const declarationVariables: ts.VariableDeclaration[] = methodSignatures.reduce( + (variables: ts.VariableDeclaration[], { parameters = [] }: MethodSignature) => { + for (const parameter of parameters) { + if (declarationVariableMap.has(parameter)) { + continue; + } + + const declarationType: ts.TypeNode | undefined = parameter.type; + if (declarationType && ts.isTypeReferenceNode(declarationType)) { + const variableIdentifier: ts.Identifier = ts.createIdentifier(`__${i++}`); + + declarationVariableMap.set(parameter, variableIdentifier); + + const declaration: ts.Declaration = TypescriptHelper.GetDeclarationFromNode(declarationType.typeName); + + variables.push( + TypescriptCreator.createVariableDeclaration( + variableIdentifier, + ts.createStringLiteral(MockDefiner.instance.getDeclarationKeyMap(declaration)), + ), + ); + } + } + + return variables; + }, [] as ts.VariableDeclaration[]); + + const statements: ts.Statement[] = []; + + if (declarationVariables.length) { + statements.push(TypescriptCreator.createVariableStatement(declarationVariables)); + } + + statements.push(ResolveSignatureElseBranch(declarationVariableMap, methodSignatures, longestParameterList)); + + const block: ts.Block = ts.createBlock(statements, true); const propertyValueFunction: ts.ArrowFunction = TypescriptCreator.createArrowFunction( block, @@ -40,7 +75,7 @@ export function GetMethodDescriptor(propertyName: ts.PropertyName, methodSignatu return TypescriptCreator.createCall(providerGetMethod, [propertyNameStringLiteral, propertyValueFunction]); } -function CreateTypeEquality(signatureType: ts.TypeNode | undefined, primaryDeclaration: ts.ParameterDeclaration): ts.Expression { +function CreateTypeEquality(signatureType: ts.Identifier | ts.TypeNode | undefined, primaryDeclaration: ts.ParameterDeclaration): ts.Expression { const identifier: ts.Identifier = ts.createIdentifier(primaryDeclaration.name.getText()); if (!signatureType) { @@ -58,24 +93,30 @@ function CreateTypeEquality(signatureType: ts.TypeNode | undefined, primaryDecla ts.createTypeOf(identifier), signatureType ? ts.createStringLiteral(signatureType.getText()) : ts.createVoidZero(), ); - } else { - // FIXME: Support `instanceof Class`, falls back to Object for now. The fallback causes undefined behavior! - return ts.createBinary(identifier, ts.SyntaxKind.InstanceOfKeyword, ts.createIdentifier('Object')); } + + if (ts.isIdentifier(signatureType)) { + return ts.createStrictEquality( + ts.createPropertyAccess(identifier, '__factory'), + signatureType, + ); + } + + return ts.createBinary(identifier, ts.SyntaxKind.InstanceOfKeyword, ts.createIdentifier('Object')); } -function CreateUnionTypeOfEquality(signatureType: ts.TypeNode | undefined, primaryDeclaration: ts.ParameterDeclaration): ts.Expression { - const typeNodes: ts.TypeNode[] = []; +function CreateUnionTypeOfEquality(signatureType: ts.Identifier | ts.TypeNode | undefined, primaryDeclaration: ts.ParameterDeclaration): ts.Expression { + const typeNodesAndVariableReferences: Array = []; if (signatureType) { - if (ts.isUnionTypeNode(signatureType)) { - typeNodes.push(...signatureType.types); + if (ts.isTypeNode(signatureType) && ts.isUnionTypeNode(signatureType)) { + typeNodesAndVariableReferences.push(...signatureType.types); } else { - typeNodes.push(signatureType); + typeNodesAndVariableReferences.push(signatureType); } } - const [firstType, ...remainingTypes]: ts.TypeNode[] = typeNodes; + const [firstType, ...remainingTypes]: Array = typeNodesAndVariableReferences; return remainingTypes.reduce( (prevStatement: ts.Expression, typeNode: ts.TypeNode) => @@ -87,22 +128,53 @@ function CreateUnionTypeOfEquality(signatureType: ts.TypeNode | undefined, prima ); } -function ResolveParameterBranch(declarations: ts.ParameterDeclaration[], allDeclarations: ts.ParameterDeclaration[], returnValue: ts.Expression, elseBranch: ts.Statement): ts.Statement { +function ResolveParameterBranch( + declarationVariableMap: Map, + declarations: ts.ParameterDeclaration[], + allDeclarations: ts.ParameterDeclaration[], + returnValue: ts.Expression, + elseBranch: ts.Statement, +): ts.Statement { const [firstDeclaration, ...remainingDeclarations]: Array = declarations; + const variableReferenceOrType: (declaration: ts.ParameterDeclaration) => ts.Identifier | ts.TypeNode | undefined = + (declaration: ts.ParameterDeclaration) => { + if (declarationVariableMap.has(declaration)) { + return declarationVariableMap.get(declaration); + } else { + return declaration.type; + } + }; + + // TODO: These conditions quickly grow in size, but it should be possible to + // squeeze things together and optimize it with something like: + // + // const typeOf = function (left, right) { return typeof left === right; } + // const evaluate = (function(left, right) { return this._ = this._ || typeOf(left, right); }).bind({}) + // + // if (evaluate(firstArg, 'boolean') && evaluate(secondArg, 'number') && ...) { + // ... + // } + // + // `this._' acts as a cache, since the control flow may evaluate the same + // conditions multiple times. const condition: ts.Expression = remainingDeclarations.reduce( (prevStatement: ts.Expression, declaration: ts.ParameterDeclaration, index: number) => ts.createLogicalAnd( prevStatement, - CreateUnionTypeOfEquality(declaration.type, allDeclarations[index + 1]), + CreateUnionTypeOfEquality(variableReferenceOrType(declaration), allDeclarations[index + 1]), ), - CreateUnionTypeOfEquality(firstDeclaration?.type, allDeclarations[0]), + CreateUnionTypeOfEquality(variableReferenceOrType(firstDeclaration), allDeclarations[0]), ); return ts.createIf(condition, ts.createReturn(returnValue), elseBranch); } -export function ResolveSignatureElseBranch(signatures: MethodSignature[], longestParameterList: ts.ParameterDeclaration[]): ts.Statement { +export function ResolveSignatureElseBranch( + declarationVariableMap: Map, + signatures: MethodSignature[], + longestParameterList: ts.ParameterDeclaration[], +): ts.Statement { const transformOverloadsOption: TsAutoMockOverloadOptions = GetTsAutoMockOverloadOptions(); const [signature, ...remainingSignatures]: MethodSignature[] = signatures.filter((_: unknown, notFirst: number) => transformOverloadsOption || !notFirst); @@ -112,10 +184,10 @@ export function ResolveSignatureElseBranch(signatures: MethodSignature[], longes return ts.createReturn(signature.returnValue); } - const elseBranch: ts.Statement = ResolveSignatureElseBranch(remainingSignatures, longestParameterList); + const elseBranch: ts.Statement = ResolveSignatureElseBranch(declarationVariableMap, remainingSignatures, longestParameterList); const currentParameters: ts.ParameterDeclaration[] = signature.parameters || []; - return ResolveParameterBranch(currentParameters, longestParameterList, signature.returnValue, elseBranch); + return ResolveParameterBranch(declarationVariableMap, currentParameters, longestParameterList, signature.returnValue, elseBranch); } function CreateProviderGetMethod(): ts.PropertyAccessExpression { diff --git a/test/transformer/descriptor/methods/overloads.test.ts b/test/transformer/descriptor/methods/overloads.test.ts index 54dce2bb1..247bb101e 100644 --- a/test/transformer/descriptor/methods/overloads.test.ts +++ b/test/transformer/descriptor/methods/overloads.test.ts @@ -2,7 +2,7 @@ import { createMock } from 'ts-auto-mock'; import { exportedDeclaredOverloadedFunction, - // ExportedDeclaredClass, + ExportedDeclaredClass, } from '../utils/typeQuery/typeQueryUtils'; describe('for overloads', () => { @@ -32,14 +32,13 @@ describe('for overloads', () => { } }); - // FIXME: Support more than just literals - // it('should assign the correct function mock for mockable inputs', () => { - // const classMock: typeof ExportedDeclaredClass = createMock(); + it('should assign the correct function mock for mockable inputs', () => { + const classMock: typeof ExportedDeclaredClass = createMock(); - // const functionMock: typeof exportedDeclaredOverloadedFunction = createMock(); + const functionMock: typeof exportedDeclaredOverloadedFunction = createMock(); - // expect(functionMock(new classMock())).toBeInstanceOf(ExportedDeclaredClass); - // }); + expect(functionMock(new classMock()).prop).toBe(0); + }); }); diff --git a/test/transformer/descriptor/utils/typeQuery/typeQueryUtils.ts b/test/transformer/descriptor/utils/typeQuery/typeQueryUtils.ts index a170a0f7f..939eb476d 100644 --- a/test/transformer/descriptor/utils/typeQuery/typeQueryUtils.ts +++ b/test/transformer/descriptor/utils/typeQuery/typeQueryUtils.ts @@ -27,11 +27,10 @@ export declare function exportedDeclaredOverloadedFunction(a: number, b: string, export declare function exportedDeclaredOverloadedFunction(a: number, b: boolean, c: number): number; export declare function exportedDeclaredOverloadedFunction(a: boolean, b: number, c: boolean): boolean; export declare function exportedDeclaredOverloadedFunction(a: boolean, b: string, c: boolean): boolean; + export declare function exportedDeclaredOverloadedFunction(a: string | number | boolean, b: string | number | boolean, c: string | number | boolean): string | number | boolean; -// TODO: ExportedClass may need to be mocked and it is not imported as of this -// writing. The transformation does take `a instanceof ExportedClass` into -// consideration though. -// export declare function exportedDeclaredOverloadedFunction(a: ExportedClass): ExportedClass; + +export declare function exportedDeclaredOverloadedFunction(a: ExportedDeclaredClass): ExportedClass; export declare function exportedDeclaredOverloadedFunction(a: boolean): boolean; export declare function exportedDeclaredOverloadedFunction(a: number): number; export declare function exportedDeclaredOverloadedFunction(a: string): string; From d33b16450371f94b8c32d3bbecfa60faf951706b Mon Sep 17 00:00:00 2001 From: Martin Jesper Low Madsen Date: Sat, 9 May 2020 14:08:24 +0200 Subject: [PATCH 13/26] chore(transformer): Narrow signatures of mock factory call routines --- src/transformer/mockFactoryCall/mockFactoryCall.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/transformer/mockFactoryCall/mockFactoryCall.ts b/src/transformer/mockFactoryCall/mockFactoryCall.ts index 901add8cd..674c1b7d4 100644 --- a/src/transformer/mockFactoryCall/mockFactoryCall.ts +++ b/src/transformer/mockFactoryCall/mockFactoryCall.ts @@ -11,13 +11,13 @@ import { MockIdentifierGenericParameter } from '../mockIdentifier/mockIdentifier import { Scope } from '../scope/scope'; import { TypescriptCreator } from '../helper/creator'; -export function GetMockFactoryCall(typeReferenceNode: ts.TypeReferenceNode, scope: Scope): ts.Expression { +export function GetMockFactoryCall(typeReferenceNode: ts.TypeReferenceNode, scope: Scope): ts.CallExpression { const declaration: ts.Declaration = TypescriptHelper.GetDeclarationFromNode(typeReferenceNode.typeName); return getDeclarationMockFactoryCall(declaration, typeReferenceNode, scope); } -export function CreateMockFactory(typeReferenceNode: ts.TypeReferenceNode, scope: Scope): ts.Expression { +export function CreateMockFactory(typeReferenceNode: ts.TypeReferenceNode, scope: Scope): ts.CallExpression { const declaration: ts.Declaration = TypescriptHelper.GetDeclarationFromNode(typeReferenceNode.typeName); MockDefiner.instance.createMockFactory(declaration); @@ -74,7 +74,7 @@ export function GetMockFactoryCallForThis(mockKey: string): ts.Expression { ); } -function getDeclarationMockFactoryCall(declaration: ts.Declaration, typeReferenceNode: ts.TypeReferenceNode, scope: Scope): ts.Expression { +function getDeclarationMockFactoryCall(declaration: ts.Declaration, typeReferenceNode: ts.TypeReferenceNode, scope: Scope): ts.CallExpression { const declarationKey: string | undefined = MockDefiner.instance.getDeclarationKeyMap(declaration); if (!declarationKey) { From c724beae5803ac24080c2dbe57f4a74b358de32b Mon Sep 17 00:00:00 2001 From: Martin Jesper Low Madsen Date: Sat, 16 May 2020 15:46:11 +0200 Subject: [PATCH 14/26] enhancement(transformer): Refactor GetMethodDescriptor implementation and utilize TypeScript's MethodSignature type --- .../descriptor/method/functionAssignment.ts | 18 ++- .../descriptor/method/functionType.ts | 15 +- src/transformer/descriptor/method/method.ts | 151 ++---------------- .../descriptor/method/methodDeclaration.ts | 30 ++-- .../descriptor/method/methodSignature.ts | 20 +-- src/transformer/descriptor/mock/mockCall.ts | 9 +- .../descriptor/mock/mockProperties.ts | 12 +- 7 files changed, 82 insertions(+), 173 deletions(-) diff --git a/src/transformer/descriptor/method/functionAssignment.ts b/src/transformer/descriptor/method/functionAssignment.ts index bd2559306..a3717d8ce 100644 --- a/src/transformer/descriptor/method/functionAssignment.ts +++ b/src/transformer/descriptor/method/functionAssignment.ts @@ -1,14 +1,26 @@ import * as ts from 'typescript'; import { Scope } from '../../scope/scope'; +import { TypescriptCreator } from '../../helper/creator'; import { PropertySignatureCache } from '../property/cache'; -import { GetReturnTypeFromBodyDescriptor } from './bodyReturnType'; +import { GetReturnNodeFromBody } from './bodyReturnType'; import { GetMethodDescriptor } from './method'; type functionAssignment = ts.ArrowFunction | ts.FunctionExpression; export function GetFunctionAssignmentDescriptor(node: functionAssignment, scope: Scope): ts.Expression { const property: ts.PropertyName = PropertySignatureCache.instance.get(); - const returnValue: ts.Expression = GetReturnTypeFromBodyDescriptor(node, scope); + const returnValue: ts.Expression = GetReturnNodeFromBody(node); - return GetMethodDescriptor(property, [{ returnValue }]); + const returnType: ts.TypeNode = ts.createLiteralTypeNode(returnValue as ts.LiteralExpression); + + return GetMethodDescriptor( + property, + [ + TypescriptCreator.createMethodSignature( + undefined, + returnType, + ), + ], + scope, + ); } diff --git a/src/transformer/descriptor/method/functionType.ts b/src/transformer/descriptor/method/functionType.ts index 1a3e4d669..b2996dc20 100644 --- a/src/transformer/descriptor/method/functionType.ts +++ b/src/transformer/descriptor/method/functionType.ts @@ -1,6 +1,6 @@ import * as ts from 'typescript'; import { Scope } from '../../scope/scope'; -import { GetDescriptor } from '../descriptor'; +import { TypescriptCreator } from '../../helper/creator'; import { PropertySignatureCache } from '../property/cache'; import { GetMethodDescriptor } from './method'; @@ -11,7 +11,14 @@ export function GetFunctionTypeDescriptor(node: ts.FunctionTypeNode | ts.CallSig throw new Error(`No type was declared for ${node.getText()}.`); } - const returnValue: ts.Expression = GetDescriptor(node.type, scope); - - return GetMethodDescriptor(property, [{ returnValue }]); + return GetMethodDescriptor( + property, + [ + TypescriptCreator.createMethodSignature( + node.parameters.map((p: ts.ParameterDeclaration) => p.type), + node.type, + ), + ], + scope, + ); } diff --git a/src/transformer/descriptor/method/method.ts b/src/transformer/descriptor/method/method.ts index 31151ce26..006582ae0 100644 --- a/src/transformer/descriptor/method/method.ts +++ b/src/transformer/descriptor/method/method.ts @@ -1,21 +1,14 @@ import ts from 'typescript'; -import { GetTsAutoMockOverloadOptions, TsAutoMockOverloadOptions } from '../../../options/overload'; -import { TypescriptCreator } from '../../helper/creator'; +import { MethodSignature, TypescriptCreator } from '../../helper/creator'; import { MockDefiner } from '../../mockDefiner/mockDefiner'; import { ModuleName } from '../../mockDefiner/modules/moduleName'; +import { Scope } from '../../scope/scope'; +import { ResolveSignatureElseBranch } from '../helper/branching'; import { TypescriptHelper } from '../helper/helper'; -export interface MethodSignature { - parameters?: ts.ParameterDeclaration[]; - returnValue: ts.Expression; -} - -export function GetMethodDescriptor(propertyName: ts.PropertyName, methodSignatures: MethodSignature[]): ts.CallExpression { +export function GetMethodDescriptor(_propertyName: ts.PropertyName, methodSignatures: MethodSignature[], scope: Scope): ts.CallExpression { const providerGetMethod: ts.PropertyAccessExpression = CreateProviderGetMethod(); - const propertyNameString: string = TypescriptHelper.GetStringPropertyName(propertyName); - const propertyNameStringLiteral: ts.StringLiteral = ts.createStringLiteral(propertyNameString); - const signatureWithMostParameters: MethodSignature = methodSignatures.reduce( (acc: MethodSignature, signature: MethodSignature) => { const longestParametersLength: number = (acc.parameters || []).length; @@ -25,23 +18,21 @@ export function GetMethodDescriptor(propertyName: ts.PropertyName, methodSignatu }, ); - const longestParameterList: ts.ParameterDeclaration[] = signatureWithMostParameters.parameters || []; - - const declarationVariableMap: Map = new Map(); + const declarationVariableMap: Map = new Map(); let i: number = 0; const declarationVariables: ts.VariableDeclaration[] = methodSignatures.reduce( - (variables: ts.VariableDeclaration[], { parameters = [] }: MethodSignature) => { + (variables: ts.VariableDeclaration[], { parameters }: MethodSignature) => { for (const parameter of parameters) { - if (declarationVariableMap.has(parameter)) { + if (declarationVariableMap.has(parameter.type)) { continue; } const declarationType: ts.TypeNode | undefined = parameter.type; if (declarationType && ts.isTypeReferenceNode(declarationType)) { - const variableIdentifier: ts.Identifier = ts.createIdentifier(`__${i++}`); + const variableIdentifier: ts.Identifier = ts.createIdentifier(`___${i++}`); - declarationVariableMap.set(parameter, variableIdentifier); + declarationVariableMap.set(parameter.type, variableIdentifier); const declaration: ts.Declaration = TypescriptHelper.GetDeclarationFromNode(declarationType.typeName); @@ -63,131 +54,20 @@ export function GetMethodDescriptor(propertyName: ts.PropertyName, methodSignatu statements.push(TypescriptCreator.createVariableStatement(declarationVariables)); } - statements.push(ResolveSignatureElseBranch(declarationVariableMap, methodSignatures, longestParameterList)); + statements.push(ResolveSignatureElseBranch(declarationVariableMap, methodSignatures, signatureWithMostParameters, scope)); const block: ts.Block = ts.createBlock(statements, true); const propertyValueFunction: ts.ArrowFunction = TypescriptCreator.createArrowFunction( block, - longestParameterList, - ); - - return TypescriptCreator.createCall(providerGetMethod, [propertyNameStringLiteral, propertyValueFunction]); -} - -function CreateTypeEquality(signatureType: ts.Identifier | ts.TypeNode | undefined, primaryDeclaration: ts.ParameterDeclaration): ts.Expression { - const identifier: ts.Identifier = ts.createIdentifier(primaryDeclaration.name.getText()); - - if (!signatureType) { - return ts.createPrefix( - ts.SyntaxKind.ExclamationToken, - ts.createPrefix( - ts.SyntaxKind.ExclamationToken, - identifier, - ), - ); - } - - if (TypescriptHelper.IsLiteralOrPrimitive(signatureType)) { - return ts.createStrictEquality( - ts.createTypeOf(identifier), - signatureType ? ts.createStringLiteral(signatureType.getText()) : ts.createVoidZero(), - ); - } - - if (ts.isIdentifier(signatureType)) { - return ts.createStrictEquality( - ts.createPropertyAccess(identifier, '__factory'), - signatureType, - ); - } - - return ts.createBinary(identifier, ts.SyntaxKind.InstanceOfKeyword, ts.createIdentifier('Object')); -} - -function CreateUnionTypeOfEquality(signatureType: ts.Identifier | ts.TypeNode | undefined, primaryDeclaration: ts.ParameterDeclaration): ts.Expression { - const typeNodesAndVariableReferences: Array = []; - - if (signatureType) { - if (ts.isTypeNode(signatureType) && ts.isUnionTypeNode(signatureType)) { - typeNodesAndVariableReferences.push(...signatureType.types); - } else { - typeNodesAndVariableReferences.push(signatureType); - } - } - - const [firstType, ...remainingTypes]: Array = typeNodesAndVariableReferences; - - return remainingTypes.reduce( - (prevStatement: ts.Expression, typeNode: ts.TypeNode) => - ts.createLogicalOr( - prevStatement, - CreateTypeEquality(typeNode, primaryDeclaration), - ), - CreateTypeEquality(firstType, primaryDeclaration), + signatureWithMostParameters.parameters, ); -} -function ResolveParameterBranch( - declarationVariableMap: Map, - declarations: ts.ParameterDeclaration[], - allDeclarations: ts.ParameterDeclaration[], - returnValue: ts.Expression, - elseBranch: ts.Statement, -): ts.Statement { - const [firstDeclaration, ...remainingDeclarations]: Array = declarations; - - const variableReferenceOrType: (declaration: ts.ParameterDeclaration) => ts.Identifier | ts.TypeNode | undefined = - (declaration: ts.ParameterDeclaration) => { - if (declarationVariableMap.has(declaration)) { - return declarationVariableMap.get(declaration); - } else { - return declaration.type; - } - }; - - // TODO: These conditions quickly grow in size, but it should be possible to - // squeeze things together and optimize it with something like: - // - // const typeOf = function (left, right) { return typeof left === right; } - // const evaluate = (function(left, right) { return this._ = this._ || typeOf(left, right); }).bind({}) - // - // if (evaluate(firstArg, 'boolean') && evaluate(secondArg, 'number') && ...) { - // ... - // } - // - // `this._' acts as a cache, since the control flow may evaluate the same - // conditions multiple times. - const condition: ts.Expression = remainingDeclarations.reduce( - (prevStatement: ts.Expression, declaration: ts.ParameterDeclaration, index: number) => - ts.createLogicalAnd( - prevStatement, - CreateUnionTypeOfEquality(variableReferenceOrType(declaration), allDeclarations[index + 1]), - ), - CreateUnionTypeOfEquality(variableReferenceOrType(firstDeclaration), allDeclarations[0]), + const propName: ts.StringLiteral = ts.createStringLiteral( + TypescriptCreator.createSignatureHash(methodSignatures), ); - return ts.createIf(condition, ts.createReturn(returnValue), elseBranch); -} - -export function ResolveSignatureElseBranch( - declarationVariableMap: Map, - signatures: MethodSignature[], - longestParameterList: ts.ParameterDeclaration[], -): ts.Statement { - const transformOverloadsOption: TsAutoMockOverloadOptions = GetTsAutoMockOverloadOptions(); - - const [signature, ...remainingSignatures]: MethodSignature[] = signatures.filter((_: unknown, notFirst: number) => transformOverloadsOption || !notFirst); - - const indistinctSignatures: boolean = signatures.every((sig: MethodSignature) => !sig.parameters?.length); - if (!remainingSignatures.length || indistinctSignatures) { - return ts.createReturn(signature.returnValue); - } - - const elseBranch: ts.Statement = ResolveSignatureElseBranch(declarationVariableMap, remainingSignatures, longestParameterList); - - const currentParameters: ts.ParameterDeclaration[] = signature.parameters || []; - return ResolveParameterBranch(declarationVariableMap, currentParameters, longestParameterList, signature.returnValue, elseBranch); + return TypescriptCreator.createCall(providerGetMethod, [propName, propertyValueFunction]); } function CreateProviderGetMethod(): ts.PropertyAccessExpression { @@ -198,5 +78,6 @@ function CreateProviderGetMethod(): ts.PropertyAccessExpression { ts.createIdentifier('Provider'), ), ts.createIdentifier('instance')), - ts.createIdentifier('getMethod')); + ts.createIdentifier('getMethod'), + ); } diff --git a/src/transformer/descriptor/method/methodDeclaration.ts b/src/transformer/descriptor/method/methodDeclaration.ts index be03a0e34..a22f5530a 100644 --- a/src/transformer/descriptor/method/methodDeclaration.ts +++ b/src/transformer/descriptor/method/methodDeclaration.ts @@ -1,9 +1,9 @@ import * as ts from 'typescript'; +import { MethodSignature, TypescriptCreator } from '../../helper/creator'; import { Scope } from '../../scope/scope'; import { TypeChecker } from '../../typeChecker/typeChecker'; -import { GetDescriptor } from '../descriptor'; -import { GetFunctionReturnType } from './functionReturnType'; -import { GetMethodDescriptor, MethodSignature } from './method'; +import { GetReturnNodeFromBody } from './bodyReturnType'; +import { GetMethodDescriptor } from './method'; export function GetMethodDeclarationDescriptor(node: ts.MethodDeclaration | ts.FunctionDeclaration, scope: Scope): ts.Expression { const declarationType: ts.Type | undefined = TypeChecker().getTypeAtLocation(node); @@ -17,7 +17,18 @@ export function GetMethodDeclarationDescriptor(node: ts.MethodDeclaration | ts.F methodDeclarations.push(node); } - const methodSignatures: MethodSignature[] = methodDeclarations.map((declaration: ts.MethodDeclaration | ts.FunctionDeclaration) => ReshapeCallableDeclaration(declaration, scope)); + const methodSignatures: MethodSignature[] = methodDeclarations.map((signature: ts.MethodDeclaration | ts.FunctionDeclaration) => { + let signatureType: ts.TypeNode | undefined = signature.type; + + if (!signatureType) { + signatureType = ts.createLiteralTypeNode(GetReturnNodeFromBody(signature) as ts.LiteralExpression); + } + + return TypescriptCreator.createMethodSignature( + signature.parameters.map((p: ts.ParameterDeclaration) => p.type), + signatureType, + ); + }); if (!node.name) { throw new Error( @@ -25,14 +36,5 @@ export function GetMethodDeclarationDescriptor(node: ts.MethodDeclaration | ts.F ); } - return GetMethodDescriptor(node.name, methodSignatures); -} - -export function ReshapeCallableDeclaration(declaration: ts.SignatureDeclaration, scope: Scope): MethodSignature { - const returnTypeNode: ts.Node = GetFunctionReturnType(declaration); - - return { - parameters: declaration.parameters.map((parameter: ts.ParameterDeclaration) => parameter), - returnValue: GetDescriptor(returnTypeNode, scope), - }; + return GetMethodDescriptor(node.name, methodSignatures, scope); } diff --git a/src/transformer/descriptor/method/methodSignature.ts b/src/transformer/descriptor/method/methodSignature.ts index e2400cc4b..f983afb58 100644 --- a/src/transformer/descriptor/method/methodSignature.ts +++ b/src/transformer/descriptor/method/methodSignature.ts @@ -1,17 +1,19 @@ import * as ts from 'typescript'; import { Scope } from '../../scope/scope'; import { GetDescriptor } from '../descriptor'; +import { TypescriptCreator } from '../../helper/creator'; import { GetNullDescriptor } from '../null/null'; import { GetMethodDescriptor } from './method'; export function GetMethodSignatureDescriptor(node: ts.MethodSignature, scope: Scope): ts.Expression { - let returnValue: ts.Expression; - - if (node.type) { - returnValue = GetDescriptor(node.type, scope); - } else { - returnValue = GetNullDescriptor(); - } - - return GetMethodDescriptor(node.name, [{ returnValue }]); + return GetMethodDescriptor( + node.name, + [ + TypescriptCreator.createMethodSignature( + node.parameters.map((p: ts.ParameterDeclaration) => p.type), + node.type, + ), + ], + scope, + ); } diff --git a/src/transformer/descriptor/mock/mockCall.ts b/src/transformer/descriptor/mock/mockCall.ts index a369c4515..494ed8b50 100644 --- a/src/transformer/descriptor/mock/mockCall.ts +++ b/src/transformer/descriptor/mock/mockCall.ts @@ -1,11 +1,12 @@ import * as ts from 'typescript'; -import { TypescriptCreator } from '../../helper/creator'; +import { TypescriptCreator, MethodSignature } from '../../helper/creator'; +import { Scope } from '../../scope/scope'; import { MockIdentifierInternalValues, MockIdentifierObjectReturnValue } from '../../mockIdentifier/mockIdentifier'; -import { GetMethodDescriptor, MethodSignature } from '../method/method'; +import { GetMethodDescriptor } from '../method/method'; import { GetMockMarkerProperty, Property } from './mockMarker'; import { PropertyAssignments } from './mockPropertiesAssignments'; -export function GetMockCall(properties: PropertyAssignments, signatures: MethodSignature[]): ts.CallExpression { +export function GetMockCall(properties: PropertyAssignments, signatures: MethodSignature[], scope: Scope): ts.CallExpression { const mockObjectReturnValueName: ts.Identifier = MockIdentifierObjectReturnValue; const variableStatements: ts.VariableDeclaration[] = [ @@ -18,7 +19,7 @@ export function GetMockCall(properties: PropertyAssignments, signatures: MethodS // FIXME: It'd probably be wise to extract the name of the callable // signature and only fallback to `new` if there is none (or something // shorter). - const callableEntry: ts.CallExpression = GetMethodDescriptor(ts.createStringLiteral('new'), signatures); + const callableEntry: ts.CallExpression = GetMethodDescriptor(ts.createStringLiteral('new'), signatures, scope); variableStatements.push( TypescriptCreator.createVariableDeclaration(mockObjectReturnValueName, callableEntry), diff --git a/src/transformer/descriptor/mock/mockProperties.ts b/src/transformer/descriptor/mock/mockProperties.ts index bc33e757f..713a4b193 100644 --- a/src/transformer/descriptor/mock/mockProperties.ts +++ b/src/transformer/descriptor/mock/mockProperties.ts @@ -1,8 +1,7 @@ import * as ts from 'typescript'; +import { MethodSignature, TypescriptCreator } from '../../helper/creator'; import { Scope } from '../../scope/scope'; import { IsTypescriptType } from '../tsLibs/typecriptLibs'; -import { MethodSignature } from '../method/method'; -import { ReshapeCallableDeclaration } from '../method/methodDeclaration'; import { GetMockCall } from './mockCall'; import { GetMockPropertiesAssignments, PropertyAssignments } from './mockPropertiesAssignments'; import { PropertyLike } from './propertyLike'; @@ -35,7 +34,12 @@ export function GetMockPropertiesFromDeclarations(list: ReadonlyArray ReshapeCallableDeclaration(declaration, scope)); + const methodSignatures: MethodSignature[] = signatures.map((signature: SignatureLike) => + TypescriptCreator.createMethodSignature( + signature.parameters.map((p: ts.ParameterDeclaration) => p.type), + signature.type, + ), + ); - return GetMockCall(accessorDeclaration, methodSignatures); + return GetMockCall(accessorDeclaration, methodSignatures, scope); } From 789beffe93c5beed917c3539c940aefdb3e7142b Mon Sep 17 00:00:00 2001 From: Martin Jesper Low Madsen Date: Sat, 16 May 2020 15:47:20 +0200 Subject: [PATCH 15/26] feature(transformer): Add serialization helpers including one for method signature-based hash --- src/transformer/helper/creator.ts | 137 +++++++++++++++++++++++++++++- 1 file changed, 136 insertions(+), 1 deletion(-) diff --git a/src/transformer/helper/creator.ts b/src/transformer/helper/creator.ts index fd5e7b1de..2b13d60a0 100644 --- a/src/transformer/helper/creator.ts +++ b/src/transformer/helper/creator.ts @@ -1,5 +1,17 @@ -import { PropertyName, VariableDeclaration, VariableStatement} from 'typescript'; +import { PropertyName, VariableDeclaration, VariableStatement } from 'typescript'; import * as ts from 'typescript'; +import { IsTypescriptType } from '../descriptor/tsLibs/typecriptLibs'; +import { TypescriptHelper } from '../descriptor/helper/helper'; +import { NodeToString } from '../printNode'; + +export interface MethodSignature extends ts.MethodSignature { + parameters: ts.NodeArray; + type: ts.TypeNode; +} + +export interface ParameterDeclaration extends ts.ParameterDeclaration { + type: ts.TypeNode; +} export namespace TypescriptCreator { export function createArrowFunction(block: ts.ConciseBody, parameter: ReadonlyArray = []): ts.ArrowFunction { @@ -91,6 +103,129 @@ export namespace TypescriptCreator { ); } + function serialize(node: ts.TypeNode): string { + if (TypescriptHelper.IsLiteralOrPrimitive(node) || ts.isTypeReferenceNode(node)) { + return NodeToString(node); + } + + if (ts.isTypeLiteralNode(node)) { + const serialized: string = node.members + .filter((member: ts.TypeElement): member is ts.PropertySignature => ts.isPropertySignature(member)) + .map((member: ts.PropertySignature) => member.type ? serialize(member.type) : '').join('|'); + return `{${serialized}}`; + } + + if (ts.isUnionTypeNode(node) || ts.isIntersectionTypeNode(node)) { + const serialized: string = node.types.map((member: ts.TypeNode) => serialize(member)).join('|'); + return `[${serialized}]`; + } + + return ''; + } + + export function createSignatureHash(signature: ts.SignatureDeclaration | ts.SignatureDeclaration[]): string { + function serializeSignature(s: ts.SignatureDeclaration): string { + const parameters: ts.NodeArray = s.parameters; + + const signatureParts: string[] = []; + + if (parameters.length) { + signatureParts.push( + ...parameters.map((p: T) => { + const type: ts.TypeNode | undefined = p.type; + + if (!type) { + return ''; + } + + if (ts.isFunctionLike(type)) { + return `(${serializeSignature(type)})`; + } + + return serialize(type); + }) + ); + } + + const signatureType: ts.TypeNode | undefined = s.type; + + if (signatureType) { + signatureParts.push(serialize(signatureType)); + } + + return signatureParts.join('|'); + } + + const signatures: ts.SignatureDeclaration[] = Array.isArray(signature) ? signature : [signature]; + + // TODO: Check debug option and emit a verbose string representation + + // TODO: Make sure this doesn't result in collisions + return Buffer.from( + [ + Array.from(signatures.map((s: ts.SignatureDeclaration) => serializeSignature(s)).join('|')) + .reduce((s: number, c: string) => { + // eslint-disable-next-line + const charCode: number = c.charCodeAt(0) | 0; + + return Math.imul(31, s) + charCode; + }, 0), + ], + ).toString('base64'); + } + + function isDefinitiveMethodSignature(signature: ts.MethodSignature): signature is MethodSignature { + return !!signature.type; + } + + function isDefinitiveParameterDeclaration(parameter: ts.ParameterDeclaration): parameter is ParameterDeclaration { + return !!parameter.type; + } + + export function createMethodSignature(parameterTypes: Array = [], returnType: ts.TypeNode | undefined): MethodSignature { + const parameters: ParameterDeclaration[] = parameterTypes + .filter((type: ts.TypeNode | undefined): type is ts.TypeNode => !!type) + .map((parameterType: ts.TypeNode, i: number) => { + // TODO: Merge/move this block with/to typescriptLibs.ts + if (ts.isTypeReferenceNode(parameterType)) { + const declaration: ts.Declaration = TypescriptHelper.GetDeclarationFromNode(parameterType.typeName); + if (IsTypescriptType(declaration)) { + parameterType = ts.createFunctionTypeNode(undefined, [], ts.createKeywordTypeNode(ts.SyntaxKind.VoidKeyword)); + } + } + + const parameter: ts.ParameterDeclaration = ts.createParameter( + undefined, + undefined, + undefined, + `__${i++}`, + undefined, + parameterType, + undefined, + ); + + if (!isDefinitiveParameterDeclaration(parameter)) { + throw new Error(); + } + + return parameter; + }); + + const signature: ts.MethodSignature = ts.createMethodSignature( + undefined, + parameters, + returnType || ts.createKeywordTypeNode(ts.SyntaxKind.NullKeyword), + '', + undefined, + ); + + if (!isDefinitiveMethodSignature(signature)) { + throw new Error(); + } + + return signature; + } + export function createVariableDeclaration(variableIdentifier: ts.Identifier, initializer: ts.Expression): ts.VariableDeclaration { return ts.createVariableDeclaration( variableIdentifier, From 5e16c9ec581a2e8a5c889f34bc8d15e73e221b57 Mon Sep 17 00:00:00 2001 From: Martin Jesper Low Madsen Date: Sat, 16 May 2020 15:40:12 +0200 Subject: [PATCH 16/26] enhancement(transformer): Add helper function to extract the first identifier of a BindingName E.g.: ``` const [[[[[var]]]]] = ... ``` --- src/transformer/descriptor/helper/helper.ts | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/src/transformer/descriptor/helper/helper.ts b/src/transformer/descriptor/helper/helper.ts index f5dd14465..bb147eec6 100644 --- a/src/transformer/descriptor/helper/helper.ts +++ b/src/transformer/descriptor/helper/helper.ts @@ -10,6 +10,22 @@ export namespace TypescriptHelper { kind: ts.SyntaxKind.LiteralType | ts.SyntaxKind.NumberKeyword | ts.SyntaxKind.ObjectKeyword | ts.SyntaxKind.BooleanKeyword | ts.SyntaxKind.StringKeyword | ts.SyntaxKind.ArrayType; } + export function ExtractFirstIdentifier(bindingName: ts.BindingName): ts.Identifier { + let identifier: ts.BindingName = bindingName; + let saneSearchLimit: number = 10; + + while (!ts.isIdentifier(identifier)) { + const [bindingElement]: Array = (identifier.elements as ts.NodeArray).filter(ts.isBindingElement); + if (!bindingElement || !--saneSearchLimit) { + throw new Error('Failed to find an identifier for the primary declaration!'); + } + + identifier = bindingElement.name; + } + + return identifier; + } + export function IsLiteralOrPrimitive(typeNode: ts.Node): typeNode is PrimitiveTypeNode { switch (typeNode.kind) { case ts.SyntaxKind.LiteralType: From 5b98fb4243c1cb66750212096dfb13e428b84adf Mon Sep 17 00:00:00 2001 From: Martin Jesper Low Madsen Date: Sun, 17 May 2020 12:51:48 +0200 Subject: [PATCH 17/26] chore(transformer): Move branching logic to its own file --- .../descriptor/helper/branching.ts | 118 ++++++++++++++++++ 1 file changed, 118 insertions(+) create mode 100644 src/transformer/descriptor/helper/branching.ts diff --git a/src/transformer/descriptor/helper/branching.ts b/src/transformer/descriptor/helper/branching.ts new file mode 100644 index 000000000..a7e5a9468 --- /dev/null +++ b/src/transformer/descriptor/helper/branching.ts @@ -0,0 +1,118 @@ +import ts from 'typescript'; +import { MethodSignature } from '../../helper/creator'; +import { TypescriptHelper } from '../helper/helper'; +import { GetDescriptor } from '../descriptor'; +import { Scope } from '../../scope/scope'; + +function CreateTypeEquality(typeVariableMap: Map, parameterType: ts.TypeNode | undefined, primaryDeclaration: ts.ParameterDeclaration): ts.Expression { + const declarationName: ts.Identifier = TypescriptHelper.ExtractFirstIdentifier(primaryDeclaration.name); + + if (!parameterType) { + return ts.createPrefix( + ts.SyntaxKind.ExclamationToken, + ts.createPrefix( + ts.SyntaxKind.ExclamationToken, + declarationName, + ), + ); + } + + if (TypescriptHelper.IsLiteralOrPrimitive(parameterType)) { + return ts.createStrictEquality( + ts.createTypeOf(declarationName), + parameterType ? ts.createStringLiteral(parameterType.getText()) : ts.createVoidZero(), + ); + } + + if (typeVariableMap.has(parameterType)) { + // eslint-disable-next-line + const parameterIdentifier: ts.StringLiteral | ts.Identifier = typeVariableMap.get(parameterType)!; + return ts.createStrictEquality( + ts.createLogicalAnd(declarationName, ts.createPropertyAccess(declarationName, '__ident')), + parameterIdentifier, + ); + } + return ts.createBinary(declarationName, ts.SyntaxKind.InstanceOfKeyword, ts.createIdentifier('Object')); +} + +function CreateUnionTypeOfEquality( + typeVariableMap: Map, + signatureType: ts.TypeNode | undefined, + primaryDeclaration: ts.ParameterDeclaration, +): ts.Expression { + const typeNodesAndVariableReferences: Array = []; + + if (signatureType) { + if (ts.isTypeNode(signatureType) && ts.isUnionTypeNode(signatureType)) { + typeNodesAndVariableReferences.push(...signatureType.types); + } else { + typeNodesAndVariableReferences.push(signatureType); + } + } + + const [firstType, ...remainingTypes]: Array = typeNodesAndVariableReferences; + + return remainingTypes.reduce( + (prevStatement: ts.Expression, typeNode: ts.TypeNode) => + ts.createLogicalOr( + prevStatement, + CreateTypeEquality(typeVariableMap, typeNode, primaryDeclaration), + ), + CreateTypeEquality(typeVariableMap, firstType, primaryDeclaration), + ); +} + +function ResolveParameterBranch( + typeVariableMap: Map, + declarations: ts.NodeArray | [undefined], + longedSignature: MethodSignature, + returnType: ts.TypeNode, + elseBranch: ts.Statement, + scope: Scope, +): ts.Statement { + // NOTE: The strange signature here is to cover an empty list of declarations, + // then firstDeclaration will be undefined. + const [firstDeclaration, ...remainingDeclarations]: ts.NodeArray | [undefined] = declarations; + + // TODO: These conditions quickly grow in size, but it should be possible to + // squeeze things together and optimize it with something like: + // + // const typeOf = function (left, right) { return typeof left === right; } + // const evaluate = (function(left, right) { return this._ = this._ || typeOf(left, right); }).bind({}) + // + // if (evaluate(firstArg, 'boolean') && evaluate(secondArg, 'number') && ...) { + // ... + // } + // + // `this._' acts as a cache, since the control flow may evaluate the same + // conditions multiple times. + const condition: ts.Expression = remainingDeclarations.reduce( + (prevStatement: ts.Expression, declaration: ts.ParameterDeclaration, index: number) => + ts.createLogicalAnd( + prevStatement, + CreateUnionTypeOfEquality(typeVariableMap, declaration.type, longedSignature.parameters[index + 1]), + ), + CreateUnionTypeOfEquality(typeVariableMap, firstDeclaration?.type, longedSignature.parameters[0]), + ); + + return ts.createIf(condition, ts.createReturn(GetDescriptor(returnType, scope)), elseBranch); +} + +export function ResolveSignatureElseBranch( + typeVariableMap: Map, + signatures: MethodSignature[], + longestParameterList: MethodSignature, + scope: Scope, +): ts.Statement { + const [signature, ...remainingSignatures]: MethodSignature[] = signatures; + + const indistinctSignatures: boolean = signatures.every((sig: ts.MethodSignature) => !sig.parameters?.length); + if (!remainingSignatures.length || indistinctSignatures) { + return ts.createReturn(GetDescriptor(signature.type, scope)); + } + + const elseBranch: ts.Statement = ResolveSignatureElseBranch(typeVariableMap, remainingSignatures, longestParameterList, scope); + + const currentParameters: ts.NodeArray = signature.parameters || []; + return ResolveParameterBranch(typeVariableMap, currentParameters, longestParameterList, signature.type, elseBranch, scope); +} From 38756df9ced0619b511806d5843bacdfd149da8b Mon Sep 17 00:00:00 2001 From: Martin Jesper Low Madsen Date: Sun, 17 May 2020 12:59:38 +0200 Subject: [PATCH 18/26] chore(transformer): Filter method signatures based on the transformOverloads option --- src/transformer/descriptor/method/method.ts | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/transformer/descriptor/method/method.ts b/src/transformer/descriptor/method/method.ts index 006582ae0..6ee8ec94c 100644 --- a/src/transformer/descriptor/method/method.ts +++ b/src/transformer/descriptor/method/method.ts @@ -1,4 +1,5 @@ import ts from 'typescript'; +import { GetTsAutoMockOverloadOptions, TsAutoMockOverloadOptions } from '../../../options/overload'; import { MethodSignature, TypescriptCreator } from '../../helper/creator'; import { MockDefiner } from '../../mockDefiner/mockDefiner'; import { ModuleName } from '../../mockDefiner/modules/moduleName'; @@ -8,8 +9,11 @@ import { TypescriptHelper } from '../helper/helper'; export function GetMethodDescriptor(_propertyName: ts.PropertyName, methodSignatures: MethodSignature[], scope: Scope): ts.CallExpression { const providerGetMethod: ts.PropertyAccessExpression = CreateProviderGetMethod(); + const transformOverloadsOption: TsAutoMockOverloadOptions = GetTsAutoMockOverloadOptions(); - const signatureWithMostParameters: MethodSignature = methodSignatures.reduce( + const signatures: MethodSignature[] = methodSignatures.filter((_: unknown, notFirst: number) => transformOverloadsOption || !notFirst); + + const signatureWithMostParameters: MethodSignature = signatures.reduce( (acc: MethodSignature, signature: MethodSignature) => { const longestParametersLength: number = (acc.parameters || []).length; const parametersLength: number = (signature.parameters || []).length; @@ -21,7 +25,7 @@ export function GetMethodDescriptor(_propertyName: ts.PropertyName, methodSignat const declarationVariableMap: Map = new Map(); let i: number = 0; - const declarationVariables: ts.VariableDeclaration[] = methodSignatures.reduce( + const declarationVariables: ts.VariableDeclaration[] = signatures.reduce( (variables: ts.VariableDeclaration[], { parameters }: MethodSignature) => { for (const parameter of parameters) { if (declarationVariableMap.has(parameter.type)) { @@ -54,7 +58,7 @@ export function GetMethodDescriptor(_propertyName: ts.PropertyName, methodSignat statements.push(TypescriptCreator.createVariableStatement(declarationVariables)); } - statements.push(ResolveSignatureElseBranch(declarationVariableMap, methodSignatures, signatureWithMostParameters, scope)); + statements.push(ResolveSignatureElseBranch(declarationVariableMap, signatures, signatureWithMostParameters, scope)); const block: ts.Block = ts.createBlock(statements, true); From cc77dc8ee9b8b4866e81106e913b2c226a82c050 Mon Sep 17 00:00:00 2001 From: Martin Jesper Low Madsen Date: Sat, 16 May 2020 15:41:30 +0200 Subject: [PATCH 19/26] chore(transformer): Adjust type signatures in bodyReturnType.ts --- .../descriptor/method/bodyReturnType.ts | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/src/transformer/descriptor/method/bodyReturnType.ts b/src/transformer/descriptor/method/bodyReturnType.ts index 10535cd34..786c5c79e 100644 --- a/src/transformer/descriptor/method/bodyReturnType.ts +++ b/src/transformer/descriptor/method/bodyReturnType.ts @@ -7,13 +7,13 @@ export function GetReturnTypeFromBodyDescriptor(node: ts.ArrowFunction | ts.Func return GetDescriptor(GetReturnNodeFromBody(node), scope); } -export function GetReturnNodeFromBody(node: ts.FunctionLikeDeclaration): ts.Node { - let returnValue: ts.Node | undefined; +export function GetReturnNodeFromBody(node: ts.FunctionLikeDeclaration): ts.Expression { + let returnValue: ts.Expression | undefined; const functionBody: ts.ConciseBody | undefined = node.body; if (functionBody && ts.isBlock(functionBody)) { - const returnStatement: ts.ReturnStatement = GetReturnStatement(functionBody); + const returnStatement: ts.ReturnStatement | undefined = GetReturnStatement(functionBody); if (returnStatement) { returnValue = returnStatement.expression; @@ -21,7 +21,7 @@ export function GetReturnNodeFromBody(node: ts.FunctionLikeDeclaration): ts.Node returnValue = GetNullDescriptor(); } } else { - returnValue = node.body; + returnValue = functionBody; } if (!returnValue) { @@ -31,6 +31,8 @@ export function GetReturnNodeFromBody(node: ts.FunctionLikeDeclaration): ts.Node return returnValue; } -function GetReturnStatement(body: ts.FunctionBody): ts.ReturnStatement { - return body.statements.find((statement: ts.Statement) => statement.kind === ts.SyntaxKind.ReturnStatement) as ts.ReturnStatement; +function GetReturnStatement(body: ts.FunctionBody): ts.ReturnStatement | undefined { + return body.statements.find( + (statement: ts.Statement): statement is ts.ReturnStatement => statement.kind === ts.SyntaxKind.ReturnStatement, + ); } From 0a2d62251225eb60b03eb35ba24b75a5c2fd8b40 Mon Sep 17 00:00:00 2001 From: Martin Jesper Low Madsen Date: Sun, 17 May 2020 13:57:00 +0200 Subject: [PATCH 20/26] chore(transformer): Removed unused imports --- src/transformer/descriptor/method/methodSignature.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/transformer/descriptor/method/methodSignature.ts b/src/transformer/descriptor/method/methodSignature.ts index f983afb58..2d0a1b252 100644 --- a/src/transformer/descriptor/method/methodSignature.ts +++ b/src/transformer/descriptor/method/methodSignature.ts @@ -1,8 +1,6 @@ import * as ts from 'typescript'; import { Scope } from '../../scope/scope'; -import { GetDescriptor } from '../descriptor'; import { TypescriptCreator } from '../../helper/creator'; -import { GetNullDescriptor } from '../null/null'; import { GetMethodDescriptor } from './method'; export function GetMethodSignatureDescriptor(node: ts.MethodSignature, scope: Scope): ts.Expression { From 03f5facf5c9b7f6cceaaf98deb1eaa07243c5d98 Mon Sep 17 00:00:00 2001 From: Martin Jesper Low Madsen Date: Sat, 16 May 2020 15:29:31 +0200 Subject: [PATCH 21/26] chore(*): Rename __factory identifier to __ident and apply it to mocked methods as well --- src/repository/repository.ts | 33 +++-------------------------- src/utils/applyIdentityProperty.ts | 34 ++++++++++++++++++++++++++++++ 2 files changed, 37 insertions(+), 30 deletions(-) create mode 100644 src/utils/applyIdentityProperty.ts diff --git a/src/repository/repository.ts b/src/repository/repository.ts index 5f3f4ac02..8a5706a52 100644 --- a/src/repository/repository.ts +++ b/src/repository/repository.ts @@ -1,3 +1,5 @@ +import { applyIdentityProperty } from '../utils/applyIdentityProperty'; + // eslint-disable-next-line type Factory = (...args: any[]) => any; @@ -16,36 +18,7 @@ export class Repository { } public registerFactory(key: string, factory: Factory): void { - const proxy: Factory = new Proxy( - factory, - { - apply(target: Factory, _this: unknown, args: Parameters): ReturnType { - const mock: ReturnType = target(...args); - - if (typeof mock === 'undefined') { - return; - } - - if (!(mock instanceof Object)) { - return mock; - } - - if (typeof mock.__factory !== 'undefined') { - return mock; - } - - Object.defineProperty(mock, '__factory', { - enumerable: false, - writable: false, - value: key, - }); - - return mock; - }, - }, - ); - - this._repository[key] = proxy; + this._repository[key] = applyIdentityProperty(factory, key); } public getFactory(key: string): Factory { diff --git a/src/utils/applyIdentityProperty.ts b/src/utils/applyIdentityProperty.ts new file mode 100644 index 000000000..1b8ce0181 --- /dev/null +++ b/src/utils/applyIdentityProperty.ts @@ -0,0 +1,34 @@ +// eslint-disable-next-line +type Function = (...args: any[]) => K; +type IdentityFlavored = K & { __ident?: string }; + +export function applyIdentityProperty>(target: T, identity: string): T { + return new Proxy( + target, + { + apply(func: T, _this: unknown, args: Parameters): IdentityFlavored | undefined { + const t: IdentityFlavored = func(...args); + + if (typeof t === 'undefined') { + return; + } + + if (!(t instanceof Object)) { + return t; + } + + if (typeof t.__ident !== 'undefined') { + return t; + } + + Object.defineProperty(t, '__ident', { + enumerable: false, + writable: false, + value: identity, + }); + + return t; + }, + }, + ); +} From 1cc0874250a3f1c5e0e24580e1135068249573eb Mon Sep 17 00:00:00 2001 From: Martin Jesper Low Madsen Date: Sun, 17 May 2020 14:10:10 +0200 Subject: [PATCH 22/26] chore(transformer): Revert to using passed in propertyName as method identifier --- src/transformer/descriptor/method/method.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/transformer/descriptor/method/method.ts b/src/transformer/descriptor/method/method.ts index 6ee8ec94c..56c3f1ed6 100644 --- a/src/transformer/descriptor/method/method.ts +++ b/src/transformer/descriptor/method/method.ts @@ -7,8 +7,12 @@ import { Scope } from '../../scope/scope'; import { ResolveSignatureElseBranch } from '../helper/branching'; import { TypescriptHelper } from '../helper/helper'; -export function GetMethodDescriptor(_propertyName: ts.PropertyName, methodSignatures: MethodSignature[], scope: Scope): ts.CallExpression { +export function GetMethodDescriptor(propertyName: ts.PropertyName, methodSignatures: MethodSignature[], scope: Scope): ts.CallExpression { const providerGetMethod: ts.PropertyAccessExpression = CreateProviderGetMethod(); + + const propertyNameString: string = TypescriptHelper.GetStringPropertyName(propertyName); + const propertyNameStringLiteral: ts.StringLiteral = ts.createStringLiteral(propertyNameString); + const transformOverloadsOption: TsAutoMockOverloadOptions = GetTsAutoMockOverloadOptions(); const signatures: MethodSignature[] = methodSignatures.filter((_: unknown, notFirst: number) => transformOverloadsOption || !notFirst); @@ -67,11 +71,7 @@ export function GetMethodDescriptor(_propertyName: ts.PropertyName, methodSignat signatureWithMostParameters.parameters, ); - const propName: ts.StringLiteral = ts.createStringLiteral( - TypescriptCreator.createSignatureHash(methodSignatures), - ); - - return TypescriptCreator.createCall(providerGetMethod, [propName, propertyValueFunction]); + return TypescriptCreator.createCall(providerGetMethod, [propertyNameStringLiteral, propertyValueFunction]); } function CreateProviderGetMethod(): ts.PropertyAccessExpression { From 18922aaef7d8f2a0f13571899186d1e680755f02 Mon Sep 17 00:00:00 2001 From: Martin Jesper Low Madsen Date: Tue, 19 May 2020 21:23:10 +0200 Subject: [PATCH 23/26] chore(test): Enable Date instance expectation in overloads test --- test/transformer/descriptor/methods/overloads.test.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/test/transformer/descriptor/methods/overloads.test.ts b/test/transformer/descriptor/methods/overloads.test.ts index 247bb101e..40b335e18 100644 --- a/test/transformer/descriptor/methods/overloads.test.ts +++ b/test/transformer/descriptor/methods/overloads.test.ts @@ -54,8 +54,7 @@ describe('for overloads', () => { const properties: InterfaceWithConstructSignatureOverload = createMock(); expect((new properties(0)).a).toBe(0); expect((new properties('')).b).toBe(''); - // FIXME: Enable after Date PR - // expect((new properties()).c).toBeInstanceOf(Date); + expect((new properties()).c).toBeInstanceOf(Date); }); }); }); From bd96df99630a57b02a0973334420cd63e89693fd Mon Sep 17 00:00:00 2001 From: Martin Jesper Low Madsen Date: Tue, 19 May 2020 21:40:22 +0200 Subject: [PATCH 24/26] chore(options): Move the overload transformation option into a features array --- src/options/default.ts | 2 +- src/options/features.ts | 11 +++++++++++ src/options/options.ts | 4 ++-- src/options/overload.ts | 7 ------- src/transformer/descriptor/method/method.ts | 6 +++--- 5 files changed, 17 insertions(+), 13 deletions(-) create mode 100644 src/options/features.ts delete mode 100644 src/options/overload.ts diff --git a/src/options/default.ts b/src/options/default.ts index 8c3f2adbe..7c5d0944a 100644 --- a/src/options/default.ts +++ b/src/options/default.ts @@ -3,5 +3,5 @@ import { TsAutoMockOptions } from './options'; export const defaultOptions: TsAutoMockOptions = { debug: false, cacheBetweenTests: true, - transformOverloads: false, + features: [], }; diff --git a/src/options/features.ts b/src/options/features.ts new file mode 100644 index 000000000..628a2369d --- /dev/null +++ b/src/options/features.ts @@ -0,0 +1,11 @@ +import { GetOptionByKey } from './options'; + +interface Features { + transformOverloads: unknown; +} + +export type TsAutoMockFeaturesOptions = Array; + +export function GetTsAutoMockFeaturesOptions(): TsAutoMockFeaturesOptions { + return GetOptionByKey('features'); +} diff --git a/src/options/options.ts b/src/options/options.ts index 6b306e8e9..24f80ba19 100644 --- a/src/options/options.ts +++ b/src/options/options.ts @@ -1,12 +1,12 @@ import { TsAutoMockCacheOptions } from './cache'; import { TsAutoMockDebugOptions } from './debug'; -import { TsAutoMockOverloadOptions } from './overload'; +import { TsAutoMockFeaturesOptions } from './features'; import { defaultOptions } from './default'; export interface TsAutoMockOptions { debug: TsAutoMockDebugOptions; cacheBetweenTests: TsAutoMockCacheOptions; - transformOverloads: TsAutoMockOverloadOptions; + features: TsAutoMockFeaturesOptions; } let tsAutoMockOptions: TsAutoMockOptions = defaultOptions; diff --git a/src/options/overload.ts b/src/options/overload.ts deleted file mode 100644 index b73c56c6c..000000000 --- a/src/options/overload.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { GetOptionByKey } from './options'; - -export type TsAutoMockOverloadOptions = boolean; - -export function GetTsAutoMockOverloadOptions(): TsAutoMockOverloadOptions { - return GetOptionByKey('transformOverloads'); -} diff --git a/src/transformer/descriptor/method/method.ts b/src/transformer/descriptor/method/method.ts index 56c3f1ed6..ce6cfd1d6 100644 --- a/src/transformer/descriptor/method/method.ts +++ b/src/transformer/descriptor/method/method.ts @@ -1,5 +1,5 @@ import ts from 'typescript'; -import { GetTsAutoMockOverloadOptions, TsAutoMockOverloadOptions } from '../../../options/overload'; +import { GetTsAutoMockFeaturesOptions, TsAutoMockFeaturesOptions } from '../../../options/features'; import { MethodSignature, TypescriptCreator } from '../../helper/creator'; import { MockDefiner } from '../../mockDefiner/mockDefiner'; import { ModuleName } from '../../mockDefiner/modules/moduleName'; @@ -13,9 +13,9 @@ export function GetMethodDescriptor(propertyName: ts.PropertyName, methodSignatu const propertyNameString: string = TypescriptHelper.GetStringPropertyName(propertyName); const propertyNameStringLiteral: ts.StringLiteral = ts.createStringLiteral(propertyNameString); - const transformOverloadsOption: TsAutoMockOverloadOptions = GetTsAutoMockOverloadOptions(); + const features: TsAutoMockFeaturesOptions = GetTsAutoMockFeaturesOptions(); - const signatures: MethodSignature[] = methodSignatures.filter((_: unknown, notFirst: number) => transformOverloadsOption || !notFirst); + const signatures: MethodSignature[] = methodSignatures.filter((_: unknown, notFirst: number) => features.includes('transformOverloads') || !notFirst); const signatureWithMostParameters: MethodSignature = signatures.reduce( (acc: MethodSignature, signature: MethodSignature) => { From 8f7104cb9f80f4d9dfe7f2c0d705a9f30380348e Mon Sep 17 00:00:00 2001 From: Martin Jesper Low Madsen Date: Tue, 19 May 2020 22:20:02 +0200 Subject: [PATCH 25/26] chore(test): Add feature-gated and matrix based unit testing in CI --- .github/workflows/test.yml | 6 +- config/karma/karma.config.base.js | 3 +- config/test/webpack.js | 10 +- .../methods/feature.overloads.test.ts | 100 ++++++++++++++++++ .../descriptor/methods/overloads.test.ts | 77 -------------- tsconfig.playground.json | 3 +- 6 files changed, 114 insertions(+), 85 deletions(-) create mode 100644 test/transformer/descriptor/methods/feature.overloads.test.ts delete mode 100644 test/transformer/descriptor/methods/overloads.test.ts diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 5619ff249..a346131d6 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -9,6 +9,7 @@ jobs: strategy: matrix: node-version: [10.x] + feature: ['', 'transformOverloads'] steps: - uses: actions/checkout@v2 @@ -16,7 +17,7 @@ jobs: uses: actions/setup-node@v1 with: node-version: ${{ matrix.node-version }} - - name: install ts auto mock and run test + - name: install ts auto mock and run test - ${{ matrix.feature }} run: | sudo apt-get update sudo apt-get install -y libgbm-dev @@ -25,5 +26,4 @@ jobs: npm test env: CI: true - - + FEATURE: ${{ matrix.feature }} diff --git a/config/karma/karma.config.base.js b/config/karma/karma.config.base.js index ced84a659..09fea030a 100644 --- a/config/karma/karma.config.base.js +++ b/config/karma/karma.config.base.js @@ -6,11 +6,12 @@ module.exports = function(config, url) { const processService = ProcessService(process); const debug = processService.getArgument('DEBUG'); const disableCache = processService.getArgument('DISABLECACHE'); + const feature = processService.getArgument('FEATURE') || process.env.FEATURE; return { basePath: '', frameworks: ['jasmine'], - webpack: webpackConfig(debug, disableCache), + webpack: webpackConfig(debug, disableCache, feature), webpackMiddleware: { stats: 'errors-only' }, diff --git a/config/test/webpack.js b/config/test/webpack.js index b8b6de91a..7bd368194 100644 --- a/config/test/webpack.js +++ b/config/test/webpack.js @@ -1,7 +1,8 @@ const transformer = require('../../dist/transformer'); const path = require('path'); +const webpack = require('webpack'); -module.exports = function (debug, disableCache) { +module.exports = function (debug, disableCache, feature = '') { return { mode: "development", resolve: { @@ -12,6 +13,11 @@ module.exports = function (debug, disableCache) { ['ts-auto-mock/extension']: path.join(__dirname, '../../dist/extension'), } }, + plugins: [ + new webpack.DefinePlugin({ + 'process.env.FEATURE': `"${feature}"`, + }), + ], module: { rules: [ { @@ -27,7 +33,7 @@ module.exports = function (debug, disableCache) { before: [transformer.default(program, { debug: debug ? debug : false, cacheBetweenTests: disableCache !== 'true', - transformOverloads: true, + features: [feature], })] }) } diff --git a/test/transformer/descriptor/methods/feature.overloads.test.ts b/test/transformer/descriptor/methods/feature.overloads.test.ts new file mode 100644 index 000000000..4fc9a25a0 --- /dev/null +++ b/test/transformer/descriptor/methods/feature.overloads.test.ts @@ -0,0 +1,100 @@ +import { createMock } from 'ts-auto-mock'; + +import { + exportedDeclaredOverloadedFunction, + ExportedDeclaredClass, +} from '../utils/typeQuery/typeQueryUtils'; + +const isFeatureEnabled: boolean = process.env.FEATURE === 'transformOverloads'; + +describe('feature', () => { + + describe('for overloads', () => { + + describe('for type query', () => { + + it('should assign the correct function mock for literal inputs', () => { + const functionMock: typeof exportedDeclaredOverloadedFunction = createMock(); + + // eslint-disable-next-line + const expectations = [ + ['', 0, false], + [false, '', 0], + [0, false, ''], + [false, false, false], + [''], + [false], + [0], + ]; + + for (const args of expectations) { + // eslint-disable-next-line + const [first] = args; + + // @ts-ignore + expect(functionMock(...args)).toEqual(isFeatureEnabled ? first : false); + } + }); + + it('should assign the correct function mock for mockable inputs', () => { + const classMock: typeof ExportedDeclaredClass = createMock(); + + const functionMock: typeof exportedDeclaredOverloadedFunction = createMock(); + + if (isFeatureEnabled) { + expect(functionMock(new classMock()).prop).toBe(0); + } else { + expect(functionMock(new classMock()).prop).toBeUndefined(); + } + }); + + }); + + describe('for interface', () => { + describe('for construct signature', () => { + interface InterfaceWithConstructSignatureOverload { + new (a: number): { a: number }; + new (b: string): { b: string }; + new (): { c: Date }; + } + + it('should use the correct signature as requested by input', () => { + const properties: InterfaceWithConstructSignatureOverload = createMock(); + + expect((new properties(0)).a).toBe(0); + + if (isFeatureEnabled) { + expect((new properties('')).b).toBe(''); + expect((new properties()).c).toBeInstanceOf(Date); + } else { + expect((new properties('')).b).toBeUndefined(); + expect((new properties()).c).toBeUndefined(); + } + }); + }); + }); + + describe('call signature', () => { + interface InterfaceWithCallSignature { + (a: number): number; + (a: string): string; + b: string; + } + + it('should consider all signature declarations and properties', () => { + const properties: InterfaceWithCallSignature = createMock(); + + expect(properties.b).toBe(''); + expect(properties(2)).toBe(0); + + if (isFeatureEnabled) { + expect(properties('2')).toBe(''); + } else { + // @ts-ignore + expect(properties('2')).toBe(0); + } + }); + }); + + }); +}); diff --git a/test/transformer/descriptor/methods/overloads.test.ts b/test/transformer/descriptor/methods/overloads.test.ts deleted file mode 100644 index 40b335e18..000000000 --- a/test/transformer/descriptor/methods/overloads.test.ts +++ /dev/null @@ -1,77 +0,0 @@ -import { createMock } from 'ts-auto-mock'; - -import { - exportedDeclaredOverloadedFunction, - ExportedDeclaredClass, -} from '../utils/typeQuery/typeQueryUtils'; - -describe('for overloads', () => { - - describe('for type query', () => { - - it('should assign the correct function mock for literal inputs', () => { - const functionMock: typeof exportedDeclaredOverloadedFunction = createMock(); - - // eslint-disable-next-line - const expectations = [ - ['', 0, false], - [false, '', 0], - [0, false, ''], - [false, false, false], - [''], - [false], - [0], - ]; - - for (const args of expectations) { - // eslint-disable-next-line - const [first] = args; - - // @ts-ignore - expect(functionMock(...args)).toEqual(first); - } - }); - - it('should assign the correct function mock for mockable inputs', () => { - const classMock: typeof ExportedDeclaredClass = createMock(); - - const functionMock: typeof exportedDeclaredOverloadedFunction = createMock(); - - expect(functionMock(new classMock()).prop).toBe(0); - }); - - }); - - describe('for interface', () => { - describe('for construct signature', () => { - interface InterfaceWithConstructSignatureOverload { - new (a: number): { a: number }; - new (b: string): { b: string }; - new (): { c: Date }; - } - - it('should use the correct signature as requested by input', () => { - const properties: InterfaceWithConstructSignatureOverload = createMock(); - expect((new properties(0)).a).toBe(0); - expect((new properties('')).b).toBe(''); - expect((new properties()).c).toBeInstanceOf(Date); - }); - }); - }); - - describe('call signature', () => { - interface InterfaceWithCallSignature { - (a: number): number; - (a: string): string; - b: string; - } - - it('should consider all signature declarations and properties', () => { - const properties: InterfaceWithCallSignature = createMock(); - expect(properties(2)).toBe(0); - expect(properties('2')).toBe(''); - expect(properties.b).toBe(''); - }); - }); - -}); diff --git a/tsconfig.playground.json b/tsconfig.playground.json index 7291fbba8..3ded2334e 100644 --- a/tsconfig.playground.json +++ b/tsconfig.playground.json @@ -6,7 +6,7 @@ { "transform": "./dist/transformer", "debug": "console", - "transformOverloads": true + "features": ["transformOverloads"] } ] }, @@ -15,4 +15,3 @@ ], "include": [] } - From 6eefd02d47fe6c76791d71f15c6bef82310e8857 Mon Sep 17 00:00:00 2001 From: Martin Jesper Low Madsen Date: Tue, 19 May 2020 22:37:12 +0200 Subject: [PATCH 26/26] chore(transformer): Apply non-nullable check for getDeclarationKeyMap use --- src/transformer/descriptor/method/method.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/transformer/descriptor/method/method.ts b/src/transformer/descriptor/method/method.ts index ce6cfd1d6..7fd479e54 100644 --- a/src/transformer/descriptor/method/method.ts +++ b/src/transformer/descriptor/method/method.ts @@ -47,7 +47,8 @@ export function GetMethodDescriptor(propertyName: ts.PropertyName, methodSignatu variables.push( TypescriptCreator.createVariableDeclaration( variableIdentifier, - ts.createStringLiteral(MockDefiner.instance.getDeclarationKeyMap(declaration)), + // FIXME: Remove the non nullable operator when/if https://github.com/Typescript-TDD/ts-auto-mock/pull/314 is merged + ts.createStringLiteral(MockDefiner.instance.getDeclarationKeyMap(declaration)!), ), ); }