Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix OOM error on recursive types #1153

Merged
merged 7 commits into from
Dec 26, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
37 changes: 23 additions & 14 deletions examples/basic/src/generics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
* @param value A generic parameter.
* @returns A generic return value.
*/
function testFunction<T>(value:T):T {
function testFunction<T>(value: T): T {
return value;
}

Expand All @@ -14,41 +14,37 @@ function testFunction<T>(value:T):T {
*
* @param T The generic type parameter.
*/
interface A<T>
{
interface A<T> {
/**
* A generic member function.
*
* @return A generic return value.
*/
getT():T;
getT(): T;
}


/**
* A generic interface with two type parameters.
*
* @param <T> The first generic type parameter.
* @param <C> The second generic type parameter.
*/
interface B<T, C>
{
interface B<T, C> {
/**
* A generic member function.
*
* @param value A generic parameter.
*/
setT(value:T):void;
setT(value: T): void;

/**
* A generic member function.
*
* @return A generic return value.
*/
getC():C;
getC(): C;
}


/**
* A generic interface extending two other generic interfaces
* and setting one of the type parameters.
Expand All @@ -57,24 +53,37 @@ interface B<T, C>
*/
interface AB<T> extends A<T>, B<T, boolean> {}


/**
* An interface extending a generic interface and setting its type parameter.
*/
interface ABString extends AB<string> {}


/**
* An interface extending a generic interface and setting its type parameter.
*/
interface ABNumber extends AB<number> {}


/**
* A function returning a generic array with type parameters.
*
* @return The return value with type arguments.
*/
function getGenericArray():Array<string> {
function getGenericArray(): Array<string> {
return [''];
}

/**
* Conditional type with infer
*/
type PopFront<T extends any[]> = ((...args: T) => any) extends ((a: any, ...r: infer R) => any) ? R : never;

/**
* See GH#1150. Calling typeChecker.typeToString on this type will send TS into an infinite
* loop, which is undesirable.
*/
type HorribleRecursiveTypeThatShouldNotBeUsedByAnyone<T extends any[], R = {}> = {
0: R,
1: HorribleRecursiveTypeThatShouldNotBeUsedByAnyone<PopFront<T>, {
[K in keyof R | keyof T[0]]: K extends keyof R ? R[K] : T[0][K]
}>
}[T['length'] extends 0 ? 0 : 1];
9 changes: 8 additions & 1 deletion scripts/rebuild_specs.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
// @ts-check

const assert = require('assert');
const fs = require('fs-extra');
const path = require('path');
const TypeDoc = require('..');
Expand Down Expand Up @@ -49,7 +50,13 @@ function rebuildConverterTests(dirs) {
TypeDoc.resetReflectionID();
before();
const result = app.convert(src);
const data = JSON.stringify(result.toObject(), null, ' ')
// Until GH#936 lands, removing toObject, ensure toObject remains consistent
// with the serializers.
const serialized = result.toObject();
const serialized2 = app.serializer.toObject(result);
assert.deepStrictEqual(serialized, serialized2);

const data = JSON.stringify(serialized, null, ' ')
.split(TypeDoc.normalizePath(base))
.join('%BASE%');
after();
Expand Down
56 changes: 56 additions & 0 deletions src/lib/converter/types/conditional.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import * as ts from 'typescript';

import { ConditionalType, Type } from '../../models/types';
import { Component, ConverterTypeComponent, TypeConverter } from '../components';
import { Context } from '../context';

@Component({name: 'type:conditional'})
export class ConditionalConverter extends ConverterTypeComponent implements TypeConverter<ts.ConditionalType, ts.ConditionalTypeNode> {
/**
* Test whether this converter can handle the given TypeScript node.
*/
supportsNode(context: Context, node: ts.ConditionalTypeNode): boolean {
return node.kind === ts.SyntaxKind.ConditionalType;
}

/**
* Test whether this converter can handle the given TypeScript type.
*/
supportsType(context: Context, type: ts.ConditionalType): boolean {
return !!(type.flags & ts.TypeFlags.Conditional);
}

/**
* Convert the given conditional type node to its type reflection.
*
* This is a node based converter, see [[convertType]] for the type equivalent.
*
* @param context The context object describing the current state the converter is in.
* @param node The conditional or intersection type node that should be converted.
* @returns The type reflection representing the given conditional type node.
*/
convertNode(context: Context, node: ts.ConditionalTypeNode): ConditionalType | undefined {
const types = this.owner.convertTypes(context, [node.checkType, node.extendsType, node.trueType, node.falseType]);
if (types.length !== 4) {
return undefined;
}
return new ConditionalType(...types as [Type, Type, Type, Type]);
}

/**
* Convert the given conditional type to its type reflection.
*
* This is a type based converter, see [[convertNode]] for the node equivalent.
*
* @param context The context object describing the current state the converter is in.
* @param type The conditional type that should be converted.
* @returns The type reflection representing the given conditional type.
*/
convertType(context: Context, type: ts.ConditionalType): ConditionalType | undefined {
const types = this.owner.convertTypes(context, [], [type.checkType, type.extendsType, type.resolvedTrueType, type.resolvedFalseType]);
if (types.length !== 4) {
return undefined;
}
return new ConditionalType(...types as [Type, Type, Type, Type]);
}
}
3 changes: 3 additions & 0 deletions src/lib/converter/types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,10 @@ export { AliasConverter } from './alias';
export { ArrayConverter } from './array';
export { BindingArrayConverter } from './binding-array';
export { BindingObjectConverter } from './binding-object';
export { ConditionalConverter } from './conditional';
export { EnumConverter } from './enum';
export { InferredConverter } from './inferred';
export { IndexedAccessConverter } from './indexed-access';
export { IntrinsicConverter } from './intrinsic';
export { PredicateConverter } from './predicate';
export { StringLiteralConverter } from './string-literal';
Expand Down
28 changes: 28 additions & 0 deletions src/lib/converter/types/indexed-access.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import * as ts from 'typescript';

import { Type, IndexedAccessType } from '../../models/index';
import {
Component,
ConverterTypeComponent,
TypeNodeConverter
} from '../components';
import { Context } from '../context';

@Component({ name: 'type:indexed-access' })
export class IndexedAccessConverter extends ConverterTypeComponent
implements TypeNodeConverter<ts.Type, ts.IndexedAccessTypeNode> {
/**
* Test whether this converter can handle the given TypeScript node.
*/
supportsNode(context: Context, node: ts.TypeNode) {
return ts.isIndexedAccessTypeNode(node);
}

convertNode(context: Context, node: ts.IndexedAccessTypeNode): Type | undefined {
const objectType = this.owner.convertType(context, node.objectType);
if (!objectType) { return; }
const indexType = this.owner.convertType(context, node.indexType);
if (!indexType) { return; }
return new IndexedAccessType(objectType, indexType);
}
}
28 changes: 28 additions & 0 deletions src/lib/converter/types/inferred.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import * as ts from 'typescript';

import { InferredType } from '../../models/types';
import { Component, ConverterTypeComponent, TypeNodeConverter } from '../components';
import { Context } from '../context';

@Component({name: 'type:inferred'})
export class InferredConverter extends ConverterTypeComponent implements TypeNodeConverter<ts.Type, ts.InferTypeNode> {
/**
* Test whether this converter can handle the given TypeScript node.
*/
supportsNode(_context: Context, node: ts.TypeNode): boolean {
return ts.isInferTypeNode(node);
}

/**
* Convert the given conditional type node to its type reflection.
*
* This is a node based converter, see [[convertType]] for the type equivalent.
*
* @param context The context object describing the current state the converter is in.
* @param node The conditional or intersection type node that should be converted.
* @returns The type reflection representing the given conditional type node.
*/
convertNode(context: Context, node: ts.InferTypeNode): InferredType | undefined {
return new InferredType(node.typeParameter.getText());
}
}
2 changes: 1 addition & 1 deletion src/lib/converter/types/union-or-intersection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ export class UnionOrIntersectionConverter extends ConverterTypeComponent impleme
/**
* Convert the given union type to its type reflection.
*
* This is a type based converter, see [[convertUnionTypeNode]] for the node equivalent.
* This is a type based converter, see [[convertNode]] for the node equivalent.
*
* ```
* let someValue: string|number;
Expand Down
72 changes: 72 additions & 0 deletions src/lib/models/types/conditional.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import { Type } from './abstract';

/**
* Represents a conditional type.
*
* ~~~
* let value: C extends E ? T : F;
* let value2: Check extends Extends ? True : False;
* ~~~
*/
export class ConditionalType extends Type {
/**
* The type name identifier.
*/
readonly type: string = 'conditional';

constructor(
public checkType: Type,
public extendsType: Type,
public trueType: Type,
public falseType: Type
) {
super();
}

/**
* Clone this type.
*
* @return A clone of this type.
*/
clone(): Type {
return new ConditionalType(this.checkType, this.extendsType, this.trueType, this.falseType);
}

/**
* Test whether this type equals the given type.
*
* @param type The type that should be checked for equality.
* @returns TRUE if the given type equals this type, FALSE otherwise.
*/
equals(type: any): boolean {
if (!(type instanceof ConditionalType)) {
return false;
}
return this.checkType.equals(type.checkType) &&
this.extendsType.equals(type.extendsType) &&
this.trueType.equals(type.trueType) &&
this.falseType.equals(type.falseType);
}

/**
* Return a raw object representation of this type.
* @deprecated Use serializers instead
*/
toObject(): any {
const result: any = super.toObject();

result.checkType = this.checkType.toObject();
result.extendsType = this.extendsType.toObject();
result.trueType = this.trueType.toObject();
result.falseType = this.falseType.toObject();

return result;
}

/**
* Return a string representation of this type.
*/
toString() {
return this.checkType + ' extends ' + this.extendsType + ' ? ' + this.trueType + ' : ' + this.falseType;
}
}
3 changes: 3 additions & 0 deletions src/lib/models/types/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
export { Type } from './abstract';
export { ArrayType } from './array';
export { ConditionalType } from './conditional';
export { IndexedAccessType } from './indexed-access';
export { InferredType } from './inferred';
export { IntersectionType } from './intersection';
export { IntrinsicType } from './intrinsic';
export { PredicateType } from './predicate';
Expand Down
61 changes: 61 additions & 0 deletions src/lib/models/types/indexed-access.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import { Type } from './index';

/**
* Represents an indexed access type.
*/
export class IndexedAccessType extends Type {
/**
* The type name identifier.
*/
readonly type = 'indexedAccess';

/**
* Create a new TupleType instance.
*
* @param elementType The type of the array's elements.
*/
constructor(public objectType: Type, public indexType: Type) {
super();
}

/**
* Clone this type.
*
* @return A clone of this type.
*/
clone(): Type {
return new IndexedAccessType(this.objectType, this.indexType);
}

/**
* Test whether this type equals the given type.
*
* @param type The type that should be checked for equality.
* @returns TRUE if the given type equals this type, FALSE otherwise.
*/
equals(type: Type): boolean {
if (!(type instanceof IndexedAccessType)) {
return false;
}
return type.objectType.equals(this.objectType) && type.indexType.equals(this.indexType);
}

/**
* Return a raw object representation of this type.
* @deprecated Use serializers instead
*/
toObject(): any {
return {
...super.toObject(),
objectType: this.objectType.toObject(),
indexType: this.indexType.toObject()
};
}

/**
* Return a string representation of this type.
*/
toString() {
return `${this.objectType.toString()}[${this.indexType.toString()}]`;
}
}