Skip to content

Commit

Permalink
enhancement(transformer): Refactor GetMethodDescriptor implementation…
Browse files Browse the repository at this point in the history
… and utilize TypeScript's MethodSignature type
  • Loading branch information
martinjlowm committed May 16, 2020
1 parent d4f3a41 commit eebf687
Show file tree
Hide file tree
Showing 7 changed files with 82 additions and 173 deletions.
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,
);
}
15 changes: 11 additions & 4 deletions src/transformer/descriptor/method/functionType.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import * as ts from 'typescript';
import { Scope } from '../../scope/scope';
import { GetDescriptor } from '../descriptor';
import { TypescriptCreator } from '../../helper/creator';
import { PropertySignatureCache } from '../property/cache';
import { GetMethodDescriptor } from './method';

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

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

return GetMethodDescriptor(property, [{ returnValue }]);
return GetMethodDescriptor(
property,
[
TypescriptCreator.createMethodSignature(
node.parameters.map((p: ts.ParameterDeclaration) => p.type),
node.type,
),
],
scope,
);
}
151 changes: 16 additions & 135 deletions src/transformer/descriptor/method/method.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,14 @@
import ts from 'typescript';
import { GetTsAutoMockOverloadOptions, TsAutoMockOverloadOptions } from '../../../options/overload';
import { TypescriptCreator } from '../../helper/creator';
import { MethodSignature, TypescriptCreator } from '../../helper/creator';
import { MockDefiner } from '../../mockDefiner/mockDefiner';
import { ModuleName } from '../../mockDefiner/modules/moduleName';
import { Scope } from '../../scope/scope';
import { ResolveSignatureElseBranch } from '../helper/branching';
import { TypescriptHelper } from '../helper/helper';

export interface MethodSignature {
parameters?: ts.ParameterDeclaration[];
returnValue: ts.Expression;
}

