Skip to content

Commit

Permalink
fix(transformer): Ensure mocked interfaces don't extend themselves in…
Browse files Browse the repository at this point in the history
…finitely if passed as generic argument (#312)

* fix(transformer): Ensure mocked interfaces don't extend themselves infinitely if passed as generic argument

* chore(docs): Add circular generics to types-not-supported.mdx and warn about its use

Co-authored-by: Vittorio Guerriero <[email protected]>
  • Loading branch information
martinjlowm and uittorio authored May 11, 2020
1 parent 14acdb5 commit 9911d94
Show file tree
Hide file tree
Showing 5 changed files with 60 additions and 2 deletions.
12 changes: 12 additions & 0 deletions src/transformer/genericDeclaration/genericDeclaration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ 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';
Expand Down Expand Up @@ -86,6 +88,16 @@ export function GenericDeclaration(scope: Scope): IGenericDeclaration {

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;
}

if (ts.isTypeParameterDeclaration(typeParameterDeclaration)) {
addGenericParameterToExisting(
extensionDeclarationTypeParameters[index],
Expand Down
7 changes: 7 additions & 0 deletions src/transformer/logger/transformerLogger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ 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 @@ -13,6 +14,12 @@ 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
3 changes: 2 additions & 1 deletion src/transformer/mockFactoryCall/mockFactoryCall.ts
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,8 @@ function addFromDeclarationExtensions(declaration: GenericDeclarationSupported,
declarationKey,
extensionDeclaration as GenericDeclarationSupported,
extensionDeclarationKey,
extension);
extension,
);

addFromDeclarationExtensions(extensionDeclaration as GenericDeclarationSupported, extensionDeclarationKey, genericDeclaration);
});
Expand Down
12 changes: 12 additions & 0 deletions test/transformer/descriptor/generic/extends.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -244,4 +244,16 @@ describe('for generic', () => {
});
});

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

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

});
28 changes: 27 additions & 1 deletion ui/src/views/types-not-supported.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -68,4 +68,30 @@ Unfortunately this functionality doesnt work yet because when getting properties
a mapped type typescript returns a different type of property that is difficult to mock.

There is a branch created with a working version but it needs more investigation because the implementation is not readable and it may cause more issues
[link](https://github.com/Typescript-TDD/ts-auto-mock/tree/feature/extends-mapped-type)
[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.

0 comments on commit 9911d94

Please sign in to comment.