Skip to content

TsCreateProgramDriver.updateFiles() untags shared SourceFiles, crashing getSemanticDiagnostics() with TS 5.9 #68164

@tomer953

Description

@tomer953

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:

  1. 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
  2. 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
  3. 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:

  1. Any Angular migration/schematic that calls ngCompiler.getDiagnostics() (e.g. cleanup-unused-imports)
  2. Angular Language Service — calls getDiagnosticsForFile() for IDE diagnostics
  3. 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

Metadata

Metadata

Assignees

No one assigned

    Labels

    area: compilerIssues related to `ngc`, Angular's template compiler

    Type

    No type

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions