Skip to content

Commit

Permalink
Fix crash with infinitely recursive type
Browse files Browse the repository at this point in the history
Fixes #2507
  • Loading branch information
Gerrit0 committed Feb 26, 2024
1 parent f92f5a8 commit 626d844
Show file tree
Hide file tree
Showing 10 changed files with 72 additions and 17 deletions.
16 changes: 13 additions & 3 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,18 +1,28 @@
# Unreleased

### Bug Fixes

- Module readmes will now be included in JSON output, #2500.
- Fixed crash when `--excludeNotDocumented` was used and the project contained a reference to a removed signature, #2496.
- Fixed crash when converting an infinitely recursive type via a new `--maxTypeConversionDepth` option, #2507.
- Type links in "Parameters" and "Type Parameters" sections of the page will now be correctly colored.

### Thanks!

- @JMBeresford

## v0.25.8 (2024-02-09)

## Features
### Features

- Added a new `--sitemapBaseUrl` option. When specified, TypeDoc will generate a `sitemap.xml` in your output folder that describes the site, #2480.
- Added support for the `@class` tag. When added to a comment on a variable or function, TypeDoc will convert the member as a class, #2479.
Note: This should only be used on symbols which actually represent a class, but are not declared as a class for some reason.
- Added support for `@groupDescription` and `@categoryDescription` to provide a description of groups and categories, #2494.
- API: Exposed `Context.getNodeComment` for plugin use, #2498.

## Bug Fixes
### Bug Fixes

- Fixed crash when `--excludeNotDocumented` was used and the project contained a reference to a removed signature, #2496.
- Fixed an issue where a namespace would not be created for merged function-namespaces which are declared as variables, #2478.
- A class which implements itself will no longer cause a crash when rendering HTML, #2495.
- Variable functions which have construct signatures will no longer be converted as functions, ignoring the construct signatures.
Expand Down
4 changes: 4 additions & 0 deletions src/lib/converter/converter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,10 @@ export class Converter extends ChildableComponent<
@Option("preserveLinkText")
accessor preserveLinkText!: boolean;

/** @internal */
@Option("maxTypeConversionDepth")
accessor maxTypeConversionDepth!: number;

private _config?: CommentParserConfig;
private _externalSymbolResolvers: Array<ExternalSymbolResolver> = [];

Expand Down
13 changes: 12 additions & 1 deletion src/lib/converter/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,7 @@ function maybeConvertType(
return convertType(context, typeOrNode);
}

