Skip to content

Commit b3a4613

Browse files
committed
enhancement(transformer): Add declared function overload support
1 parent 0563912 commit b3a4613

File tree

10 files changed

+208
-18
lines changed

10 files changed

+208
-18
lines changed
Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
// eslint-disable-next-line @typescript-eslint/no-explicit-any
2-
export function functionMethod(name: string, value: () => any): any {
2+
export function functionMethod(name: string, value: (...args: any[]) => any): any {
33
// eslint-disable-next-line @typescript-eslint/no-explicit-any
4-
return (): any => value();
4+
return (...args: any[]): any => value(...args);
55
}

src/merge/merge.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { merge} from 'lodash-es';
1+
import { merge } from 'lodash-es';
22
import { DeepPartial } from '../partial/deepPartial';
33

44
export class Merge {

src/transformer/descriptor/helper/helper.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,22 @@ export namespace TypescriptHelper {
119119
return !!((symbol.flags & ts.SymbolFlags.Alias) || (symbol.flags & ts.SymbolFlags.AliasExcludes));
120120
}
121121

122+
export interface RuntimeTypeNode extends ts.TypeNode {
123+
kind: ts.SyntaxKind.NumberKeyword | ts.SyntaxKind.ObjectKeyword | ts.SyntaxKind.BooleanKeyword | ts.SyntaxKind.StringKeyword | ts.SyntaxKind.UndefinedKeyword;
124+
}
125+
126+
export function isLiteralRuntimeTypeNode(typeNode: ts.TypeNode): typeNode is RuntimeTypeNode {
127+
switch (typeNode.kind) {
128+
case ts.SyntaxKind.NumberKeyword:
129+
case ts.SyntaxKind.ObjectKeyword:
130+
case ts.SyntaxKind.BooleanKeyword:
131+
case ts.SyntaxKind.StringKeyword:
132+
return true;
133+
}
134+
135+
return false;
136+
}
137+
122138
function isImportExportDeclaration(declaration: ts.Declaration): declaration is ImportDeclaration {
123139
return ts.isImportEqualsDeclaration(declaration) || ts.isImportOrExportSpecifier(declaration) || ts.isImportClause(declaration);
124140
}

src/transformer/descriptor/method/functionAssignment.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,5 +10,5 @@ export function GetFunctionAssignmentDescriptor(node: functionAssignment, scope:
1010
const property: ts.PropertyName = PropertySignatureCache.instance.get();
1111
const returnValue: ts.Expression = GetReturnTypeFromBodyDescriptor(node, scope);
1212

13-
return GetMethodDescriptor(property, returnValue);
13+
return GetMethodDescriptor(property, [{ returnValue }]);
1414
}

src/transformer/descriptor/method/functionType.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,5 +13,5 @@ export function GetFunctionTypeDescriptor(node: ts.FunctionTypeNode | ts.CallSig
1313

1414
const returnValue: ts.Expression = GetDescriptor(node.type, scope);
1515

16-
return GetMethodDescriptor(property, returnValue);
16+
return GetMethodDescriptor(property, [{ returnValue }]);
1717
}

src/transformer/descriptor/method/method.ts

Lines changed: 101 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,119 @@
1-
import * as ts from 'typescript';
1+
import ts from 'typescript';
22
import { TypescriptCreator } from '../../helper/creator';
33
import { MockDefiner } from '../../mockDefiner/mockDefiner';
44
import { ModuleName } from '../../mockDefiner/modules/moduleName';
55
import { TypescriptHelper } from '../helper/helper';
66

7-
export function GetMethodDescriptor(propertyName: ts.PropertyName, returnValue: ts.Expression): ts.Expression {
7+
export interface MethodSignature {
8+
parameters?: ts.ParameterDeclaration[];
9+
returnValue: ts.Expression;
10+
}
11+
12+
export function GetMethodDescriptor(propertyName: ts.PropertyName, methodSignatures: MethodSignature[]): ts.Expression {
813
const providerGetMethod: ts.PropertyAccessExpression = CreateProviderGetMethod();
914

1015
const propertyNameString: string = TypescriptHelper.GetStringPropertyName(propertyName);
1116
const propertyNameStringLiteral: ts.StringLiteral = ts.createStringLiteral(propertyNameString);
1217

13-
const propertyValueFunction: ts.ArrowFunction = TypescriptCreator.createArrowFunction(ts.createBlock(
14-
[ts.createReturn(returnValue)],
18+
const [signatureWithMostParameters]: MethodSignature[] = [...methodSignatures].sort(
19+
(
20+
{ parameters: leftParameters = [] }: MethodSignature,
21+
{ parameters: rightParameters = [] }: MethodSignature,
22+
) => rightParameters.length - leftParameters.length,
23+
);
24+
25+
const longestParameterList: ts.ParameterDeclaration[] = signatureWithMostParameters.parameters || [];
26+
27+
const block: ts.Block = ts.createBlock(
28+
[
29+
ResolveSignatureElseBranch(methodSignatures, longestParameterList),
30+
],
1531
true,
16-
));
32+
);
33+
34+
const propertyValueFunction: ts.ArrowFunction = TypescriptCreator.createArrowFunction(
35+
block,
36+
longestParameterList,
37+
);
1738

1839
return TypescriptCreator.createCall(providerGetMethod, [propertyNameStringLiteral, propertyValueFunction]);
1940
}
2041

42+
function CreateTypeEquality(signatureType: ts.TypeNode | undefined, primaryDeclaration: ts.ParameterDeclaration): ts.Expression {
43+
const identifier: ts.Identifier = ts.createIdentifier(primaryDeclaration.name.getText());
44+
45+
if (!signatureType) {
46+
return ts.createPrefix(
47+
ts.SyntaxKind.ExclamationToken,
48+
ts.createPrefix(
49+
ts.SyntaxKind.ExclamationToken,
50+
identifier,
51+
),
52+
);
53+
}
54+
55+
if (TypescriptHelper.isLiteralRuntimeTypeNode(signatureType)) {
56+
return ts.createStrictEquality(
57+
ts.createTypeOf(identifier),
58+
signatureType ? ts.createStringLiteral(signatureType.getText()) : ts.createVoidZero(),
59+
);
60+
} else {
61+
return ts.createBinary(identifier, ts.SyntaxKind.InstanceOfKeyword, ts.createIdentifier(signatureType.getText()));
62+
}
63+
}
64+
65+
function CreateUnionTypeOfEquality(signatureType: ts.TypeNode | undefined, primaryDeclaration: ts.ParameterDeclaration): ts.Expression {
66+
const typeNodes: ts.TypeNode[] = [];
67+
68+
if (signatureType) {
69+
if (ts.isUnionTypeNode(signatureType)) {
70+
typeNodes.push(...signatureType.types);
71+
} else {
72+
typeNodes.push(signatureType);
73+
}
74+
}
75+
76+
const [firstType, ...remainingTypes]: ts.TypeNode[] = typeNodes;
77+
78+
return remainingTypes.reduce(
79+
(prevStatement: ts.Expression, typeNode: ts.TypeNode) =>
80+
ts.createLogicalOr(
81+
prevStatement,
82+
CreateTypeEquality(typeNode, primaryDeclaration),
83+
),
84+
CreateTypeEquality(firstType, primaryDeclaration),
85+
);
86+
}
87+
88+
function ResolveParameterBranch(declarations: ts.ParameterDeclaration[], allDeclarations: ts.ParameterDeclaration[], returnValue: ts.Expression, elseBranch: ts.Statement): ts.Statement {
89+
const [firstDeclaration, ...remainingDeclarations]: ts.ParameterDeclaration[] = declarations;
90+
91+
const condition: ts.Expression = remainingDeclarations.reduce(
92+
(prevStatement: ts.Expression, declaration: ts.ParameterDeclaration, index: number) =>
93+
ts.createLogicalAnd(
94+
prevStatement,
95+
CreateUnionTypeOfEquality(declaration.type, allDeclarations[index + 1]),
96+
),
97+
CreateUnionTypeOfEquality(firstDeclaration.type, allDeclarations[0]),
98+
);
99+
100+
101+
return ts.createIf(condition, ts.createReturn(returnValue), elseBranch);
102+
}
103+
104+
function ResolveSignatureElseBranch(signatures: MethodSignature[], longestParameterList: ts.ParameterDeclaration[]): ts.Statement {
105+
const [signature, ...remainingSignatures]: MethodSignature[] = signatures;
106+
107+
if (remainingSignatures.length) {
108+
const elseBranch: ts.Statement = ResolveSignatureElseBranch(remainingSignatures, longestParameterList);
109+
110+
const currentParameters: ts.ParameterDeclaration[] = signature.parameters || [];
111+
return ResolveParameterBranch(currentParameters, longestParameterList, signature.returnValue, elseBranch);
112+
} else {
113+
return ts.createReturn(signature.returnValue);
114+
}
115+
}
116+
21117
function CreateProviderGetMethod(): ts.PropertyAccessExpression {
22118
return ts.createPropertyAccess(
23119
ts.createPropertyAccess(
Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,38 @@
11
import * as ts from 'typescript';
22
import { Scope } from '../../scope/scope';
3+
import { TypeChecker } from '../../typeChecker/typeChecker';
34
import { GetDescriptor } from '../descriptor';
45
import { GetFunctionReturnType } from './functionReturnType';
5-
import { GetMethodDescriptor } from './method';
6+
import { GetMethodDescriptor, MethodSignature } from './method';
67

78
export function GetMethodDeclarationDescriptor(node: ts.MethodDeclaration | ts.FunctionDeclaration, scope: Scope): ts.Expression {
8-
const returnTypeNode: ts.Node = GetFunctionReturnType(node);
9-
const returnType: ts.Expression = GetDescriptor(returnTypeNode, scope);
9+
const declarationType: ts.Type | undefined = TypeChecker().getTypeAtLocation(node);
10+
const methodDeclarations: Array<ts.MethodDeclaration | ts.FunctionDeclaration> = declarationType.symbol.declarations
11+
.filter(
12+
(declaration: ts.Declaration): declaration is ts.MethodDeclaration | ts.FunctionDeclaration =>
13+
ts.isMethodDeclaration(declaration) || ts.isFunctionDeclaration(declaration)
14+
);
15+
16+
if (!methodDeclarations.length) {
17+
methodDeclarations.push(node);
18+
}
19+
20+
const methodSignatures: MethodSignature[] = methodDeclarations.map(
21+
(declaration: ts.MethodDeclaration | ts.FunctionDeclaration) => {
22+
const returnTypeNode: ts.Node = GetFunctionReturnType(declaration);
23+
24+
return {
25+
parameters: declaration.parameters.map((parameter: ts.ParameterDeclaration) => parameter),
26+
returnValue: GetDescriptor(returnTypeNode, scope),
27+
};
28+
}
29+
);
1030

1131
if (!node.name) {
1232
throw new Error(
1333
`The transformer couldn't determine the name of ${node.getText()}. Please report this incident.`,
1434
);
1535
}
1636

17-
return GetMethodDescriptor(node.name, returnType);
37+
return GetMethodDescriptor(node.name, methodSignatures);
1838
}

src/transformer/descriptor/method/methodSignature.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,13 @@ import { GetNullDescriptor } from '../null/null';
55
import { GetMethodDescriptor } from './method';
66

77
export function GetMethodSignatureDescriptor(node: ts.MethodSignature, scope: Scope): ts.Expression {
8-
let returnType: ts.Expression;
8+
let returnValue: ts.Expression;
99

1010
if (node.type) {
11-
returnType = GetDescriptor(node.type, scope);
11+
returnValue = GetDescriptor(node.type, scope);
1212
} else {
13-
returnType = GetNullDescriptor();
13+
returnValue = GetNullDescriptor();
1414
}
1515

16-
return GetMethodDescriptor(node.name, returnType);
16+
return GetMethodDescriptor(node.name, [{ returnValue }]);
1717
}

test/transformer/descriptor/typeQuery/typeQuery.test.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import {
66
ExportedClass,
77
ExportedDeclaredClass,
88
exportedDeclaredFunction,
9+
exportedDeclaredOverloadedFunction,
910
ExportedEnum,
1011
exportedFunction,
1112
WrapExportedClass,
@@ -41,6 +42,27 @@ describe('typeQuery', () => {
4142
expect(functionMock()).toEqual('');
4243
});
4344

45+
it('should assign the correct function mock for an imported and overloaded function declaration', () => {
46+
const functionMock: typeof exportedDeclaredOverloadedFunction = createMock<typeof exportedDeclaredOverloadedFunction>();
47+
48+
// eslint-disable-next-line
49+
const expectations = [
50+
{ args: ['', 0, false], returnValue: '' },
51+
{ args: [false, '', 0], returnValue: false },
52+
{ args: [0, false, ''], returnValue: 0 },
53+
{ args: [false, false, false], returnValue: false },
54+
{ args: [''], returnValue: '' },
55+
{ args: [false], returnValue: false },
56+
{ args: [0], returnValue: 0 },
57+
];
58+
59+
for (const { args, returnValue } of expectations) {
60+
// eslint-disable-next-line
61+
const [first, second, third] = args;
62+
expect(functionMock(first, second, third)).toEqual(returnValue);
63+
}
64+
});
65+
4466
it('should assign the function mock for an imported function declaration with body', () => {
4567
const functionMock: typeof exportedFunction = createMock<typeof exportedFunction>();
4668

test/transformer/descriptor/utils/typeQuery/typeQueryUtils.ts

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,41 @@
11
export declare function exportedDeclaredFunction(): string;
22

3+
export declare function exportedDeclaredOverloadedFunction(a: boolean, b: boolean, c: number): boolean;
4+
export declare function exportedDeclaredOverloadedFunction(a: boolean, b: boolean, c: string): boolean;
5+
export declare function exportedDeclaredOverloadedFunction(a: boolean, b: number, c: number): boolean;
6+
export declare function exportedDeclaredOverloadedFunction(a: boolean, b: number, c: string): boolean;
7+
export declare function exportedDeclaredOverloadedFunction(a: boolean, b: string, c: number): boolean;
8+
export declare function exportedDeclaredOverloadedFunction(a: boolean, b: string, c: string): boolean;
9+
export declare function exportedDeclaredOverloadedFunction(a: number, b: boolean, c: boolean): number;
10+
export declare function exportedDeclaredOverloadedFunction(a: number, b: boolean, c: string): number;
11+
export declare function exportedDeclaredOverloadedFunction(a: number, b: number, c: boolean): number;
12+
export declare function exportedDeclaredOverloadedFunction(a: number, b: number, c: string): number;
13+
export declare function exportedDeclaredOverloadedFunction(a: number, b: string, c: boolean): number;
14+
export declare function exportedDeclaredOverloadedFunction(a: number, b: string, c: string): number;
15+
export declare function exportedDeclaredOverloadedFunction(a: string, b: boolean, c: boolean): string;
16+
export declare function exportedDeclaredOverloadedFunction(a: string, b: boolean, c: number): string;
17+
export declare function exportedDeclaredOverloadedFunction(a: string, b: number, c: boolean): string;
18+
export declare function exportedDeclaredOverloadedFunction(a: string, b: number, c: number): string;
19+
export declare function exportedDeclaredOverloadedFunction(a: string, b: string, c: boolean): string;
20+
export declare function exportedDeclaredOverloadedFunction(a: string, b: string, c: number): string;
21+
export declare function exportedDeclaredOverloadedFunction(a: number, b: number, c: number): number;
22+
export declare function exportedDeclaredOverloadedFunction(a: boolean, b: boolean, c: boolean): boolean;
23+
export declare function exportedDeclaredOverloadedFunction(a: string, b: string, c: string): string;
24+
export declare function exportedDeclaredOverloadedFunction(a: string, b: number, c: string): string;
25+
export declare function exportedDeclaredOverloadedFunction(a: string, b: boolean, c: string): string;
26+
export declare function exportedDeclaredOverloadedFunction(a: number, b: string, c: number): number;
27+
export declare function exportedDeclaredOverloadedFunction(a: number, b: boolean, c: number): number;
28+
export declare function exportedDeclaredOverloadedFunction(a: boolean, b: number, c: boolean): boolean;
29+
export declare function exportedDeclaredOverloadedFunction(a: boolean, b: string, c: boolean): boolean;
30+
export declare function exportedDeclaredOverloadedFunction(a: string | number | boolean, b: string | number | boolean, c: string | number | boolean): string | number | boolean;
31+
// TODO: ExportedClass may need to be mocked and it is not imported as of this
32+
// writing. The transformation does take `a instanceof ExportedClass` into
33+
// consideration though.
34+
// export declare function exportedDeclaredOverloadedFunction(a: ExportedClass): ExportedClass;
35+
export declare function exportedDeclaredOverloadedFunction(a: boolean): boolean;
36+
export declare function exportedDeclaredOverloadedFunction(a: number): number;
37+
export declare function exportedDeclaredOverloadedFunction(a: string): string;
38+
339
export declare class ExportedDeclaredClass {
440
public prop: string;
541
}

0 commit comments

Comments
 (0)