Skip to content

Commit

Permalink
enhancement(transformer): Support non-primitive function arguments
Browse files Browse the repository at this point in the history
  • Loading branch information
martinjlowm committed May 9, 2020
1 parent d69e0b6 commit 4e5a9bc
Show file tree
Hide file tree
Showing 3 changed files with 108 additions and 40 deletions.
128 changes: 99 additions & 29 deletions src/transformer/descriptor/method/method.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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[];
Expand All @@ -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<ts.ParameterDeclaration, ts.Identifier> = new Map<ts.ParameterDeclaration, ts.Identifier>();

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,
Expand All @@ -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) {
Expand All @@ -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<ts.TypeNode | ts.Identifier> = [];

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<ts.TypeNode | ts.Identifier> = typeNodesAndVariableReferences;

return remainingTypes.reduce(
(prevStatement: ts.Expression, typeNode: ts.TypeNode) =>
Expand All @@ -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<ts.ParameterDeclaration, ts.Identifier>,
declarations: ts.ParameterDeclaration[],
allDeclarations: ts.ParameterDeclaration[],
returnValue: ts.Expression,
elseBranch: ts.Statement,
): ts.Statement {
const [firstDeclaration, ...remainingDeclarations]: Array<ts.ParameterDeclaration | undefined> = 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<ts.ParameterDeclaration, ts.Identifier>,
signatures: MethodSignature[],
longestParameterList: ts.ParameterDeclaration[],
): ts.Statement {
const transformOverloadsOption: TsAutoMockOverloadOptions = GetTsAutoMockOverloadOptions();

const [signature, ...remainingSignatures]: MethodSignature[] = signatures.filter((_: unknown, notFirst: number) => transformOverloadsOption || !notFirst);
Expand All @@ -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 {
Expand Down
13 changes: 6 additions & 7 deletions test/transformer/descriptor/methods/overloads.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { createMock } from 'ts-auto-mock';

import {
exportedDeclaredOverloadedFunction,
// ExportedDeclaredClass,
ExportedDeclaredClass,
} from '../utils/typeQuery/typeQueryUtils';

describe('for overloads', () => {
Expand Down Expand Up @@ -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<typeof ExportedDeclaredClass>();
it('should assign the correct function mock for mockable inputs', () => {
const classMock: typeof ExportedDeclaredClass = createMock<typeof ExportedDeclaredClass>();

// const functionMock: typeof exportedDeclaredOverloadedFunction = createMock<typeof exportedDeclaredOverloadedFunction>();
const functionMock: typeof exportedDeclaredOverloadedFunction = createMock<typeof exportedDeclaredOverloadedFunction>();

// expect(functionMock(new classMock())).toBeInstanceOf(ExportedDeclaredClass);
// });
expect(functionMock(new classMock()).prop).toBe(0);
});

});

Expand Down
7 changes: 3 additions & 4 deletions test/transformer/descriptor/utils/typeQuery/typeQueryUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down

0 comments on commit 4e5a9bc

Please sign in to comment.