Skip to content

Commit 4dbaf72

Browse files
committed
feature(transformer): First iteration of conditional type support
1 parent 236184f commit 4dbaf72

File tree

4 files changed

+233
-2
lines changed

4 files changed

+233
-2
lines changed
Lines changed: 219 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,219 @@
1+
import ts from 'typescript';
2+
import { GetTsAutoMockOverloadOptions, TsAutoMockOverloadOptions } from '../../../options/overload';
3+
import { TypescriptCreator } from '../../helper/creator';
4+
import { MockDefiner } from '../../mockDefiner/mockDefiner';
5+
import { ModuleName } from '../../mockDefiner/modules/moduleName';
6+
import { MockIdentifierGenericParameterValue } from '../../mockIdentifier/mockIdentifier';
7+
import { Scope } from '../../scope/scope';
8+
import { TypescriptHelper } from '../helper/helper';
9+
import { GetNullDescriptor } from '../null/null';
10+
import { GetDescriptor } from '../descriptor';
11+
import { GetTypeParameterDescriptor } from '../typeParameter/typeParameter';
12+
13+
export interface MethodSignature {
14+
parameters?: ts.TypeNode[];
15+
returnValue: ts.Expression;
16+
}
17+
18+
function isDeclarationWithTypeParameterChildren(node: ts.Node): node is ts.DeclarationWithTypeParameterChildren {
19+
return ts.isFunctionLike(node) ||
20+
ts.isClassLike(node) ||
21+
ts.isInterfaceDeclaration(node) ||
22+
ts.isTypeAliasDeclaration(node);
23+
}
24+
25+
export function GetConditionalTypeDescriptor(node: ts.ConditionalTypeNode, scope: Scope): ts.Expression {
26+
const parentNode: ts.Node = node.parent;
27+
if (!isDeclarationWithTypeParameterChildren(parentNode)) {
28+
return GetNullDescriptor();
29+
}
30+
31+
const checkType: ts.TypeNode = node.checkType;
32+
if (!ts.isTypeReferenceNode(checkType)) {
33+
return GetNullDescriptor();
34+
}
35+
36+
const typeName: ts.EntityName = checkType.typeName;
37+
if (ts.isQualifiedName(typeName)) {
38+
return GetNullDescriptor();
39+
}
40+
41+
const declarations: readonly ts.TypeParameterDeclaration[] = ts.getEffectiveTypeParameterDeclarations(parentNode);
42+
43+
const declaration: ts.TypeParameterDeclaration | undefined = declarations.find(
44+
(parameter: ts.TypeParameterDeclaration) => parameter.name.escapedText === typeName.escapedText,
45+
);
46+
47+
if (!declaration) {
48+
return GetNullDescriptor();
49+
}
50+
51+
const genericValue: ts.CallExpression = GetTypeParameterDescriptor(declaration, scope);
52+
53+
const statements: ts.Statement[] = [];
54+
55+
const valueDeclaration: ts.VariableDeclaration = TypescriptCreator.createVariableDeclaration(
56+
MockIdentifierGenericParameterValue,
57+
genericValue,
58+
);
59+
60+
statements.push(
61+
TypescriptCreator.createVariableStatement([
62+
valueDeclaration,
63+
]),
64+
);
65+
66+
statements.push(ResolveSignatureElseBranch(new Map(), ConstructSignatures(node, scope), [valueDeclaration]));
67+
68+
return TypescriptCreator.createIIFE(ts.createBlock(statements, true));
69+
}
70+
71+
function ConstructSignatures(node: ts.ConditionalTypeNode, scope: Scope, signatures: MethodSignature[] = []): MethodSignature[] {
72+
const parameters: ts.TypeNode[] = [node.extendsType];
73+
74+
if (ts.isConditionalTypeNode(node.trueType)) {
75+
return ConstructSignatures(node.trueType, scope, signatures);
76+
}
77+
78+
signatures.push({
79+
parameters,
80+
returnValue: GetDescriptor(node.trueType, scope),
81+
});
82+
83+
if (ts.isConditionalTypeNode(node.falseType)) {
84+
return ConstructSignatures(node.falseType, scope, signatures);
85+
}
86+
87+
signatures.push({
88+
parameters,
89+
returnValue: GetDescriptor(node.falseType, scope),
90+
});
91+
92+
return signatures;
93+
}
94+
95+
function CreateTypeEquality(signatureType: ts.Identifier | ts.TypeNode | undefined, primaryDeclaration: ts.ParameterDeclaration | ts.VariableDeclaration): ts.Expression {
96+
// TODO: Factor this into a helper - guess it can be helpful in other places.
97+
let declarationName: ts.BindingName = primaryDeclaration.name;
98+
99+
while (!ts.isIdentifier(declarationName)) {
100+
const [bindingElement]: Array<ts.BindingElement | undefined> = (declarationName.elements as ts.NodeArray<ts.ArrayBindingElement>).filter(ts.isBindingElement);
101+
if (!bindingElement) {
102+
throw new Error('Failed to find an identifier for the primary declaration!');
103+
}
104+
105+
declarationName = bindingElement.name;
106+
}
107+
108+
if (!signatureType) {
109+
return ts.createPrefix(
110+
ts.SyntaxKind.ExclamationToken,
111+
ts.createPrefix(
112+
ts.SyntaxKind.ExclamationToken,
113+
declarationName,
114+
),
115+
);
116+
}
117+
118+
if (TypescriptHelper.IsLiteralOrPrimitive(signatureType)) {
119+
return ts.createStrictEquality(
120+
ts.createTypeOf(declarationName),
121+
signatureType ? ts.createStringLiteral(signatureType.getText()) : ts.createVoidZero(),
122+
);
123+
}
124+
125+
if (ts.isIdentifier(signatureType)) {
126+
return ts.createStrictEquality(
127+
ts.createPropertyAccess(declarationName, '__factory'),
128+
signatureType,
129+
);
130+
}
131+
132+
return ts.createBinary(declarationName, ts.SyntaxKind.InstanceOfKeyword, ts.createIdentifier('Object'));
133+
}
134+
135+
function CreateUnionTypeOfEquality(signatureType: ts.Identifier | ts.TypeNode | undefined, primaryDeclaration: ts.ParameterDeclaration | ts.VariableDeclaration): ts.Expression {
136+
const typeNodesAndVariableReferences: Array<ts.TypeNode | ts.Identifier> = [];
137+
138+
if (signatureType) {
139+
if (ts.isTypeNode(signatureType) && ts.isUnionTypeNode(signatureType)) {
140+
typeNodesAndVariableReferences.push(...signatureType.types);
141+
} else {
142+
typeNodesAndVariableReferences.push(signatureType);
143+
}
144+
}
145+
146+
const [firstType, ...remainingTypes]: Array<ts.TypeNode | ts.Identifier> = typeNodesAndVariableReferences;
147+
148+
return remainingTypes.reduce(
149+
(prevStatement: ts.Expression, typeNode: ts.TypeNode) =>
150+
ts.createLogicalOr(
151+
prevStatement,
152+
CreateTypeEquality(typeNode, primaryDeclaration),
153+
),
154+
CreateTypeEquality(firstType, primaryDeclaration),
155+
);
156+
}
157+
158+
function ResolveParameterBranch(
159+
declarationVariableMap: Map<ts.Declaration, ts.Identifier>,
160+
declarations: ts.TypeNode[],
161+
allDeclarations: Array<ts.ParameterDeclaration | ts.VariableDeclaration>,
162+
returnValue: ts.Expression,
163+
elseBranch: ts.Statement,
164+
): ts.Statement {
165+
const [firstDeclaration, ...remainingDeclarations]: Array<ts.TypeNode | undefined> = declarations;
166+
167+
const variableReferenceOrType: (t: ts.TypeNode | undefined) => ts.Identifier | ts.TypeNode | undefined = (t: ts.TypeNode | undefined) => t;
168+
// const variableReferenceOrType: (declaration: ts.ParameterDeclaration) => ts.Identifier | ts.TypeNode | undefined =
169+
// (declaration: ts.ParameterDeclaration) => {
170+
// if (declarationVariableMap.has(declaration)) {
171+
// return declarationVariableMap.get(declaration);
172+
// } else {
173+
// return declaration.type;
174+
// }
175+
// };
176+
177+
// TODO: These conditions quickly grow in size, but it should be possible to
178+
// squeeze things together and optimize it with something like:
179+
//
180+
// const typeOf = function (left, right) { return typeof left === right; }
181+
// const evaluate = (function(left, right) { return this._ = this._ || typeOf(left, right); }).bind({})
182+
//
183+
// if (evaluate(firstArg, 'boolean') && evaluate(secondArg, 'number') && ...) {
184+
// ...
185+
// }
186+
//
187+
// `this._' acts as a cache, since the control flow may evaluate the same
188+
// conditions multiple times.
189+
const condition: ts.Expression = remainingDeclarations.reduce(
190+
(prevStatement: ts.Expression, node: ts.TypeNode | undefined, index: number) =>
191+
ts.createLogicalAnd(
192+
prevStatement,
193+
CreateUnionTypeOfEquality(variableReferenceOrType(node), allDeclarations[index + 1]),
194+
),
195+
CreateUnionTypeOfEquality(variableReferenceOrType(firstDeclaration), allDeclarations[0]),
196+
);
197+
198+
return ts.createIf(condition, ts.createReturn(returnValue), elseBranch);
199+
}
200+
201+
export function ResolveSignatureElseBranch(
202+
declarationVariableMap: Map<ts.ParameterDeclaration, ts.Identifier>,
203+
signatures: MethodSignature[],
204+
longestParameterList: Array<ts.ParameterDeclaration | ts.VariableDeclaration>,
205+
): ts.Statement {
206+
const transformOverloadsOption: TsAutoMockOverloadOptions = GetTsAutoMockOverloadOptions();
207+
208+
const [signature, ...remainingSignatures]: MethodSignature[] = signatures.filter((_: unknown, notFirst: number) => transformOverloadsOption || !notFirst);
209+
210+
const indistinctSignatures: boolean = signatures.every((sig: MethodSignature) => !sig.parameters?.length);
211+
if (!remainingSignatures.length || indistinctSignatures) {
212+
return ts.createReturn(signature.returnValue);
213+
}
214+
215+
const elseBranch: ts.Statement = ResolveSignatureElseBranch(declarationVariableMap, remainingSignatures, longestParameterList);
216+
217+
const currentParameters: ts.TypeNode[] = signature.parameters || [];
218+
return ResolveParameterBranch(declarationVariableMap, currentParameters, longestParameterList, signature.returnValue, elseBranch);
219+
}