let typeConversionDepth = 0;
export function convertType(
context: Context,
typeOrNode: ts.Type | ts.TypeNode | undefined,
Expand All @@ -123,11 +124,18 @@ export function convertType(
return new IntrinsicType("any");
}

if (typeConversionDepth > context.converter.maxTypeConversionDepth) {
return new UnknownType("...");
}

loadConverters();
if ("kind" in typeOrNode) {
const converter = converters.get(typeOrNode.kind);
if (converter) {
return converter.convert(context, typeOrNode);
++typeConversionDepth;
const result = converter.convert(context, typeOrNode);
--typeConversionDepth;
return result;
}
return requestBugReport(context, typeOrNode);
}
Expand All @@ -137,6 +145,7 @@ export function convertType(
// will use the origin when serializing
// aliasSymbol check is important - #2468
if (typeOrNode.isUnion() && typeOrNode.origin && !typeOrNode.aliasSymbol) {
// Don't increment typeConversionDepth as this is a transparent step to the user.
return convertType(context, typeOrNode.origin);
}

Expand Down Expand Up @@ -167,7 +176,9 @@ export function convertType(
}

seenTypes.add(typeOrNode.id);
++typeConversionDepth;
const result = converter.convertType(context, typeOrNode, node);
--typeConversionDepth;
seenTypes.delete(typeOrNode.id);
return result;
}
Expand Down
12 changes: 3 additions & 9 deletions src/lib/models/reflections/abstract.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import { ok } from "assert";
import { Comment } from "../comments/comment";
import { splitUnquotedString } from "./utils";
import type { ProjectReflection } from "./project";
Expand Down Expand Up @@ -278,14 +277,8 @@ export abstract class Reflection {
@NonEnumerable // So that it doesn't show up in console.log
parent?: Reflection;

get project(): ProjectReflection {
if (this.isProject()) return this;
ok(
this.parent,
"Tried to get the project on a reflection not in a project",
);
return this.parent.project;
}
@NonEnumerable
project: ProjectReflection;

/**
* The parsed documentation comment attached to this reflection.
Expand Down Expand Up @@ -322,6 +315,7 @@ export abstract class Reflection {
constructor(name: string, kind: ReflectionKind, parent?: Reflection) {
this.id = REFLECTION_ID++;
this.parent = parent;
this.project = parent?.project || (this as any as ProjectReflection);
this.name = name;
this.kind = kind;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ export function memberSignatureBody(
<ul class="tsd-parameter-list">
{props.parameters.map((item) => (
<li>
<h5>
<span>
{context.reflectionFlags(item)}
{!!item.flags.isRest && <span class="tsd-signature-symbol">...</span>}
<span class="tsd-kind-parameter">{item.name}</span>
Expand All @@ -35,7 +35,7 @@ export function memberSignatureBody(
{item.defaultValue}
</span>
)}
</h5>
</span>
{context.commentSummary(item)}
{context.commentTags(item)}
{item.type instanceof ReflectionType && context.parameter(item.type.declaration)}
Expand Down
4 changes: 2 additions & 2 deletions src/lib/output/themes/default/partials/typeParameters.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ export function typeParameters(context: DefaultThemeRenderContext, typeParameter
<ul class="tsd-type-parameter-list">
{typeParameters?.map((item) => (
<li>
<h4>
<span>
<a id={item.anchor} class="tsd-anchor"></a>
{item.flags.isConst && <span class="tsd-signature-keyword">const </span>}
{item.varianceModifier && (
Expand All @@ -29,7 +29,7 @@ export function typeParameters(context: DefaultThemeRenderContext, typeParameter
{context.type(item.default)}
</>
)}
</h4>
</span>
{context.commentSummary(item)}
{context.commentTags(item)}
</li>
Expand Down
1 change: 1 addition & 0 deletions src/lib/utils/options/declaration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,7 @@ export interface TypeDocOptionMap {
excludeProtected: boolean;
excludeReferences: boolean;
excludeCategories: string[];
maxTypeConversionDepth: number;
name: string;
includeVersion: boolean;
disableSources: boolean;
Expand Down
6 changes: 6 additions & 0 deletions src/lib/utils/options/sources/typedoc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -291,6 +291,12 @@ export function addTypeDocOptions(options: Pick<Options, "addDeclaration">) {
}
},
});
options.addDeclaration({
name: "maxTypeConversionDepth",
help: "Set the maximum depth of types to be converted.",
defaultValue: 10,
type: ParameterType.Number,
});
options.addDeclaration({
name: "name",
help: "Set the name of the project that will be used in the header of the template.",
Expand Down
11 changes: 11 additions & 0 deletions src/test/converter2/issues/gh2507.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
export interface Value {
values: Value[];
}

export function fromPartial<I extends Exact<Value, I>>(object: I): void {
throw 1;
}

export type Exact<P, I extends P> = P extends P
? P & { [K in keyof P]: Exact<P[K], I[K]> }
: never;
18 changes: 18 additions & 0 deletions src/test/issues.c2.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1388,4 +1388,22 @@ describe("Issue Tests", () => {
app.options.setValue("excludeNotDocumented", true);
convert();
});

it("Handles an infinitely recursive type, #2507", () => {
const project = convert();
const type = querySig(project, "fromPartial").typeParameters![0].type;

// function fromPartial<I extends Value & {
// values: Value[] & (Value & {
// values: Value[] & (Value & {
// values: Value[] & (Value & {
// values: Value[] & (Value & {
// ...;
// })[];
// })[];
// })[];
// })[];
// }>(object: I): void
equal(type?.toString(), "Value & Object");
});
});

0 comments on commit 626d844

Please sign in to comment.