Skip to content

Commit 99cb2de

Browse files
committed
feat(transformer): Support circular interface extensions
Utilize constructors to support circular generics for instantiable types. If the transformer experiences circular generics, the scope descriptor parameter is now used to preserve a nested state to avoid looping descriptors forever. The scope enables the generic descriptor to determine whether to emit a new instance of the extension or to reuse the parent's constructor (which would emit an instance of the same prototype), i.e.: ``` getFactory("@factory")([{ i: ["@parameterId"], w: function () { return new function () { Object.assign(this, ɵRepository.ɵRepository.instance.getFactory("@factory")([{ i: ["@parameterId"], w: function () { return new this.constructor; } }])); }; } }]) ```
1 parent edc272d commit 99cb2de

File tree

4 files changed

+86
-35
lines changed

4 files changed

+86
-35
lines changed

src/transformer/descriptor/helper/helper.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -60,10 +60,10 @@ export namespace TypescriptHelper {
6060
return declarations[0];
6161
}
6262

63-
export function GetParameterOfNode(node: ts.EntityName): ts.NodeArray<ts.TypeParameterDeclaration> {
63+
export function GetParameterOfNode(node: ts.EntityName): ts.NodeArray<ts.TypeParameterDeclaration> | undefined {
6464
const declaration: ts.Declaration = GetDeclarationFromNode(node);
6565

66-
const { typeParameters = ts.createNodeArray([]) }: Declaration = (declaration as Declaration);
66+
const { typeParameters }: Declaration = (declaration as Declaration);
6767

6868
return typeParameters;
6969
}

src/transformer/genericDeclaration/genericDeclaration.ts

Lines changed: 75 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -2,22 +2,34 @@ import * as ts from 'typescript';
22
import { GetDescriptor } from '../descriptor/descriptor';
33
import { TypescriptHelper } from '../descriptor/helper/helper';
44
import { TypescriptCreator } from '../helper/creator';
5-
import { TransformerLogger } from '../logger/transformerLogger';
6-
import { MockDefiner } from '../mockDefiner/mockDefiner';
75
import { MockIdentifierGenericParameterIds, MockIdentifierGenericParameterValue } from '../mockIdentifier/mockIdentifier';
86
import { Scope } from '../scope/scope';
97
import { IGenericDeclaration } from './genericDeclaration.interface';
108
import { GenericDeclarationSupported } from './genericDeclarationSupported';
119
import { GenericParameter } from './genericParameter';
1210

11+
function isInstantiable(node: ts.Declaration | undefined): boolean {
12+
let actualType: ts.Node | undefined = node;
13+
14+
if (!actualType) {
15+
return false;
16+
}
17+
18+
while (ts.isTypeAliasDeclaration(actualType)) {
19+
actualType = actualType.type;
20+
}
21+
22+
return !TypescriptHelper.IsLiteralOrPrimitive(actualType);
23+
}
24+
1325
export function GenericDeclaration(scope: Scope): IGenericDeclaration {
1426
const generics: GenericParameter[] = [];
1527

1628
function isGenericProvided<T extends ts.TypeReferenceNode | ts.ExpressionWithTypeArguments>(node: T, index: number): node is T & Required<ts.NodeWithTypeArguments> {
1729
return !!node.typeArguments && !!node.typeArguments[index];
1830
}
1931

20-
function getGenericNode(node: ts.TypeReferenceNode | ts.ExpressionWithTypeArguments, nodeDeclaration: ts.TypeParameterDeclaration, index: number): ts.Node {
32+
function getGenericNode(node: ts.TypeReferenceNode | ts.ExpressionWithTypeArguments, nodeDeclaration: ts.TypeParameterDeclaration, index: number): ts.TypeNode {
2133
if (isGenericProvided(node, index)) {
2234
return node.typeArguments[index];
2335
}
@@ -40,11 +52,50 @@ export function GenericDeclaration(scope: Scope): IGenericDeclaration {
4052
}
4153
}
4254

43-
function createGenericParameter(ownerKey: string, nodeOwnerParameter: ts.TypeParameterDeclaration, genericDescriptor: ts.Expression): GenericParameter {
55+
function createGenericParameter(ownerKey: string, nodeOwnerParameter: ts.TypeParameterDeclaration, genericDescriptor: ts.Expression | undefined, instantiable: boolean): GenericParameter {
4456
const uniqueName: string = ownerKey + nodeOwnerParameter.name.escapedText.toString();
45-
const genericFunction: ts.FunctionExpression = TypescriptCreator.createFunctionExpression(ts.createBlock(
46-
[ts.createReturn(genericDescriptor)],
47-
));
57+
58+
const genericValueDescriptor: ts.Expression = ((): ts.Expression => {
59+
if (!instantiable) {
60+
return genericDescriptor || ts.createNull();
61+
}
62+
63+
return ts.createNew(
64+
genericDescriptor ? TypescriptCreator.createFunctionExpression(
65+
ts.createBlock(
66+
[
67+
ts.createExpressionStatement(
68+
ts.createCall(
69+
ts.createPropertyAccess(
70+
ts.createIdentifier('Object'),
71+
ts.createIdentifier('assign'),
72+
),
73+
undefined,
74+
[
75+
ts.createIdentifier('this'),
76+
genericDescriptor,
77+
]
78+
),
79+
),
80+
],
81+
),
82+
) : ts.createPropertyAccess(
83+
ts.createIdentifier('this'),
84+
ts.createIdentifier('constructor'),
85+
),
86+
undefined,
87+
undefined,
88+
);
89+
})();
90+
91+
const genericFunction: ts.FunctionExpression =
92+
TypescriptCreator.createFunctionExpression(
93+
ts.createBlock([
94+
ts.createReturn(
95+
genericValueDescriptor,
96+
),
97+
]),
98+
);
4899

49100
return {
50101
ids: [uniqueName],
@@ -54,9 +105,9 @@ export function GenericDeclaration(scope: Scope): IGenericDeclaration {
54105

55106
return {
56107
addFromTypeReferenceNode(node: ts.TypeReferenceNode, declarationKey: string): void {
57-
const typeParameterDeclarations: ts.NodeArray<ts.TypeParameterDeclaration> = TypescriptHelper.GetParameterOfNode(node.typeName);
108+
const typeParameterDeclarations: ts.NodeArray<ts.TypeParameterDeclaration> | undefined = TypescriptHelper.GetParameterOfNode(node.typeName);
58109

59-
if (!typeParameterDeclarations) {
110+
if (!typeParameterDeclarations?.length) {
60111
return;
61112
}
62113

@@ -66,7 +117,9 @@ export function GenericDeclaration(scope: Scope): IGenericDeclaration {
66117
const genericParameter: GenericParameter = createGenericParameter(
67118
declarationKey,
68119
typeParameterDeclarations[index],
69-
GetDescriptor(genericNode, scope));
120+
GetDescriptor(genericNode, scope),
121+
false,
122+
);
70123

71124
generics.push(genericParameter);
72125
});
@@ -79,24 +132,18 @@ export function GenericDeclaration(scope: Scope): IGenericDeclaration {
79132
extension: ts.ExpressionWithTypeArguments): void {
80133
const extensionDeclarationTypeParameters: ts.NodeArray<ts.TypeParameterDeclaration> | undefined = extensionDeclaration.typeParameters;
81134

82-
if (!extensionDeclarationTypeParameters) {
135+
if (!extensionDeclarationTypeParameters?.length) {
83136
return;
84137
}
85138

86139
extensionDeclarationTypeParameters.reduce((acc: GenericParameter[], declaration: ts.TypeParameterDeclaration, index: number) => {
87140
const genericNode: ts.Node = getGenericNode(extension, declaration, index);
88141

142+
let typeParameterDeclaration: ts.Declaration | undefined;
143+
let genericValueDescriptor: ts.Expression | undefined;
144+
89145
if (ts.isTypeReferenceNode(genericNode)) {
90-
const typeParameterDeclaration: ts.Declaration = TypescriptHelper.GetDeclarationFromNode(genericNode.typeName);
91-
92-
const isExtendingItself: boolean = MockDefiner.instance.getDeclarationKeyMap(typeParameterDeclaration) === declarationKey;
93-
if (isExtendingItself) {
94-
// FIXME: Currently, circular generics aren't supported. See
95-
// https://github.com/Typescript-TDD/ts-auto-mock/pull/312 for more
96-
// details.
97-
TransformerLogger().circularGenericNotSupported(genericNode.getText());
98-
return acc;
99-
}
146+
typeParameterDeclaration = TypescriptHelper.GetDeclarationFromNode(genericNode.typeName);
100147

101148
if (ts.isTypeParameterDeclaration(typeParameterDeclaration)) {
102149
addGenericParameterToExisting(
@@ -110,10 +157,15 @@ export function GenericDeclaration(scope: Scope): IGenericDeclaration {
110157
}
111158
}
112159

160+
if (!typeParameterDeclaration || scope.currentMockKey !== declarationKey) {
161+
genericValueDescriptor = GetDescriptor(genericNode, new Scope(declarationKey));
162+
}
163+
113164
const genericParameter: GenericParameter = createGenericParameter(
114165
extensionDeclarationKey,
115-
extensionDeclarationTypeParameters[index],
116-
GetDescriptor(genericNode, scope),
166+
declaration,
167+
genericValueDescriptor,
168+
isInstantiable(typeParameterDeclaration),
117169
);
118170

119171
acc.push(genericParameter);

src/transformer/logger/transformerLogger.ts

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ import { ILogger } from '../../logger/logger.interface';
44
let logger: ILogger;
55

66
export interface TransformerLogger {
7-
circularGenericNotSupported(nodeName: string): void;
87
unexpectedCreateMock(mockFileName: string, expectedFileName: string): void;
98
typeNotSupported(type: string): void;
109
typeOfFunctionCallNotFound(node: string): void;
@@ -15,12 +14,6 @@ export function TransformerLogger(): TransformerLogger {
1514
logger = logger || Logger('Transformer');
1615

1716
return {
18-
circularGenericNotSupported(nodeName: string): void {
19-
logger.warning(
20-
`Found a circular generic of \`${nodeName}' and such generics are currently not supported. ` +
21-
'The generated mock will be incomplete.',
22-
);
23-
},
2417
unexpectedCreateMock(mockFileName: string, expectedFileName: string): void {
2518
logger.warning(`I\'ve found a mock creator but it comes from a different folder
2619
found: ${mockFileName}

test/transformer/descriptor/generic/extends.test.ts

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -248,11 +248,17 @@ describe('for generic', () => {
248248
interface A extends ClassWithGenerics<A> {
249249
b: number;
250250
}
251+
interface B extends ClassWithGenerics<B> {
252+
c: string;
253+
}
251254

252255
it('should avoid infinite extension', () => {
253-
const properties: A = createMock<A>();
254-
expect(properties.a).toBeDefined();
255-
expect(properties.b).toBe(0);
256+
const propertiesA: A = createMock<A>();
257+
const propertiesB: B = createMock<B>();
258+
expect(propertiesA.a.b).toBe(0);
259+
expect(propertiesA.b).toBe(0);
260+
expect(propertiesB.a.c).toBe('');
261+
expect(propertiesB.c).toBe('');
256262
});
257263
});
258264

0 commit comments

Comments
 (0)