Skip to content

Commit 9911d94

Browse files
fix(transformer): Ensure mocked interfaces don't extend themselves infinitely 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]>
1 parent 14acdb5 commit 9911d94

File tree

5 files changed

+60
-2
lines changed

5 files changed

+60
-2
lines changed

src/transformer/genericDeclaration/genericDeclaration.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ 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';
57
import { MockIdentifierGenericParameterIds, MockIdentifierGenericParameterValue } from '../mockIdentifier/mockIdentifier';
68
import { Scope } from '../scope/scope';
79
import { IGenericDeclaration } from './genericDeclaration.interface';
@@ -86,6 +88,16 @@ export function GenericDeclaration(scope: Scope): IGenericDeclaration {
8688

8789
if (ts.isTypeReferenceNode(genericNode)) {
8890
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+
}
100+
89101
if (ts.isTypeParameterDeclaration(typeParameterDeclaration)) {
90102
addGenericParameterToExisting(
91103
extensionDeclarationTypeParameters[index],

src/transformer/logger/transformerLogger.ts

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

66
export interface TransformerLogger {
7+
circularGenericNotSupported(nodeName: string): void;
78
unexpectedCreateMock(mockFileName: string, expectedFileName: string): void;
89
typeNotSupported(type: string): void;
910
typeOfFunctionCallNotFound(node: string): void;
@@ -13,6 +14,12 @@ export function TransformerLogger(): TransformerLogger {
1314
logger = logger || Logger('Transformer');
1415

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

src/transformer/mockFactoryCall/mockFactoryCall.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -116,7 +116,8 @@ function addFromDeclarationExtensions(declaration: GenericDeclarationSupported,
116116
declarationKey,
117117
extensionDeclaration as GenericDeclarationSupported,
118118
extensionDeclarationKey,
119-
extension);
119+
extension,
120+
);
120121

121122
addFromDeclarationExtensions(extensionDeclaration as GenericDeclarationSupported, extensionDeclarationKey, genericDeclaration);
122123
});

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

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -244,4 +244,16 @@ describe('for generic', () => {
244244
});
245245
});
246246

247+
describe('with circular', () => {
248+
interface A extends ClassWithGenerics<A> {
249+
b: number;
250+
}
251+
252+
it('should avoid infinite extension', () => {
253+
const properties: A = createMock<A>();
254+
expect(properties.a).toBeDefined();
255+
expect(properties.b).toBe(0);
256+
});
257+
});
258+
247259
});

ui/src/views/types-not-supported.mdx

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,4 +68,30 @@ Unfortunately this functionality doesnt work yet because when getting properties
6868
a mapped type typescript returns a different type of property that is difficult to mock.
6969

7070
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
71-
[link](https://github.com/Typescript-TDD/ts-auto-mock/tree/feature/extends-mapped-type)
71+
[link](https://github.com/Typescript-TDD/ts-auto-mock/tree/feature/extends-mapped-type)
72+
73+
74+
## Circular Generics
75+
76+
```ts
77+
class C<T> {
78+
public propC: T
79+
public test: string
80+
}
81+
82+
class A extends C<A> {
83+
public propA: number
84+
}
85+
const a: A = createMock<A>();
86+
87+
// This will fail because we will not support generics of the same type.
88+
expect(a.propC.propC.test).toBe("");
89+
```
90+
91+
These are discussed here:
92+
[link](https://github.com/Typescript-TDD/ts-auto-mock/pull/312). As of this
93+
writing, the problem with circular generics is that the generated AST will
94+
circle `A` over and over, and result in an infinite nested tree of declaration
95+
references. The intended behavior is to have the first back-reference stored
96+
elsewhere in the generated output and let it reference itself, making the
97+
runtime a lazy-evaluated sequence of getters.

0 commit comments

Comments
 (0)