@@ -11,7 +11,7 @@ import ts from 'typescript';
1111import { absoluteFrom , AbsoluteFsPath , getSourceFileOrError } from '../../file_system' ;
1212import { runInEachFileSystem } from '../../file_system/testing' ;
1313import { FileUpdate , TsCreateProgramDriver , UpdateMode } from '../../program_driver' ;
14- import { sfExtensionData , ShimReferenceTagger } from '../../shims' ;
14+ import { sfExtensionData , ShimReferenceTagger , untagAllTsFiles } from '../../shims' ;
1515import { expectCompleteReuse , makeProgram } from '../../testing' ;
1616import { OptimizeFor } from '../api' ;
1717
@@ -64,9 +64,89 @@ runInEachFileSystem(() => {
6464
6565 expectCompleteReuse ( programStrategy . getProgram ( ) ) ;
6666 } ) ;
67+
68+ it ( 'should retain shim tags on shared SourceFiles after updateFiles' , ( ) => {
69+ assertShimTagsRetainedAfterUpdate ( UpdateMode . Complete ) ;
70+ } ) ;
71+
72+ it ( 'should retain shim tags on shared SourceFiles after incremental updateFiles' , ( ) => {
73+ const { program, host, options, typecheckPath, mainPath} =
74+ makeProgramWhereShimIsIncludedViaReference ( ) ;
75+
76+ const programStrategy = new TsCreateProgramDriver ( program , host , options , [ 'ngtypecheck' ] ) ;
77+
78+ programStrategy . updateFiles (
79+ new Map ( [ [ typecheckPath , createUpdate ( 'export const VERSION = 2;' ) ] ] ) ,
80+ UpdateMode . Complete ,
81+ ) ;
82+ programStrategy . updateFiles (
83+ new Map ( [ [ typecheckPath , createUpdate ( 'export const VERSION = 3;' ) ] ] ) ,
84+ UpdateMode . Incremental ,
85+ ) ;
86+
87+ assertSharedSourceFileShimTags ( programStrategy . getProgram ( ) , mainPath ) ;
88+ } ) ;
89+
90+ it ( 'should allow untagging the input program for emit after updateFiles' , ( ) => {
91+ const { program, host, options, typecheckPath, mainPath} =
92+ makeProgramWhereShimIsIncludedViaReference ( ) ;
93+
94+ const programStrategy = new TsCreateProgramDriver ( program , host , options , [ 'ngtypecheck' ] ) ;
95+
96+ // Update only the shim file so /main.ts remains a shared SourceFile between programs.
97+ programStrategy . updateFiles (
98+ new Map ( [ [ typecheckPath , createUpdate ( 'export const VERSION = 2;' ) ] ] ) ,
99+ UpdateMode . Complete ,
100+ ) ;
101+
102+ const mainSf = getSourceFileOrError ( program , mainPath ) ;
103+ const ext = sfExtensionData ( mainSf ) ;
104+
105+ expect ( ext . taggedReferenceFiles ) . not . toBeNull ( ) ;
106+ expect ( mainSf . referencedFiles . length ) . toEqual ( ext . taggedReferenceFiles ! . length ) ;
107+
108+ // prepareEmit() untags the input program before emit (#56945).
109+ untagAllTsFiles ( program ) ;
110+
111+ expect ( ext . originalReferencedFiles ) . not . toBeNull ( ) ;
112+ expect ( mainSf . referencedFiles ) . toEqual ( ext . originalReferencedFiles ! ) ;
113+ for ( const ref of mainSf . referencedFiles ) {
114+ expect ( ref . fileName ) . not . toContain ( 'ngtypecheck' ) ;
115+ }
116+ } ) ;
67117 } ) ;
68118} ) ;
69119
120+ function assertShimTagsRetainedAfterUpdate ( updateMode : UpdateMode ) : void {
121+ const { program, host, options, typecheckPath, mainPath} =
122+ makeProgramWhereShimIsIncludedViaReference ( ) ;
123+
124+ const programStrategy = new TsCreateProgramDriver ( program , host , options , [ 'ngtypecheck' ] ) ;
125+
126+ // Update only the shim file so /main.ts remains a shared SourceFile between programs.
127+ programStrategy . updateFiles (
128+ new Map ( [ [ typecheckPath , createUpdate ( 'export const VERSION = 2;' ) ] ] ) ,
129+ updateMode ,
130+ ) ;
131+
132+ assertSharedSourceFileShimTags ( programStrategy . getProgram ( ) , mainPath ) ;
133+ }
134+
135+ function assertSharedSourceFileShimTags ( program : ts . Program , mainPath : AbsoluteFsPath ) : void {
136+ const mainSf = getSourceFileOrError ( program , mainPath ) ;
137+ const ext = sfExtensionData ( mainSf ) ;
138+
139+ // Without the fix, untagging the old program also untags shared SourceFiles in the new
140+ // program, leaving referencedFiles shorter than taggedReferenceFiles.
141+ expect ( ext . taggedReferenceFiles ) . not . toBeNull ( ) ;
142+ expect ( mainSf . referencedFiles . length ) . toEqual ( ext . taggedReferenceFiles ! . length ) ;
143+
144+ // Replicates the TS 5.5+ crash from #68164: with composite, a shim included only via
145+ // referencedFiles gets a lazy "not listed in project" diagnostic. Expanding that diagnostic
146+ // walks the ReferenceFile include reason and reads main.referencedFiles[index].
147+ expect ( ( ) => program . getSemanticDiagnostics ( mainSf ) ) . not . toThrow ( ) ;
148+ }
149+
70150function createUpdate ( text : string ) : FileUpdate {
71151 return {
72152 newText : text ,
@@ -116,3 +196,76 @@ function makeSingleFileProgramWithTypecheckShim(): {
116196
117197 return { program, host, options, mainPath, typecheckPath} ;
118198}
199+
200+ /**
201+ * Build a program where the ngtypecheck shim is pulled in only through a tagged
202+ * `referencedFiles` entry (not as a root). Combined with `composite`, TypeScript records a lazy
203+ * project-file-list diagnostic whose expansion exercises `referencedFiles[index]` — the path that
204+ * crashes when shared SourceFiles are untagged after `updateFiles()`.
205+ */
206+ function makeProgramWhereShimIsIncludedViaReference ( ) : {
207+ program : ts . Program ;
208+ host : ts . CompilerHost ;
209+ options : ts . CompilerOptions ;
210+ mainPath : AbsoluteFsPath ;
211+ typecheckPath : AbsoluteFsPath ;
212+ } {
213+ const mainPath = absoluteFrom ( '/main.ts' ) ;
214+ const typecheckPath = absoluteFrom ( '/main.ngtypecheck.ts' ) ;
215+ const { host, options} = makeProgram (
216+ [
217+ {
218+ name : mainPath ,
219+ contents : 'export const NOT_A_COMPONENT = true;' ,
220+ } ,
221+ {
222+ name : typecheckPath ,
223+ contents : 'export const VERSION = 1;' ,
224+ isRoot : false ,
225+ } ,
226+ ] ,
227+ {
228+ composite : true ,
229+ declaration : true ,
230+ } ,
231+ /* host */ undefined ,
232+ /* checkForErrors */ false ,
233+ ) ;
234+
235+ const shimTagger = new ShimReferenceTagger ( [ 'ngtypecheck' ] ) ;
236+ // Inherit prototype methods from the real host; object spread would drop them.
237+ const taggingHost = Object . create ( host ) as ts . CompilerHost ;
238+ taggingHost . getSourceFile = (
239+ fileName : string ,
240+ languageVersionOrOptions : ts . ScriptTarget | ts . CreateSourceFileOptions ,
241+ onError ?: ( message : string ) => void ,
242+ shouldCreateNewSourceFile ?: boolean ,
243+ ) : ts . SourceFile | undefined => {
244+ const sf = host . getSourceFile (
245+ fileName ,
246+ languageVersionOrOptions ,
247+ onError ,
248+ shouldCreateNewSourceFile ,
249+ ) ;
250+ if ( sf !== undefined ) {
251+ shimTagger . tag ( sf ) ;
252+ }
253+ return sf ;
254+ } ;
255+
256+ const program = ts . createProgram ( {
257+ rootNames : [ host . getCanonicalFileName ( mainPath ) ] ,
258+ options,
259+ host : taggingHost ,
260+ } ) ;
261+ shimTagger . finalize ( ) ;
262+
263+ const typecheckSf = getSourceFileOrError ( program , typecheckPath ) ;
264+ sfExtensionData ( typecheckSf ) . fileShim = {
265+ extension : 'ngtypecheck' ,
266+ generatedFrom : mainPath ,
267+ } ;
268+
269+ // Return the unwrapped host; TsCreateProgramDriver installs its own tagging host on updates.
270+ return { program, host, options, mainPath, typecheckPath} ;
271+ }
0 commit comments