Skip to content

Commit ee46953

Browse files
Baseline Project Service State for easy diffing in changes (#57255)
Co-authored-by: Jake Bailey <[email protected]>
1 parent ee2090d commit ee46953

File tree

1,074 files changed

+103645
-2664
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

1,074 files changed

+103645
-2664
lines changed

src/harness/harnessLanguageService.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,9 @@ import * as vpath from "./_namespaces/vpath";
1414
import {
1515
incrementalVerifier,
1616
} from "./incrementalUtils";
17+
import {
18+
patchServiceForStateBaseline,
19+
} from "./projectServiceStateLogger";
1720
import {
1821
createLoggerWithInMemoryLogs,
1922
HarnessLSCouldNotResolveModule,
@@ -595,6 +598,7 @@ class SessionServerHost implements ts.server.ServerHost {
595598
class FourslashSession extends ts.server.Session {
596599
constructor(opts: ts.server.SessionOptions, readonly baselineHost: (when: string) => void) {
597600
super(opts);
601+
patchServiceForStateBaseline(this.projectService);
598602
}
599603
getText(fileName: string) {
600604
return ts.getSnapshotText(this.projectService.getDefaultProjectForFile(ts.server.toNormalizedPath(fileName), /*ensureProject*/ true)!.getScriptSnapshot(fileName)!);
@@ -609,6 +613,10 @@ class FourslashSession extends ts.server.Session {
609613
super.onMessage(message);
610614
this.baselineHost("After Request");
611615
}
616+
617+
getProjectService() {
618+
return this.projectService;
619+
}
612620
}
613621

614622
export class ServerLanguageServiceAdapter implements LanguageServiceAdapter {
@@ -642,6 +650,10 @@ export class ServerLanguageServiceAdapter implements LanguageServiceAdapter {
642650
if (baseline.length) {
643651
this.logger.log(when);
644652
baseline.forEach(s => this.logger.log(s));
653+
this.server.getProjectService().baseline();
654+
}
655+
else {
656+
this.server.getProjectService().baseline(when);
645657
}
646658
});
647659

Lines changed: 251 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,251 @@
1+
import {
2+
arrayFrom,
3+
compareStringsCaseSensitive,
4+
Debug,
5+
noop,
6+
} from "./_namespaces/ts";
7+
import {
8+
AutoImportProviderProject,
9+
AuxiliaryProject,
10+
LogLevel,
11+
Project,
12+
ProjectKind,
13+
ProjectService,
14+
ScriptInfo,
15+
} from "./_namespaces/ts.server";
16+
import {
17+
LoggerWithInMemoryLogs,
18+
} from "./tsserverLogger";
19+
20+
interface ProjectData {
21+
projectStateVersion: Project["projectStateVersion"];
22+
projectProgramVersion: Project["projectProgramVersion"];
23+
autoImportProviderHost: Project["autoImportProviderHost"];
24+
noDtsResolutionProject: Project["noDtsResolutionProject"];
25+
}
26+
27+
interface ScriptInfoData {
28+
open: boolean;
29+
version: string;
30+
pendingReloadFromDisk: boolean;
31+
containingProjects: Set<Project>;
32+
}
33+
34+
enum Diff {
35+
None = "",
36+
New = " *new*",
37+
Change = " *changed*",
38+
Deleted = " *deleted*",
39+
}
40+
41+
type StateItemLog = [string, string[], string[]?];
42+
43+
export function patchServiceForStateBaseline(service: ProjectService) {
44+
if (!service.logger.isTestLogger || !service.logger.hasLevel(LogLevel.verbose)) return;
45+
if (service.baseline !== noop) return; // Already patched
46+
47+
const projects = new Map<Project, ProjectData>();
48+
const scriptInfos = new Map<ScriptInfo, ScriptInfoData>();
49+
const allPropertiesOfProjectData: (keyof ProjectData & keyof Project)[] = [
50+
"projectStateVersion",
51+
"projectProgramVersion",
52+
"autoImportProviderHost",
53+
"noDtsResolutionProject",
54+
];
55+
const logger = service.logger as LoggerWithInMemoryLogs;
56+
service.baseline = title => {
57+
const projectLogs = baselineProjects();
58+
const infoLogs = baselineScriptInfos();
59+
if (projectLogs?.length || infoLogs?.length) {
60+
if (title) logger.log(title);
61+
sendLogsToLogger("Projects::", projectLogs);
62+
sendLogsToLogger("ScriptInfos::", infoLogs);
63+
}
64+
};
65+
66+
function sendLogsToLogger(title: string, logs: StateItemLog[] | undefined) {
67+
if (!logs) return;
68+
logger.log(title);
69+
logs.sort((a, b) => compareStringsCaseSensitive(a[0], b[0]))
70+
.forEach(([title, propertyLogs, additionalPropertyLogs]) => {
71+
logger.log(title);
72+
propertyLogs.forEach(s => logger.log(s));
73+
additionalPropertyLogs?.forEach(s => logger.log(s));
74+
});
75+
logger.log("");
76+
}
77+
78+
function baselineProjects() {
79+
const autoImportProviderProjects = [] as AutoImportProviderProject[];
80+
const auxiliaryProjects = [] as AuxiliaryProject[];
81+
return baselineState(
82+
[service.externalProjects, service.configuredProjects, service.inferredProjects, autoImportProviderProjects, auxiliaryProjects],
83+
projects,
84+
(logs, project, data) => {
85+
if (project.autoImportProviderHost) autoImportProviderProjects.push(project.autoImportProviderHost);
86+
if (project.noDtsResolutionProject) auxiliaryProjects.push(project.noDtsResolutionProject);
87+
let projectDiff = newOrDeleted(project, projects, data);
88+
const projectPropertyLogs = [] as string[];
89+
allPropertiesOfProjectData.forEach(key => {
90+
let value: Project[typeof key] | string = project[key];
91+
if (key === "autoImportProviderHost" || key === "noDtsResolutionProject") {
92+
if (project[key]) value = (project[key] as Project).projectName;
93+
else if (project[key] === undefined && data?.[key] === undefined) return;
94+
}
95+
projectDiff = printProperty(PrintPropertyWhen.Always, data, key, project[key], projectDiff, projectPropertyLogs, value);
96+
});
97+
logs.push([`${project.projectName} (${ProjectKind[project.projectKind]})${projectDiff}`, projectPropertyLogs]);
98+
return projectDiff;
99+
},
100+
project => ({
101+
projectStateVersion: project.projectStateVersion,
102+
projectProgramVersion: project.projectProgramVersion,
103+
autoImportProviderHost: project.autoImportProviderHost,
104+
noDtsResolutionProject: project.noDtsResolutionProject,
105+
}),
106+
);
107+
}
108+
109+
function baselineScriptInfos() {
110+
return baselineState(
111+
[service.filenameToScriptInfo],
112+
scriptInfos,
113+
(logs, info, data) => {
114+
let infoDiff = newOrDeleted(info, scriptInfos, data);
115+
const infoPropertyLogs = [] as string[];
116+
const isOpen = info.isScriptOpen();
117+
infoDiff = printProperty(PrintPropertyWhen.Changed, data, "open", isOpen, infoDiff, infoPropertyLogs);
118+
infoDiff = printProperty(PrintPropertyWhen.Always, data, "version", info.textStorage.getVersion(), infoDiff, infoPropertyLogs);
119+
infoDiff = printProperty(PrintPropertyWhen.TruthyOrChangedOrNew, data, "pendingReloadFromDisk", info.textStorage.pendingReloadFromDisk, infoDiff, infoPropertyLogs);
120+
const containingProjectsLogs = [] as string[];
121+
let containingProjectsDiff = Diff.None;
122+
const defaultProject = isOpen && info.containingProjects.length ? info.getDefaultProject() : undefined;
123+
info.containingProjects.forEach(project =>
124+
containingProjectsDiff = printPropertyWorker(
125+
PrintPropertyWhen.Always,
126+
` ${project.projectName}${defaultProject === project ? " *default*" : ""}`,
127+
containingProjectsLogs,
128+
containingProjectsDiff,
129+
!!data && !data.containingProjects.has(project),
130+
Diff.New,
131+
project,
132+
)
133+
);
134+
const infoProjects = new Set(info.containingProjects);
135+
data?.containingProjects.forEach(project => {
136+
if (infoProjects.has(project)) return;
137+
containingProjectsDiff = Diff.Change;
138+
containingProjectsLogs.push(` ${project.projectName}${Diff.Deleted}`);
139+
});
140+
infoDiff = printPropertyWorker(
141+
PrintPropertyWhen.Always,
142+
`containingProjects: ${info.containingProjects.length}`,
143+
infoPropertyLogs,
144+
infoDiff,
145+
!!containingProjectsDiff,
146+
Diff.Change,
147+
info.containingProjects,
148+
);
149+
logs.push([
150+
`${info.fileName}${isOpen ? " (Open)" : ""}${infoDiff}`,
151+
infoPropertyLogs,
152+
containingProjectsLogs,
153+
]);
154+
return infoDiff;
155+
},
156+
info => ({
157+
open: info.isScriptOpen(),
158+
version: info.textStorage.getVersion(),
159+
pendingReloadFromDisk: info.textStorage.pendingReloadFromDisk,
160+
containingProjects: new Set(info.containingProjects),
161+
}),
162+
);
163+
}
164+
165+
function baselineState<T, Data>(
166+
currentCaches: (T[] | Map<any, T>)[],
167+
dataMap: Map<T, Data>,
168+
printWorker: (logs: StateItemLog[], current: T, data: Data | undefined) => Diff,
169+
toData: (current: T) => Data,
170+
) {
171+
const logs = [] as StateItemLog[];
172+
let hasChange = false;
173+
const currentSet = new Set<T>();
174+
currentCaches.forEach(currentCache => currentCache.forEach(printCurrent));
175+
if (currentSet.size !== dataMap.size) {
176+
for (const [key, data] of arrayFrom(dataMap.entries())) {
177+
if (!currentSet.has(key)) {
178+
dataMap.delete(key);
179+
printWorker(logs, key, data);
180+
}
181+
}
182+
hasChange = true;
183+
}
184+
return hasChange ? logs : undefined;
185+
186+
function printCurrent(current: T) {
187+
if (!current) return;
188+
currentSet.add(current);
189+
if (printWorker(logs, current, dataMap.get(current))) {
190+
hasChange = true;
191+
dataMap.set(current, toData(current));
192+
}
193+
}
194+
}
195+
196+
function newOrDeleted<T, Data>(current: T, dataMap: Map<T, Data>, data: Data | undefined) {
197+
return !data ? Diff.New : !dataMap.has(current) ? Diff.Deleted : Diff.None;
198+
}
199+
200+
enum PrintPropertyWhen {
201+
Always,
202+
TruthyOrChangedOrNew,
203+
Changed,
204+
}
205+
function printProperty<Data, Key extends keyof Data & string>(
206+
printWhen: PrintPropertyWhen,
207+
data: Data | undefined,
208+
key: Key,
209+
value: Data[Key],
210+
dataDiff: Diff,
211+
propertyLogs: string[],
212+
stringValue?: Data[Key] | string,
213+
) {
214+
return printPropertyWorker(
215+
printWhen,
216+
`${key}: ${stringValue === undefined ? value : stringValue}`,
217+
propertyLogs,
218+
dataDiff,
219+
!!data && data[key] !== value,
220+
Diff.Change,
221+
value,
222+
);
223+
}
224+
225+
function printPropertyWorker(
226+
printWhen: PrintPropertyWhen,
227+
stringValue: string,
228+
propertyLogs: string[],
229+
dataDiff: Diff,
230+
propertyChanged: boolean,
231+
propertyChangeDiff: Diff.Change | Diff.New,
232+
value: any,
233+
) {
234+
const propertyDiff = propertyChanged ? propertyChangeDiff : Diff.None;
235+
const result = !dataDiff && propertyDiff ? propertyChangeDiff : dataDiff;
236+
switch (printWhen) {
237+
case PrintPropertyWhen.Changed:
238+
if (!propertyDiff) return result;
239+
break;
240+
case PrintPropertyWhen.TruthyOrChangedOrNew:
241+
if (!value && !propertyDiff) return result;
242+
break;
243+
case PrintPropertyWhen.Always:
244+
break;
245+
default:
246+
Debug.assertNever(printWhen);
247+
}
248+
propertyLogs.push(` ${stringValue}${propertyDiff}`);
249+
return result;
250+
}
251+
}

src/server/editorServices.ts

Lines changed: 21 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1139,6 +1139,7 @@ export class ProjectService {
11391139
private pendingPluginEnablements?: Map<Project, Promise<BeginEnablePluginResult>[]>;
11401140
private currentPluginEnablementPromise?: Promise<void>;
11411141

1142+
/** @internal */ baseline: (title?: string) => void = noop;
11421143
/** @internal */ verifyDocumentRegistry = noop;
11431144
/** @internal */ verifyProgram: (project: Project) => void = noop;
11441145
/** @internal */ onProjectCreation: (project: Project) => void = noop;
@@ -3513,6 +3514,9 @@ export class ProjectService {
35133514
});
35143515
this.inferredProjects.forEach(project => this.clearSemanticCache(project));
35153516
this.ensureProjectForOpenFiles();
3517+
3518+
this.logger.info("After reloading projects..");
3519+
this.printProjects();
35163520
}
35173521

35183522
/**
@@ -4195,7 +4199,10 @@ export class ProjectService {
41954199
}
41964200
}
41974201

4198-
closeExternalProject(uncheckedFileName: string): void {
4202+
closeExternalProject(uncheckedFileName: string): void;
4203+
/** @internal */
4204+
closeExternalProject(uncheckedFileName: string, print: boolean): void;
4205+
closeExternalProject(uncheckedFileName: string, print?: boolean): void {
41994206
const fileName = toNormalizedPath(uncheckedFileName);
42004207
const configFiles = this.externalProjectToConfiguredProjectMap.get(fileName);
42014208
if (configFiles) {
@@ -4211,6 +4218,7 @@ export class ProjectService {
42114218
this.removeProject(externalProject);
42124219
}
42134220
}
4221+
if (print) this.printProjects();
42144222
}
42154223

42164224
openExternalProjects(projects: protocol.ExternalProject[]): void {
@@ -4221,15 +4229,17 @@ export class ProjectService {
42214229
});
42224230

42234231
for (const externalProject of projects) {
4224-
this.openExternalProject(externalProject);
4232+
this.openExternalProject(externalProject, /*print*/ false);
42254233
// delete project that is present in input list
42264234
projectsToClose.delete(externalProject.projectFileName);
42274235
}
42284236

42294237
// close projects that were missing in the input list
42304238
forEachKey(projectsToClose, externalProjectName => {
4231-
this.closeExternalProject(externalProjectName);
4239+
this.closeExternalProject(externalProjectName, /*print*/ false);
42324240
});
4241+
4242+
this.printProjects();
42334243
}
42344244

42354245
/** Makes a filename safe to insert in a RegExp */
@@ -4351,7 +4361,10 @@ export class ProjectService {
43514361
return excludedFiles;
43524362
}
43534363

4354-
openExternalProject(proj: protocol.ExternalProject): void {
4364+
openExternalProject(proj: protocol.ExternalProject): void;
4365+
/** @internal */
4366+
openExternalProject(proj: protocol.ExternalProject, print: boolean): void;
4367+
openExternalProject(proj: protocol.ExternalProject, print?: boolean): void {
43554368
proj.typeAcquisition = proj.typeAcquisition || {};
43564369
proj.typeAcquisition.include = proj.typeAcquisition.include || [];
43574370
proj.typeAcquisition.exclude = proj.typeAcquisition.exclude || [];
@@ -4399,17 +4412,18 @@ export class ProjectService {
43994412
// The graph update here isnt postponed since any file open operation needs all updated external projects
44004413
this.updateRootAndOptionsOfNonInferredProject(externalProject, proj.rootFiles, externalFilePropertyReader, compilerOptions, proj.typeAcquisition, proj.options.compileOnSave, watchOptionsAndErrors?.watchOptions);
44014414
externalProject.updateGraph();
4415+
if (print) this.printProjects();
44024416
return;
44034417
}
44044418
// some config files were added to external project (that previously were not there)
44054419
// close existing project and later we'll open a set of configured projects for these files
4406-
this.closeExternalProject(proj.projectFileName);
4420+
this.closeExternalProject(proj.projectFileName, /*print*/ false);
44074421
}
44084422
else if (this.externalProjectToConfiguredProjectMap.get(proj.projectFileName)) {
44094423
// this project used to include config files
44104424
if (!tsConfigFiles) {
44114425
// config files were removed from the project - close existing external project which in turn will close configured projects
4412-
this.closeExternalProject(proj.projectFileName);
4426+
this.closeExternalProject(proj.projectFileName, /*print*/ false);
44134427
}
44144428
else {
44154429
// project previously had some config files - compare them with new set of files and close all configured projects that correspond to unused files
@@ -4464,6 +4478,7 @@ export class ProjectService {
44644478
const project = this.createExternalProject(proj.projectFileName, rootFiles, proj.options, proj.typeAcquisition, excludedFiles);
44654479
project.updateGraph();
44664480
}
4481+
if (print) this.printProjects();
44674482
}
44684483

44694484
hasDeferredExtension() {

0 commit comments

Comments
 (0)