export function GetMethodDescriptor(propertyName: ts.PropertyName, methodSignatures: MethodSignature[]): ts.CallExpression {
export function GetMethodDescriptor(_propertyName: ts.PropertyName, methodSignatures: MethodSignature[], scope: Scope): ts.CallExpression {
const providerGetMethod: ts.PropertyAccessExpression = CreateProviderGetMethod();

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

const signatureWithMostParameters: MethodSignature = methodSignatures.reduce(
(acc: MethodSignature, signature: MethodSignature) => {
const longestParametersLength: number = (acc.parameters || []).length;
Expand All @@ -25,23 +18,21 @@ export function GetMethodDescriptor(propertyName: ts.PropertyName, methodSignatu
},
);

const longestParameterList: ts.ParameterDeclaration[] = signatureWithMostParameters.parameters || [];

const declarationVariableMap: Map<ts.ParameterDeclaration, ts.Identifier> = new Map<ts.ParameterDeclaration, ts.Identifier>();
const declarationVariableMap: Map<ts.TypeNode, ts.Identifier> = new Map<ts.TypeNode, ts.Identifier>();

let i: number = 0;
const declarationVariables: ts.VariableDeclaration[] = methodSignatures.reduce(
(variables: ts.VariableDeclaration[], { parameters = [] }: MethodSignature) => {
(variables: ts.VariableDeclaration[], { parameters }: MethodSignature) => {
for (const parameter of parameters) {
if (declarationVariableMap.has(parameter)) {
if (declarationVariableMap.has(parameter.type)) {
continue;
}

const declarationType: ts.TypeNode | undefined = parameter.type;
if (declarationType && ts.isTypeReferenceNode(declarationType)) {
const variableIdentifier: ts.Identifier = ts.createIdentifier(`__${i++}`);
const variableIdentifier: ts.Identifier = ts.createIdentifier(`___${i++}`);

declarationVariableMap.set(parameter, variableIdentifier);
declarationVariableMap.set(parameter.type, variableIdentifier);

const declaration: ts.Declaration = TypescriptHelper.GetDeclarationFromNode(declarationType.typeName);

Expand All @@ -63,131 +54,20 @@ export function GetMethodDescriptor(propertyName: ts.PropertyName, methodSignatu
statements.push(TypescriptCreator.createVariableStatement(declarationVariables));
}

statements.push(ResolveSignatureElseBranch(declarationVariableMap, methodSignatures, longestParameterList));
statements.push(ResolveSignatureElseBranch(declarationVariableMap, methodSignatures, signatureWithMostParameters, scope));

const block: ts.Block = ts.createBlock(statements, true);

const propertyValueFunction: ts.ArrowFunction = TypescriptCreator.createArrowFunction(
block,
longestParameterList,
);

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

function CreateTypeEquality(signatureType: ts.Identifier | 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.IsLiteralOrPrimitive(signatureType)) {
return ts.createStrictEquality(
ts.createTypeOf(identifier),
signatureType ? ts.createStringLiteral(signatureType.getText()) : ts.createVoidZero(),
);
}

if (ts.isIdentifier(signatureType)) {
return ts.createStrictEquality(
ts.createPropertyAccess(identifier, '__factory'),
signatureType,
);
}

return ts.createBinary(identifier, ts.SyntaxKind.InstanceOfKeyword, ts.createIdentifier('Object'));
}

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

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

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

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

function ResolveParameterBranch(
declarationVariableMap: Map<ts.ParameterDeclaration, ts.Identifier>,
declarations: ts.ParameterDeclaration[],
allDeclarations: ts.ParameterDeclaration[],
returnValue: ts.Expression,
elseBranch: ts.Statement,
): ts.Statement {
const [firstDeclaration, ...remainingDeclarations]: Array<ts.ParameterDeclaration | undefined> = declarations;

const variableReferenceOrType: (declaration: ts.ParameterDeclaration) => ts.Identifier | ts.TypeNode | undefined =
(declaration: ts.ParameterDeclaration) => {
if (declarationVariableMap.has(declaration)) {
return declarationVariableMap.get(declaration);
} else {
return declaration.type;
}
};

// 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(variableReferenceOrType(declaration), allDeclarations[index + 1]),
),
CreateUnionTypeOfEquality(variableReferenceOrType(firstDeclaration), allDeclarations[0]),
const propName: ts.StringLiteral = ts.createStringLiteral(
TypescriptCreator.createSignatureHash(methodSignatures),
);

return ts.createIf(condition, ts.createReturn(returnValue), elseBranch);
}

export function ResolveSignatureElseBranch(
declarationVariableMap: Map<ts.ParameterDeclaration, ts.Identifier>,
signatures: MethodSignature[],
longestParameterList: ts.ParameterDeclaration[],
): ts.Statement {
const transformOverloadsOption: TsAutoMockOverloadOptions = GetTsAutoMockOverloadOptions();

const [signature, ...remainingSignatures]: MethodSignature[] = signatures.filter((_: unknown, notFirst: number) => transformOverloadsOption || !notFirst);

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

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

const currentParameters: ts.ParameterDeclaration[] = signature.parameters || [];
return ResolveParameterBranch(declarationVariableMap, currentParameters, longestParameterList, signature.returnValue, elseBranch);
return TypescriptCreator.createCall(providerGetMethod, [propName, propertyValueFunction]);
}

function CreateProviderGetMethod(): ts.PropertyAccessExpression {
Expand All @@ -198,5 +78,6 @@ function CreateProviderGetMethod(): ts.PropertyAccessExpression {
ts.createIdentifier('Provider'),
),
ts.createIdentifier('instance')),
ts.createIdentifier('getMethod'));
ts.createIdentifier('getMethod'),
);
}
30 changes: 16 additions & 14 deletions src/transformer/descriptor/method/methodDeclaration.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import * as ts from 'typescript';
import { MethodSignature, TypescriptCreator } from '../../helper/creator';
import { Scope } from '../../scope/scope';
import { TypeChecker } from '../../typeChecker/typeChecker';
import { GetDescriptor } from '../descriptor';
import { GetFunctionReturnType } from './functionReturnType';
import { GetMethodDescriptor, MethodSignature } from './method';
import { GetReturnNodeFromBody } from './bodyReturnType';
import { GetMethodDescriptor } from './method';

export function GetMethodDeclarationDescriptor(node: ts.MethodDeclaration | ts.FunctionDeclaration, scope: Scope): ts.Expression {
const declarationType: ts.Type | undefined = TypeChecker().getTypeAtLocation(node);
Expand All @@ -17,22 +17,24 @@ export function GetMethodDeclarationDescriptor(node: ts.MethodDeclaration | ts.F
methodDeclarations.push(node);
}

const methodSignatures: MethodSignature[] = methodDeclarations.map((declaration: ts.MethodDeclaration | ts.FunctionDeclaration) => ReshapeCallableDeclaration(declaration, scope));
const methodSignatures: MethodSignature[] = methodDeclarations.map((signature: ts.MethodDeclaration | ts.FunctionDeclaration) => {
let signatureType: ts.TypeNode | undefined = signature.type;

if (!signatureType) {
signatureType = ts.createLiteralTypeNode(GetReturnNodeFromBody(signature) as ts.LiteralExpression);
}

return TypescriptCreator.createMethodSignature(
signature.parameters.map((p: ts.ParameterDeclaration) => p.type),
signatureType,
);
});

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

return GetMethodDescriptor(node.name, methodSignatures);
}

export function ReshapeCallableDeclaration(declaration: ts.SignatureDeclaration, scope: Scope): MethodSignature {
const returnTypeNode: ts.Node = GetFunctionReturnType(declaration);

return {
parameters: declaration.parameters.map((parameter: ts.ParameterDeclaration) => parameter),
returnValue: GetDescriptor(returnTypeNode, scope),
};
return GetMethodDescriptor(node.name, methodSignatures, scope);
}
20 changes: 11 additions & 9 deletions src/transformer/descriptor/method/methodSignature.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,19 @@
import * as ts from 'typescript';
import { Scope } from '../../scope/scope';
import { GetDescriptor } from '../descriptor';
import { TypescriptCreator } from '../../helper/creator';
import { GetNullDescriptor } from '../null/null';
import { GetMethodDescriptor } from './method';

export function GetMethodSignatureDescriptor(node: ts.MethodSignature, scope: Scope): ts.Expression {
let returnValue: ts.Expression;

if (node.type) {
returnValue = GetDescriptor(node.type, scope);
} else {
returnValue = GetNullDescriptor();
}

return GetMethodDescriptor(node.name, [{ returnValue }]);
return GetMethodDescriptor(
node.name,
[
TypescriptCreator.createMethodSignature(
node.parameters.map((p: ts.ParameterDeclaration) => p.type),
node.type,
),
],
scope,
);
}
9 changes: 5 additions & 4 deletions src/transformer/descriptor/mock/mockCall.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import * as ts from 'typescript';
import { TypescriptCreator } from '../../helper/creator';
import { TypescriptCreator, MethodSignature } from '../../helper/creator';
import { Scope } from '../../scope/scope';
import { MockIdentifierInternalValues, MockIdentifierObjectReturnValue } from '../../mockIdentifier/mockIdentifier';
import { GetMethodDescriptor, MethodSignature } from '../method/method';
import { GetMethodDescriptor } from '../method/method';
import { GetMockMarkerProperty, Property } from './mockMarker';
import { PropertyAssignments } from './mockPropertiesAssignments';

export function GetMockCall(properties: PropertyAssignments, signatures: MethodSignature[]): ts.CallExpression {
export function GetMockCall(properties: PropertyAssignments, signatures: MethodSignature[], scope: Scope): ts.CallExpression {
const mockObjectReturnValueName: ts.Identifier = MockIdentifierObjectReturnValue;

const variableStatements: ts.VariableDeclaration[] = [
Expand All @@ -18,7 +19,7 @@ export function GetMockCall(properties: PropertyAssignments, signatures: MethodS
// FIXME: It'd probably be wise to extract the name of the callable
// signature and only fallback to `new` if there is none (or something
// shorter).
const callableEntry: ts.CallExpression = GetMethodDescriptor(ts.createStringLiteral('new'), signatures);
const callableEntry: ts.CallExpression = GetMethodDescriptor(ts.createStringLiteral('new'), signatures, scope);

variableStatements.push(
TypescriptCreator.createVariableDeclaration(mockObjectReturnValueName, callableEntry),
Expand Down
12 changes: 8 additions & 4 deletions src/transformer/descriptor/mock/mockProperties.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import * as ts from 'typescript';
import { MethodSignature, TypescriptCreator } from '../../helper/creator';
import { Scope } from '../../scope/scope';
import { IsTypescriptType } from '../tsLibs/typecriptLibs';
import { MethodSignature } from '../method/method';
import { ReshapeCallableDeclaration } from '../method/methodDeclaration';
import { GetMockCall } from './mockCall';
import { GetMockPropertiesAssignments, PropertyAssignments } from './mockPropertiesAssignments';
import { PropertyLike } from './propertyLike';
Expand Down Expand Up @@ -35,7 +34,12 @@ export function GetMockPropertiesFromDeclarations(list: ReadonlyArray<PropertyLi

const accessorDeclaration: PropertyAssignments = GetMockPropertiesAssignments(propertiesFilter, scope);

const methodSignatures: MethodSignature[] = signatures.map((declaration: ts.CallSignatureDeclaration | ts.ConstructSignatureDeclaration) => ReshapeCallableDeclaration(declaration, scope));
const methodSignatures: MethodSignature[] = signatures.map((signature: SignatureLike) =>
TypescriptCreator.createMethodSignature(
signature.parameters.map((p: ts.ParameterDeclaration) => p.type),
signature.type,
),
);

return GetMockCall(accessorDeclaration, methodSignatures);
return GetMockCall(accessorDeclaration, methodSignatures, scope);
}

0 comments on commit eebf687

Please sign in to comment.