11import type { TSTypeLiteral } from '@typescript-eslint/types/dist/ts-estree' ;
22import { AST_NODE_TYPES } from '@typescript-eslint/typescript-estree' ;
33import * 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' ;
510import type { ClassLoader } from './ClassLoader' ;
611import type { CommentLoader } from './CommentLoader' ;
712import 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