Skip to content

Commit 626d844

Browse files
committed
Fix crash with infinitely recursive type
Fixes #2507
1 parent f92f5a8 commit 626d844

File tree

10 files changed

+72
-17
lines changed

10 files changed

+72
-17
lines changed

CHANGELOG.md

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,28 @@
11
# Unreleased
22

3+
### Bug Fixes
4+
5+
- Module readmes will now be included in JSON output, #2500.
6+
- Fixed crash when `--excludeNotDocumented` was used and the project contained a reference to a removed signature, #2496.
7+
- Fixed crash when converting an infinitely recursive type via a new `--maxTypeConversionDepth` option, #2507.
8+
- Type links in "Parameters" and "Type Parameters" sections of the page will now be correctly colored.
9+
10+
### Thanks!
11+
12+
- @JMBeresford
13+
314
## v0.25.8 (2024-02-09)
415

5-
## Features
16+
### Features
617

718
- Added a new `--sitemapBaseUrl` option. When specified, TypeDoc will generate a `sitemap.xml` in your output folder that describes the site, #2480.
819
- 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.
920
Note: This should only be used on symbols which actually represent a class, but are not declared as a class for some reason.
1021
- Added support for `@groupDescription` and `@categoryDescription` to provide a description of groups and categories, #2494.
1122
- API: Exposed `Context.getNodeComment` for plugin use, #2498.
1223

13-
## Bug Fixes
24+
### Bug Fixes
1425

15-
- Fixed crash when `--excludeNotDocumented` was used and the project contained a reference to a removed signature, #2496.
1626
- Fixed an issue where a namespace would not be created for merged function-namespaces which are declared as variables, #2478.
1727
- A class which implements itself will no longer cause a crash when rendering HTML, #2495.
1828
- Variable functions which have construct signatures will no longer be converted as functions, ignoring the construct signatures.

src/lib/converter/converter.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,10 @@ export class Converter extends ChildableComponent<
9797
@Option("preserveLinkText")
9898
accessor preserveLinkText!: boolean;
9999

100+
/** @internal */
101+
@Option("maxTypeConversionDepth")
102+
accessor maxTypeConversionDepth!: number;
103+
100104
private _config?: CommentParserConfig;
101105
private _externalSymbolResolvers: Array<ExternalSymbolResolver> = [];
102106

src/lib/converter/types.ts

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,7 @@ function maybeConvertType(
115115
return convertType(context, typeOrNode);
116116
}
117117

