Skip to content

Commit 3c4c060

Browse files
authored
Adds tests and fixes issues by verifying incremental project updates are handling language service ref counting correctly (#54504)
1 parent 93ac5c6 commit 3c4c060

File tree

32 files changed

+492
-79
lines changed

32 files changed

+492
-79
lines changed

src/compiler/debug.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@ import {
7272
ObjectFlags,
7373
ObjectType,
7474
RelationComparisonResult,
75+
ScriptKind,
7576
Signature,
7677
SignatureCheckMode,
7778
SignatureFlags,
@@ -439,6 +440,10 @@ export namespace Debug {
439440
return formatEnum(kind, (ts as any).SnippetKind, /*isFlags*/ false);
440441
}
441442

443+
export function formatScriptKind(kind: ScriptKind | undefined): string {
444+
return formatEnum(kind, (ts as any).ScriptKind, /*isFlags*/ false);
445+
}
446+
442447
export function formatNodeFlags(flags: NodeFlags | undefined): string {
443448
return formatEnum(flags, (ts as any).NodeFlags, /*isFlags*/ true);
444449
}

src/compiler/moduleNameResolver.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -880,7 +880,7 @@ function compilerOptionValueToString(value: unknown): string {
880880

881881
/** @internal */
882882
export function getKeyForCompilerOptions(options: CompilerOptions, affectingOptionDeclarations: readonly CommandLineOption[]) {
883-
return affectingOptionDeclarations.map(option => compilerOptionValueToString(getCompilerOptionValue(options, option))).join("|") + (options.pathsBasePath ? `|${options.pathsBasePath}` : undefined);
883+
return affectingOptionDeclarations.map(option => compilerOptionValueToString(getCompilerOptionValue(options, option))).join("|") + `|${options.pathsBasePath}`;
884884
}
885885

886886
/** @internal */

src/compiler/program.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2369,8 +2369,8 @@ export function createProgram(rootNamesOrOptions: readonly string[] | CreateProg
23692369
for (const oldSourceFile of oldSourceFiles) {
23702370
const sourceFileOptions = getCreateSourceFileOptions(oldSourceFile.fileName, moduleResolutionCache, host, options);
23712371
let newSourceFile = host.getSourceFileByPath
2372-
? host.getSourceFileByPath(oldSourceFile.fileName, oldSourceFile.resolvedPath, sourceFileOptions, /*onError*/ undefined, shouldCreateNewSourceFile || sourceFileOptions.impliedNodeFormat !== oldSourceFile.impliedNodeFormat)
2373-
: host.getSourceFile(oldSourceFile.fileName, sourceFileOptions, /*onError*/ undefined, shouldCreateNewSourceFile || sourceFileOptions.impliedNodeFormat !== oldSourceFile.impliedNodeFormat); // TODO: GH#18217
2372+
? host.getSourceFileByPath(oldSourceFile.fileName, oldSourceFile.resolvedPath, sourceFileOptions, /*onError*/ undefined, shouldCreateNewSourceFile)
2373+
: host.getSourceFile(oldSourceFile.fileName, sourceFileOptions, /*onError*/ undefined, shouldCreateNewSourceFile); // TODO: GH#18217
23742374

23752375
if (!newSourceFile) {
23762376
return StructureIsReused.Not;
@@ -3615,7 +3615,7 @@ export function createProgram(rootNamesOrOptions: readonly string[] | CreateProg
36153615
fileName,
36163616
sourceFileOptions,
36173617
hostErrorMessage => addFilePreprocessingFileExplainingDiagnostic(/*file*/ undefined, reason, Diagnostics.Cannot_read_file_0_Colon_1, [fileName, hostErrorMessage]),
3618-
shouldCreateNewSourceFile || (oldProgram?.getSourceFileByPath(toPath(fileName))?.impliedNodeFormat !== sourceFileOptions.impliedNodeFormat)
3618+
shouldCreateNewSourceFile,
36193619
);
36203620

36213621
if (packageId) {

src/compiler/watchPublic.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -724,7 +724,8 @@ export function createWatchProgram<T extends BuilderProgram>(host: WatchCompiler
724724
}
725725

726726
// Create new source file if requested or the versions dont match
727-
if (hostSourceFile === undefined || shouldCreateNewSourceFile || isFilePresenceUnknownOnHost(hostSourceFile)) {
727+
const impliedNodeFormat = typeof languageVersionOrOptions === "object" ? languageVersionOrOptions.impliedNodeFormat : undefined;
728+
if (hostSourceFile === undefined || shouldCreateNewSourceFile || isFilePresenceUnknownOnHost(hostSourceFile) || hostSourceFile.sourceFile.impliedNodeFormat !== impliedNodeFormat) {
728729
const sourceFile = getNewSourceFile(fileName, languageVersionOrOptions, onError);
729730
if (hostSourceFile) {
730731
if (sourceFile) {

src/harness/harnessLanguageService.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import * as ts from "./_namespaces/ts";
99
import { getNewLineCharacter } from "./_namespaces/ts";
1010
import * as vfs from "./_namespaces/vfs";
1111
import * as vpath from "./_namespaces/vpath";
12+
import { incrementalVerifier } from "./incrementalUtils";
1213

1314
export function makeDefaultProxy(info: ts.server.PluginCreateInfo): ts.LanguageService {
1415
const proxy = Object.create(/*o*/ null); // eslint-disable-line no-null/no-null
@@ -1016,7 +1017,8 @@ export class ServerLanguageServiceAdapter implements LanguageServiceAdapter {
10161017
byteLength: Buffer.byteLength,
10171018
hrtime: process.hrtime,
10181019
logger: serverHost,
1019-
canUseEvents: true
1020+
canUseEvents: true,
1021+
incrementalVerifier,
10201022
};
10211023
this.server = new FourslashSession(opts);
10221024

src/harness/incrementalUtils.ts

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
import * as ts from "./_namespaces/ts";
2+
3+
export function reportDocumentRegistryStats(documentRegistry: ts.DocumentRegistry) {
4+
const str: string[] = [];
5+
documentRegistry.getBuckets().forEach((bucketEntries, key) => {
6+
str.push(` Key:: ${key}`);
7+
bucketEntries.forEach((entry, path) => {
8+
if (ts.isDocumentRegistryEntry(entry)) {
9+
str.push(` ${path}: ${ts.Debug.formatScriptKind(entry.sourceFile.scriptKind)} ${entry.languageServiceRefCount}`);
10+
}
11+
else {
12+
entry.forEach((real, kind) => str.push(` ${path}: ${ts.Debug.formatScriptKind(kind)} ${real.languageServiceRefCount}`));
13+
}
14+
});
15+
});
16+
return str;
17+
}
18+
19+
type DocumentRegistryExpectedStats = Map<ts.DocumentRegistryBucketKeyWithMode, Map<ts.Path, Map<ts.ScriptKind, number>>>;
20+
function verifyDocumentRegistryStats(
21+
documentRegistry: ts.DocumentRegistry,
22+
stats: DocumentRegistryExpectedStats,
23+
) {
24+
documentRegistry.getBuckets().forEach((bucketEntries, key) => {
25+
const statsByPath = stats.get(key);
26+
bucketEntries.forEach((entry, path) => {
27+
const expected = statsByPath?.get(path);
28+
if (ts.isDocumentRegistryEntry(entry)) {
29+
ts.Debug.assert(
30+
expected?.size === 1 && expected.has(entry.sourceFile.scriptKind) && expected.get(entry.sourceFile.scriptKind) === entry.languageServiceRefCount,
31+
`Document registry has unexpected language service ref count for ${key} ${path} ${ts.Debug.formatScriptKind(entry.sourceFile.scriptKind)} ${entry.languageServiceRefCount}`,
32+
reportStats,
33+
);
34+
}
35+
else {
36+
entry.forEach((real, kind) => ts.Debug.assert(
37+
real.languageServiceRefCount === expected?.get(kind),
38+
`Document registry has unexpected language service ref count for ${key} ${path} ${ts.Debug.formatScriptKind(kind)} ${real.languageServiceRefCount}`,
39+
reportStats,
40+
));
41+
expected?.forEach((value, kind) => ts.Debug.assert(
42+
entry.has(kind),
43+
`Document registry expected language service ref count for ${key} ${path} ${ts.Debug.formatScriptKind(kind)} ${value}`,
44+
reportStats,
45+
));
46+
}
47+
});
48+
statsByPath?.forEach((_value, path) => ts.Debug.assert(
49+
bucketEntries.has(path),
50+
`Document registry does not contain entry for ${key}, ${path}`,
51+
reportStats,
52+
));
53+
});
54+
stats.forEach((_value, key) => ts.Debug.assert(
55+
documentRegistry.getBuckets().has(key),
56+
`Document registry does not contain entry for key: ${key}`,
57+
reportStats,
58+
));
59+
60+
function reportStats() {
61+
const str: string[] = ["", "Actual::", ...reportDocumentRegistryStats(documentRegistry)];
62+
str.push("Expected::");
63+
stats?.forEach((statsByPath, key) => {
64+
str.push(` Key:: ${key}`);
65+
statsByPath.forEach((entry, path) => entry.forEach((refCount, kind) => str.push(` ${path}: ${ts.Debug.formatScriptKind(kind)} ${refCount}`)));
66+
});
67+
return str.join("\n");
68+
}
69+
}
70+
71+
function verifyDocumentRegistry(service: ts.server.ProjectService) {
72+
const stats: DocumentRegistryExpectedStats = new Map();
73+
const collectStats = (project: ts.server.Project) => {
74+
if (project.autoImportProviderHost) collectStats(project.autoImportProviderHost);
75+
if (project.noDtsResolutionProject) collectStats(project.noDtsResolutionProject);
76+
const program = project.getCurrentProgram();
77+
if (!program) return;
78+
const key = service.documentRegistry.getKeyForCompilationSettings(program.getCompilerOptions());
79+
program.getSourceFiles().forEach(f => {
80+
const keyWithMode = service.documentRegistry.getDocumentRegistryBucketKeyWithMode(key, f.impliedNodeFormat);
81+
let mapForKeyWithMode = stats.get(keyWithMode);
82+
let result: Map<ts.ScriptKind, number> | undefined;
83+
if (mapForKeyWithMode === undefined) {
84+
stats.set(keyWithMode, mapForKeyWithMode = new Map());
85+
mapForKeyWithMode.set(f.resolvedPath, result = new Map());
86+
}
87+
else {
88+
result = mapForKeyWithMode.get(f.resolvedPath);
89+
if (!result) mapForKeyWithMode.set(f.resolvedPath, result = new Map());
90+
}
91+
result.set(f.scriptKind, (result.get(f.scriptKind) || 0) + 1);
92+
});
93+
};
94+
service.forEachProject(collectStats);
95+
verifyDocumentRegistryStats(service.documentRegistry, stats);
96+
}
97+
98+
export function incrementalVerifier(service: ts.server.ProjectService) {
99+
service.verifyDocumentRegistry = () => verifyDocumentRegistry(service);
100+
}

src/server/editorServices.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -590,6 +590,7 @@ export interface ProjectServiceOptions {
590590
typesMapLocation?: string;
591591
serverMode?: LanguageServiceMode;
592592
session: Session<unknown> | undefined;
593+
/** @internal */ incrementalVerifier?: (service: ProjectService) => void;
593594
}
594595

595596
interface OriginalFileInfo { fileName: NormalizedPath; path: Path; }
@@ -989,12 +990,13 @@ export class ProjectService {
989990
/** @internal */
990991
readonly session: Session<unknown> | undefined;
991992

992-
993993
private performanceEventHandler?: PerformanceEventHandler;
994994

995995
private pendingPluginEnablements?: Map<Project, Promise<BeginEnablePluginResult>[]>;
996996
private currentPluginEnablementPromise?: Promise<void>;
997997

998+
/** @internal */ verifyDocumentRegistry = noop;
999+
9981000
constructor(opts: ProjectServiceOptions) {
9991001
this.host = opts.host;
10001002
this.logger = opts.logger;
@@ -1057,6 +1059,7 @@ export class ProjectService {
10571059
watchDirectory: returnNoopFileWatcher,
10581060
} :
10591061
getWatchFactory(this.host, watchLogLevel, log, getDetailWatchInfo);
1062+
opts.incrementalVerifier?.(this);
10601063
}
10611064

10621065
toPath(fileName: string) {
@@ -1334,7 +1337,7 @@ export class ProjectService {
13341337
}
13351338

13361339
/** @internal */
1337-
private forEachProject(cb: (project: Project) => void) {
1340+
forEachProject(cb: (project: Project) => void) {
13381341
this.externalProjects.forEach(cb);
13391342
this.configuredProjects.forEach(cb);
13401343
this.inferredProjects.forEach(cb);
@@ -2640,6 +2643,7 @@ export class ProjectService {
26402643
private clearSemanticCache(project: Project) {
26412644
project.resolutionCache.clear();
26422645
project.getLanguageService(/*ensureSynchronized*/ false).cleanupSemanticCache();
2646+
project.cleanupProgram();
26432647
project.markAsDirty();
26442648
}
26452649

src/server/project.ts

Lines changed: 25 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -393,7 +393,7 @@ export abstract class Project implements LanguageServiceHost, ModuleResolutionHo
393393
private packageJsonsForAutoImport: Set<string> | undefined;
394394

395395
/** @internal */
396-
private noDtsResolutionProject?: AuxiliaryProject | undefined;
396+
noDtsResolutionProject?: AuxiliaryProject | undefined;
397397

398398
/** @internal */
399399
getResolvedProjectReferenceToRedirect(_fileName: string): ResolvedProjectReference | undefined {
@@ -969,13 +969,27 @@ export abstract class Project implements LanguageServiceHost, ModuleResolutionHo
969969
this.projectService.onUpdateLanguageServiceStateForProject(this, /*languageServiceEnabled*/ true);
970970
}
971971

972+
/** @internal */
973+
cleanupProgram() {
974+
if (this.program) {
975+
// Root files are always attached to the project irrespective of program
976+
for (const f of this.program.getSourceFiles()) {
977+
this.detachScriptInfoIfNotRoot(f.fileName);
978+
}
979+
this.program.forEachResolvedProjectReference(ref =>
980+
this.detachScriptInfoFromProject(ref.sourceFile.fileName));
981+
this.program = undefined;
982+
}
983+
}
984+
972985
disableLanguageService(lastFileExceededProgramSize?: string) {
973986
if (!this.languageServiceEnabled) {
974987
return;
975988
}
976989
Debug.assert(this.projectService.serverMode !== LanguageServiceMode.Syntactic);
977990
this.languageService.cleanupSemanticCache();
978991
this.languageServiceEnabled = false;
992+
this.cleanupProgram();
979993
this.lastFileExceededProgramSize = lastFileExceededProgramSize;
980994
this.builderState = undefined;
981995
if (this.autoImportProviderHost) {
@@ -984,6 +998,7 @@ export abstract class Project implements LanguageServiceHost, ModuleResolutionHo
984998
this.autoImportProviderHost = undefined;
985999
this.resolutionCache.closeTypeRootsWatch();
9861000
this.clearGeneratedFileWatch();
1001+
this.projectService.verifyDocumentRegistry();
9871002
this.projectService.onUpdateLanguageServiceStateForProject(this, /*languageServiceEnabled*/ false);
9881003
}
9891004

@@ -1030,17 +1045,10 @@ export abstract class Project implements LanguageServiceHost, ModuleResolutionHo
10301045
close() {
10311046
this.projectService.typingsCache.onProjectClosed(this);
10321047
this.closeWatchingTypingLocations();
1033-
if (this.program) {
1034-
// if we have a program - release all files that are enlisted in program but arent root
1035-
// The releasing of the roots happens later
1036-
// The project could have pending update remaining and hence the info could be in the files but not in program graph
1037-
for (const f of this.program.getSourceFiles()) {
1038-
this.detachScriptInfoIfNotRoot(f.fileName);
1039-
}
1040-
this.program.forEachResolvedProjectReference(ref =>
1041-
this.detachScriptInfoFromProject(ref.sourceFile.fileName));
1042-
}
1043-
1048+
// if we have a program - release all files that are enlisted in program but arent root
1049+
// The releasing of the roots happens later
1050+
// The project could have pending update remaining and hence the info could be in the files but not in program graph
1051+
this.cleanupProgram();
10441052
// Release external files
10451053
forEach(this.externalFiles, externalFile => this.detachScriptInfoIfNotRoot(externalFile));
10461054
// Always remove root files from the project
@@ -1492,6 +1500,7 @@ export abstract class Project implements LanguageServiceHost, ModuleResolutionHo
14921500

14931501
private updateGraphWorker() {
14941502
const oldProgram = this.languageService.getCurrentProgram();
1503+
Debug.assert(oldProgram === this.program);
14951504
Debug.assert(!this.isClosed(), "Called update graph worker of closed project");
14961505
this.writeLog(`Starting updateGraphWorker: Project: ${this.getProjectName()}`);
14971506
const start = timestamp();
@@ -1633,6 +1642,8 @@ export abstract class Project implements LanguageServiceHost, ModuleResolutionHo
16331642
else if (this.program !== oldProgram) {
16341643
this.writeLog(`Different program with same set of files`);
16351644
}
1645+
// Verify the document registry count
1646+
this.projectService.verifyDocumentRegistry();
16361647
return hasNewProgram;
16371648
}
16381649

@@ -2342,7 +2353,8 @@ export class InferredProject extends Project {
23422353
}
23432354
}
23442355

2345-
class AuxiliaryProject extends Project {
2356+
/** @internal */
2357+
export class AuxiliaryProject extends Project {
23462358
constructor(projectService: ProjectService, documentRegistry: DocumentRegistry, compilerOptions: CompilerOptions, currentDirectory: string) {
23472359
super(projectService.newAuxiliaryProjectName(),
23482360
ProjectKind.Auxiliary,
@@ -2361,7 +2373,6 @@ class AuxiliaryProject extends Project {
23612373
return true;
23622374
}
23632375

2364-
/** @internal */
23652376
override scheduleInvalidateResolutionsOfFailedLookupLocations(): void {
23662377
// Invalidation will happen on-demand as part of updateGraph
23672378
return;

src/server/session.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -946,6 +946,7 @@ export interface SessionOptions {
946946
pluginProbeLocations?: readonly string[];
947947
allowLocalPluginLoads?: boolean;
948948
typesMapLocation?: string;
949+
/** @internal */ incrementalVerifier?: (service: ProjectService) => void;
949950
}
950951

951952
export class Session<TMessage = string> implements EventSender {
@@ -1010,7 +1011,8 @@ export class Session<TMessage = string> implements EventSender {
10101011
allowLocalPluginLoads: opts.allowLocalPluginLoads,
10111012
typesMapLocation: opts.typesMapLocation,
10121013
serverMode: opts.serverMode,
1013-
session: this
1014+
session: this,
1015+
incrementalVerifier: opts.incrementalVerifier,
10141016
};
10151017
this.projectService = new ProjectService(settings);
10161018
this.projectService.setPerformanceEventHandler(this.performanceEventHandler.bind(this));
@@ -1339,6 +1341,7 @@ export class Session<TMessage = string> implements EventSender {
13391341
this.logger.info(`cleaning ${caption}`);
13401342
for (const p of projects) {
13411343
p.getLanguageService(/*ensureSynchronized*/ false).cleanupSemanticCache();
1344+
p.cleanupProgram();
13421345
}
13431346
}
13441347

0 commit comments

Comments
 (0)