mirror of
https://github.com/angular/angular.git
synced 2026-09-14 13:54:52 +08:00
fix(compiler-cli): re-tag SourceFiles after TsCreateProgramDriver.updateFiles()
TypeScript reuses SourceFile objects between old and new programs, so untagging the old program also untags shared files in the new program. Re-apply shim tags on the new program to prevent getSemanticDiagnostics() crashes with TS 5.5+.
This commit is contained in:
@@ -272,5 +272,12 @@ export class TsCreateProgramDriver implements ProgramDriver {
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ import ts from 'typescript';
|
||||
import {absoluteFrom, AbsoluteFsPath, getSourceFileOrError} from '../../file_system';
|
||||
import {runInEachFileSystem} from '../../file_system/testing';
|
||||
import {FileUpdate, TsCreateProgramDriver, UpdateMode} from '../../program_driver';
|
||||
import {sfExtensionData, ShimReferenceTagger} from '../../shims';
|
||||
import {sfExtensionData, ShimReferenceTagger, untagAllTsFiles} from '../../shims';
|
||||
import {expectCompleteReuse, makeProgram} from '../../testing';
|
||||
import {OptimizeFor} from '../api';
|
||||
|
||||
@@ -64,9 +64,89 @@ runInEachFileSystem(() => {
|
||||
|
||||
expectCompleteReuse(programStrategy.getProgram());
|
||||
});
|
||||
|
||||
it('should retain shim tags on shared SourceFiles after updateFiles', () => {
|
||||
assertShimTagsRetainedAfterUpdate(UpdateMode.Complete);
|
||||
});
|
||||
|
||||
it('should retain shim tags on shared SourceFiles after incremental updateFiles', () => {
|
||||
const {program, host, options, typecheckPath, mainPath} =
|
||||
makeProgramWhereShimIsIncludedViaReference();
|
||||
|
||||
const programStrategy = new TsCreateProgramDriver(program, host, options, ['ngtypecheck']);
|
||||
|
||||
programStrategy.updateFiles(
|
||||
new Map([[typecheckPath, createUpdate('export const VERSION = 2;')]]),
|
||||
UpdateMode.Complete,
|
||||
);
|
||||
programStrategy.updateFiles(
|
||||
new Map([[typecheckPath, createUpdate('export const VERSION = 3;')]]),
|
||||
UpdateMode.Incremental,
|
||||
);
|
||||
|
||||
assertSharedSourceFileShimTags(programStrategy.getProgram(), mainPath);
|
||||
});
|
||||
|
||||
it('should allow untagging the input program for emit after updateFiles', () => {
|
||||
const {program, host, options, typecheckPath, mainPath} =
|
||||
makeProgramWhereShimIsIncludedViaReference();
|
||||
|
||||
const programStrategy = new TsCreateProgramDriver(program, host, options, ['ngtypecheck']);
|
||||
|
||||
// Update only the shim file so /main.ts remains a shared SourceFile between programs.
|
||||
programStrategy.updateFiles(
|
||||
new Map([[typecheckPath, createUpdate('export const VERSION = 2;')]]),
|
||||
UpdateMode.Complete,
|
||||
);
|
||||
|
||||
const mainSf = getSourceFileOrError(program, mainPath);
|
||||
const ext = sfExtensionData(mainSf);
|
||||
|
||||
expect(ext.taggedReferenceFiles).not.toBeNull();
|
||||
expect(mainSf.referencedFiles.length).toEqual(ext.taggedReferenceFiles!.length);
|
||||
|
||||
// prepareEmit() untags the input program before emit (#56945).
|
||||
untagAllTsFiles(program);
|
||||
|
||||
expect(ext.originalReferencedFiles).not.toBeNull();
|
||||
expect(mainSf.referencedFiles).toEqual(ext.originalReferencedFiles!);
|
||||
for (const ref of mainSf.referencedFiles) {
|
||||
expect(ref.fileName).not.toContain('ngtypecheck');
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
function assertShimTagsRetainedAfterUpdate(updateMode: UpdateMode): void {
|
||||
const {program, host, options, typecheckPath, mainPath} =
|
||||
makeProgramWhereShimIsIncludedViaReference();
|
||||
|
||||
const programStrategy = new TsCreateProgramDriver(program, host, options, ['ngtypecheck']);
|
||||
|
||||
// Update only the shim file so /main.ts remains a shared SourceFile between programs.
|
||||
programStrategy.updateFiles(
|
||||
new Map([[typecheckPath, createUpdate('export const VERSION = 2;')]]),
|
||||
updateMode,
|
||||
);
|
||||
|
||||
assertSharedSourceFileShimTags(programStrategy.getProgram(), mainPath);
|
||||
}
|
||||
|
||||
function assertSharedSourceFileShimTags(program: ts.Program, mainPath: AbsoluteFsPath): void {
|
||||
const mainSf = getSourceFileOrError(program, mainPath);
|
||||
const ext = sfExtensionData(mainSf);
|
||||
|
||||
// Without the fix, untagging the old program also untags shared SourceFiles in the new
|
||||
// program, leaving referencedFiles shorter than taggedReferenceFiles.
|
||||
expect(ext.taggedReferenceFiles).not.toBeNull();
|
||||
expect(mainSf.referencedFiles.length).toEqual(ext.taggedReferenceFiles!.length);
|
||||
|
||||
// Replicates the TS 5.5+ crash from #68164: with composite, a shim included only via
|
||||
// referencedFiles gets a lazy "not listed in project" diagnostic. Expanding that diagnostic
|
||||
// walks the ReferenceFile include reason and reads main.referencedFiles[index].
|
||||
expect(() => program.getSemanticDiagnostics(mainSf)).not.toThrow();
|
||||
}
|
||||
|
||||
function createUpdate(text: string): FileUpdate {
|
||||
return {
|
||||
newText: text,
|
||||
@@ -116,3 +196,76 @@ function makeSingleFileProgramWithTypecheckShim(): {
|
||||
|
||||
return {program, host, options, mainPath, typecheckPath};
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a program where the ngtypecheck shim is pulled in only through a tagged
|
||||
* `referencedFiles` entry (not as a root). Combined with `composite`, TypeScript records a lazy
|
||||
* project-file-list diagnostic whose expansion exercises `referencedFiles[index]` — the path that
|
||||
* crashes when shared SourceFiles are untagged after `updateFiles()`.
|
||||
*/
|
||||
function makeProgramWhereShimIsIncludedViaReference(): {
|
||||
program: ts.Program;
|
||||
host: ts.CompilerHost;
|
||||
options: ts.CompilerOptions;
|
||||
mainPath: AbsoluteFsPath;
|
||||
typecheckPath: AbsoluteFsPath;
|
||||
} {
|
||||
const mainPath = absoluteFrom('/main.ts');
|
||||
const typecheckPath = absoluteFrom('/main.ngtypecheck.ts');
|
||||
const {host, options} = makeProgram(
|
||||
[
|
||||
{
|
||||
name: mainPath,
|
||||
contents: 'export const NOT_A_COMPONENT = true;',
|
||||
},
|
||||
{
|
||||
name: typecheckPath,
|
||||
contents: 'export const VERSION = 1;',
|
||||
isRoot: false,
|
||||
},
|
||||
],
|
||||
{
|
||||
composite: true,
|
||||
declaration: true,
|
||||
},
|
||||
/* host */ undefined,
|
||||
/* checkForErrors */ false,
|
||||
);
|
||||
|
||||
const shimTagger = new ShimReferenceTagger(['ngtypecheck']);
|
||||
// Inherit prototype methods from the real host; object spread would drop them.
|
||||
const taggingHost = Object.create(host) as ts.CompilerHost;
|
||||
taggingHost.getSourceFile = (
|
||||
fileName: string,
|
||||
languageVersionOrOptions: ts.ScriptTarget | ts.CreateSourceFileOptions,
|
||||
onError?: (message: string) => void,
|
||||
shouldCreateNewSourceFile?: boolean,
|
||||
): ts.SourceFile | undefined => {
|
||||
const sf = host.getSourceFile(
|
||||
fileName,
|
||||
languageVersionOrOptions,
|
||||
onError,
|
||||
shouldCreateNewSourceFile,
|
||||
);
|
||||
if (sf !== undefined) {
|
||||
shimTagger.tag(sf);
|
||||
}
|
||||
return sf;
|
||||
};
|
||||
|
||||
const program = ts.createProgram({
|
||||
rootNames: [host.getCanonicalFileName(mainPath)],
|
||||
options,
|
||||
host: taggingHost,
|
||||
});
|
||||
shimTagger.finalize();
|
||||
|
||||
const typecheckSf = getSourceFileOrError(program, typecheckPath);
|
||||
sfExtensionData(typecheckSf).fileShim = {
|
||||
extension: 'ngtypecheck',
|
||||
generatedFrom: mainPath,
|
||||
};
|
||||
|
||||
// Return the unwrapped host; TsCreateProgramDriver installs its own tagging host on updates.
|
||||
return {program, host, options, mainPath, typecheckPath};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user