Skip to content

Commit 9816599

Browse files
committed
Fix process halting for infinite recursions in nested fields
1 parent 6107758 commit 9816599

2 files changed

Lines changed: 162 additions & 89 deletions

File tree

lib/parse/ParameterResolver.ts

Lines changed: 65 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,12 @@
11
import type { TSTypeLiteral } from '@typescript-eslint/types/dist/ts-estree';
22
import { AST_NODE_TYPES } from '@typescript-eslint/typescript-estree';
33
import * as LRUCache from 'lru-cache';
4-
import type { ClassIndex, ClassReference, ClassReferenceLoaded, InterfaceLoaded } from './ClassIndex';
4+
import type {
5+
ClassIndex,
6+
ClassReference,
7+
ClassReferenceLoaded,
8+
InterfaceLoaded,
9+
} from './ClassIndex';
510
import type { ClassLoader } from './ClassLoader';
611
import type { CommentLoader } from './CommentLoader';
712
import type { ConstructorData } from './ConstructorLoader';
@@ -61,6 +66,7 @@ export class ParameterResolver {
6166
unresolvedConstructorData.parameters,
6267
unresolvedConstructorData.classLoaded,
6368
{},
69+
new Set(),
6470
)).filter(parameter => parameter.type === 'field'),
6571
classLoaded: unresolvedConstructorData.classLoaded,
6672
};
@@ -106,7 +112,7 @@ export class ParameterResolver {
106112
.map(async generic => ({
107113
...generic,
108114
range: generic.range ?
109-
await this.resolveRange(generic.range, owningClass, genericTypeRemappings, false) :
115+
await this.resolveRange(generic.range, owningClass, genericTypeRemappings, false, new Set()) :
110116
undefined,
111117
})));
112118
}
@@ -116,16 +122,18 @@ export class ParameterResolver {
116122
* @param parameters An array of unresolved parameters.
117123
* @param owningClass The class in which the given parameters are declared.
118124
* @param genericTypeRemappings A remapping of generic type names.
125+
* @param handlingInterfaces The names of interfaces that are being handled, and this interface is a part of.
119126
*/
120127
public async resolveParameterData(
121128
parameters: ParameterData<ParameterRangeUnresolved>[],
122129
owningClass: ClassReferenceLoaded,
123130
genericTypeRemappings: Record<string, ParameterRangeUnresolved>,
131+
handlingInterfaces: Set<string>,
124132
): Promise<ParameterData<ParameterRangeResolved>[]> {
125133
return await Promise.all(parameters
126134
.map(async parameter => ({
127135
...parameter,
128-
range: await this.resolveRange(parameter.range, owningClass, genericTypeRemappings, true),
136+
range: await this.resolveRange(parameter.range, owningClass, genericTypeRemappings, true, handlingInterfaces),
129137
})));
130138
}
131139

@@ -168,6 +176,7 @@ export class ParameterResolver {
168176
owningClass,
169177
genericTypeRemappings,
170178
false,
179+
new Set(),
171180
))),
172181
})));
173182
}
@@ -185,12 +194,14 @@ export class ParameterResolver {
185194
* @param owningClass The class this range was defined in.
186195
* @param genericTypeRemappings A remapping of generic type names.
187196
* @param getNestedFields If Records and interfaces should produce nested field ranges.
197+
* @param handlingInterfaces The names of interfaces that are being handled, and this interface is a part of.
188198
*/
189199
public async resolveRange(
190200
range: ParameterRangeUnresolved,
191201
owningClass: ClassReferenceLoaded,
192202
genericTypeRemappings: Record<string, ParameterRangeUnresolved>,
193203
getNestedFields: boolean,
204+
handlingInterfaces: Set<string>,
194205
): Promise<ParameterRangeResolved> {
195206
switch (range.type) {
196207
case 'raw':
@@ -203,6 +214,19 @@ export class ParameterResolver {
203214
type: 'undefined',
204215
};
205216
}
217+
218+
// If we detect an infinite recursion for a nested interface field, stop the recursion.
219+
// eslint-disable-next-line no-case-declarations
220+
if (getNestedFields) {
221+
const interfaceKey = this.hashParameterRangeUnresolved(range);
222+
if (handlingInterfaces.has(interfaceKey)) {
223+
getNestedFields = false;
224+
} else {
225+
handlingInterfaces = new Set(handlingInterfaces);
226+
handlingInterfaces.add(interfaceKey);
227+
}
228+
}
229+
206230
return await this.resolveRangeInterface(
207231
range.value,
208232
range.qualifiedPath,
@@ -211,11 +235,13 @@ export class ParameterResolver {
211235
owningClass,
212236
genericTypeRemappings,
213237
getNestedFields,
238+
handlingInterfaces,
214239
);
215240
case 'hash':
216241
return {
217242
type: 'nested',
218-
value: await this.getNestedFieldsFromHash(range.value, owningClass, genericTypeRemappings),
243+
value: await this
244+
.getNestedFieldsFromHash(range.value, owningClass, genericTypeRemappings, handlingInterfaces),
219245
};
220246
case 'undefined':
221247
return {
@@ -227,23 +253,26 @@ export class ParameterResolver {
227253
return {
228254
type: range.type,
229255
elements: await Promise.all(range.elements
230-
.map(child => this.resolveRange(child, owningClass, genericTypeRemappings, getNestedFields))),
256+
.map(child => this
257+
.resolveRange(child, owningClass, genericTypeRemappings, getNestedFields, handlingInterfaces))),
231258
};
232259
case 'array':
233260
case 'rest':
234261
case 'keyof':
235262
return {
236263
type: range.type,
237264
// TODO: remove the following any cast when TS bug is fixed
238-
value: <any> await this.resolveRange(range.value, owningClass, genericTypeRemappings, getNestedFields),
265+
value: <any> await this
266+
.resolveRange(range.value, owningClass, genericTypeRemappings, getNestedFields, handlingInterfaces),
239267
};
240268
case 'genericTypeReference':
241269
// If this generic type was remapped, return that remapped type
242270
if (range.value in genericTypeRemappings) {
243271
const mapped = genericTypeRemappings[range.value];
244272
// Avoid infinite recursion via mapping to itself
245273
if (mapped.type !== 'genericTypeReference' || mapped.value !== range.value) {
246-
return this.resolveRange(mapped, owningClass, genericTypeRemappings, getNestedFields);
274+
return this
275+
.resolveRange(mapped, owningClass, genericTypeRemappings, getNestedFields, handlingInterfaces);
247276
}
248277
}
249278
return {
@@ -292,6 +321,7 @@ export class ParameterResolver {
292321
* @param rootOwningClass The top-level class this interface was used in. Necessary for generic type resolution.
293322
* @param genericTypeRemappings A remapping of generic type names.
294323
* @param getNestedFields If Records and interfaces should produce nested field ranges.
324+
* @param handlingInterfaces The names of interfaces that are being handled, and this interface is a part of.
295325
*/
296326
public resolveRangeInterface(
297327
interfaceName: string,
@@ -301,26 +331,28 @@ export class ParameterResolver {
301331
rootOwningClass: ClassReferenceLoaded,
302332
genericTypeRemappings: Record<string, ParameterRangeUnresolved>,
303333
getNestedFields: boolean,
334+
handlingInterfaces: Set<string>,
304335
): Promise<ParameterRangeResolved> {
305336
const cacheKeyGenerics = genericTypeParameterInstances ?
306337
genericTypeParameterInstances.map(genericTypeParameterInstance => this
307338
.hashParameterRangeUnresolved(genericTypeParameterInstance)).join(',') :
308339
'';
309-
const cacheKey = `${interfaceName}::${(qualifiedPath || []).join('.')}::${cacheKeyGenerics}::${owningClass.fileName}`;
310-
let resolved = this.cacheInterfaceRange.get(cacheKey);
311-
if (!resolved) {
312-
resolved = this.resolveRangeInterfaceInner(
340+
const cacheKey = `${interfaceName}::${(qualifiedPath || []).join('.')}::${cacheKeyGenerics}::${owningClass.fileName}::${getNestedFields}`;
341+
let promise = this.cacheInterfaceRange.get(cacheKey);
342+
if (!promise) {
343+
promise = this.resolveRangeInterfaceInner(
313344
interfaceName,
314345
qualifiedPath,
315346
genericTypeParameterInstances,
316347
owningClass,
317348
rootOwningClass,
318349
genericTypeRemappings,
319350
getNestedFields,
351+
handlingInterfaces,
320352
);
321-
this.cacheInterfaceRange.set(cacheKey, resolved);
353+
this.cacheInterfaceRange.set(cacheKey, promise);
322354
}
323-
return resolved;
355+
return promise;
324356
}
325357

326358
protected async resolveRangeInterfaceInner(
@@ -331,6 +363,7 @@ export class ParameterResolver {
331363
rootOwningClass: ClassReferenceLoaded,
332364
genericTypeRemappings: Record<string, ParameterRangeUnresolved>,
333365
getNestedFields: boolean,
366+
handlingInterfaces: Set<string>,
334367
): Promise<ParameterRangeResolved> {
335368
const classOrInterface = await this.loadClassOrInterfacesChain({
336369
packageName: owningClass.packageName,
@@ -349,8 +382,13 @@ export class ParameterResolver {
349382
value: classOrInterface,
350383
genericTypeParameterInstances: genericTypeParameterInstances ?
351384
await Promise.all(genericTypeParameterInstances
352-
.map(genericTypeParameter => this
353-
.resolveRange(genericTypeParameter, rootOwningClass, genericTypeRemappings, getNestedFields))) :
385+
.map(genericTypeParameter => this.resolveRange(
386+
genericTypeParameter,
387+
rootOwningClass,
388+
genericTypeRemappings,
389+
getNestedFields,
390+
handlingInterfaces,
391+
))) :
354392
undefined,
355393
};
356394
}
@@ -363,7 +401,8 @@ export class ParameterResolver {
363401
classOrInterface.declaration.typeAnnotation,
364402
`type alias ${classOrInterface.localName} in ${classOrInterface.fileName}`,
365403
);
366-
return this.resolveRange(unresolvedFields, classOrInterface, genericTypeRemappings, getNestedFields);
404+
return this
405+
.resolveRange(unresolvedFields, classOrInterface, genericTypeRemappings, getNestedFields, handlingInterfaces);
367406
}
368407

369408
// If we find an enum, just interpret the enum value, and return as union type
@@ -381,7 +420,7 @@ export class ParameterResolver {
381420
range: <any> undefined,
382421
},
383422
`enum ${classOrInterface.localName} in ${classOrInterface.fileName}`,
384-
), owningClass, genericTypeRemappings, getNestedFields);
423+
), owningClass, genericTypeRemappings, getNestedFields, handlingInterfaces);
385424
}
386425
throw new Error(`Detected enum ${classOrInterface.localName} having an unsupported member (member ${i}) in ${classOrInterface.fileName}`);
387426
}));
@@ -400,9 +439,11 @@ export class ParameterResolver {
400439
genericTypeRemappings[ifaceGenericTypes[i]] = genericTypeParameterInstance;
401440
}
402441
}
442+
403443
return {
404444
type: 'nested',
405-
value: await this.getNestedFieldsFromInterface(classOrInterface, rootOwningClass, genericTypeRemappings),
445+
value: await this
446+
.getNestedFieldsFromInterface(classOrInterface, rootOwningClass, genericTypeRemappings, handlingInterfaces),
406447
};
407448
}
408449

@@ -478,31 +519,35 @@ export class ParameterResolver {
478519
* @param iface A loaded interface.
479520
* @param owningClass The class this hash is declared in.
480521
* @param genericTypeRemappings A remapping of generic type names.
522+
* @param handlingInterfaces The names of interfaces that are being handled, and this interface is a part of.
481523
*/
482524
public async getNestedFieldsFromInterface(
483525
iface: InterfaceLoaded,
484526
owningClass: ClassReferenceLoaded,
485527
genericTypeRemappings: Record<string, ParameterRangeUnresolved>,
528+
handlingInterfaces: Set<string>,
486529
): Promise<ParameterData<ParameterRangeResolved>[]> {
487530
const parameterLoader = new ParameterLoader({ commentLoader: this.commentLoader });
488531
const unresolvedFields = parameterLoader.loadInterfaceFields(iface);
489-
return this.resolveParameterData(unresolvedFields, owningClass, genericTypeRemappings);
532+
return await this.resolveParameterData(unresolvedFields, owningClass, genericTypeRemappings, handlingInterfaces);
490533
}
491534

492535
/**
493536
* Recursively get all fields from the given hash.
494537
* @param hash A hash object.
495538
* @param owningClass The class this hash is declared in.
496539
* @param genericTypeRemappings A remapping of generic type names.
540+
* @param handlingInterfaces The names of interfaces that are being handled, and this interface is a part of.
497541
*/
498542
public async getNestedFieldsFromHash(
499543
hash: TSTypeLiteral,
500544
owningClass: ClassReferenceLoaded,
501545
genericTypeRemappings: Record<string, ParameterRangeUnresolved>,
546+
handlingInterfaces: Set<string>,
502547
): Promise<ParameterData<ParameterRangeResolved>[]> {
503548
const parameterLoader = new ParameterLoader({ commentLoader: this.commentLoader });
504549
const unresolvedFields = parameterLoader.loadHashFields(owningClass, hash);
505-
return this.resolveParameterData(unresolvedFields, owningClass, genericTypeRemappings);
550+
return this.resolveParameterData(unresolvedFields, owningClass, genericTypeRemappings, handlingInterfaces);
506551
}
507552
}
508553

0 commit comments

Comments
 (0)