Skip to content

Commit c1f7cb2

Browse files
committed
feat(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(<signature>, []) ``` 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[<signature>] || (mockedNewFunction[<signature>] = function() { Object.assign(this, mockedNewFunction.apply(<signature>, [])); }))() ``` 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 = { ['<signature-1>']: () => <signature-1-return-descriptor>, ['<signature-2>']: () => <signature-2-return-descriptor>, ... }; 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.
1 parent edc272d commit c1f7cb2

File tree

23 files changed

+366
-95
lines changed

23 files changed

+366
-95
lines changed

config/utils/features.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,8 @@ function DetermineFeaturesFromEnvironment() {
66

77
if (features) {
88
return [
9-
'random'
9+
'overloads',
10+
'random',
1011
];
1112
}
1213

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
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 function(...args: any[]): any {
5+
return value.apply(this, args);
6+
};
57
}

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/options/features.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
export type TsAutoMockFeaturesOption = 'random';
1+
export type TsAutoMockFeaturesOption = 'random' | 'overloads';

src/options/overloads.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import { GetOptionByKey } from './options';
2+
3+
export function IsTsAutoMockOverloadsEnabled(): boolean {
4+
return GetOptionByKey('features').includes('overloads');
5+
}

src/transformer/base/base.ts

Lines changed: 61 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,17 @@
1-
import * as ts from 'typescript';
1+
import ts from 'typescript';
2+
import { IsTsAutoMockOverloadsEnabled } from '../../options/overloads';
23
import { SetTsAutoMockOptions, TsAutoMockOptions } from '../../options/options';
34
import { SetTypeChecker } from '../typeChecker/typeChecker';
45
import { MockDefiner } from '../mockDefiner/mockDefiner';
56
import { SetProgram } from '../program/program';
67
import { TypescriptHelper } from '../descriptor/helper/helper';
8+
import { TypescriptCreator } from '../helper/creator';
79
import {
810
CustomFunction,
911
isFunctionFromThisLibrary,
1012
} from '../matcher/matcher';
1113

12-
export type Visitor = (node: ts.CallExpression & { typeArguments: ts.NodeArray<ts.TypeNode> }, declaration: ts.FunctionDeclaration) => ts.Node;
14+
export type Visitor = (node: ts.CallLikeExpression & { typeArguments: ts.NodeArray<ts.TypeNode> }, declaration: ts.SignatureDeclaration) => ts.Node;
1315

1416
export function baseTransformer(visitor: Visitor, customFunctions: CustomFunction[]): (program: ts.Program, options?: TsAutoMockOptions) => ts.TransformerFactory<ts.SourceFile> {
1517
return (program: ts.Program, options?: TsAutoMockOptions): ts.TransformerFactory<ts.SourceFile> => {
@@ -47,14 +49,69 @@ function isObjectWithProperty<T extends {}, K extends keyof T>(
4749
return typeof obj[key] !== 'undefined';
4850
}
4951

52+
function isMockedByThisLibrary(declaration: ts.Declaration): boolean {
53+
return MockDefiner.instance.hasKeyForDeclaration(declaration);
54+
}
55+
5056
function visitNode(node: ts.Node, visitor: Visitor, customFunctions: CustomFunction[]): ts.Node {
51-
if (!ts.isCallExpression(node)) {
57+
if (!ts.isCallExpression(node) && !ts.isNewExpression(node)) {
5258
return node;
5359
}
5460

5561
const signature: ts.Signature | undefined = TypescriptHelper.getSignatureOfCallExpression(node);
62+
const declaration: ts.Declaration | undefined = signature?.declaration;
63+
64+
if (!declaration || !ts.isFunctionLike(declaration)) {
65+
return node;
66+
}
67+
68+
if (IsTsAutoMockOverloadsEnabled() && isMockedByThisLibrary(declaration)) {
69+
const mockKey: string = MockDefiner.instance.getDeclarationKeyMap(declaration);
70+
const mockKeyLiteral: ts.StringLiteral = ts.createStringLiteral(mockKey);
71+
72+
const boundSignatureCall: ts.CallExpression = TypescriptCreator.createCall(
73+
ts.createPropertyAccess(
74+
node.expression,
75+
ts.createIdentifier('apply'),
76+
),
77+
[mockKeyLiteral, ts.createArrayLiteral(node.arguments)],
78+
);
79+
80+
if (ts.isCallExpression(node)) {
81+
return boundSignatureCall;
82+
}
5683

57-
if (!signature || !isFunctionFromThisLibrary(signature, customFunctions)) {
84+
return ts.createNew(
85+
TypescriptCreator.createCachedAssignment(
86+
ts.createElementAccess(
87+
node.expression,
88+
mockKeyLiteral,
89+
),
90+
TypescriptCreator.createFunctionExpression(
91+
ts.createBlock(
92+
[
93+
ts.createExpressionStatement(
94+
TypescriptCreator.createCall(
95+
ts.createPropertyAccess(
96+
ts.createIdentifier('Object'),
97+
ts.createIdentifier('assign'),
98+
),
99+
[
100+
ts.createIdentifier('this'),
101+
boundSignatureCall,
102+
]
103+
),
104+
),
105+
],
106+
)
107+
),
108+
),
109+
undefined,
110+
undefined,
111+
);
112+
}
113+
114+
if (!isFunctionFromThisLibrary(declaration, customFunctions)) {
58115
return node;
59116
}
60117

@@ -73,7 +130,5 @@ function visitNode(node: ts.Node, visitor: Visitor, customFunctions: CustomFunct
73130
MockDefiner.instance.setFileNameFromNode(nodeToMock);
74131
MockDefiner.instance.setTsAutoMockImportIdentifier();
75132

76-
const declaration: ts.FunctionDeclaration = signature.declaration as ts.FunctionDeclaration;
77-
78133
return visitor(node, declaration);
79134
}

src/transformer/descriptor/helper/helper.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,7 @@ export namespace TypescriptHelper {
104104
}
105105

106106

107-
export function getSignatureOfCallExpression(node: ts.CallExpression): ts.Signature | undefined {
107+
export function getSignatureOfCallExpression(node: ts.CallLikeExpression): ts.Signature | undefined {
108108
const typeChecker: ts.TypeChecker = TypeChecker();
109109

110110
return typeChecker.getResolvedSignature(node);

src/transformer/descriptor/method/bodyReturnType.ts

Lines changed: 15 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -7,30 +7,28 @@ export function GetReturnTypeFromBodyDescriptor(node: ts.ArrowFunction | ts.Func
77
return GetDescriptor(GetReturnNodeFromBody(node), scope);
88
}
99

10-
export function GetReturnNodeFromBody(node: ts.FunctionLikeDeclaration): ts.Node {
11-
let returnValue: ts.Node | undefined;
12-
10+
export function GetReturnNodeFromBody<T extends ts.Node & { body?: ts.ConciseBody }>(node: T): ts.Expression {
1311
const functionBody: ts.ConciseBody | undefined = node.body;
1412

15-
if (functionBody && ts.isBlock(functionBody)) {
16-
const returnStatement: ts.ReturnStatement = GetReturnStatement(functionBody);
13+
if (!functionBody) {
14+
return GetNullDescriptor();
15+
}
1716

18-
if (returnStatement) {
19-
returnValue = returnStatement.expression;
20-
} else {
21-
returnValue = GetNullDescriptor();
22-
}
23-
} else {
24-
returnValue = node.body;
17+
if (!ts.isBlock(functionBody)) {
18+
return functionBody;
2519
}
2620

27-
if (!returnValue) {
28-
throw new Error(`Failed to determine the return value of ${node.getText()}.`);
21+
const returnStatement: ts.ReturnStatement | undefined = GetReturnStatement(functionBody);
22+
23+
if (!returnStatement?.expression) {
24+
return GetNullDescriptor();
2925
}
3026

31-
return returnValue;
27+
return returnStatement.expression;
3228
}
3329

34-
function GetReturnStatement(body: ts.FunctionBody): ts.ReturnStatement {
35-
return body.statements.find((statement: ts.Statement) => statement.kind === ts.SyntaxKind.ReturnStatement) as ts.ReturnStatement;
30+
function GetReturnStatement(body: ts.FunctionBody): ts.ReturnStatement | undefined {
31+
return body.statements.find(
32+
(statement: ts.Statement): statement is ts.ReturnStatement => statement.kind === ts.SyntaxKind.ReturnStatement,
33+
);
3634
}
Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,12 @@
1-
import * as ts from 'typescript';
1+
import ts from 'typescript';
22
import { Scope } from '../../scope/scope';
33
import { PropertySignatureCache } from '../property/cache';
4-
import { GetReturnTypeFromBodyDescriptor } from './bodyReturnType';
54
import { GetMethodDescriptor } from './method';
65

76
type FunctionAssignment = ts.ArrowFunction | ts.FunctionExpression;
87

98
export function GetFunctionAssignmentDescriptor(node: FunctionAssignment, scope: Scope): ts.Expression {
109
const property: ts.PropertyName = PropertySignatureCache.instance.get();
11-
const returnValue: ts.Expression = GetReturnTypeFromBodyDescriptor(node, scope);
1210

13-
return GetMethodDescriptor(property, returnValue);
11+
return GetMethodDescriptor(property, [node], scope);
1412
}
Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import * as ts from 'typescript';
22
import { Scope } from '../../scope/scope';
3-
import { GetDescriptor } from '../descriptor';
43
import { PropertySignatureCache } from '../property/cache';
54
import { GetMethodDescriptor } from './method';
65

@@ -11,7 +10,5 @@ export function GetFunctionTypeDescriptor(node: ts.FunctionTypeNode | ts.CallSig
1110
throw new Error(`No type was declared for ${node.getText()}.`);
1211
}
1312

14-
const returnValue: ts.Expression = GetDescriptor(node.type, scope);
15-
16-
return GetMethodDescriptor(property, returnValue);
13+
return GetMethodDescriptor(property, [node], scope);
1714
}

0 commit comments

Comments
 (0)