diff --git a/src/transformer/descriptor/method/method.ts b/src/transformer/descriptor/method/method.ts index 6e0eaeda8..31151ce26 100644 --- a/src/transformer/descriptor/method/method.ts +++ b/src/transformer/descriptor/method/method.ts @@ -4,7 +4,6 @@ import { TypescriptCreator } from '../../helper/creator'; import { MockDefiner } from '../../mockDefiner/mockDefiner'; import { ModuleName } from '../../mockDefiner/modules/moduleName'; import { TypescriptHelper } from '../helper/helper'; -import { TransformerLogger } from '../../logger/transformerLogger'; export interface MethodSignature { parameters?: ts.ParameterDeclaration[]; @@ -17,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, @@ -41,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) { @@ -59,25 +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! - TransformerLogger().overloadNonLiteralParameterNotSupported(signatureType.getText()); - 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) => @@ -89,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); @@ -114,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;