Skip to content

Commit 99a8da5

Browse files
committed
Improve recursive type detection
1 parent 914f9bb commit 99a8da5

File tree

7 files changed

+52
-19
lines changed

7 files changed

+52
-19
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
### Bug Fixes
44

55
- Fixed infinite loop caused by a fix for some complicated union/intersection types, #2468.
6+
- Improved infinite loop detection in type converter to reduce false positives.
67

78
## v0.25.5 (2024-01-01)
89

src/lib/converter/types.ts

Lines changed: 9 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,7 @@ export function loadConverters() {
102102

103103
// This ought not be necessary, but we need some way to discover recursively
104104
// typed symbols which do not have type nodes. See the `recursive` symbol in the variables test.
105-
const seenTypeSymbols = new Set<ts.Symbol>();
105+
const seenTypes = new Set<number>();
106106

107107
function maybeConvertType(
108108
context: Context,
@@ -153,20 +153,12 @@ export function convertType(
153153
);
154154
assert(node); // According to the TS source of typeToString, this is a bug if it does not hold.
155155

156-
const symbol = typeOrNode.getSymbol();
157-
if (symbol) {
158-
if (
159-
node.kind !== ts.SyntaxKind.TypeReference &&
160-
node.kind !== ts.SyntaxKind.ArrayType &&
161-
seenTypeSymbols.has(symbol)
162-
) {
163-
const typeString = context.checker.typeToString(typeOrNode);
164-
context.logger.verbose(
165-
`Refusing to recurse when converting type: ${typeString}`,
166-
);
167-
return new UnknownType(typeString);
168-
}
169-
seenTypeSymbols.add(symbol);
156+
if (seenTypes.has(typeOrNode.id)) {
157+
const typeString = context.checker.typeToString(typeOrNode);
158+
context.logger.verbose(
159+
`Refusing to recurse when converting type: ${typeString}`,
160+
);
161+
return new UnknownType(typeString);
170162
}
171163

172164
let converter = converters.get(node.kind);
@@ -179,8 +171,9 @@ export function convertType(
179171
converter = typeLiteralConverter;
180172
}
181173

174+
seenTypes.add(typeOrNode.id);
182175
const result = converter.convertType(context, typeOrNode, node);
183-
if (symbol) seenTypeSymbols.delete(symbol);
176+
seenTypes.delete(typeOrNode.id);
184177
return result;
185178
}
186179

src/lib/models/reflections/abstract.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -314,8 +314,6 @@ export abstract class Reflection {
314314

315315
/**
316316
* Url safe alias for this reflection.
317-
*
318-
* @see {@link BaseReflection.getAlias}
319317
*/
320318
private _alias?: string;
321319

src/lib/types/ts-internal/index.d.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,10 @@ declare module "typescript" {
1515
parent?: ts.Symbol;
1616
}
1717

18+
interface Type {
19+
id: number;
20+
}
21+
1822
// https://github.com/microsoft/TypeScript/blob/v5.0.2/src/compiler/utilities.ts#L7432
1923
export function getCheckFlags(symbol: ts.Symbol): CheckFlags;
2024

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

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -160,7 +160,6 @@ export class TypeDocReader implements OptionsReader {
160160
*
161161
* @param path Path to the typedoc.(js|json) file. If path is a directory
162162
* typedoc file will be attempted to be found at the root of this path
163-
* @param logger
164163
* @return the typedoc.(js|json) file path or undefined
165164
*/
166165
private findTypedocFile(path: string): string | undefined {

src/test/behavior.c2.test.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1085,4 +1085,13 @@ describe("Behavior Tests", () => {
10851085
}
10861086
logger.expectNoOtherMessages();
10871087
});
1088+
1089+
it("Should not produce warnings when processing an object type twice due to intersection", () => {
1090+
const project = convert("refusingToRecurse");
1091+
const schemaTypeBased = query(project, "schemaTypeBased");
1092+
equal(schemaTypeBased.type?.toString(), "Object & Object");
1093+
1094+
logger.expectMessage("debug: Begin readme.md*");
1095+
logger.expectNoOtherMessages({ ignoreDebug: false });
1096+
});
10881097
});
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
type OptionalKeys<T> = {
2+
[K in keyof T]: T[K] extends { $opt: any } ? K : never;
3+
}[keyof T];
4+
5+
type FromSchema<T> = T extends typeof String
6+
? string
7+
: T extends readonly [typeof Array, infer U]
8+
? FromSchema<U>[]
9+
: {
10+
[K in OptionalKeys<T>]?: FromSchema<
11+
(T[K] & { $opt: unknown })["$opt"]
12+
>;
13+
} & {
14+
[K in Exclude<keyof T, OptionalKeys<T>>]: FromSchema<T[K]>;
15+
};
16+
17+
export const schema = {
18+
x: [
19+
Array,
20+
{
21+
z: String,
22+
y: { $opt: String },
23+
},
24+
],
25+
} as const;
26+
27+
export type Schema = FromSchema<typeof schema>;
28+
29+
export const schemaTypeBased = null! as Schema;

0 commit comments

Comments
 (0)