Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
15 commits
Select commit Hold shift + click to select a range
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
4 changes: 2 additions & 2 deletions src/transformer/descriptor/helper/helper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,10 +60,10 @@ export namespace TypescriptHelper {
return declarations[0];
}

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

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

return typeParameters;
}
Expand Down
123 changes: 99 additions & 24 deletions src/transformer/genericDeclaration/genericDeclaration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,22 +2,34 @@ 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 { MockIdentifierGenericParameterIds, MockIdentifierGenericParameterValue, MockPrivatePrefix } 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[] = [];

function isGenericProvided<T extends ts.TypeReferenceNode | ts.ExpressionWithTypeArguments>(node: T, index: number): node is T & Required<ts.NodeWithTypeArguments> {
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];
}
Expand All @@ -40,11 +52,69 @@ 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,
nextScope: Scope,
): 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();
}

const scopeOwnerName: string = [nextScope.currentMockKey, uniqueName].join('_').replace(/@/g, '');
const ownerReference: ts.Identifier = ts.createIdentifier([MockPrivatePrefix, scopeOwnerName].join(''));

return ts.createNew(
genericDescriptor ? TypescriptCreator.createFunctionExpression(
ts.createBlock(
[
TypescriptCreator.createVariableStatement([
TypescriptCreator.createVariableDeclaration(ownerReference, ts.createIdentifier('this')),
]),
ts.createExpressionStatement(
ts.createCall(
ts.createPropertyAccess(
ts.createIdentifier('Object'),
ts.createIdentifier('defineProperties'),
),
undefined,
[
ts.createIdentifier('this'),
ts.createCall(
ts.createPropertyAccess(
ts.createIdentifier('Object'),
ts.createIdentifier('getOwnPropertyDescriptors'),
),
undefined,
[genericDescriptor]
),
]
),
),
],
),
) : ts.createPropertyAccess(
ownerReference,
ts.createIdentifier('constructor'),
),
undefined,
undefined,
);
})();

const genericFunction: ts.FunctionExpression =
TypescriptCreator.createFunctionExpression(
ts.createBlock([
ts.createReturn(
genericValueDescriptor,
),
]),
);

return {
ids: [uniqueName],
Expand All @@ -54,9 +124,9 @@ export function GenericDeclaration(scope: Scope): IGenericDeclaration {

return {
addFromTypeReferenceNode(node: ts.TypeReferenceNode, declarationKey: string): void {
const typeParameterDeclarations: ts.NodeArray<ts.TypeParameterDeclaration> = TypescriptHelper.GetParameterOfNode(node.typeName);
const typeParameterDeclarations: ts.NodeArray<ts.TypeParameterDeclaration> | undefined = TypescriptHelper.GetParameterOfNode(node.typeName);

if (!typeParameterDeclarations) {
if (!typeParameterDeclarations?.length) {
return;
}

Expand All @@ -66,7 +136,10 @@ export function GenericDeclaration(scope: Scope): IGenericDeclaration {
const genericParameter: GenericParameter = createGenericParameter(
declarationKey,
typeParameterDeclarations[index],
GetDescriptor(genericNode, scope));
GetDescriptor(genericNode, scope),
false,
scope,
);

generics.push(genericParameter);
});
Expand All @@ -77,26 +150,22 @@ export function GenericDeclaration(scope: Scope): IGenericDeclaration {
extensionDeclaration: GenericDeclarationSupported,
extensionDeclarationKey: string,
extension: ts.ExpressionWithTypeArguments): void {
const nextScope: Scope = scope.newNestedScope(declarationKey);

const extensionDeclarationTypeParameters: ts.NodeArray<ts.TypeParameterDeclaration> | 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(
Expand All @@ -110,10 +179,16 @@ export function GenericDeclaration(scope: Scope): IGenericDeclaration {
}
}

if (!typeParameterDeclaration || !nextScope.isBoundFor(extensionDeclarationKey)) {
genericValueDescriptor = GetDescriptor(genericNode, nextScope.bindFor(extensionDeclarationKey));
}

const genericParameter: GenericParameter = createGenericParameter(
extensionDeclarationKey,
extensionDeclarationTypeParameters[index],
GetDescriptor(genericNode, scope),
declaration,
genericValueDescriptor,
isInstantiable(typeParameterDeclaration),
nextScope,
);

acc.push(genericParameter);
Expand Down
7 changes: 0 additions & 7 deletions src/transformer/logger/transformerLogger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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}
Expand Down
40 changes: 40 additions & 0 deletions src/transformer/scope/scope.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,52 @@
import * as ts from 'typescript';

export type InterfaceOrClassDeclaration = ts.InterfaceDeclaration | ts.ClassDeclaration;

export class Scope {
constructor(currentMockKey?: string) {
this._currentMockKey = currentMockKey;
}

private _boundFor: string | undefined;
private readonly _currentMockKey: string | undefined;
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 = key;

return this;
}

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 {
return this._currentMockKey;
Expand Down
52 changes: 47 additions & 5 deletions test/transformer/descriptor/generic/extends.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -245,14 +245,56 @@ describe('for generic', () => {
});

describe('with circular', () => {
interface A extends ClassWithGenerics<A> {
b: number;
interface GenericC<T> {
c: T;
}

interface GenericD<T> {
d: T;
}

interface GenericE<T> {
e: T;
}

// NOTE: A, A, B
interface A extends GenericC<A>, GenericD<A>, GenericE<B> {
a: number;
B: B;
}

// NOTE: B, A, A
interface B extends GenericC<B>, GenericD<A>, GenericE<A> {
b: string;
A: A;
}

it('should avoid infinite extension', () => {
const properties: A = createMock<A>();
expect(properties.a).toBeDefined();
expect(properties.b).toBe(0);
const propertiesA: A = createMock<A>();
const propertiesB: B = createMock<B>();

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);
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('');
});
});

Expand Down
28 changes: 0 additions & 28 deletions ui/src/views/types-not-supported.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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<T> {
public propC: T
public test: string
}

class A extends C<A> {
public propA: number
}
const a: A = createMock<A>();

// 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 {
Expand Down Expand Up @@ -128,6 +103,3 @@ interface StandardInterface {

type Hello = StandardInterface['prop'];
```