118+
let typeConversionDepth = 0;
118119
export function convertType(
119120
context: Context,
120121
typeOrNode: ts.Type | ts.TypeNode | undefined,
@@ -123,11 +124,18 @@ export function convertType(
123124
return new IntrinsicType("any");
124125
}
125126

127+
if (typeConversionDepth > context.converter.maxTypeConversionDepth) {
128+
return new UnknownType("...");
129+
}
130+
126131
loadConverters();
127132
if ("kind" in typeOrNode) {
128133
const converter = converters.get(typeOrNode.kind);
129134
if (converter) {
130-
return converter.convert(context, typeOrNode);
135+
++typeConversionDepth;
136+
const result = converter.convert(context, typeOrNode);
137+
--typeConversionDepth;
138+
return result;
131139
}
132140
return requestBugReport(context, typeOrNode);
133141
}
@@ -137,6 +145,7 @@ export function convertType(
137145
// will use the origin when serializing
138146
// aliasSymbol check is important - #2468
139147
if (typeOrNode.isUnion() && typeOrNode.origin && !typeOrNode.aliasSymbol) {
148+
// Don't increment typeConversionDepth as this is a transparent step to the user.
140149
return convertType(context, typeOrNode.origin);
141150
}
142151

@@ -167,7 +176,9 @@ export function convertType(
167176
}
168177

169178
seenTypes.add(typeOrNode.id);
179+
++typeConversionDepth;
170180
const result = converter.convertType(context, typeOrNode, node);
181+
--typeConversionDepth;
171182
seenTypes.delete(typeOrNode.id);
172183
return result;
173184
}

src/lib/models/reflections/abstract.ts

Lines changed: 3 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import { ok } from "assert";
21
import { Comment } from "../comments/comment";
32
import { splitUnquotedString } from "./utils";
43
import type { ProjectReflection } from "./project";
@@ -278,14 +277,8 @@ export abstract class Reflection {
278277
@NonEnumerable // So that it doesn't show up in console.log
279278
parent?: Reflection;
280279

281-
get project(): ProjectReflection {
282-
if (this.isProject()) return this;
283-
ok(
284-
this.parent,
285-
"Tried to get the project on a reflection not in a project",
286-
);
287-
return this.parent.project;
288-
}
280+
@NonEnumerable
281+
project: ProjectReflection;
289282

290283
/**
291284
* The parsed documentation comment attached to this reflection.
@@ -322,6 +315,7 @@ export abstract class Reflection {
322315
constructor(name: string, kind: ReflectionKind, parent?: Reflection) {
323316
this.id = REFLECTION_ID++;
324317
this.parent = parent;
318+
this.project = parent?.project || (this as any as ProjectReflection);
325319
this.name = name;
326320
this.kind = kind;
327321

src/lib/output/themes/default/partials/member.signature.body.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ export function memberSignatureBody(
2323
<ul class="tsd-parameter-list">
2424
{props.parameters.map((item) => (
2525
<li>
26-
<h5>
26+
<span>
2727
{context.reflectionFlags(item)}
2828
{!!item.flags.isRest && <span class="tsd-signature-symbol">...</span>}
2929
<span class="tsd-kind-parameter">{item.name}</span>
@@ -35,7 +35,7 @@ export function memberSignatureBody(
3535
{item.defaultValue}
3636
</span>
3737
)}
38-
</h5>
38+
</span>
3939
{context.commentSummary(item)}
4040
{context.commentTags(item)}
4141
{item.type instanceof ReflectionType && context.parameter(item.type.declaration)}

src/lib/output/themes/default/partials/typeParameters.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ export function typeParameters(context: DefaultThemeRenderContext, typeParameter
1010
<ul class="tsd-type-parameter-list">
1111
{typeParameters?.map((item) => (
1212
<li>
13-
<h4>
13+
<span>
1414
<a id={item.anchor} class="tsd-anchor"></a>
1515
{item.flags.isConst && <span class="tsd-signature-keyword">const </span>}
1616
{item.varianceModifier && (
@@ -29,7 +29,7 @@ export function typeParameters(context: DefaultThemeRenderContext, typeParameter
2929
{context.type(item.default)}
3030
</>
3131
)}
32-
</h4>
32+
</span>
3333
{context.commentSummary(item)}
3434
{context.commentTags(item)}
3535
</li>

src/lib/utils/options/declaration.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,7 @@ export interface TypeDocOptionMap {
106106
excludeProtected: boolean;
107107
excludeReferences: boolean;
108108
excludeCategories: string[];
109+
maxTypeConversionDepth: number;
109110
name: string;
110111
includeVersion: boolean;
111112
disableSources: boolean;

src/lib/utils/options/sources/typedoc.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -291,6 +291,12 @@ export function addTypeDocOptions(options: Pick<Options, "addDeclaration">) {
291291
}
292292
},
293293
});
294+
options.addDeclaration({
295+
name: "maxTypeConversionDepth",
296+
help: "Set the maximum depth of types to be converted.",
297+
defaultValue: 10,
298+
type: ParameterType.Number,
299+
});
294300
options.addDeclaration({
295301
name: "name",
296302
help: "Set the name of the project that will be used in the header of the template.",
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
export interface Value {
2+
values: Value[];
3+
}
4+
5+
export function fromPartial<I extends Exact<Value, I>>(object: I): void {
6+
throw 1;
7+
}
8+
9+
export type Exact<P, I extends P> = P extends P
10+
? P & { [K in keyof P]: Exact<P[K], I[K]> }
11+
: never;

src/test/issues.c2.test.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1388,4 +1388,22 @@ describe("Issue Tests", () => {
13881388
app.options.setValue("excludeNotDocumented", true);
13891389
convert();
13901390
});
1391+
1392+
it("Handles an infinitely recursive type, #2507", () => {
1393+
const project = convert();
1394+
const type = querySig(project, "fromPartial").typeParameters![0].type;
1395+
1396+
// function fromPartial<I extends Value & {
1397+
// values: Value[] & (Value & {
1398+
// values: Value[] & (Value & {
1399+
// values: Value[] & (Value & {
1400+
// values: Value[] & (Value & {
1401+
// ...;
1402+
// })[];
1403+
// })[];
1404+
// })[];
1405+
// })[];
1406+
// }>(object: I): void
1407+
equal(type?.toString(), "Value & Object");
1408+
});
13911409
});

0 commit comments

Comments
 (0)