From fcd0bb0db83576ef0bc13c5c32f158d95efbedd5 Mon Sep 17 00:00:00 2001 From: Andrei Chmelev Date: Thu, 26 Mar 2026 23:54:21 +0300 Subject: [PATCH] fix(compiler-cli): prevent recursive scope checks for invalid NgModule imports Avoid recursive local scope lookups when invalid NgModule imports create import cycles. --- .../compiler-cli/src/ngtsc/scope/src/local.ts | 35 ++++++++++++------- .../compiler-cli/test/ngtsc/ngtsc_spec.ts | 24 +++++++++++++ 2 files changed, 46 insertions(+), 13 deletions(-) diff --git a/packages/compiler-cli/src/ngtsc/scope/src/local.ts b/packages/compiler-cli/src/ngtsc/scope/src/local.ts index f8537594f17..7e735819b96 100644 --- a/packages/compiler-cli/src/ngtsc/scope/src/local.ts +++ b/packages/compiler-cli/src/ngtsc/scope/src/local.ts @@ -151,10 +151,12 @@ export class LocalModuleScopeRegistry implements MetadataRegistry, ComponentScop registerPipeMetadata(pipe: PipeMeta): void {} getScopeForComponent(clazz: ClassDeclaration): LocalModuleScope | null { - const scope = !this.declarationToModule.has(clazz) - ? null - : this.getScopeOfModule(this.declarationToModule.get(clazz)!.ngModule); - return scope; + if (!this.declarationToModule.has(clazz)) { + return null; + } + + const module = this.declarationToModule.get(clazz)!.ngModule; + return this.getScopeOfModule(module); } /** @@ -181,9 +183,12 @@ export class LocalModuleScopeRegistry implements MetadataRegistry, ComponentScop * defined, or the string `'error'` if the scope contained errors. */ getScopeOfModule(clazz: ClassDeclaration): LocalModuleScope | null { - return this.moduleToRef.has(clazz) - ? this.getScopeOfModuleReference(this.moduleToRef.get(clazz)!) - : null; + if (!this.moduleToRef.has(clazz)) { + return null; + } + + const scope = this.getScopeOfModuleReference(this.moduleToRef.get(clazz)!); + return scope === 'cycle' ? null : scope; } /** @@ -249,13 +254,17 @@ export class LocalModuleScopeRegistry implements MetadataRegistry, ComponentScop /** * Implementation of `getScopeOfModule` which accepts a reference to a class. */ - private getScopeOfModuleReference(ref: Reference): LocalModuleScope | null { + private getScopeOfModuleReference( + ref: Reference, + ): LocalModuleScope | null | 'cycle' { if (this.cache.has(ref.node)) { const cachedValue = this.cache.get(ref.node); - if (cachedValue !== IN_PROGRESS_RESOLUTION) { - return cachedValue as LocalModuleScope | null; + if (cachedValue === IN_PROGRESS_RESOLUTION) { + return 'cycle'; } + + return cachedValue as LocalModuleScope | null; } this.cache.set(ref.node, IN_PROGRESS_RESOLUTION); @@ -593,7 +602,8 @@ export class LocalModuleScopeRegistry implements MetadataRegistry, ComponentScop } return this.dependencyScopeReader.resolve(ref); } else { - if (this.cache.get(ref.node) === IN_PROGRESS_RESOLUTION) { + const scope = this.getScopeOfModuleReference(ref); + if (scope === 'cycle') { diagnostics.push( makeDiagnostic( type === 'import' @@ -603,11 +613,10 @@ export class LocalModuleScopeRegistry implements MetadataRegistry, ComponentScop `NgModule "${type}" field contains a cycle`, ), ); - return 'cycle'; } // The NgModule is declared locally in the current program. Resolve it from the registry. - return this.getScopeOfModuleReference(ref); + return scope; } } diff --git a/packages/compiler-cli/test/ngtsc/ngtsc_spec.ts b/packages/compiler-cli/test/ngtsc/ngtsc_spec.ts index 4ed7636fa28..aa7887fecb7 100644 --- a/packages/compiler-cli/test/ngtsc/ngtsc_spec.ts +++ b/packages/compiler-cli/test/ngtsc/ngtsc_spec.ts @@ -11492,6 +11492,30 @@ runInEachFileSystem((os: string) => { `The pipe 'TestPipe' appears in 'imports', but is not standalone`, ); }); + + it('should not recurse when a non-standalone component is both declared and imported', () => { + env.write( + '/test.ts', + ` + import {Component, NgModule} from '@angular/core'; + + @Component({standalone: false, template: ''}) + export class TestComp {} + + @NgModule({ + declarations: [TestComp], + imports: [TestComp], + }) + export class TestModule {} + `, + ); + + const diags = env.driveDiagnostics(); + expect(diags.length).toBe(1); + expect(diags[0].messageText).toContain( + `The component 'TestComp' appears in 'imports', but is not standalone`, + ); + }); }); });