From 340b5624bf2129063d3a9985ac5d60f8211f38cf Mon Sep 17 00:00:00 2001 From: Martin Jesper Low Madsen Date: Sat, 9 May 2020 13:43:57 +0200 Subject: [PATCH] feature(transformer): Support overloaded functions by attaching signatures on use Add overloads feature flag that enables this feature. Enabling it makes the transformer process function calls if their declaration was previously marked for mocking (via getMethod). From a type-perspective, typed methods shouldn't bother to consider their inputs in order to determine the output in runtime. At transformation time, the type checker resolves the matching overload and that information can be used to attach to the function, by utilizing the "instance" (`this`) of it. The transformer changes transform functions in the following way. ``` mockedFunction() -> mockedFunction.apply(, []) ``` As for constructor instantiation signatures in interfaces, those can be wrapped by an intermediate function that will copy the mocked properties to preserve the instantiation behavior. ``` new mockedNewFunction() | `-> new (mockedNewFunction[] || (mockedNewFunction[] = function() { Object.assign(this, mockedNewFunction.apply(, [])); }))() ``` These attached interfaces will determine the branching at runtime and to reduce as much overhead as possible, all signatures of an overloaded function are mapped to the resolved return type and stored in a jump table, i.e.: ``` getMethod("functionName", function () { const jt = { ['']: () => , ['']: () => , ... }; return jt[this](); }) ``` It should be noted, that if spies are introduced using the method provider, then `this` will be occupied by the signature key. --- config/utils/features.js | 3 +- .../method/provider/functionMethod.ts | 6 +- src/merge/merge.ts | 2 +- src/options/features.ts | 2 +- src/options/overloads.ts | 5 + src/transformer/base/base.ts | 85 ++++++++++++++-- src/transformer/descriptor/helper/helper.ts | 2 +- .../descriptor/method/bodyReturnType.ts | 32 +++---- .../descriptor/method/functionAssignment.ts | 6 +- .../descriptor/method/functionType.ts | 5 +- src/transformer/descriptor/method/method.ts | 96 +++++++++++++++++-- .../descriptor/method/methodDeclaration.ts | 20 ++-- .../descriptor/method/methodSignature.ts | 14 +-- src/transformer/descriptor/mock/mockCall.ts | 48 +++++++--- .../descriptor/mock/mockProperties.ts | 4 +- src/transformer/matcher/matcher.ts | 14 +-- src/transformer/mockDefiner/mockDefiner.ts | 6 +- .../mockFactoryCall/mockFactoryCall.ts | 6 +- .../mockIdentifier/mockIdentifier.ts | 1 + test/features/overloads/interfaces.test.ts | 46 +++++++++ test/features/overloads/typeQuery.test.ts | 30 ++++++ .../utils/typeQuery/typeQueryUtils.ts | 35 +++++++ 22 files changed, 373 insertions(+), 95 deletions(-) create mode 100644 src/options/overloads.ts create mode 100644 test/features/overloads/interfaces.test.ts create mode 100644 test/features/overloads/typeQuery.test.ts diff --git a/config/utils/features.js b/config/utils/features.js index f135e5ada..85ca07396 100644 --- a/config/utils/features.js +++ b/config/utils/features.js @@ -6,7 +6,8 @@ function DetermineFeaturesFromEnvironment() { if (features) { return [ - 'random' + 'overloads', + 'random', ]; } diff --git a/src/extension/method/provider/functionMethod.ts b/src/extension/method/provider/functionMethod.ts index 9a7b08aa1..80a659d2a 100644 --- a/src/extension/method/provider/functionMethod.ts +++ b/src/extension/method/provider/functionMethod.ts @@ -1,5 +1,7 @@ // 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 function(...args: any[]): any { + return value.apply(this, 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/options/features.ts b/src/options/features.ts index 2ce6f870d..c9cd1708c 100644 --- a/src/options/features.ts +++ b/src/options/features.ts @@ -1 +1 @@ -export type TsAutoMockFeaturesOption = 'random'; +export type TsAutoMockFeaturesOption = 'random' | 'overloads'; diff --git a/src/options/overloads.ts b/src/options/overloads.ts new file mode 100644 index 000000000..fe9581459 --- /dev/null +++ b/src/options/overloads.ts @@ -0,0 +1,5 @@ +import { GetOptionByKey } from './options'; + +export function IsTsAutoMockOverloadsEnabled(): boolean { + return GetOptionByKey('features').includes('overloads'); +} diff --git a/src/transformer/base/base.ts b/src/transformer/base/base.ts index 38bf96d1d..d09bb306c 100644 --- a/src/transformer/base/base.ts +++ b/src/transformer/base/base.ts @@ -1,4 +1,5 @@ -import * as ts from 'typescript'; +import ts from 'typescript'; +import { IsTsAutoMockOverloadsEnabled } from '../../options/overloads'; import { SetTsAutoMockOptions, TsAutoMockOptions } from '../../options/options'; import { SetTypeChecker } from '../typeChecker/typeChecker'; import { MockDefiner } from '../mockDefiner/mockDefiner'; @@ -9,7 +10,7 @@ import { isFunctionFromThisLibrary, } from '../matcher/matcher'; -export type Visitor = (node: ts.CallExpression & { typeArguments: ts.NodeArray }, declaration: ts.FunctionDeclaration) => ts.Node; +export type Visitor = (node: ts.CallLikeExpression & { typeArguments: ts.NodeArray }, declaration: ts.SignatureDeclaration) => ts.Node; export function baseTransformer(visitor: Visitor, customFunctions: CustomFunction[]): (program: ts.Program, options?: TsAutoMockOptions) => ts.TransformerFactory { return (program: ts.Program, options?: TsAutoMockOptions): ts.TransformerFactory => { @@ -47,14 +48,88 @@ function isObjectWithProperty( return typeof obj[key] !== 'undefined'; } +function isMockedByThisLibrary(declaration: ts.Declaration): boolean { + return MockDefiner.instance.hasKeyForDeclaration(declaration); +} + function visitNode(node: ts.Node, visitor: Visitor, customFunctions: CustomFunction[]): ts.Node { - if (!ts.isCallExpression(node)) { + if (!ts.isCallExpression(node) && !ts.isNewExpression(node)) { return node; } const signature: ts.Signature | undefined = TypescriptHelper.getSignatureOfCallExpression(node); + const declaration: ts.Declaration | undefined = signature?.declaration; - if (!signature || !isFunctionFromThisLibrary(signature, customFunctions)) { + if (!declaration || !ts.isFunctionLike(declaration)) { + return node; + } + + if (IsTsAutoMockOverloadsEnabled() && isMockedByThisLibrary(declaration)) { + const mockKey: string = MockDefiner.instance.getDeclarationKeyMap(declaration); + const mockKeyLiteral: ts.StringLiteral = ts.createStringLiteral(mockKey); + + const boundSignatureCall: ts.CallExpression = ts.createCall( + ts.createPropertyAccess( + node.expression, + ts.createIdentifier('apply'), + ), + undefined, + [mockKeyLiteral, ts.createArrayLiteral(node.arguments)], + ); + + if (ts.isCallExpression(node)) { + return boundSignatureCall; + } + + const cachedConstructor: ts.ElementAccessExpression = ts.createElementAccess( + node.expression, + mockKeyLiteral, + ); + + return ts.createNew( + ts.createParen( + ts.createBinary( + cachedConstructor, + ts.SyntaxKind.BarBarToken, + ts.createParen( + ts.createBinary( + cachedConstructor, + ts.SyntaxKind.EqualsToken, + ts.createFunctionExpression( + undefined, + undefined, + '', + undefined, + undefined, + undefined, + ts.createBlock( + [ + ts.createExpressionStatement( + ts.createCall( + ts.createPropertyAccess( + ts.createIdentifier('Object'), + ts.createIdentifier('assign'), + ), + undefined, + [ + ts.createIdentifier('this'), + boundSignatureCall, + ] + ), + ), + ], + ), + ), + ), + ), + ), + ), + undefined, + undefined, + ); + } + + if (!isFunctionFromThisLibrary(declaration, customFunctions)) { return node; } @@ -73,7 +148,5 @@ function visitNode(node: ts.Node, visitor: Visitor, customFunctions: CustomFunct MockDefiner.instance.setFileNameFromNode(nodeToMock); MockDefiner.instance.setTsAutoMockImportIdentifier(); - const declaration: ts.FunctionDeclaration = signature.declaration as ts.FunctionDeclaration; - return visitor(node, declaration); } diff --git a/src/transformer/descriptor/helper/helper.ts b/src/transformer/descriptor/helper/helper.ts index ea86d74b7..46893141f 100644 --- a/src/transformer/descriptor/helper/helper.ts +++ b/src/transformer/descriptor/helper/helper.ts @@ -104,7 +104,7 @@ export namespace TypescriptHelper { } - export function getSignatureOfCallExpression(node: ts.CallExpression): ts.Signature | undefined { + export function getSignatureOfCallExpression(node: ts.CallLikeExpression): ts.Signature | undefined { const typeChecker: ts.TypeChecker = TypeChecker(); return typeChecker.getResolvedSignature(node); diff --git a/src/transformer/descriptor/method/bodyReturnType.ts b/src/transformer/descriptor/method/bodyReturnType.ts index 10535cd34..7eba2c39b 100644 --- a/src/transformer/descriptor/method/bodyReturnType.ts +++ b/src/transformer/descriptor/method/bodyReturnType.ts @@ -7,30 +7,28 @@ 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: T): ts.Expression { const functionBody: ts.ConciseBody | undefined = node.body; - if (functionBody && ts.isBlock(functionBody)) { - const returnStatement: ts.ReturnStatement = GetReturnStatement(functionBody); + if (!functionBody) { + return GetNullDescriptor(); + } - if (returnStatement) { - returnValue = returnStatement.expression; - } else { - returnValue = GetNullDescriptor(); - } - } else { - returnValue = node.body; + if (!ts.isBlock(functionBody)) { + return functionBody; } - if (!returnValue) { - throw new Error(`Failed to determine the return value of ${node.getText()}.`); + const returnStatement: ts.ReturnStatement | undefined = GetReturnStatement(functionBody); + + if (!returnStatement?.expression) { + return GetNullDescriptor(); } - return returnValue; + return returnStatement.expression; } -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, + ); } diff --git a/src/transformer/descriptor/method/functionAssignment.ts b/src/transformer/descriptor/method/functionAssignment.ts index 316da73cd..4680da758 100644 --- a/src/transformer/descriptor/method/functionAssignment.ts +++ b/src/transformer/descriptor/method/functionAssignment.ts @@ -1,14 +1,12 @@ -import * as ts from 'typescript'; +import ts from 'typescript'; import { Scope } from '../../scope/scope'; import { PropertySignatureCache } from '../property/cache'; -import { GetReturnTypeFromBodyDescriptor } 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); - return GetMethodDescriptor(property, returnValue); + return GetMethodDescriptor(property, [node], scope); } diff --git a/src/transformer/descriptor/method/functionType.ts b/src/transformer/descriptor/method/functionType.ts index a044b8463..4e6987bfd 100644 --- a/src/transformer/descriptor/method/functionType.ts +++ b/src/transformer/descriptor/method/functionType.ts @@ -1,6 +1,5 @@ import * as ts from 'typescript'; import { Scope } from '../../scope/scope'; -import { GetDescriptor } from '../descriptor'; import { PropertySignatureCache } from '../property/cache'; import { GetMethodDescriptor } from './method'; @@ -11,7 +10,5 @@ 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, [node], scope); } diff --git a/src/transformer/descriptor/method/method.ts b/src/transformer/descriptor/method/method.ts index 64db3e66e..a26c16fed 100644 --- a/src/transformer/descriptor/method/method.ts +++ b/src/transformer/descriptor/method/method.ts @@ -1,19 +1,100 @@ -import * as ts from 'typescript'; +import ts from 'typescript'; +import { IsTsAutoMockOverloadsEnabled } from '../../../options/overloads'; import { TypescriptCreator } from '../../helper/creator'; import { MockDefiner } from '../../mockDefiner/mockDefiner'; import { ModuleName } from '../../mockDefiner/modules/moduleName'; +import { MockIdentifierJumpTable } from '../../mockIdentifier/mockIdentifier'; +import { Scope } from '../../scope/scope'; import { TypescriptHelper } from '../helper/helper'; +import { GetDescriptor } from '../descriptor'; +import { GetReturnNodeFromBody } from './bodyReturnType'; -export function GetMethodDescriptor(propertyName: ts.PropertyName, returnValue: ts.Expression): ts.Expression { +type MethodDeclaration = + | ts.ArrowFunction + | ts.FunctionExpression + | ts.MethodSignature + | ts.FunctionTypeNode + | ts.CallSignatureDeclaration + | ts.ConstructSignatureDeclaration + | ts.MethodDeclaration + | ts.FunctionDeclaration; + +function GetDeclarationType(declaration: ts.SignatureDeclaration): ts.TypeNode { + if (declaration.type) { + return declaration.type; + } + + return ts.createLiteralTypeNode(GetReturnNodeFromBody(declaration) as ts.LiteralExpression); +} + +export function GetMethodDescriptor( + propertyName: ts.PropertyName, + methodDeclarations: ReadonlyArray, + scope: Scope, +): ts.CallExpression { 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)], - true, - )); + const statements: ts.Statement[] = []; + + const [primaryDeclaration, ...remainingDeclarations]: ReadonlyArray = methodDeclarations; + + if (remainingDeclarations.length && IsTsAutoMockOverloadsEnabled()) { + const jumpTableEntries: ts.PropertyAssignment[] = methodDeclarations.map((declaration: ts.FunctionDeclaration) => + ts.createPropertyAssignment( + ts.createComputedPropertyName( + ts.createStringLiteral( + MockDefiner.instance.getDeclarationKeyMap(declaration), + ), + ), + ts.createArrowFunction( + undefined, + undefined, + [], + undefined, + undefined, + GetDescriptor(GetDeclarationType(declaration), scope), + ), + ), + ); + + statements.push( + TypescriptCreator.createVariableStatement([ + TypescriptCreator.createVariableDeclaration( + MockIdentifierJumpTable, + ts.createObjectLiteral(jumpTableEntries), + ), + ]), + ); + + statements.push( + ts.createReturn( + ts.createCall( + ts.createElementAccess( + MockIdentifierJumpTable, + ts.createIdentifier('this'), + ), + undefined, + undefined, + ), + ), + ); + } else { + statements.push( + ts.createReturn( + GetDescriptor(GetDeclarationType(primaryDeclaration), scope), + ), + ); + } + + const block: ts.Block = ts.createBlock(statements, true); + + const propertyValueFunction: ts.FunctionExpression = TypescriptCreator.createFunctionExpression( + block, + [], + ); return TypescriptCreator.createCall(providerGetMethod, [propertyNameStringLiteral, propertyValueFunction]); } @@ -26,5 +107,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 528fc4120..17b087eb6 100644 --- a/src/transformer/descriptor/method/methodDeclaration.ts +++ b/src/transformer/descriptor/method/methodDeclaration.ts @@ -1,12 +1,20 @@ -import * as ts from 'typescript'; +import ts from 'typescript'; import { Scope } from '../../scope/scope'; -import { GetDescriptor } from '../descriptor'; -import { GetFunctionReturnType } from './functionReturnType'; +import { TypeChecker } from '../../typeChecker/typeChecker'; + import { GetMethodDescriptor } 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); + } if (!node.name) { throw new Error( @@ -14,5 +22,5 @@ export function GetMethodDeclarationDescriptor(node: ts.MethodDeclaration | ts.F ); } - return GetMethodDescriptor(node.name, returnType); + return GetMethodDescriptor(node.name, methodDeclarations, scope); } diff --git a/src/transformer/descriptor/method/methodSignature.ts b/src/transformer/descriptor/method/methodSignature.ts index 80993c613..ce4285d0b 100644 --- a/src/transformer/descriptor/method/methodSignature.ts +++ b/src/transformer/descriptor/method/methodSignature.ts @@ -1,17 +1,7 @@ -import * as ts from 'typescript'; +import ts from 'typescript'; import { Scope } from '../../scope/scope'; -import { GetDescriptor } from '../descriptor'; -import { GetNullDescriptor } from '../null/null'; import { GetMethodDescriptor } from './method'; export function GetMethodSignatureDescriptor(node: ts.MethodSignature, scope: Scope): ts.Expression { - let returnType: ts.Expression; - - if (node.type) { - returnType = GetDescriptor(node.type, scope); - } else { - returnType = GetNullDescriptor(); - } - - return GetMethodDescriptor(node.name, returnType); + return GetMethodDescriptor(node.name, [node], scope); } diff --git a/src/transformer/descriptor/mock/mockCall.ts b/src/transformer/descriptor/mock/mockCall.ts index 5e1be62ba..8d42c72a0 100644 --- a/src/transformer/descriptor/mock/mockCall.ts +++ b/src/transformer/descriptor/mock/mockCall.ts @@ -1,28 +1,46 @@ -import * as ts from 'typescript'; +import ts from 'typescript'; import { TypescriptCreator } from '../../helper/creator'; +import { Scope } from '../../scope/scope'; import { MockIdentifierInternalValues, MockIdentifierObjectReturnValue } from '../../mockIdentifier/mockIdentifier'; +import { GetMethodDescriptor } 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: ReadonlyArray, scope: Scope): ts.CallExpression { const mockObjectReturnValueName: ts.Identifier = MockIdentifierObjectReturnValue; - const statements: ts.Statement[] = [ - TypescriptCreator.createVariableStatement([ - TypescriptCreator.createVariableDeclaration(MockIdentifierInternalValues, ts.createObjectLiteral()), - TypescriptCreator.createVariableDeclaration(mockObjectReturnValueName, signature || ts.createObjectLiteral(properties.literals)), - ]), + + const variableStatements: ts.VariableDeclaration[] = [ + TypescriptCreator.createVariableDeclaration(MockIdentifierInternalValues, ts.createObjectLiteral()), ]; + const propertyAssignmentStatements: ts.ExpressionStatement[] = []; - if (signature) { - let literalProperty: ts.PropertyAssignment; - let index: number = 0; + const isCallable: boolean = !!signatures.length; + if (isCallable) { + // 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, scope); - // tslint:disable-next-line:no-conditional-assignment - while ((literalProperty = properties.literals[index++])) { - statements.push(AssignLiteralPropertyTo(mockObjectReturnValueName, literalProperty)); - } + 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(variableStatements), + ...propertyAssignmentStatements, + ]; + if (properties.lazy.length) { const addPropertiesToUniqueVariable: ts.ExpressionStatement = AssignPropertiesTo(properties.lazy, mockObjectReturnValueName); statements.push(addPropertiesToUniqueVariable); @@ -30,12 +48,12 @@ 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, [], []); } diff --git a/src/transformer/descriptor/mock/mockProperties.ts b/src/transformer/descriptor/mock/mockProperties.ts index 0fa48afe5..6a19abc0c 100644 --- a/src/transformer/descriptor/mock/mockProperties.ts +++ b/src/transformer/descriptor/mock/mockProperties.ts @@ -1,6 +1,5 @@ import * as ts from 'typescript'; import { Scope } from '../../scope/scope'; -import { GetDescriptor } from '../descriptor'; import { IsTypescriptType } from '../tsLibs/typecriptLibs'; import { GetMockCall } from './mockCall'; import { GetMockPropertiesAssignments, PropertyAssignments } from './mockPropertiesAssignments'; @@ -34,6 +33,5 @@ export function GetMockPropertiesFromDeclarations(list: ReadonlyArray { const functionUrl: string = path.join(__dirname, customFunction.sourceUrl); @@ -28,7 +24,3 @@ export function isFunctionFromThisLibrary(signature: ts.Signature, customFunctio return isFileNameFunctionUrl; }); } - -function isDeclarationDefined(signature: ts.Signature): boolean { - return signature && !!signature.declaration; -} diff --git a/src/transformer/mockDefiner/mockDefiner.ts b/src/transformer/mockDefiner/mockDefiner.ts index 6dbe8c2e4..feb947798 100644 --- a/src/transformer/mockDefiner/mockDefiner.ts +++ b/src/transformer/mockDefiner/mockDefiner.ts @@ -148,7 +148,7 @@ export class MockDefiner { } public getDeclarationKeyMap(declaration: ts.Declaration): string { - if (!this._declarationCache.has(declaration)) { + if (!this.hasKeyForDeclaration(declaration)) { this._declarationCache.set(declaration, this._factoryUniqueName.createForDeclaration(declaration as PossibleDeclaration)); } @@ -157,6 +157,10 @@ export class MockDefiner { return this._declarationCache.get(declaration) as string; } + public hasKeyForDeclaration(declaration: ts.Declaration): boolean { + return this._declarationCache.has(declaration); + } + public storeRegisterMockFor(declaration: ts.Declaration, factory: ts.FunctionExpression): void { const key: string = this.getDeclarationKeyMap(declaration); diff --git a/src/transformer/mockFactoryCall/mockFactoryCall.ts b/src/transformer/mockFactoryCall/mockFactoryCall.ts index 8b9592ef6..070665308 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); @@ -68,7 +68,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) { diff --git a/src/transformer/mockIdentifier/mockIdentifier.ts b/src/transformer/mockIdentifier/mockIdentifier.ts index 4ed50c992..4cfaad15e 100644 --- a/src/transformer/mockIdentifier/mockIdentifier.ts +++ b/src/transformer/mockIdentifier/mockIdentifier.ts @@ -6,6 +6,7 @@ export const MockIdentifierGenericParameterValue: ts.Identifier = ts.createIdent export const MockIdentifierInternalValues: ts.Identifier = ts.createIdentifier('d'); export const MockIdentifierObjectReturnValue: ts.Identifier = ts.createIdentifier('m'); export const MockIdentifierSetParameterName: ts.Identifier = ts.createIdentifier('v'); +export const MockIdentifierJumpTable: ts.Identifier = ts.createIdentifier('jt'); export const MockCallAnonymousText: string = '*'; export const MockCallLiteralText: string = 'L'; export const MockPrivatePrefix: string = 'ɵ'; diff --git a/test/features/overloads/interfaces.test.ts b/test/features/overloads/interfaces.test.ts new file mode 100644 index 000000000..d592e4b59 --- /dev/null +++ b/test/features/overloads/interfaces.test.ts @@ -0,0 +1,46 @@ +import { createMock } from 'ts-auto-mock'; + +describe('Overloads 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(typeof (new properties(0)).a).toBe('number'); + expect(typeof (new properties('')).b).toBe('string'); + expect((new properties()).c).toBeInstanceOf(Date); + }); + + it('should (re-)use the same constructor on a per-signature basis', () => { + const properties: InterfaceWithConstructSignatureOverload = createMock(); + + const firstInstance: { a: number } = new properties(0); + const secondInstance: { a: number } = new properties(0); + const thirdInstance: { b: string } = new properties(''); + + expect(firstInstance.constructor).toBe(secondInstance.constructor); + expect(secondInstance.constructor).not.toBe(thirdInstance.constructor); + }); + }); + + 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(typeof properties.b).toBe('string'); + expect(typeof properties(2)).toBe('number'); + expect(typeof properties('2')).toBe('string'); + }); + }); +}); diff --git a/test/features/overloads/typeQuery.test.ts b/test/features/overloads/typeQuery.test.ts new file mode 100644 index 000000000..a9756a95a --- /dev/null +++ b/test/features/overloads/typeQuery.test.ts @@ -0,0 +1,30 @@ +import { createMock } from 'ts-auto-mock'; + +import { + exportedDeclaredOverloadedFunction, + ExportedDeclaredClass, +} from '../../transformer/descriptor/utils/typeQuery/typeQueryUtils'; + +describe('Overloads type query', () => { + + it('should assign the correct function mock for literal inputs', () => { + const functionMock: typeof exportedDeclaredOverloadedFunction = createMock(); + + expect(functionMock('', 0, false)).toMatch(/.{6}$/); + expect(typeof functionMock(false, '', 0)).toBe('boolean'); + expect(typeof functionMock(0, false, '')).toBe('number'); + expect(typeof functionMock(false, false, false)).toBe('boolean'); + expect(functionMock('')).toMatch(/.{6}$/); + expect(typeof functionMock(false)).toBe('boolean'); + expect(typeof functionMock(0)).toBe('number'); + }); + + it('should assign the correct function mock for mockable inputs', () => { + const classMock: typeof ExportedDeclaredClass = createMock(); + + const functionMock: typeof exportedDeclaredOverloadedFunction = createMock(); + + expect(typeof functionMock(new classMock()).prop).toBe('number'); + }); + +}); diff --git a/test/transformer/descriptor/utils/typeQuery/typeQueryUtils.ts b/test/transformer/descriptor/utils/typeQuery/typeQueryUtils.ts index 4bb98ed7f..939eb476d 100644 --- a/test/transformer/descriptor/utils/typeQuery/typeQueryUtils.ts +++ b/test/transformer/descriptor/utils/typeQuery/typeQueryUtils.ts @@ -1,5 +1,40 @@ 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; + +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; + export declare class ExportedDeclaredClass { public prop: string; }