src/transformer/descriptor/descriptor.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { GetBooleanFalseDescriptor } from './boolean/booleanFalse';
99
import { GetBooleanTrueDescriptor } from './boolean/booleanTrue';
1010
import { GetCallExpressionDescriptor } from './callExpression/callExpression';
1111
import { GetClassDeclarationDescriptor } from './class/classDeclaration';
12+
import { GetConditionalTypeDescriptor } from './conditionalType/conditionalType';
1213
import { GetConstructorTypeDescriptor } from './constructor/constructorType';
1314
import { GetEnumDeclarationDescriptor } from './enum/enumDeclaration';
1415
import { GetExpressionWithTypeArgumentsDescriptor } from './expression/expressionWithTypeArguments';
@@ -143,6 +144,8 @@ export function GetDescriptor(node: ts.Node, scope: Scope): ts.Expression {
143144
return GetUndefinedDescriptor();
144145
case ts.SyntaxKind.CallExpression:
145146
return GetCallExpressionDescriptor(node as ts.CallExpression, scope);
147+
case ts.SyntaxKind.ConditionalType:
148+
return GetConditionalTypeDescriptor(node as ts.ConditionalTypeNode, scope);
146149
default:
147150
TransformerLogger().typeNotSupported(ts.SyntaxKind[node.kind]);
148151
return GetNullDescriptor();

src/transformer/descriptor/helper/helper.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,14 @@ type ImportDeclaration = ts.ImportEqualsDeclaration | ts.ImportOrExportSpecifier
77

88
export namespace TypescriptHelper {
99
export interface PrimitiveTypeNode extends ts.TypeNode {
10-
kind: ts.SyntaxKind.LiteralType | ts.SyntaxKind.NumberKeyword | ts.SyntaxKind.ObjectKeyword | ts.SyntaxKind.BooleanKeyword | ts.SyntaxKind.StringKeyword | ts.SyntaxKind.ArrayType;
10+
kind:
11+
| ts.SyntaxKind.LiteralType
12+
| ts.SyntaxKind.NumberKeyword
13+
| ts.SyntaxKind.ObjectKeyword
14+
| ts.SyntaxKind.BooleanKeyword
15+
| ts.SyntaxKind.StringKeyword
16+
| ts.SyntaxKind.ArrayType
17+
| ts.SyntaxKind.UndefinedKeyword;
1118
}
1219

1320
export function IsLiteralOrPrimitive(typeNode: ts.Node): typeNode is PrimitiveTypeNode {
@@ -17,6 +24,7 @@ export namespace TypescriptHelper {
1724
case ts.SyntaxKind.ObjectKeyword:
1825
case ts.SyntaxKind.BooleanKeyword:
1926
case ts.SyntaxKind.StringKeyword:
27+
case ts.SyntaxKind.UndefinedKeyword:
2028
case ts.SyntaxKind.ArrayType:
2129
return true;
2230
}

src/transformer/descriptor/typeParameter/typeParameter.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,13 @@ import { GetDescriptor } from '../descriptor';
88
import { TypescriptHelper } from '../helper/helper';
99
import { GetNullDescriptor } from '../null/null';
1010

11-
export function GetTypeParameterDescriptor(node: ts.TypeParameterDeclaration, scope: Scope): ts.Expression {
11+
export function GetTypeParameterDescriptor(node: ts.TypeParameterDeclaration, scope: Scope): ts.CallExpression {
1212
const type: ts.TypeParameter = TypeChecker().getTypeAtLocation(node);
1313

1414
const descriptor: ts.Expression = node.default ? GetDescriptor(node.default, scope) : GetNullDescriptor();
1515

1616
const declaration: ts.Declaration = type.symbol.declarations[0];
17+
1718
const typeDeclaration: ts.Declaration | undefined = TypescriptHelper.GetTypeParameterOwnerMock(declaration);
1819

1920
if (!typeDeclaration) {

0 commit comments

Comments
 (0)