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): Conditional typing #318

Draft
wants to merge 30 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 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
17bb657
chore(transformer): Enforce definitive signature for getDeclarationKe…
martinjlowm May 9, 2020
31945d2
feature(transformer): First iteration of conditional type support
martinjlowm May 10, 2020
4959ed9
chore(*): Rename __factory identifier to __ident and apply it to mock…
martinjlowm May 16, 2020
e92a2b3
chore(transformer): Refactor branch logic and support conditional typing
martinjlowm May 16, 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);
}
7 changes: 6 additions & 1 deletion src/extension/method/provider/provider.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { applyIdentityProperty } from '../../../utils/applyIdentityProperty';
import { functionMethod } from './functionMethod';
// eslint-disable-next-line @typescript-eslint/no-explicit-any
type Method = (name: string, value: any) => () => any;
Expand Down Expand Up @@ -45,6 +46,10 @@ export class Provider {
return this._method(name, value());
}

return this._method(name, value);
// FIXME: Do this smarter, it's a bit counter intuitive to return a new
// proxy every single time this function is called. It should probably mock
// based on name if that ends up being a string representing the type
// signature.
return applyIdentityProperty(this._method, name)(name, value);
}
}
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
100 changes: 100 additions & 0 deletions src/transformer/descriptor/conditionalType/conditionalType.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
import ts from 'typescript';
import { MethodSignature, TypescriptCreator } from '../../helper/creator';
import { Scope } from '../../scope/scope';
import { ResolveSignatureElseBranch } from '../helper/branching';
import { TypescriptHelper } from '../helper/helper';
import { GetNullDescriptor } from '../null/null';
import { GetTypeParameterDescriptor } from '../typeParameter/typeParameter';

function isDeclarationWithTypeParameterChildren(node: ts.Node): node is ts.DeclarationWithTypeParameterChildren {
return ts.isFunctionLike(node) ||
ts.isClassLike(node) ||
ts.isInterfaceDeclaration(node) ||
ts.isTypeAliasDeclaration(node);
}

export function GetConditionalTypeDescriptor(node: ts.ConditionalTypeNode, scope: Scope): ts.Expression {
const parentNode: ts.Node = node.parent;
if (!isDeclarationWithTypeParameterChildren(parentNode)) {
return GetNullDescriptor();
}

const checkType: ts.TypeNode = node.checkType;
if (!ts.isTypeReferenceNode(checkType)) {
return GetNullDescriptor();
}

const typeName: ts.EntityName = checkType.typeName;
if (ts.isQualifiedName(typeName)) {
return GetNullDescriptor();
}

const declarations: readonly ts.TypeParameterDeclaration[] = ts.getEffectiveTypeParameterDeclarations(parentNode);

const declaration: ts.TypeParameterDeclaration | undefined = declarations.find(
(parameter: ts.TypeParameterDeclaration) => parameter.name.escapedText === typeName.escapedText,
);

if (!declaration) {
return GetNullDescriptor();
}

const statements: ts.Statement[] = [];

const genericValue: ts.CallExpression = GetTypeParameterDescriptor(declaration, scope);

const signatures: MethodSignature[] = ConstructSignatures(node);
const [signature]: MethodSignature[] = signatures;

const parameterIdentifier: ts.Identifier = TypescriptHelper.ExtractFirstIdentifier(signature.parameters[0].name);
const valueDeclaration: ts.VariableDeclaration = TypescriptCreator.createVariableDeclaration(
parameterIdentifier,
genericValue,
);

statements.push(
TypescriptCreator.createVariableStatement([
valueDeclaration,
]),
);

const typeVariableMap: Map<ts.TypeNode, ts.StringLiteral | ts.Identifier> = new Map(
signatures.reduce((typeHashTuples: [ts.TypeNode, ts.StringLiteral][], s: MethodSignature) => {
const [parameter]: typeof s.parameters | [undefined] = s.parameters;
if (!parameter) {
return typeHashTuples;
}

if (ts.isFunctionLike(parameter.type)) {
typeHashTuples.push([
parameter.type,
ts.createStringLiteral(
TypescriptCreator.createSignatureHash(parameter.type),
),
]);
}

return typeHashTuples;
}, [] as [ts.TypeNode, ts.StringLiteral][]),
);

statements.push(ResolveSignatureElseBranch(typeVariableMap, signatures, signature, scope));

return TypescriptCreator.createIIFE(ts.createBlock(statements, true));
}

function ConstructSignatures(node: ts.ConditionalTypeNode, signatures: MethodSignature[] = []): MethodSignature[] {
if (ts.isConditionalTypeNode(node.trueType)) {
return ConstructSignatures(node.trueType, signatures);
}

signatures.push(TypescriptCreator.createMethodSignature([node.extendsType], node.trueType));

if (ts.isConditionalTypeNode(node.falseType)) {
return ConstructSignatures(node.falseType, signatures);
}

signatures.push(TypescriptCreator.createMethodSignature(undefined, node.falseType));

return signatures;
}
3 changes: 3 additions & 0 deletions src/transformer/descriptor/descriptor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { GetBooleanFalseDescriptor } from './boolean/booleanFalse';
import { GetBooleanTrueDescriptor } from './boolean/booleanTrue';
import { GetCallExpressionDescriptor } from './callExpression/callExpression';
import { GetClassDeclarationDescriptor } from './class/classDeclaration';
import { GetConditionalTypeDescriptor } from './conditionalType/conditionalType';
import { GetConstructorTypeDescriptor } from './constructor/constructorType';
import { GetEnumDeclarationDescriptor } from './enum/enumDeclaration';
import { GetExpressionWithTypeArgumentsDescriptor } from './expression/expressionWithTypeArguments';
Expand Down Expand Up @@ -143,6 +144,8 @@ export function GetDescriptor(node: ts.Node, scope: Scope): ts.Expression {
return GetUndefinedDescriptor();
case ts.SyntaxKind.CallExpression:
return GetCallExpressionDescriptor(node as ts.CallExpression, scope);
case ts.SyntaxKind.ConditionalType:
return GetConditionalTypeDescriptor(node as ts.ConditionalTypeNode, scope);
default:
TransformerLogger().typeNotSupported(ts.SyntaxKind[node.kind]);
return GetNullDescriptor();
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);
}
Loading