Skip to content

Commit

Permalink
enhancement(transformer): Add declared function overload support
Browse files Browse the repository at this point in the history
  • Loading branch information
martinjlowm committed Apr 11, 2020
1 parent dac8968 commit 00902c1
Show file tree
Hide file tree
Showing 10 changed files with 208 additions and 18 deletions.
4 changes: 2 additions & 2 deletions src/extension/method/provider/functionMethod.ts
Original file line number Diff line number Diff line change
@@ -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);
}
2 changes: 1 addition & 1 deletion src/merge/merge.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { merge} from 'lodash-es';
import { merge } from 'lodash-es';
import { DeepPartial } from '../partial/deepPartial';

export class Merge {
Expand Down
16 changes: 16 additions & 0 deletions src/transformer/descriptor/helper/helper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,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);
}
Expand Down
2 changes: 1 addition & 1 deletion src/transformer/descriptor/method/functionAssignment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 }]);
}
2 changes: 1 addition & 1 deletion src/transformer/descriptor/method/functionType.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 }]);
}
106 changes: 101 additions & 5 deletions src/transformer/descriptor/method/method.ts
Original file line number Diff line number Diff line change
@@ -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(
Expand Down
28 changes: 24 additions & 4 deletions src/transformer/descriptor/method/methodDeclaration.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,36 @@
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<ts.MethodDeclaration | ts.FunctionDeclaration> = 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('Unhandled');
}

return GetMethodDescriptor(node.name, returnType);
return GetMethodDescriptor(node.name, methodSignatures);
}
8 changes: 4 additions & 4 deletions src/transformer/descriptor/method/methodSignature.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 }]);
}
22 changes: 22 additions & 0 deletions test/transformer/descriptor/typeQuery/typeQuery.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
ExportedClass,
ExportedDeclaredClass,
exportedDeclaredFunction,
exportedDeclaredOverloadedFunction,
ExportedEnum,
exportedFunction,
WrapExportedClass,
Expand Down Expand Up @@ -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<typeof exportedDeclaredOverloadedFunction>();

// 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<typeof exportedFunction>();

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

0 comments on commit 00902c1

Please sign in to comment.