Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[RFC] feature(transformer): Add function overload support #303

Draft
wants to merge 26 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
96e9695
enhancement(repository): Add hidden __factory property to mocks to di…
martinjlowm May 9, 2020
2bcf6d2
enhancement(transformer): Add declared function overload support
martinjlowm Apr 11, 2020
28bb478
enhancement(transformer): Add transformOverloads option
martinjlowm May 4, 2020
0268507
enhancement(transformer): Support overloads on interfaces
martinjlowm May 4, 2020
21ec311
enhancement(test): Extend method signature overload test to condition…
martinjlowm May 4, 2020
00d256f
fix(transformer): Revert to terminating early if a function declares …
martinjlowm May 4, 2020
fcf95de
enhancement(transformer): Fallback to `instanceof Object' control flo…
martinjlowm May 8, 2020
f0f1d13
fix(transformer): Properly cover overloads where no signatures declar…
martinjlowm May 8, 2020
001b087
enhancement(transformer): Implement conditional typing for direct moc…
martinjlowm May 8, 2020
0f1da32
chore(transformer): Move overload tests into its own file nested unde…
martinjlowm May 8, 2020
9921c9c
chore(transformer): Merge isLiteralRuntimeTypeNode and IsLiteralOrPri…
martinjlowm May 9, 2020
36deaa6
enhancement(transformer): Support non-primitive function arguments
martinjlowm May 9, 2020
d33b164
chore(transformer): Narrow signatures of mock factory call routines
martinjlowm May 9, 2020
c724bea
enhancement(transformer): Refactor GetMethodDescriptor implementation…
martinjlowm May 16, 2020
789beff
feature(transformer): Add serialization helpers including one for met…
martinjlowm May 16, 2020
5e16c9e
enhancement(transformer): Add helper function to extract the first id…
martinjlowm May 16, 2020
5b98fb4
chore(transformer): Move branching logic to its own file
martinjlowm May 17, 2020
38756df
chore(transformer): Filter method signatures based on the transformOv…
martinjlowm May 17, 2020
cc77dc8
chore(transformer): Adjust type signatures in bodyReturnType.ts
martinjlowm May 16, 2020
0a2d622
chore(transformer): Removed unused imports
martinjlowm May 17, 2020
03f5fac
chore(*): Rename __factory identifier to __ident and apply it to mock…
martinjlowm May 16, 2020
1cc0874
chore(transformer): Revert to using passed in propertyName as method …
martinjlowm May 17, 2020
18922aa
chore(test): Enable Date instance expectation in overloads test
martinjlowm May 19, 2020
bd96df9
chore(options): Move the overload transformation option into a featur…
martinjlowm May 19, 2020
8f7104c
chore(test): Add feature-gated and matrix based unit testing in CI
martinjlowm May 19, 2020
6eefd02
chore(transformer): Apply non-nullable check for getDeclarationKeyMap…
martinjlowm May 19, 2020
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,15 @@ jobs:
strategy:
matrix:
node-version: [10.x]
feature: ['', 'transformOverloads']

steps:
- uses: actions/checkout@v2
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v1
with:
node-version: ${{ matrix.node-version }}
- name: install ts auto mock and run test
- name: install ts auto mock and run test - ${{ matrix.feature }}
run: |
sudo apt-get update
sudo apt-get install -y libgbm-dev
Expand All @@ -25,5 +26,4 @@ jobs:
npm test
env:
CI: true


FEATURE: ${{ matrix.feature }}
3 changes: 2 additions & 1 deletion config/karma/karma.config.base.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,12 @@ module.exports = function(config, url) {
const processService = ProcessService(process);
const debug = processService.getArgument('DEBUG');
const disableCache = processService.getArgument('DISABLECACHE');
const feature = processService.getArgument('FEATURE') || process.env.FEATURE;

return {
basePath: '',
frameworks: ['jasmine'],
webpack: webpackConfig(debug, disableCache),
webpack: webpackConfig(debug, disableCache, feature),
webpackMiddleware: {
stats: 'errors-only'
},
Expand Down
11 changes: 9 additions & 2 deletions config/test/webpack.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
const transformer = require('../../dist/transformer');
const path = require('path');
const webpack = require('webpack');

module.exports = function (debug, disableCache) {
module.exports = function (debug, disableCache, feature = '') {
return {
mode: "development",
resolve: {
Expand All @@ -12,6 +13,11 @@ module.exports = function (debug, disableCache) {
['ts-auto-mock/extension']: path.join(__dirname, '../../dist/extension'),
}
},
plugins: [
new webpack.DefinePlugin({
'process.env.FEATURE': `"${feature}"`,
}),
],
module: {
rules: [
{
Expand All @@ -26,7 +32,8 @@ module.exports = function (debug, disableCache) {
getCustomTransformers: (program) => ({
before: [transformer.default(program, {
debug: debug ? debug : false,
cacheBetweenTests: disableCache !== 'true'
cacheBetweenTests: disableCache !== 'true',
features: [feature],
})]
})
}
Expand Down
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
1 change: 1 addition & 0 deletions src/options/default.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,5 @@ import { TsAutoMockOptions } from './options';
export const defaultOptions: TsAutoMockOptions = {
debug: false,
cacheBetweenTests: true,
features: [],
};
11 changes: 11 additions & 0 deletions src/options/features.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { GetOptionByKey } from './options';

interface Features {
transformOverloads: unknown;
}

export type TsAutoMockFeaturesOptions = Array<keyof Features>;

export function GetTsAutoMockFeaturesOptions(): TsAutoMockFeaturesOptions {
return GetOptionByKey('features');
}
2 changes: 2 additions & 0 deletions src/options/options.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import { TsAutoMockCacheOptions } from './cache';
import { TsAutoMockDebugOptions } from './debug';
import { TsAutoMockFeaturesOptions } from './features';
import { defaultOptions } from './default';

export interface TsAutoMockOptions {
debug: TsAutoMockDebugOptions;
cacheBetweenTests: TsAutoMockCacheOptions;
features: TsAutoMockFeaturesOptions;
}

let tsAutoMockOptions: TsAutoMockOptions = defaultOptions;
Expand Down
7 changes: 5 additions & 2 deletions src/repository/repository.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
type Factory = Function;
import { applyIdentityProperty } from '../utils/applyIdentityProperty';

// eslint-disable-next-line
type Factory = (...args: any[]) => any;

export class Repository {
private readonly _repository: { [key: string]: Factory };
Expand All @@ -15,7 +18,7 @@ export class Repository {
}

public registerFactory(key: string, factory: Factory): void {
this._repository[key] = factory;
this._repository[key] = applyIdentityProperty(factory, key);
}

public getFactory(key: string): Factory {
Expand Down
118 changes: 118 additions & 0 deletions src/transformer/descriptor/helper/branching.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
import ts from 'typescript';
import { MethodSignature } from '../../helper/creator';
import { TypescriptHelper } from '../helper/helper';
import { GetDescriptor } from '../descriptor';
import { Scope } from '../../scope/scope';

function CreateTypeEquality(typeVariableMap: Map<ts.TypeNode, ts.StringLiteral | ts.Identifier>, parameterType: ts.TypeNode | undefined, primaryDeclaration: ts.ParameterDeclaration): ts.Expression {
const declarationName: ts.Identifier = TypescriptHelper.ExtractFirstIdentifier(primaryDeclaration.name);

if (!parameterType) {
return ts.createPrefix(
ts.SyntaxKind.ExclamationToken,
ts.createPrefix(
ts.SyntaxKind.ExclamationToken,
declarationName,
),
);
}

if (TypescriptHelper.IsLiteralOrPrimitive(parameterType)) {
return ts.createStrictEquality(
ts.createTypeOf(declarationName),
parameterType ? ts.createStringLiteral(parameterType.getText()) : ts.createVoidZero(),
);
}

if (typeVariableMap.has(parameterType)) {
// eslint-disable-next-line
const parameterIdentifier: ts.StringLiteral | ts.Identifier = typeVariableMap.get(parameterType)!;
return ts.createStrictEquality(
ts.createLogicalAnd(declarationName, ts.createPropertyAccess(declarationName, '__ident')),
parameterIdentifier,
);
}
return ts.createBinary(declarationName, ts.SyntaxKind.InstanceOfKeyword, ts.createIdentifier('Object'));
}

function CreateUnionTypeOfEquality(
typeVariableMap: Map<ts.TypeNode, ts.StringLiteral | ts.Identifier>,
signatureType: ts.TypeNode | undefined,
primaryDeclaration: ts.ParameterDeclaration,
): ts.Expression {
const typeNodesAndVariableReferences: Array<ts.TypeNode> = [];

if (signatureType) {
if (ts.isTypeNode(signatureType) && ts.isUnionTypeNode(signatureType)) {
typeNodesAndVariableReferences.push(...signatureType.types);
} else {
typeNodesAndVariableReferences.push(signatureType);
}
}

const [firstType, ...remainingTypes]: Array<ts.TypeNode> = typeNodesAndVariableReferences;

return remainingTypes.reduce(
(prevStatement: ts.Expression, typeNode: ts.TypeNode) =>
ts.createLogicalOr(
prevStatement,
CreateTypeEquality(typeVariableMap, typeNode, primaryDeclaration),
),
CreateTypeEquality(typeVariableMap, firstType, primaryDeclaration),
);
}

function ResolveParameterBranch(
typeVariableMap: Map<ts.TypeNode, ts.StringLiteral | ts.Identifier>,
declarations: ts.NodeArray<ts.ParameterDeclaration> | [undefined],
longedSignature: MethodSignature,
returnType: ts.TypeNode,
elseBranch: ts.Statement,
scope: Scope,
): ts.Statement {
// NOTE: The strange signature here is to cover an empty list of declarations,
// then firstDeclaration will be undefined.
const [firstDeclaration, ...remainingDeclarations]: ts.NodeArray<ts.ParameterDeclaration> | [undefined] = declarations;

// 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(typeVariableMap, declaration.type, longedSignature.parameters[index + 1]),
),
CreateUnionTypeOfEquality(typeVariableMap, firstDeclaration?.type, longedSignature.parameters[0]),
);

return ts.createIf(condition, ts.createReturn(GetDescriptor(returnType, scope)), elseBranch);
}

export function ResolveSignatureElseBranch(
typeVariableMap: Map<ts.TypeNode, ts.StringLiteral | ts.Identifier>,
signatures: MethodSignature[],
longestParameterList: MethodSignature,
scope: Scope,
): ts.Statement {
const [signature, ...remainingSignatures]: MethodSignature[] = signatures;

const indistinctSignatures: boolean = signatures.every((sig: ts.MethodSignature) => !sig.parameters?.length);
if (!remainingSignatures.length || indistinctSignatures) {
return ts.createReturn(GetDescriptor(signature.type, scope));
}

const elseBranch: ts.Statement = ResolveSignatureElseBranch(typeVariableMap, remainingSignatures, longestParameterList, scope);

const currentParameters: ts.NodeArray<ts.ParameterDeclaration> = signature.parameters || [];
return ResolveParameterBranch(typeVariableMap, currentParameters, longestParameterList, signature.type, elseBranch, scope);
}
38 changes: 32 additions & 6 deletions src/transformer/descriptor/helper/helper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,38 @@ type Declaration = ts.InterfaceDeclaration | ts.ClassDeclaration | ts.TypeAliasD
type ImportDeclaration = ts.ImportEqualsDeclaration | ts.ImportOrExportSpecifier | ts.ImportClause;

export namespace TypescriptHelper {
export function IsLiteralOrPrimitive(typeNode: ts.Node): boolean {
return ts.isLiteralTypeNode(typeNode) ||
typeNode.kind === ts.SyntaxKind.StringKeyword ||
typeNode.kind === ts.SyntaxKind.BooleanKeyword ||
typeNode.kind === ts.SyntaxKind.NumberKeyword ||
typeNode.kind === ts.SyntaxKind.ArrayType;
export interface PrimitiveTypeNode extends ts.TypeNode {
kind: ts.SyntaxKind.LiteralType | ts.SyntaxKind.NumberKeyword | ts.SyntaxKind.ObjectKeyword | ts.SyntaxKind.BooleanKeyword | ts.SyntaxKind.StringKeyword | ts.SyntaxKind.ArrayType;
}

export function ExtractFirstIdentifier(bindingName: ts.BindingName): ts.Identifier {
let identifier: ts.BindingName = bindingName;
let saneSearchLimit: number = 10;

while (!ts.isIdentifier(identifier)) {
const [bindingElement]: Array<ts.BindingElement | undefined> = (identifier.elements as ts.NodeArray<ts.ArrayBindingElement>).filter(ts.isBindingElement);
if (!bindingElement || !--saneSearchLimit) {
throw new Error('Failed to find an identifier for the primary declaration!');
}

identifier = bindingElement.name;
}

return identifier;
}

export function IsLiteralOrPrimitive(typeNode: ts.Node): typeNode is PrimitiveTypeNode {
switch (typeNode.kind) {
case ts.SyntaxKind.LiteralType:
case ts.SyntaxKind.NumberKeyword:
case ts.SyntaxKind.ObjectKeyword:
case ts.SyntaxKind.BooleanKeyword:
case ts.SyntaxKind.StringKeyword:
case ts.SyntaxKind.ArrayType:
return true;
}

return false;
}

export function GetDeclarationFromNode(node: ts.Node): ts.Declaration {
Expand Down
14 changes: 8 additions & 6 deletions src/transformer/descriptor/method/bodyReturnType.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,21 +7,21 @@ export function GetReturnTypeFromBodyDescriptor(node: ts.ArrowFunction | ts.Func
return GetDescriptor(GetReturnNodeFromBody(node), scope);
}

export function GetReturnNodeFromBody(node: ts.FunctionLikeDeclaration): ts.Node {
let returnValue: ts.Node | undefined;
export function GetReturnNodeFromBody(node: ts.FunctionLikeDeclaration): ts.Expression {
let returnValue: ts.Expression | undefined;

const functionBody: ts.ConciseBody | undefined = node.body;

if (functionBody && ts.isBlock(functionBody)) {
const returnStatement: ts.ReturnStatement = GetReturnStatement(functionBody);
const returnStatement: ts.ReturnStatement | undefined = GetReturnStatement(functionBody);

if (returnStatement) {
returnValue = returnStatement.expression;
} else {
returnValue = GetNullDescriptor();
}
} else {
returnValue = node.body;
returnValue = functionBody;
}

if (!returnValue) {
Expand All @@ -31,6 +31,8 @@ export function GetReturnNodeFromBody(node: ts.FunctionLikeDeclaration): ts.Node
return returnValue;
}

function GetReturnStatement(body: ts.FunctionBody): ts.ReturnStatement {
return body.statements.find((statement: ts.Statement) => statement.kind === ts.SyntaxKind.ReturnStatement) as ts.ReturnStatement;
function GetReturnStatement(body: ts.FunctionBody): ts.ReturnStatement | undefined {
return body.statements.find(
(statement: ts.Statement): statement is ts.ReturnStatement => statement.kind === ts.SyntaxKind.ReturnStatement,
);
}
18 changes: 15 additions & 3 deletions src/transformer/descriptor/method/functionAssignment.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,26 @@
import * as ts from 'typescript';
import { Scope } from '../../scope/scope';
import { TypescriptCreator } from '../../helper/creator';
import { PropertySignatureCache } from '../property/cache';
import { GetReturnTypeFromBodyDescriptor } from './bodyReturnType';
import { GetReturnNodeFromBody } from './bodyReturnType';
import { GetMethodDescriptor } from './method';

type functionAssignment = ts.ArrowFunction | ts.FunctionExpression;

export function GetFunctionAssignmentDescriptor(node: functionAssignment, scope: Scope): ts.Expression {
const property: ts.PropertyName = PropertySignatureCache.instance.get();
const returnValue: ts.Expression = GetReturnTypeFromBodyDescriptor(node, scope);
const returnValue: ts.Expression = GetReturnNodeFromBody(node);

return GetMethodDescriptor(property, returnValue);
const returnType: ts.TypeNode = ts.createLiteralTypeNode(returnValue as ts.LiteralExpression);

return GetMethodDescriptor(
property,
[
TypescriptCreator.createMethodSignature(
undefined,
returnType,
),
],
scope,
);
}
Loading