@@ -45,6 +45,9 @@ export interface LocalNgModuleData {
45
45
exports : Reference < ClassDeclaration > [ ] ;
46
46
}
47
47
48
+ /** Value used to mark a module whose scope is in the process of being resolved. */
49
+ const IN_PROGRESS_RESOLUTION = { } ;
50
+
48
51
/**
49
52
* A registry which collects information about NgModules, Directives, Components, and Pipes which
50
53
* are local (declared in the ts.Program being compiled), and can produce `LocalModuleScope`s
@@ -96,7 +99,10 @@ export class LocalModuleScopeRegistry implements MetadataRegistry, ComponentScop
96
99
* A cache of calculated `LocalModuleScope`s for each NgModule declared in the current program.
97
100
98
101
*/
99
- private cache = new Map < ClassDeclaration , LocalModuleScope | null > ( ) ;
102
+ private cache = new Map <
103
+ ClassDeclaration ,
104
+ LocalModuleScope | typeof IN_PROGRESS_RESOLUTION | null
105
+ > ( ) ;
100
106
101
107
/**
102
108
* Tracks the `RemoteScope` for components requiring "remote scoping".
@@ -245,9 +251,15 @@ export class LocalModuleScopeRegistry implements MetadataRegistry, ComponentScop
245
251
*/
246
252
private getScopeOfModuleReference ( ref : Reference < ClassDeclaration > ) : LocalModuleScope | null {
247
253
if ( this . cache . has ( ref . node ) ) {
248
- return this . cache . get ( ref . node ) ! ;
254
+ const cachedValue = this . cache . get ( ref . node ) ;
255
+
256
+ if ( cachedValue !== IN_PROGRESS_RESOLUTION ) {
257
+ return cachedValue as LocalModuleScope | null ;
258
+ }
249
259
}
250
260
261
+ this . cache . set ( ref . node , IN_PROGRESS_RESOLUTION ) ;
262
+
251
263
// Seal the registry to protect the integrity of the `LocalModuleScope` cache.
252
264
this . sealed = true ;
253
265
@@ -301,14 +313,22 @@ export class LocalModuleScopeRegistry implements MetadataRegistry, ComponentScop
301
313
for ( const decl of ngModule . imports ) {
302
314
const importScope = this . getExportedScope ( decl , diagnostics , ref . node , 'import' ) ;
303
315
if ( importScope !== null ) {
304
- if ( importScope === 'invalid' || importScope . exported . isPoisoned ) {
316
+ if (
317
+ importScope === 'invalid' ||
318
+ importScope === 'cycle' ||
319
+ importScope . exported . isPoisoned
320
+ ) {
305
321
// An import was an NgModule but contained errors of its own. Record this as an error too,
306
322
// because this scope is always going to be incorrect if one of its imports could not be
307
323
// read.
308
- diagnostics . push ( invalidTransitiveNgModuleRef ( decl , ngModule . rawImports , 'import' ) ) ;
309
324
isPoisoned = true ;
310
325
311
- if ( importScope === 'invalid' ) {
326
+ // Prevent the module from reporting a diagnostic about itself when there's a cycle.
327
+ if ( importScope !== 'cycle' ) {
328
+ diagnostics . push ( invalidTransitiveNgModuleRef ( decl , ngModule . rawImports , 'import' ) ) ;
329
+ }
330
+
331
+ if ( importScope === 'invalid' || importScope === 'cycle' ) {
312
332
continue ;
313
333
}
314
334
}
@@ -427,14 +447,22 @@ export class LocalModuleScopeRegistry implements MetadataRegistry, ComponentScop
427
447
for ( const decl of ngModule . exports ) {
428
448
// Attempt to resolve decl as an NgModule.
429
449
const exportScope = this . getExportedScope ( decl , diagnostics , ref . node , 'export' ) ;
430
- if ( exportScope === 'invalid' || ( exportScope !== null && exportScope . exported . isPoisoned ) ) {
450
+ if (
451
+ exportScope === 'invalid' ||
452
+ exportScope === 'cycle' ||
453
+ ( exportScope !== null && exportScope . exported . isPoisoned )
454
+ ) {
431
455
// An export was an NgModule but contained errors of its own. Record this as an error too,
432
456
// because this scope is always going to be incorrect if one of its exports could not be
433
457
// read.
434
- diagnostics . push ( invalidTransitiveNgModuleRef ( decl , ngModule . rawExports , 'export' ) ) ;
435
458
isPoisoned = true ;
436
459
437
- if ( exportScope === 'invalid' ) {
460
+ // Prevent the module from reporting a diagnostic about itself when there's a cycle.
461
+ if ( exportScope !== 'cycle' ) {
462
+ diagnostics . push ( invalidTransitiveNgModuleRef ( decl , ngModule . rawExports , 'export' ) ) ;
463
+ }
464
+
465
+ if ( exportScope === 'invalid' || exportScope === 'cycle' ) {
438
466
continue ;
439
467
}
440
468
} else if ( exportScope !== null ) {
@@ -544,7 +572,7 @@ export class LocalModuleScopeRegistry implements MetadataRegistry, ComponentScop
544
572
diagnostics : ts . Diagnostic [ ] ,
545
573
ownerForErrors : DeclarationNode ,
546
574
type : 'import' | 'export' ,
547
- ) : ExportScope | null | 'invalid' {
575
+ ) : ExportScope | null | 'invalid' | 'cycle' {
548
576
if ( ref . node . getSourceFile ( ) . isDeclarationFile ) {
549
577
// The NgModule is declared in a .d.ts file. Resolve it with the `DependencyScopeReader`.
550
578
if ( ! ts . isClassDeclaration ( ref . node ) ) {
@@ -565,6 +593,19 @@ export class LocalModuleScopeRegistry implements MetadataRegistry, ComponentScop
565
593
}
566
594
return this . dependencyScopeReader . resolve ( ref ) ;
567
595
} else {
596
+ if ( this . cache . get ( ref . node ) === IN_PROGRESS_RESOLUTION ) {
597
+ diagnostics . push (
598
+ makeDiagnostic (
599
+ type === 'import'
600
+ ? ErrorCode . NGMODULE_INVALID_IMPORT
601
+ : ErrorCode . NGMODULE_INVALID_EXPORT ,
602
+ identifierOfNode ( ref . node ) || ref . node ,
603
+ `NgModule "${ type } " field contains a cycle` ,
604
+ ) ,
605
+ ) ;
606
+ return 'cycle' ;
607
+ }
608
+
568
609
// The NgModule is declared locally in the current program. Resolve it from the registry.
569
610
return this . getScopeOfModuleReference ( ref ) ;
570
611
}
0 commit comments