@@ -45,6 +45,9 @@ export interface LocalNgModuleData {
4545 exports : Reference < ClassDeclaration > [ ] ;
4646}
4747
48+ /** Value used to mark a module whose scope is in the process of being resolved. */
49+ const IN_PROGRESS_RESOLUTION = { } ;
50+
4851/**
4952 * A registry which collects information about NgModules, Directives, Components, and Pipes which
5053 * are local (declared in the ts.Program being compiled), and can produce `LocalModuleScope`s
@@ -96,7 +99,10 @@ export class LocalModuleScopeRegistry implements MetadataRegistry, ComponentScop
9699 * A cache of calculated `LocalModuleScope`s for each NgModule declared in the current program.
97100
98101 */
99- private cache = new Map < ClassDeclaration , LocalModuleScope | null > ( ) ;
102+ private cache = new Map <
103+ ClassDeclaration ,
104+ LocalModuleScope | typeof IN_PROGRESS_RESOLUTION | null
105+ > ( ) ;
100106
101107 /**
102108 * Tracks the `RemoteScope` for components requiring "remote scoping".
@@ -245,9 +251,15 @@ export class LocalModuleScopeRegistry implements MetadataRegistry, ComponentScop
245251 */
246252 private getScopeOfModuleReference ( ref : Reference < ClassDeclaration > ) : LocalModuleScope | null {
247253 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+ }
249259 }
250260
261+ this . cache . set ( ref . node , IN_PROGRESS_RESOLUTION ) ;
262+
251263 // Seal the registry to protect the integrity of the `LocalModuleScope` cache.
252264 this . sealed = true ;
253265
@@ -301,14 +313,22 @@ export class LocalModuleScopeRegistry implements MetadataRegistry, ComponentScop
301313 for ( const decl of ngModule . imports ) {
302314 const importScope = this . getExportedScope ( decl , diagnostics , ref . node , 'import' ) ;
303315 if ( importScope !== null ) {
304- if ( importScope === 'invalid' || importScope . exported . isPoisoned ) {
316+ if (
317+ importScope === 'invalid' ||
318+ importScope === 'cycle' ||
319+ importScope . exported . isPoisoned
320+ ) {
305321 // An import was an NgModule but contained errors of its own. Record this as an error too,
306322 // because this scope is always going to be incorrect if one of its imports could not be
307323 // read.
308- diagnostics . push ( invalidTransitiveNgModuleRef ( decl , ngModule . rawImports , 'import' ) ) ;
309324 isPoisoned = true ;
310325
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' ) {
312332 continue ;
313333 }
314334 }
@@ -427,14 +447,22 @@ export class LocalModuleScopeRegistry implements MetadataRegistry, ComponentScop
427447 for ( const decl of ngModule . exports ) {
428448 // Attempt to resolve decl as an NgModule.
429449 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+ ) {
431455 // An export was an NgModule but contained errors of its own. Record this as an error too,
432456 // because this scope is always going to be incorrect if one of its exports could not be
433457 // read.
434- diagnostics . push ( invalidTransitiveNgModuleRef ( decl , ngModule . rawExports , 'export' ) ) ;
435458 isPoisoned = true ;
436459
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' ) {
438466 continue ;
439467 }
440468 } else if ( exportScope !== null ) {
@@ -544,7 +572,7 @@ export class LocalModuleScopeRegistry implements MetadataRegistry, ComponentScop
544572 diagnostics : ts . Diagnostic [ ] ,
545573 ownerForErrors : DeclarationNode ,
546574 type : 'import' | 'export' ,
547- ) : ExportScope | null | 'invalid' {
575+ ) : ExportScope | null | 'invalid' | 'cycle' {
548576 if ( ref . node . getSourceFile ( ) . isDeclarationFile ) {
549577 // The NgModule is declared in a .d.ts file. Resolve it with the `DependencyScopeReader`.
550578 if ( ! ts . isClassDeclaration ( ref . node ) ) {
@@ -565,6 +593,19 @@ export class LocalModuleScopeRegistry implements MetadataRegistry, ComponentScop
565593 }
566594 return this . dependencyScopeReader . resolve ( ref ) ;
567595 } 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+
568609 // The NgModule is declared locally in the current program. Resolve it from the registry.
569610 return this . getScopeOfModuleReference ( ref ) ;
570611 }
0 commit comments