Skip to content

Commit 4b556e3

Browse files
authored
Handle document Registry to distinguish between files with same name and document registry key(compiler options affecting source file) but different ScriptKind (microsoft#43474)
* Test that fails because of change in scriptKind of untitled file * buckets are keyed with DocumentRegistryBucketKey * Use scriptKind in document Registry to distinguish between files Fixes microsoft#42613
1 parent 2d66517 commit 4b556e3

File tree

6 files changed

+139
-26
lines changed

6 files changed

+139
-26
lines changed

src/services/documentRegistry.ts

Lines changed: 77 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -83,12 +83,26 @@ namespace ts {
8383
* @param fileName The name of the file to be released
8484
* @param compilationSettings The compilation settings used to acquire the file
8585
*/
86+
/**@deprecated pass scriptKind for correctness */
8687
releaseDocument(fileName: string, compilationSettings: CompilerOptions): void;
87-
88+
/**
89+
* Informs the DocumentRegistry that a file is not needed any longer.
90+
*
91+
* Note: It is not allowed to call release on a SourceFile that was not acquired from
92+
* this registry originally.
93+
*
94+
* @param fileName The name of the file to be released
95+
* @param compilationSettings The compilation settings used to acquire the file
96+
* @param scriptKind The script kind of the file to be released
97+
*/
98+
releaseDocument(fileName: string, compilationSettings: CompilerOptions, scriptKind: ScriptKind): void; // eslint-disable-line @typescript-eslint/unified-signatures
99+
/**
100+
* @deprecated pass scriptKind for correctness */
88101
releaseDocumentWithKey(path: Path, key: DocumentRegistryBucketKey): void;
102+
releaseDocumentWithKey(path: Path, key: DocumentRegistryBucketKey, scriptKind: ScriptKind): void; // eslint-disable-line @typescript-eslint/unified-signatures
89103

90104
/*@internal*/
91-
getLanguageServiceRefCounts(path: Path): [string, number | undefined][];
105+
getLanguageServiceRefCounts(path: Path, scriptKind: ScriptKind): [string, number | undefined][];
92106

93107
reportStats(): string;
94108
}
@@ -110,6 +124,11 @@ namespace ts {
110124
languageServiceRefCount: number;
111125
}
112126

127+
type BucketEntry = DocumentRegistryEntry | ESMap<ScriptKind, DocumentRegistryEntry>;
128+
function isDocumentRegistryEntry(entry: BucketEntry): entry is DocumentRegistryEntry {
129+
return !!(entry as DocumentRegistryEntry).sourceFile;
130+
}
131+
113132
export function createDocumentRegistry(useCaseSensitiveFileNames?: boolean, currentDirectory?: string): DocumentRegistry {
114133
return createDocumentRegistryInternal(useCaseSensitiveFileNames, currentDirectory);
115134
}
@@ -118,18 +137,24 @@ namespace ts {
118137
export function createDocumentRegistryInternal(useCaseSensitiveFileNames?: boolean, currentDirectory = "", externalCache?: ExternalDocumentCache): DocumentRegistry {
119138
// Maps from compiler setting target (ES3, ES5, etc.) to all the cached documents we have
120139
// for those settings.
121-
const buckets = new Map<string, ESMap<Path, DocumentRegistryEntry>>();
140+
const buckets = new Map<DocumentRegistryBucketKey, ESMap<Path, BucketEntry>>();
122141
const getCanonicalFileName = createGetCanonicalFileName(!!useCaseSensitiveFileNames);
123142

124143
function reportStats() {
125144
const bucketInfoArray = arrayFrom(buckets.keys()).filter(name => name && name.charAt(0) === "_").map(name => {
126145
const entries = buckets.get(name)!;
127-
const sourceFiles: { name: string; refCount: number; }[] = [];
146+
const sourceFiles: { name: string; scriptKind: ScriptKind, refCount: number; }[] = [];
128147
entries.forEach((entry, name) => {
129-
sourceFiles.push({
130-
name,
131-
refCount: entry.languageServiceRefCount
132-
});
148+
if (isDocumentRegistryEntry(entry)) {
149+
sourceFiles.push({
150+
name,
151+
scriptKind: entry.sourceFile.scriptKind,
152+
refCount: entry.languageServiceRefCount
153+
});
154+
}
155+
else {
156+
entry.forEach((value, scriptKind) => sourceFiles.push({ name, scriptKind, refCount: value.languageServiceRefCount }));
157+
}
133158
});
134159
sourceFiles.sort((x, y) => y.refCount - x.refCount);
135160
return {
@@ -160,6 +185,12 @@ namespace ts {
160185
return acquireOrUpdateDocument(fileName, path, compilationSettings, key, scriptSnapshot, version, /*acquiring*/ false, scriptKind);
161186
}
162187

188+
function getDocumentRegistryEntry(bucketEntry: BucketEntry, scriptKind: ScriptKind | undefined) {
189+
const entry = isDocumentRegistryEntry(bucketEntry) ? bucketEntry : bucketEntry.get(Debug.checkDefined(scriptKind, "If there are more than one scriptKind's for same document the scriptKind should be provided"));
190+
Debug.assert(scriptKind === undefined || !entry || entry.sourceFile.scriptKind === scriptKind, `Script kind should match provided ScriptKind:${scriptKind} and sourceFile.scriptKind: ${entry?.sourceFile.scriptKind}, !entry: ${!entry}`);
191+
return entry;
192+
}
193+
163194
function acquireOrUpdateDocument(
164195
fileName: string,
165196
path: Path,
@@ -169,10 +200,11 @@ namespace ts {
169200
version: string,
170201
acquiring: boolean,
171202
scriptKind?: ScriptKind): SourceFile {
172-
173-
const bucket = getOrUpdate(buckets, key, () => new Map<Path, DocumentRegistryEntry>());
174-
let entry = bucket.get(path);
203+
scriptKind = ensureScriptKind(fileName, scriptKind);
175204
const scriptTarget = scriptKind === ScriptKind.JSON ? ScriptTarget.JSON : compilationSettings.target || ScriptTarget.ES5;
205+
const bucket = getOrUpdate(buckets, key, () => new Map());
206+
const bucketEntry = bucket.get(path);
207+
let entry = bucketEntry && getDocumentRegistryEntry(bucketEntry, scriptKind);
176208
if (!entry && externalCache) {
177209
const sourceFile = externalCache.getDocument(key, path);
178210
if (sourceFile) {
@@ -181,7 +213,7 @@ namespace ts {
181213
sourceFile,
182214
languageServiceRefCount: 0
183215
};
184-
bucket.set(path, entry);
216+
setBucketEntry();
185217
}
186218
}
187219

@@ -195,7 +227,7 @@ namespace ts {
195227
sourceFile,
196228
languageServiceRefCount: 1,
197229
};
198-
bucket.set(path, entry);
230+
setBucketEntry();
199231
}
200232
else {
201233
// We have an entry for this file. However, it may be for a different version of
@@ -221,28 +253,53 @@ namespace ts {
221253
Debug.assert(entry.languageServiceRefCount !== 0);
222254

223255
return entry.sourceFile;
256+
257+
function setBucketEntry() {
258+
if (!bucketEntry) {
259+
bucket.set(path, entry!);
260+
}
261+
else if (isDocumentRegistryEntry(bucketEntry)) {
262+
const scriptKindMap = new Map<ScriptKind, DocumentRegistryEntry>();
263+
scriptKindMap.set(bucketEntry.sourceFile.scriptKind, bucketEntry);
264+
scriptKindMap.set(scriptKind!, entry!);
265+
bucket.set(path, scriptKindMap);
266+
}
267+
else {
268+
bucketEntry.set(scriptKind!, entry!);
269+
}
270+
}
224271
}
225272

226-
function releaseDocument(fileName: string, compilationSettings: CompilerOptions): void {
273+
function releaseDocument(fileName: string, compilationSettings: CompilerOptions, scriptKind?: ScriptKind): void {
227274
const path = toPath(fileName, currentDirectory, getCanonicalFileName);
228275
const key = getKeyForCompilationSettings(compilationSettings);
229-
return releaseDocumentWithKey(path, key);
276+
return releaseDocumentWithKey(path, key, scriptKind);
230277
}
231278

232-
function releaseDocumentWithKey(path: Path, key: DocumentRegistryBucketKey): void {
279+
function releaseDocumentWithKey(path: Path, key: DocumentRegistryBucketKey, scriptKind?: ScriptKind): void {
233280
const bucket = Debug.checkDefined(buckets.get(key));
234-
const entry = bucket.get(path)!;
281+
const bucketEntry = bucket.get(path)!;
282+
const entry = getDocumentRegistryEntry(bucketEntry, scriptKind)!;
235283
entry.languageServiceRefCount--;
236284

237285
Debug.assert(entry.languageServiceRefCount >= 0);
238286
if (entry.languageServiceRefCount === 0) {
239-
bucket.delete(path);
287+
if (isDocumentRegistryEntry(bucketEntry)) {
288+
bucket.delete(path);
289+
}
290+
else {
291+
bucketEntry.delete(scriptKind!);
292+
if (bucketEntry.size === 1) {
293+
bucket.set(path, firstDefinedIterator(bucketEntry.values(), identity)!);
294+
}
295+
}
240296
}
241297
}
242298

243-
function getLanguageServiceRefCounts(path: Path) {
299+
function getLanguageServiceRefCounts(path: Path, scriptKind: ScriptKind) {
244300
return arrayFrom(buckets.entries(), ([key, bucket]): [string, number | undefined] => {
245-
const entry = bucket.get(path);
301+
const bucketEntry = bucket.get(path);
302+
const entry = bucketEntry && getDocumentRegistryEntry(bucketEntry, scriptKind);
246303
return [key, entry && entry.languageServiceRefCount];
247304
});
248305
}

src/services/services.ts

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1444,7 +1444,7 @@ namespace ts {
14441444
// not part of the new program.
14451445
function onReleaseOldSourceFile(oldSourceFile: SourceFile, oldOptions: CompilerOptions) {
14461446
const oldSettingsKey = documentRegistry.getKeyForCompilationSettings(oldOptions);
1447-
documentRegistry.releaseDocumentWithKey(oldSourceFile.resolvedPath, oldSettingsKey);
1447+
documentRegistry.releaseDocumentWithKey(oldSourceFile.resolvedPath, oldSettingsKey, oldSourceFile.scriptKind);
14481448
}
14491449

14501450
function getOrCreateSourceFile(fileName: string, languageVersion: ScriptTarget, onError?: (message: string) => void, shouldCreateNewSourceFile?: boolean): SourceFile | undefined {
@@ -1493,9 +1493,13 @@ namespace ts {
14931493
// We do not support the scenario where a host can modify a registered
14941494
// file's script kind, i.e. in one project some file is treated as ".ts"
14951495
// and in another as ".js"
1496-
Debug.assertEqual(hostFileInformation.scriptKind, oldSourceFile.scriptKind, "Registered script kind should match new script kind.");
1497-
1498-
return documentRegistry.updateDocumentWithKey(fileName, path, newSettings, documentRegistryBucketKey, hostFileInformation.scriptSnapshot, hostFileInformation.version, hostFileInformation.scriptKind);
1496+
if (hostFileInformation.scriptKind === oldSourceFile.scriptKind) {
1497+
return documentRegistry.updateDocumentWithKey(fileName, path, newSettings, documentRegistryBucketKey, hostFileInformation.scriptSnapshot, hostFileInformation.version, hostFileInformation.scriptKind);
1498+
}
1499+
else {
1500+
// Release old source file and fall through to aquire new file with new script kind
1501+
documentRegistry.releaseDocumentWithKey(oldSourceFile.resolvedPath, documentRegistry.getKeyForCompilationSettings(program.getCompilerOptions()), oldSourceFile.scriptKind);
1502+
}
14991503
}
15001504

15011505
// We didn't already have the file. Fall through and acquire it from the registry.
@@ -1531,7 +1535,7 @@ namespace ts {
15311535
// Use paths to ensure we are using correct key and paths as document registry could be created with different current directory than host
15321536
const key = documentRegistry.getKeyForCompilationSettings(program.getCompilerOptions());
15331537
forEach(program.getSourceFiles(), f =>
1534-
documentRegistry.releaseDocumentWithKey(f.resolvedPath, key));
1538+
documentRegistry.releaseDocumentWithKey(f.resolvedPath, key, f.scriptKind));
15351539
program = undefined!; // TODO: GH#18217
15361540
}
15371541
host = undefined!;

src/testRunner/unittests/tsserver/documentRegistry.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ namespace ts.projectSystem {
2727
assert.isDefined(moduleInfo);
2828
assert.equal(moduleInfo.isOrphan(), moduleIsOrphan);
2929
const key = service.documentRegistry.getKeyForCompilationSettings(project.getCompilationSettings());
30-
assert.deepEqual(service.documentRegistry.getLanguageServiceRefCounts(moduleInfo.path), [[key, moduleIsOrphan ? undefined : 1]]);
30+
assert.deepEqual(service.documentRegistry.getLanguageServiceRefCounts(moduleInfo.path, moduleInfo.scriptKind), [[key, moduleIsOrphan ? undefined : 1]]);
3131
}
3232

3333
function createServiceAndHost() {

src/testRunner/unittests/tsserver/dynamicFiles.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,28 @@ var x = 10;`
121121
service.openClientFile(file.path);
122122
checkNumberOfProjects(service, { configuredProjects: 1 });
123123
});
124+
125+
it("when changing scriptKind of the untitled files", () => {
126+
const host = createServerHost([libFile], { useCaseSensitiveFileNames: true });
127+
const service = createProjectService(host, { useInferredProjectPerProjectRoot: true });
128+
service.openClientFile(untitledFile, "const x = 10;", ScriptKind.TS, tscWatch.projectRoot);
129+
checkNumberOfProjects(service, { inferredProjects: 1 });
130+
checkProjectActualFiles(service.inferredProjects[0], [untitledFile, libFile.path]);
131+
const program = service.inferredProjects[0].getCurrentProgram()!;
132+
const sourceFile = program.getSourceFile(untitledFile)!;
133+
134+
// Close untitled file
135+
service.closeClientFile(untitledFile);
136+
137+
// Open untitled file with different mode
138+
service.openClientFile(untitledFile, "const x = 10;", ScriptKind.TSX, tscWatch.projectRoot);
139+
checkNumberOfProjects(service, { inferredProjects: 1 });
140+
checkProjectActualFiles(service.inferredProjects[0], [untitledFile, libFile.path]);
141+
const newProgram = service.inferredProjects[0].getCurrentProgram()!;
142+
const newSourceFile = newProgram.getSourceFile(untitledFile)!;
143+
assert.notStrictEqual(newProgram, program);
144+
assert.notStrictEqual(newSourceFile, sourceFile);
145+
});
124146
});
125147

126148
describe("unittests:: tsserver:: dynamicFiles:: ", () => {

tests/baselines/reference/api/tsserverlibrary.d.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6516,8 +6516,23 @@ declare namespace ts {
65166516
* @param fileName The name of the file to be released
65176517
* @param compilationSettings The compilation settings used to acquire the file
65186518
*/
6519+
/**@deprecated pass scriptKind for correctness */
65196520
releaseDocument(fileName: string, compilationSettings: CompilerOptions): void;
6521+
/**
6522+
* Informs the DocumentRegistry that a file is not needed any longer.
6523+
*
6524+
* Note: It is not allowed to call release on a SourceFile that was not acquired from
6525+
* this registry originally.
6526+
*
6527+
* @param fileName The name of the file to be released
6528+
* @param compilationSettings The compilation settings used to acquire the file
6529+
* @param scriptKind The script kind of the file to be released
6530+
*/
6531+
releaseDocument(fileName: string, compilationSettings: CompilerOptions, scriptKind: ScriptKind): void;
6532+
/**
6533+
* @deprecated pass scriptKind for correctness */
65206534
releaseDocumentWithKey(path: Path, key: DocumentRegistryBucketKey): void;
6535+
releaseDocumentWithKey(path: Path, key: DocumentRegistryBucketKey, scriptKind: ScriptKind): void;
65216536
reportStats(): string;
65226537
}
65236538
type DocumentRegistryBucketKey = string & {

tests/baselines/reference/api/typescript.d.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6516,8 +6516,23 @@ declare namespace ts {
65166516
* @param fileName The name of the file to be released
65176517
* @param compilationSettings The compilation settings used to acquire the file
65186518
*/
6519+
/**@deprecated pass scriptKind for correctness */
65196520
releaseDocument(fileName: string, compilationSettings: CompilerOptions): void;
6521+
/**
6522+
* Informs the DocumentRegistry that a file is not needed any longer.
6523+
*
6524+
* Note: It is not allowed to call release on a SourceFile that was not acquired from
6525+
* this registry originally.
6526+
*
6527+
* @param fileName The name of the file to be released
6528+
* @param compilationSettings The compilation settings used to acquire the file
6529+
* @param scriptKind The script kind of the file to be released
6530+
*/
6531+
releaseDocument(fileName: string, compilationSettings: CompilerOptions, scriptKind: ScriptKind): void;
6532+
/**
6533+
* @deprecated pass scriptKind for correctness */
65206534
releaseDocumentWithKey(path: Path, key: DocumentRegistryBucketKey): void;
6535+
releaseDocumentWithKey(path: Path, key: DocumentRegistryBucketKey, scriptKind: ScriptKind): void;
65216536
reportStats(): string;
65226537
}
65236538
type DocumentRegistryBucketKey = string & {

0 commit comments

Comments
 (0)