Which @angular/* package(s) are the source of the bug?
compiler-cli
Is this a regression?
Yes
Description
Running ng generate @angular/core:cleanup-unused-imports crashes with:
TypeError: Cannot destructure property 'pos' of 'file.referencedFiles[index]' as it is undefined.
at getReferencedFileLocation (node_modules/typescript/lib/typescript.js:126713:10)
at fileIncludeReasonToDiagnostics (node_modules/typescript/lib/typescript.js:133554:31)
at processReason (node_modules/typescript/lib/typescript.js:130117:62)
at Array.forEach (<anonymous>)
at createDiagnosticExplainingFile (node_modules/typescript/lib/typescript.js:130069:42)
at getCombinedDiagnostics (node_modules/typescript/lib/typescript.js:130018:11)
at getProgramDiagnostics (node_modules/typescript/lib/typescript.js:127966:57)
at getSemanticDiagnosticsForFile (node_modules/typescript/lib/typescript.js:127997:7)
Root Cause
The bug is in TsCreateProgramDriver.updateFiles() in the @angular/compiler-cli package.
The method does the following:
- Retags the old program's source files with shim references via
retagAllTsFiles(oldProgram) — this adds synthetic .ngtypecheck.ts entries to each ts.SourceFile's referencedFiles array
- Creates a new
ts.Program via ts.createProgram({ oldProgram, ... }) — TypeScript records FileIncludeReason entries with kind: ReferenceFile and index values pointing into the tagged (extended) referencedFiles arrays
- Untags the old program via
untagAllTsFiles(oldProgram) — this restores sf.referencedFiles to the original (shorter) arrays
The problem is that TypeScript reuses the same ts.SourceFile objects between the old and new programs. So untagging the old program's files at step 3 also untags the new program's files, because they are the same objects.
When typeCheckProgram.getSemanticDiagnostics() is subsequently called on the new program, it invokes getCombinedDiagnostics() which iterates over fileProcessingDiagnostics. These diagnostics contain FileIncludeReason entries with indices that were valid during program creation (step 2, when referencedFiles was tagged/extended), but are now out of bounds because the array was shrunk back to its original size (step 3).
The existing comment in the code acknowledges this concern:
// Only untag the old program. The new program needs to keep the tagged files, because as of
// TS 5.5 not having the files tagged while producing diagnostics can lead to errors. See:
// https://github.com/microsoft/TypeScript/pull/58398
untagAllTsFiles(oldProgram);
But the comment's intent ("only untag the old program") is not achieved because both programs share the same ts.SourceFile objects — untagging one untags both.
Proposed Fix
Add retagAllTsFiles(this.program) after untagAllTsFiles(oldProgram) to re-tag the new program's files:
// Only untag the old program. The new program needs to keep the tagged files, because as of
// TS 5.5 not having the files tagged while producing diagnostics can lead to errors. See:
// https://github.com/microsoft/TypeScript/pull/58398
untagAllTsFiles(oldProgram);
// Re-tag the new program's files. Since TS reuses SourceFile objects between old and new
// programs, untagging the old program also untags shared files in the new program.
// Without re-tagging, getSemanticDiagnostics() crashes with "Cannot destructure property
// 'pos' of 'file.referencedFiles[index]'" because fileProcessingDiagnostics recorded
// indices into the tagged (longer) referencedFiles array during program creation.
retagAllTsFiles(this.program);
Minimal Reproduction
import ts from 'typescript';
// 1. Create a program with shim-tagged referencedFiles (simulating Angular's ShimReferenceTagger)
const taggedFiles = new Map();
const host = {
...ts.createCompilerHost(options),
getSourceFile(fileName, languageVersion, onError, shouldCreateNewSourceFile) {
const sf = originalHost.getSourceFile(fileName, languageVersion, onError, shouldCreateNewSourceFile);
if (sf && !sf.isDeclarationFile && !fileName.includes('node_modules')) {
const origRefs = sf.referencedFiles;
sf.referencedFiles = [...origRefs, { fileName: fileName + '.ngtypecheck.ts', pos: 0, end: 0 }];
taggedFiles.set(sf, origRefs);
}
return sf;
}
};
const program = ts.createProgram({ rootNames, options, host });
// 2. Untag (restore original referencedFiles) — simulating untagAllTsFiles(oldProgram)
for (const sf of program.getSourceFiles()) {
if (taggedFiles.has(sf)) {
sf.referencedFiles = taggedFiles.get(sf);
}
}
// 3. This crashes with: Cannot destructure property 'pos' of 'file.referencedFiles[index]'
for (const sf of program.getSourceFiles()) {
program.getSemanticDiagnostics(sf); // 💥
}
Please provide a link to a minimal reproduction of the bug
No response
Please provide the exception or error you saw
TypeError: Cannot destructure property 'pos' of 'file.referencedFiles[index]' as it is undefined.
at getReferencedFileLocation (node_modules/typescript/lib/typescript.js:126713:10)
at fileIncludeReasonToDiagnostics (node_modules/typescript/lib/typescript.js:133554:31)
at processReason (node_modules/typescript/lib/typescript.js:130117:62)
at Array.forEach (<anonymous>)
at createDiagnosticExplainingFile (node_modules/typescript/lib/typescript.js:130069:42)
at getCombinedDiagnostics (node_modules/typescript/lib/typescript.js:130018:11)
at getProgramDiagnostics (node_modules/typescript/lib/typescript.js:127966:57)
at getSemanticDiagnosticsForFile (node_modules/typescript/lib/typescript.js:127997:7)
Please provide the environment you discovered this bug in (run ng version)
Angular CLI: 20.3.12
Node: (current LTS)
Package Manager: npm 10.8.2
OS: darwin arm64
Angular: 20.3.18
@angular-devkit/architect 0.2003.12
@angular-devkit/build-angular 20.3.12
@angular-devkit/core 20.3.12
@angular-devkit/schematics 20.3.12
@angular/cli 20.3.12
@angular/compiler 20.3.18
@angular/compiler-cli 20.3.18
typescript 5.9.2
Anything else?
Scope of impact
This bug is not specific to cleanup-unused-imports. It is in the core TsCreateProgramDriver class and affects any code path that calls typeCheckProgram.getSemanticDiagnostics() after TsCreateProgramDriver.updateFiles() has been invoked. This includes:
- Any Angular migration/schematic that calls
ngCompiler.getDiagnostics() (e.g. cleanup-unused-imports)
- Angular Language Service — calls
getDiagnosticsForFile() for IDE diagnostics
- Any future migration that needs template type-checking diagnostics
The cleanup-unused-imports migration is the most visible trigger because it explicitly calls info.ngCompiler?.getDiagnostics() to find UNUSED_STANDALONE_IMPORTS errors. Most other migrations work purely with the TypeScript AST and don't invoke the template type checker.
Workaround
Patch node_modules/@angular/core/schematics/bundles/index-DUHSh_FI.cjs (or the equivalent bundled file) by adding retagAllTsFiles(this.program) after the untagAllTsFiles(oldProgram) call in TsCreateProgramDriver.updateFiles().
Source file: packages/compiler-cli/src/ngtsc/program_driver/src/ts_create_program_driver.ts
Which @angular/* package(s) are the source of the bug?
compiler-cli
Is this a regression?
Yes
Description
Running
ng generate @angular/core:cleanup-unused-importscrashes with:Root Cause
The bug is in
TsCreateProgramDriver.updateFiles()in the@angular/compiler-clipackage.The method does the following:
retagAllTsFiles(oldProgram)— this adds synthetic.ngtypecheck.tsentries to eachts.SourceFile'sreferencedFilesarrayts.Programviats.createProgram({ oldProgram, ... })— TypeScript recordsFileIncludeReasonentries withkind: ReferenceFileandindexvalues pointing into the tagged (extended)referencedFilesarraysuntagAllTsFiles(oldProgram)— this restoressf.referencedFilesto the original (shorter) arraysThe problem is that TypeScript reuses the same
ts.SourceFileobjects between the old and new programs. So untagging the old program's files at step 3 also untags the new program's files, because they are the same objects.When
typeCheckProgram.getSemanticDiagnostics()is subsequently called on the new program, it invokesgetCombinedDiagnostics()which iterates overfileProcessingDiagnostics. These diagnostics containFileIncludeReasonentries with indices that were valid during program creation (step 2, whenreferencedFileswas tagged/extended), but are now out of bounds because the array was shrunk back to its original size (step 3).The existing comment in the code acknowledges this concern:
But the comment's intent ("only untag the old program") is not achieved because both programs share the same
ts.SourceFileobjects — untagging one untags both.Proposed Fix
Add
retagAllTsFiles(this.program)afteruntagAllTsFiles(oldProgram)to re-tag the new program's files:Minimal Reproduction
Please provide a link to a minimal reproduction of the bug
No response
Please provide the exception or error you saw
Please provide the environment you discovered this bug in (run
ng version)Anything else?
Scope of impact
This bug is not specific to
cleanup-unused-imports. It is in the coreTsCreateProgramDriverclass and affects any code path that callstypeCheckProgram.getSemanticDiagnostics()afterTsCreateProgramDriver.updateFiles()has been invoked. This includes:ngCompiler.getDiagnostics()(e.g.cleanup-unused-imports)getDiagnosticsForFile()for IDE diagnosticsThe
cleanup-unused-importsmigration is the most visible trigger because it explicitly callsinfo.ngCompiler?.getDiagnostics()to findUNUSED_STANDALONE_IMPORTSerrors. Most other migrations work purely with the TypeScript AST and don't invoke the template type checker.Workaround
Patch
node_modules/@angular/core/schematics/bundles/index-DUHSh_FI.cjs(or the equivalent bundled file) by addingretagAllTsFiles(this.program)after theuntagAllTsFiles(oldProgram)call inTsCreateProgramDriver.updateFiles().Source file:
packages/compiler-cli/src/ngtsc/program_driver/src/ts_create_program_driver.ts