From da1750c8f0cd585d563c8433dc206997dcfaeea8 Mon Sep 17 00:00:00 2001 From: Martin Jesper Low Madsen Date: Fri, 12 Jun 2020 20:31:01 +0200 Subject: [PATCH 01/11] feat(transformer): Support circular interface extensions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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; } }])); }; } }]) ``` --- src/transformer/descriptor/helper/helper.ts | 4 +- .../genericDeclaration/genericDeclaration.ts | 98 ++++++++++++++----- src/transformer/logger/transformerLogger.ts | 7 -- .../descriptor/generic/extends.test.ts | 12 ++- 4 files changed, 86 insertions(+), 35 deletions(-) diff --git a/src/transformer/descriptor/helper/helper.ts b/src/transformer/descriptor/helper/helper.ts index ea86d74b7..b10f8b328 100644 --- a/src/transformer/descriptor/helper/helper.ts +++ b/src/transformer/descriptor/helper/helper.ts @@ -60,10 +60,10 @@ export namespace TypescriptHelper { return declarations[0]; } - export function GetParameterOfNode(node: ts.EntityName): ts.NodeArray { + export function GetParameterOfNode(node: ts.EntityName): ts.NodeArray | undefined { const declaration: ts.Declaration = GetDeclarationFromNode(node); - const { typeParameters = ts.createNodeArray([]) }: Declaration = (declaration as Declaration); + const { typeParameters }: Declaration = (declaration as Declaration); return typeParameters; } diff --git a/src/transformer/genericDeclaration/genericDeclaration.ts b/src/transformer/genericDeclaration/genericDeclaration.ts index 2320d43b6..6a7887747 100644 --- a/src/transformer/genericDeclaration/genericDeclaration.ts +++ b/src/transformer/genericDeclaration/genericDeclaration.ts @@ -2,14 +2,26 @@ import * as ts from 'typescript'; import { GetDescriptor } from '../descriptor/descriptor'; import { TypescriptHelper } from '../descriptor/helper/helper'; import { TypescriptCreator } from '../helper/creator'; -import { TransformerLogger } from '../logger/transformerLogger'; -import { MockDefiner } from '../mockDefiner/mockDefiner'; import { MockIdentifierGenericParameterIds, MockIdentifierGenericParameterValue } from '../mockIdentifier/mockIdentifier'; import { Scope } from '../scope/scope'; import { IGenericDeclaration } from './genericDeclaration.interface'; import { GenericDeclarationSupported } from './genericDeclarationSupported'; import { GenericParameter } from './genericParameter'; +function isInstantiable(node: ts.Declaration | undefined): boolean { + let actualType: ts.Node | undefined = node; + + if (!actualType) { + return false; + } + + while (ts.isTypeAliasDeclaration(actualType)) { + actualType = actualType.type; + } + + return !TypescriptHelper.IsLiteralOrPrimitive(actualType); +} + export function GenericDeclaration(scope: Scope): IGenericDeclaration { const generics: GenericParameter[] = []; @@ -17,7 +29,7 @@ export function GenericDeclaration(scope: Scope): IGenericDeclaration { return !!node.typeArguments && !!node.typeArguments[index]; } - function getGenericNode(node: ts.TypeReferenceNode | ts.ExpressionWithTypeArguments, nodeDeclaration: ts.TypeParameterDeclaration, index: number): ts.Node { + function getGenericNode(node: ts.TypeReferenceNode | ts.ExpressionWithTypeArguments, nodeDeclaration: ts.TypeParameterDeclaration, index: number): ts.TypeNode { if (isGenericProvided(node, index)) { return node.typeArguments[index]; } @@ -40,11 +52,50 @@ export function GenericDeclaration(scope: Scope): IGenericDeclaration { } } - function createGenericParameter(ownerKey: string, nodeOwnerParameter: ts.TypeParameterDeclaration, genericDescriptor: ts.Expression): GenericParameter { + function createGenericParameter(ownerKey: string, nodeOwnerParameter: ts.TypeParameterDeclaration, genericDescriptor: ts.Expression | undefined, instantiable: boolean): GenericParameter { const uniqueName: string = ownerKey + nodeOwnerParameter.name.escapedText.toString(); - const genericFunction: ts.FunctionExpression = TypescriptCreator.createFunctionExpression(ts.createBlock( - [ts.createReturn(genericDescriptor)], - )); + + const genericValueDescriptor: ts.Expression = ((): ts.Expression => { + if (!instantiable) { + return genericDescriptor || ts.createNull(); + } + + return ts.createNew( + genericDescriptor ? TypescriptCreator.createFunctionExpression( + ts.createBlock( + [ + ts.createExpressionStatement( + ts.createCall( + ts.createPropertyAccess( + ts.createIdentifier('Object'), + ts.createIdentifier('assign'), + ), + undefined, + [ + ts.createIdentifier('this'), + genericDescriptor, + ] + ), + ), + ], + ), + ) : ts.createPropertyAccess( + ts.createIdentifier('this'), + ts.createIdentifier('constructor'), + ), + undefined, + undefined, + ); + })(); + + const genericFunction: ts.FunctionExpression = + TypescriptCreator.createFunctionExpression( + ts.createBlock([ + ts.createReturn( + genericValueDescriptor, + ), + ]), + ); return { ids: [uniqueName], @@ -54,9 +105,9 @@ export function GenericDeclaration(scope: Scope): IGenericDeclaration { return { addFromTypeReferenceNode(node: ts.TypeReferenceNode, declarationKey: string): void { - const typeParameterDeclarations: ts.NodeArray = TypescriptHelper.GetParameterOfNode(node.typeName); + const typeParameterDeclarations: ts.NodeArray | undefined = TypescriptHelper.GetParameterOfNode(node.typeName); - if (!typeParameterDeclarations) { + if (!typeParameterDeclarations?.length) { return; } @@ -66,7 +117,9 @@ export function GenericDeclaration(scope: Scope): IGenericDeclaration { const genericParameter: GenericParameter = createGenericParameter( declarationKey, typeParameterDeclarations[index], - GetDescriptor(genericNode, scope)); + GetDescriptor(genericNode, scope), + false, + ); generics.push(genericParameter); }); @@ -79,24 +132,18 @@ export function GenericDeclaration(scope: Scope): IGenericDeclaration { extension: ts.ExpressionWithTypeArguments): void { const extensionDeclarationTypeParameters: ts.NodeArray | undefined = extensionDeclaration.typeParameters; - if (!extensionDeclarationTypeParameters) { + if (!extensionDeclarationTypeParameters?.length) { return; } extensionDeclarationTypeParameters.reduce((acc: GenericParameter[], declaration: ts.TypeParameterDeclaration, index: number) => { const genericNode: ts.Node = getGenericNode(extension, declaration, index); + let typeParameterDeclaration: ts.Declaration | undefined; + let genericValueDescriptor: ts.Expression | undefined; + if (ts.isTypeReferenceNode(genericNode)) { - const typeParameterDeclaration: ts.Declaration = TypescriptHelper.GetDeclarationFromNode(genericNode.typeName); - - const isExtendingItself: boolean = MockDefiner.instance.getDeclarationKeyMap(typeParameterDeclaration) === declarationKey; - if (isExtendingItself) { - // FIXME: Currently, circular generics aren't supported. See - // https://github.com/Typescript-TDD/ts-auto-mock/pull/312 for more - // details. - TransformerLogger().circularGenericNotSupported(genericNode.getText()); - return acc; - } + typeParameterDeclaration = TypescriptHelper.GetDeclarationFromNode(genericNode.typeName); if (ts.isTypeParameterDeclaration(typeParameterDeclaration)) { addGenericParameterToExisting( @@ -110,10 +157,15 @@ export function GenericDeclaration(scope: Scope): IGenericDeclaration { } } + if (!typeParameterDeclaration || scope.currentMockKey !== declarationKey) { + genericValueDescriptor = GetDescriptor(genericNode, new Scope(declarationKey)); + } + const genericParameter: GenericParameter = createGenericParameter( extensionDeclarationKey, - extensionDeclarationTypeParameters[index], - GetDescriptor(genericNode, scope), + declaration, + genericValueDescriptor, + isInstantiable(typeParameterDeclaration), ); acc.push(genericParameter); diff --git a/src/transformer/logger/transformerLogger.ts b/src/transformer/logger/transformerLogger.ts index 5464ef08e..d52e9e228 100644 --- a/src/transformer/logger/transformerLogger.ts +++ b/src/transformer/logger/transformerLogger.ts @@ -4,7 +4,6 @@ import { ILogger } from '../../logger/logger.interface'; let logger: ILogger; export interface TransformerLogger { - circularGenericNotSupported(nodeName: string): void; unexpectedCreateMock(mockFileName: string, expectedFileName: string): void; typeNotSupported(type: string): void; typeOfFunctionCallNotFound(node: string): void; @@ -15,12 +14,6 @@ export function TransformerLogger(): TransformerLogger { logger = logger || Logger('Transformer'); return { - circularGenericNotSupported(nodeName: string): void { - logger.warning( - `Found a circular generic of \`${nodeName}' and such generics are currently not supported. ` + - 'The generated mock will be incomplete.', - ); - }, unexpectedCreateMock(mockFileName: string, expectedFileName: string): void { logger.warning(`I\'ve found a mock creator but it comes from a different folder found: ${mockFileName} diff --git a/test/transformer/descriptor/generic/extends.test.ts b/test/transformer/descriptor/generic/extends.test.ts index 149dfa982..cc87d7efd 100644 --- a/test/transformer/descriptor/generic/extends.test.ts +++ b/test/transformer/descriptor/generic/extends.test.ts @@ -248,11 +248,17 @@ describe('for generic', () => { interface A extends ClassWithGenerics { b: number; } + interface B extends ClassWithGenerics { + c: string; + } it('should avoid infinite extension', () => { - const properties: A = createMock(); - expect(properties.a).toBeDefined(); - expect(properties.b).toBe(0); + const propertiesA: A = createMock(); + const propertiesB: B = createMock(); + expect(propertiesA.a.b).toBe(0); + expect(propertiesA.b).toBe(0); + expect(propertiesB.a.c).toBe(''); + expect(propertiesB.c).toBe(''); }); }); From c35347bca0fc39fb32890f060479ef07ee5ae409 Mon Sep 17 00:00:00 2001 From: Martin Jesper Low Madsen Date: Sat, 20 Jun 2020 22:35:36 +0200 Subject: [PATCH 02/11] chore(docs): Remove circular generics from types-not-supported.mdx --- ui/src/views/types-not-supported.mdx | 28 ---------------------------- 1 file changed, 28 deletions(-) diff --git a/ui/src/views/types-not-supported.mdx b/ui/src/views/types-not-supported.mdx index 54c66ed8f..76377ecf6 100644 --- a/ui/src/views/types-not-supported.mdx +++ b/ui/src/views/types-not-supported.mdx @@ -71,31 +71,6 @@ There is a branch created with a working version but it needs more investigation [link](https://github.com/Typescript-TDD/ts-auto-mock/tree/feature/extends-mapped-type) -## Circular Generics - -```ts -class C { - public propC: T - public test: string -} - -class A extends C { - public propA: number -} -const a: A = createMock(); - -// This will fail because we will not support generics of the same type. -expect(a.propC.propC.test).toBe(""); -``` - -These are discussed here: -[link](https://github.com/Typescript-TDD/ts-auto-mock/pull/312). As of this -writing, the problem with circular generics is that the generated AST will -circle `A` over and over, and result in an infinite nested tree of declaration -references. The intended behavior is to have the first back-reference stored -elsewhere in the generated output and let it reference itself, making the -runtime a lazy-evaluated sequence of getters. - ## Indexed access type with generics ```ts interface StandardInterface { @@ -128,6 +103,3 @@ interface StandardInterface { type Hello = StandardInterface['prop']; ``` - - - From 3f0bd8c5abe3087a6f32f9e5671969618e1e44fb Mon Sep 17 00:00:00 2001 From: Martin Jesper Low Madsen Date: Sat, 27 Jun 2020 10:06:21 +0200 Subject: [PATCH 03/11] fix(transformer): Store reference to parent generic to avoid a rebound this --- .../genericDeclaration/genericDeclaration.ts | 18 ++++++++++++++---- .../mockIdentifier/mockIdentifier.ts | 1 + .../descriptor/generic/extends.test.ts | 4 ++-- 3 files changed, 17 insertions(+), 6 deletions(-) diff --git a/src/transformer/genericDeclaration/genericDeclaration.ts b/src/transformer/genericDeclaration/genericDeclaration.ts index 6a7887747..b7f65205c 100644 --- a/src/transformer/genericDeclaration/genericDeclaration.ts +++ b/src/transformer/genericDeclaration/genericDeclaration.ts @@ -2,7 +2,7 @@ import * as ts from 'typescript'; import { GetDescriptor } from '../descriptor/descriptor'; import { TypescriptHelper } from '../descriptor/helper/helper'; import { TypescriptCreator } from '../helper/creator'; -import { MockIdentifierGenericParameterIds, MockIdentifierGenericParameterValue } from '../mockIdentifier/mockIdentifier'; +import { MockIdentifierGenericCircularReference, MockIdentifierGenericParameterIds, MockIdentifierGenericParameterValue } from '../mockIdentifier/mockIdentifier'; import { Scope } from '../scope/scope'; import { IGenericDeclaration } from './genericDeclaration.interface'; import { GenericDeclarationSupported } from './genericDeclarationSupported'; @@ -64,23 +64,33 @@ export function GenericDeclaration(scope: Scope): IGenericDeclaration { genericDescriptor ? TypescriptCreator.createFunctionExpression( ts.createBlock( [ + TypescriptCreator.createVariableStatement([ + TypescriptCreator.createVariableDeclaration(MockIdentifierGenericCircularReference, ts.createIdentifier('this')), + ]), ts.createExpressionStatement( ts.createCall( ts.createPropertyAccess( ts.createIdentifier('Object'), - ts.createIdentifier('assign'), + ts.createIdentifier('defineProperties'), ), undefined, [ ts.createIdentifier('this'), - genericDescriptor, + ts.createCall( + ts.createPropertyAccess( + ts.createIdentifier('Object'), + ts.createIdentifier('getOwnPropertyDescriptors'), + ), + undefined, + [genericDescriptor] + ), ] ), ), ], ), ) : ts.createPropertyAccess( - ts.createIdentifier('this'), + MockIdentifierGenericCircularReference, ts.createIdentifier('constructor'), ), undefined, diff --git a/src/transformer/mockIdentifier/mockIdentifier.ts b/src/transformer/mockIdentifier/mockIdentifier.ts index 4ed50c992..49bc177e1 100644 --- a/src/transformer/mockIdentifier/mockIdentifier.ts +++ b/src/transformer/mockIdentifier/mockIdentifier.ts @@ -1,5 +1,6 @@ import * as ts from 'typescript'; +export const MockIdentifierGenericCircularReference: ts.Identifier = ts.createIdentifier('that'); export const MockIdentifierGenericParameter: ts.Identifier = ts.createIdentifier('t'); export const MockIdentifierGenericParameterIds: ts.Identifier = ts.createIdentifier('i'); export const MockIdentifierGenericParameterValue: ts.Identifier = ts.createIdentifier('w'); diff --git a/test/transformer/descriptor/generic/extends.test.ts b/test/transformer/descriptor/generic/extends.test.ts index cc87d7efd..7f331374c 100644 --- a/test/transformer/descriptor/generic/extends.test.ts +++ b/test/transformer/descriptor/generic/extends.test.ts @@ -255,9 +255,9 @@ describe('for generic', () => { it('should avoid infinite extension', () => { const propertiesA: A = createMock(); const propertiesB: B = createMock(); - expect(propertiesA.a.b).toBe(0); + expect(propertiesA.a.a.b).toBe(0); expect(propertiesA.b).toBe(0); - expect(propertiesB.a.c).toBe(''); + expect(propertiesB.a.a.c).toBe(''); expect(propertiesB.c).toBe(''); }); }); From 086b1daaf63a30f58dad65b418e9b16210bc7579 Mon Sep 17 00:00:00 2001 From: Martin Jesper Low Madsen Date: Sat, 27 Jun 2020 10:12:32 +0200 Subject: [PATCH 04/11] fix(transformer): Keep a bound-state regardless of declaration keys, since such keys may be cached and reused in parallel A bound state is an indicator whether a new function instance is emitted, binding this in the current scope. The scope's constructor can then be referenced with `this.constructor` enabling a reference to the parent constructor. --- .../genericDeclaration/genericDeclaration.ts | 4 ++-- src/transformer/scope/scope.ts | 22 ++++++++++++++++++- 2 files changed, 23 insertions(+), 3 deletions(-) diff --git a/src/transformer/genericDeclaration/genericDeclaration.ts b/src/transformer/genericDeclaration/genericDeclaration.ts index b7f65205c..93a603ee3 100644 --- a/src/transformer/genericDeclaration/genericDeclaration.ts +++ b/src/transformer/genericDeclaration/genericDeclaration.ts @@ -167,8 +167,8 @@ export function GenericDeclaration(scope: Scope): IGenericDeclaration { } } - if (!typeParameterDeclaration || scope.currentMockKey !== declarationKey) { - genericValueDescriptor = GetDescriptor(genericNode, new Scope(declarationKey)); + if (!typeParameterDeclaration || !scope.isBound()) { + genericValueDescriptor = GetDescriptor(genericNode, (new Scope(declarationKey)).bind()); } const genericParameter: GenericParameter = createGenericParameter( diff --git a/src/transformer/scope/scope.ts b/src/transformer/scope/scope.ts index 0d8cc1a3d..04bd1e30e 100644 --- a/src/transformer/scope/scope.ts +++ b/src/transformer/scope/scope.ts @@ -3,12 +3,32 @@ import * as ts from 'typescript'; export type InterfaceOrClassDeclaration = ts.InterfaceDeclaration | ts.ClassDeclaration; export class Scope { constructor(currentMockKey?: string) { + this._bound = false; this._currentMockKey = currentMockKey; } private readonly _currentMockKey: string | undefined; + private _bound: boolean; + + private _appendConstructorMarker(): string { + return this._bound ? '_C' : ''; + } + + public bind(): this { + this._bound = true; + + return this; + } + + public isBound(): boolean { + return this._bound; + } public get currentMockKey(): string | undefined { - return this._currentMockKey; + if (this._currentMockKey === undefined) { + return; + } + + return this._currentMockKey + this._appendConstructorMarker(); } } From aa5285f8c2d29b09f919e1f208188ba298718f8c Mon Sep 17 00:00:00 2001 From: Martin Jesper Low Madsen Date: Sat, 27 Jun 2020 10:20:28 +0200 Subject: [PATCH 05/11] chore(test): Bump the nesting of the generic recursion test for good measure --- test/transformer/descriptor/generic/extends.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/transformer/descriptor/generic/extends.test.ts b/test/transformer/descriptor/generic/extends.test.ts index 7f331374c..f50c578cf 100644 --- a/test/transformer/descriptor/generic/extends.test.ts +++ b/test/transformer/descriptor/generic/extends.test.ts @@ -255,9 +255,9 @@ describe('for generic', () => { it('should avoid infinite extension', () => { const propertiesA: A = createMock(); const propertiesB: B = createMock(); - expect(propertiesA.a.a.b).toBe(0); + expect(propertiesA.a.a.a.b).toBe(0); expect(propertiesA.b).toBe(0); - expect(propertiesB.a.a.c).toBe(''); + expect(propertiesB.a.a.a.c).toBe(''); expect(propertiesB.c).toBe(''); }); }); From 0b80962a34240b12a1d96072eb0d538f924f7911 Mon Sep 17 00:00:00 2001 From: Martin Jesper Low Madsen Date: Sat, 27 Jun 2020 10:34:48 +0200 Subject: [PATCH 06/11] chore(test): Extend the circular generic test with cross references between two interfaces --- test/transformer/descriptor/generic/extends.test.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/test/transformer/descriptor/generic/extends.test.ts b/test/transformer/descriptor/generic/extends.test.ts index f50c578cf..94afe5a43 100644 --- a/test/transformer/descriptor/generic/extends.test.ts +++ b/test/transformer/descriptor/generic/extends.test.ts @@ -247,9 +247,11 @@ describe('for generic', () => { describe('with circular', () => { interface A extends ClassWithGenerics { b: number; + B: B; } interface B extends ClassWithGenerics { c: string; + A: A; } it('should avoid infinite extension', () => { @@ -259,6 +261,7 @@ describe('for generic', () => { expect(propertiesA.b).toBe(0); expect(propertiesB.a.a.a.c).toBe(''); expect(propertiesB.c).toBe(''); + expect(propertiesB.A.B.A.a.b).toBe(0); }); }); From 84762b8dcfd7590137253e1734f23f64e338eb67 Mon Sep 17 00:00:00 2001 From: Martin Jesper Low Madsen Date: Sun, 19 Jul 2020 20:01:26 +0200 Subject: [PATCH 07/11] fix(transformer): Adjust the Scope bind interface to make it work for forked extensions --- .../genericDeclaration/genericDeclaration.ts | 4 ++-- src/transformer/scope/scope.ts | 13 ++++++------- 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/src/transformer/genericDeclaration/genericDeclaration.ts b/src/transformer/genericDeclaration/genericDeclaration.ts index 93a603ee3..052aa36b5 100644 --- a/src/transformer/genericDeclaration/genericDeclaration.ts +++ b/src/transformer/genericDeclaration/genericDeclaration.ts @@ -167,8 +167,8 @@ export function GenericDeclaration(scope: Scope): IGenericDeclaration { } } - if (!typeParameterDeclaration || !scope.isBound()) { - genericValueDescriptor = GetDescriptor(genericNode, (new Scope(declarationKey)).bind()); + if (!typeParameterDeclaration || !scope.isBoundFor(extensionDeclarationKey)) { + genericValueDescriptor = GetDescriptor(genericNode, (new Scope(declarationKey)).bindFor(extensionDeclarationKey)); } const genericParameter: GenericParameter = createGenericParameter( diff --git a/src/transformer/scope/scope.ts b/src/transformer/scope/scope.ts index 04bd1e30e..81a0278d8 100644 --- a/src/transformer/scope/scope.ts +++ b/src/transformer/scope/scope.ts @@ -3,25 +3,24 @@ import * as ts from 'typescript'; export type InterfaceOrClassDeclaration = ts.InterfaceDeclaration | ts.ClassDeclaration; export class Scope { constructor(currentMockKey?: string) { - this._bound = false; this._currentMockKey = currentMockKey; } private readonly _currentMockKey: string | undefined; - private _bound: boolean; + private _boundFor: string | undefined; private _appendConstructorMarker(): string { - return this._bound ? '_C' : ''; + return this._boundFor !== undefined ? '_C' : ''; } - public bind(): this { - this._bound = true; + public bindFor(key: string): this { + this._boundFor = key; return this; } - public isBound(): boolean { - return this._bound; + public isBoundFor(key: string): boolean { + return this._boundFor === key; } public get currentMockKey(): string | undefined { From 454713fb2261d14c4dcb4fa5b06b87c7dcbb0117 Mon Sep 17 00:00:00 2001 From: Martin Jesper Low Madsen Date: Mon, 27 Jul 2020 19:57:29 +0200 Subject: [PATCH 08/11] fix(transformer): Properly distinguish scoped references in a generic context --- .../genericDeclaration/genericDeclaration.ts | 14 +++--- .../mockIdentifier/mockIdentifier.ts | 1 - src/transformer/scope/scope.ts | 7 +-- .../descriptor/generic/extends.test.ts | 45 +++++++++++++++---- 4 files changed, 49 insertions(+), 18 deletions(-) diff --git a/src/transformer/genericDeclaration/genericDeclaration.ts b/src/transformer/genericDeclaration/genericDeclaration.ts index 052aa36b5..e24d500a5 100644 --- a/src/transformer/genericDeclaration/genericDeclaration.ts +++ b/src/transformer/genericDeclaration/genericDeclaration.ts @@ -2,7 +2,7 @@ import * as ts from 'typescript'; import { GetDescriptor } from '../descriptor/descriptor'; import { TypescriptHelper } from '../descriptor/helper/helper'; import { TypescriptCreator } from '../helper/creator'; -import { MockIdentifierGenericCircularReference, MockIdentifierGenericParameterIds, MockIdentifierGenericParameterValue } from '../mockIdentifier/mockIdentifier'; +import { MockIdentifierGenericParameterIds, MockIdentifierGenericParameterValue } from '../mockIdentifier/mockIdentifier'; import { Scope } from '../scope/scope'; import { IGenericDeclaration } from './genericDeclaration.interface'; import { GenericDeclarationSupported } from './genericDeclarationSupported'; @@ -60,12 +60,14 @@ export function GenericDeclaration(scope: Scope): IGenericDeclaration { return genericDescriptor || ts.createNull(); } + const ownerReference: ts.Identifier = ts.createIdentifier(uniqueName.replace('@', '_')); + return ts.createNew( genericDescriptor ? TypescriptCreator.createFunctionExpression( ts.createBlock( [ TypescriptCreator.createVariableStatement([ - TypescriptCreator.createVariableDeclaration(MockIdentifierGenericCircularReference, ts.createIdentifier('this')), + TypescriptCreator.createVariableDeclaration(ownerReference, ts.createIdentifier('this')), ]), ts.createExpressionStatement( ts.createCall( @@ -90,7 +92,7 @@ export function GenericDeclaration(scope: Scope): IGenericDeclaration { ], ), ) : ts.createPropertyAccess( - MockIdentifierGenericCircularReference, + ownerReference, ts.createIdentifier('constructor'), ), undefined, @@ -140,6 +142,8 @@ export function GenericDeclaration(scope: Scope): IGenericDeclaration { extensionDeclaration: GenericDeclarationSupported, extensionDeclarationKey: string, extension: ts.ExpressionWithTypeArguments): void { + const nextScope: Scope = scope.currentMockKey ? scope : new Scope(declarationKey); + const extensionDeclarationTypeParameters: ts.NodeArray | undefined = extensionDeclaration.typeParameters; if (!extensionDeclarationTypeParameters?.length) { @@ -167,8 +171,8 @@ export function GenericDeclaration(scope: Scope): IGenericDeclaration { } } - if (!typeParameterDeclaration || !scope.isBoundFor(extensionDeclarationKey)) { - genericValueDescriptor = GetDescriptor(genericNode, (new Scope(declarationKey)).bindFor(extensionDeclarationKey)); + if (!typeParameterDeclaration || !nextScope.isBoundFor(extensionDeclarationKey)) { + genericValueDescriptor = GetDescriptor(genericNode, nextScope.bindFor(extensionDeclarationKey)); } const genericParameter: GenericParameter = createGenericParameter( diff --git a/src/transformer/mockIdentifier/mockIdentifier.ts b/src/transformer/mockIdentifier/mockIdentifier.ts index 49bc177e1..4ed50c992 100644 --- a/src/transformer/mockIdentifier/mockIdentifier.ts +++ b/src/transformer/mockIdentifier/mockIdentifier.ts @@ -1,6 +1,5 @@ import * as ts from 'typescript'; -export const MockIdentifierGenericCircularReference: ts.Identifier = ts.createIdentifier('that'); export const MockIdentifierGenericParameter: ts.Identifier = ts.createIdentifier('t'); export const MockIdentifierGenericParameterIds: ts.Identifier = ts.createIdentifier('i'); export const MockIdentifierGenericParameterValue: ts.Identifier = ts.createIdentifier('w'); diff --git a/src/transformer/scope/scope.ts b/src/transformer/scope/scope.ts index 81a0278d8..2739866c2 100644 --- a/src/transformer/scope/scope.ts +++ b/src/transformer/scope/scope.ts @@ -3,24 +3,25 @@ import * as ts from 'typescript'; export type InterfaceOrClassDeclaration = ts.InterfaceDeclaration | ts.ClassDeclaration; export class Scope { constructor(currentMockKey?: string) { + this._boundFor = new Set(); this._currentMockKey = currentMockKey; } private readonly _currentMockKey: string | undefined; - private _boundFor: string | undefined; + private _boundFor: Set; private _appendConstructorMarker(): string { return this._boundFor !== undefined ? '_C' : ''; } public bindFor(key: string): this { - this._boundFor = key; + this._boundFor.add(key); return this; } public isBoundFor(key: string): boolean { - return this._boundFor === key; + return this._boundFor.has(key); } public get currentMockKey(): string | undefined { diff --git a/test/transformer/descriptor/generic/extends.test.ts b/test/transformer/descriptor/generic/extends.test.ts index 94afe5a43..517a512cb 100644 --- a/test/transformer/descriptor/generic/extends.test.ts +++ b/test/transformer/descriptor/generic/extends.test.ts @@ -245,23 +245,50 @@ describe('for generic', () => { }); describe('with circular', () => { - interface A extends ClassWithGenerics { - b: number; + interface GenericC { + c: T; + } + + interface GenericD { + d: T; + } + + interface GenericE { + e: T; + } + + // NOTE: A, A, B + interface A extends GenericC, GenericD, GenericE { + a: number; B: B; } - interface B extends ClassWithGenerics { - c: string; + + // NOTE: B, A, A + interface B extends GenericC, GenericD, GenericE { + b: string; A: A; } it('should avoid infinite extension', () => { const propertiesA: A = createMock(); const propertiesB: B = createMock(); - expect(propertiesA.a.a.a.b).toBe(0); - expect(propertiesA.b).toBe(0); - expect(propertiesB.a.a.a.c).toBe(''); - expect(propertiesB.c).toBe(''); - expect(propertiesB.A.B.A.a.b).toBe(0); + + // NOTE: First generic reference becomes a new instance and the second + // reference becomes a call to the parent's constructor. + expect(propertiesA.c.c.c.a).toBe(0); + expect(propertiesB.c.c.c.b).toBe(''); + + expect(propertiesA.d.d.d.a).toBe(0); + expect(propertiesB.d.d.d.a).toBe(0); + + expect(propertiesA.e.e.e.b).toBe(''); + expect(propertiesB.e.e.e.a).toBe(0); + + expect(propertiesA.e.e.e.A.a).toBe(0); + expect(propertiesB.e.e.e.B.b).toBe(''); + + expect(propertiesA.c.d.e.b).toBe(''); + expect(propertiesB.c.d.e.b).toBe(''); }); }); From b8fa5ce01d61ffafce974025bb911f1ea58a11aa Mon Sep 17 00:00:00 2001 From: Martin Jesper Low Madsen Date: Mon, 27 Jul 2020 20:13:06 +0200 Subject: [PATCH 09/11] fix(transformer): Remove constructor marker --- src/transformer/scope/scope.ts | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/src/transformer/scope/scope.ts b/src/transformer/scope/scope.ts index 2739866c2..3048af276 100644 --- a/src/transformer/scope/scope.ts +++ b/src/transformer/scope/scope.ts @@ -10,10 +10,6 @@ export class Scope { private readonly _currentMockKey: string | undefined; private _boundFor: Set; - private _appendConstructorMarker(): string { - return this._boundFor !== undefined ? '_C' : ''; - } - public bindFor(key: string): this { this._boundFor.add(key); @@ -25,10 +21,6 @@ export class Scope { } public get currentMockKey(): string | undefined { - if (this._currentMockKey === undefined) { - return; - } - - return this._currentMockKey + this._appendConstructorMarker(); + return this._currentMockKey; } } From b66779a561bdd62fc5588414d777bf4d49b0af11 Mon Sep 17 00:00:00 2001 From: Martin Jesper Low Madsen Date: Fri, 31 Jul 2020 21:43:00 +0200 Subject: [PATCH 10/11] chore(transformer): Use MockPrivatePrefix for generic owner references --- src/transformer/genericDeclaration/genericDeclaration.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/transformer/genericDeclaration/genericDeclaration.ts b/src/transformer/genericDeclaration/genericDeclaration.ts index e24d500a5..da83e3581 100644 --- a/src/transformer/genericDeclaration/genericDeclaration.ts +++ b/src/transformer/genericDeclaration/genericDeclaration.ts @@ -2,7 +2,7 @@ import * as ts from 'typescript'; import { GetDescriptor } from '../descriptor/descriptor'; import { TypescriptHelper } from '../descriptor/helper/helper'; import { TypescriptCreator } from '../helper/creator'; -import { MockIdentifierGenericParameterIds, MockIdentifierGenericParameterValue } from '../mockIdentifier/mockIdentifier'; +import { MockIdentifierGenericParameterIds, MockIdentifierGenericParameterValue, MockPrivatePrefix } from '../mockIdentifier/mockIdentifier'; import { Scope } from '../scope/scope'; import { IGenericDeclaration } from './genericDeclaration.interface'; import { GenericDeclarationSupported } from './genericDeclarationSupported'; @@ -60,7 +60,8 @@ export function GenericDeclaration(scope: Scope): IGenericDeclaration { return genericDescriptor || ts.createNull(); } - const ownerReference: ts.Identifier = ts.createIdentifier(uniqueName.replace('@', '_')); + const scopeOwnerName: string = [nextScope.currentMockKey, uniqueName].join('_').replace(/@/g, ''); + const ownerReference: ts.Identifier = ts.createIdentifier([MockPrivatePrefix, scopeOwnerName].join('')); return ts.createNew( genericDescriptor ? TypescriptCreator.createFunctionExpression( From a9b50135a1fcf627a52aadbcc10a9d105580f63a Mon Sep 17 00:00:00 2001 From: Martin Jesper Low Madsen Date: Fri, 31 Jul 2020 21:47:36 +0200 Subject: [PATCH 11/11] fix(transformer): Correct scope nesting and owner reference through a singly linked list --- .../genericDeclaration/genericDeclaration.ts | 12 +++++- src/transformer/scope/scope.ts | 38 ++++++++++++++++--- .../descriptor/generic/extends.test.ts | 6 +++ 3 files changed, 49 insertions(+), 7 deletions(-) diff --git a/src/transformer/genericDeclaration/genericDeclaration.ts b/src/transformer/genericDeclaration/genericDeclaration.ts index da83e3581..2a619c120 100644 --- a/src/transformer/genericDeclaration/genericDeclaration.ts +++ b/src/transformer/genericDeclaration/genericDeclaration.ts @@ -52,7 +52,13 @@ export function GenericDeclaration(scope: Scope): IGenericDeclaration { } } - function createGenericParameter(ownerKey: string, nodeOwnerParameter: ts.TypeParameterDeclaration, genericDescriptor: ts.Expression | undefined, instantiable: boolean): GenericParameter { + function createGenericParameter( + ownerKey: string, + nodeOwnerParameter: ts.TypeParameterDeclaration, + genericDescriptor: ts.Expression | undefined, + instantiable: boolean, + nextScope: Scope, + ): GenericParameter { const uniqueName: string = ownerKey + nodeOwnerParameter.name.escapedText.toString(); const genericValueDescriptor: ts.Expression = ((): ts.Expression => { @@ -132,6 +138,7 @@ export function GenericDeclaration(scope: Scope): IGenericDeclaration { typeParameterDeclarations[index], GetDescriptor(genericNode, scope), false, + scope, ); generics.push(genericParameter); @@ -143,7 +150,7 @@ export function GenericDeclaration(scope: Scope): IGenericDeclaration { extensionDeclaration: GenericDeclarationSupported, extensionDeclarationKey: string, extension: ts.ExpressionWithTypeArguments): void { - const nextScope: Scope = scope.currentMockKey ? scope : new Scope(declarationKey); + const nextScope: Scope = scope.newNestedScope(declarationKey); const extensionDeclarationTypeParameters: ts.NodeArray | undefined = extensionDeclaration.typeParameters; @@ -181,6 +188,7 @@ export function GenericDeclaration(scope: Scope): IGenericDeclaration { declaration, genericValueDescriptor, isInstantiable(typeParameterDeclaration), + nextScope, ); acc.push(genericParameter); diff --git a/src/transformer/scope/scope.ts b/src/transformer/scope/scope.ts index 3048af276..37fdb130a 100644 --- a/src/transformer/scope/scope.ts +++ b/src/transformer/scope/scope.ts @@ -1,23 +1,51 @@ import * as ts from 'typescript'; export type InterfaceOrClassDeclaration = ts.InterfaceDeclaration | ts.ClassDeclaration; + export class Scope { constructor(currentMockKey?: string) { - this._boundFor = new Set(); this._currentMockKey = currentMockKey; } + private _boundFor: string | undefined; private readonly _currentMockKey: string | undefined; - private _boundFor: Set; + private _parent: this | undefined; + + public newNestedScope(currentMockKey: string): Scope { + const nestedScope: Scope = new Scope(currentMockKey); + + nestedScope._parent = this; + + return nestedScope; + } public bindFor(key: string): this { - this._boundFor.add(key); + this._boundFor = key; return this; } - public isBoundFor(key: string): boolean { - return this._boundFor.has(key); + public isBoundFor(extensionKey: string): boolean { + let isBound: boolean = this._boundFor === extensionKey; + + if (isBound) { + return isBound; + } + + let parent: Scope | undefined = this._parent; + + while (parent) { + isBound = this._currentMockKey === parent._currentMockKey && parent._boundFor === extensionKey; + + if (isBound) { + break; + } + + parent = parent._parent; + + } + + return isBound; } public get currentMockKey(): string | undefined { diff --git a/test/transformer/descriptor/generic/extends.test.ts b/test/transformer/descriptor/generic/extends.test.ts index 517a512cb..3ec9f5682 100644 --- a/test/transformer/descriptor/generic/extends.test.ts +++ b/test/transformer/descriptor/generic/extends.test.ts @@ -273,6 +273,12 @@ describe('for generic', () => { const propertiesA: A = createMock(); const propertiesB: B = createMock(); + expect(propertiesA.B.d.a).toBe(0); + expect(propertiesA.B.e.a).toBe(0); + + expect(propertiesB.A.d.a).toBe(0); + expect(propertiesB.A.e.b).toBe(''); + // NOTE: First generic reference becomes a new instance and the second // reference becomes a call to the parent's constructor. expect(propertiesA.c.c.c.a).toBe(0);