Files
TypeScript/src/harness/projectServiceStateLogger.ts

484 lines
22 KiB
TypeScript

import {
arrayFrom,
compareStringsCaseSensitive,
Debug,
DocumentPositionMapper,
emptyArray,
identity,
identitySourceMapConsumer,
isString,
noop,
SourceMapper,
toSorted,
} from "./_namespaces/ts.js";
import {
AutoImportProviderProject,
AuxiliaryProject,
ConfiguredProject,
isBackgroundProject,
isConfiguredProject,
LogLevel,
Project,
ProjectKind,
ProjectService,
ScriptInfo,
SourceMapFileWatcher,
TextStorage,
} from "./_namespaces/ts.server.js";
import { LoggerWithInMemoryLogs } from "./tsserverLogger.js";
interface ProjectData {
projectStateVersion: Project["projectStateVersion"];
projectProgramVersion: Project["projectProgramVersion"];
dirty: Project["dirty"];
initialLoadPending: Project["initialLoadPending"];
isClosed: ReturnType<Project["isClosed"]>;
isOrphan: ReturnType<Project["isOrphan"]>;
noOpenRef: boolean;
deferredClose: ConfiguredProject["deferredClose"];
documentPositionMappers: SourceMapper["documentPositionMappers"];
autoImportProviderHost: Project["autoImportProviderHost"];
noDtsResolutionProject: Project["noDtsResolutionProject"];
originalConfiguredProjects: Project["originalConfiguredProjects"];
}
interface SourceMapFileWatcherData {
sourceInfos: SourceMapFileWatcher["sourceInfos"];
}
interface ScriptInfoData {
open: ReturnType<ScriptInfo["isScriptOpen"]>;
version: ReturnType<TextStorage["getVersion"]>;
pendingReloadFromDisk: TextStorage["pendingReloadFromDisk"];
deferredDelete: ScriptInfo["deferredDelete"];
sourceMapFilePath: Exclude<ScriptInfo["sourceMapFilePath"], SourceMapFileWatcher> | SourceMapFileWatcherData | undefined;
declarationInfoPath: ScriptInfo["declarationInfoPath"];
sourceInfos: ScriptInfo["sourceInfos"];
documentPositionMapper: ScriptInfo["documentPositionMapper"];
containingProjects: Set<Project>;
}
enum Diff {
None = "",
New = " *new*",
Change = " *changed*",
Deleted = " *deleted*",
}
type StatePropertyLog = string | string[];
type StateItemLog = [string, StatePropertyLog[]];
export function patchServiceForStateBaseline(service: ProjectService): void {
if (!service.logger.isTestLogger || !service.logger.hasLevel(LogLevel.verbose)) return;
if (service.baseline !== noop) return; // Already patched
const projects = new Map<Project, ProjectData>();
const scriptInfos = new Map<ScriptInfo, ScriptInfoData>();
const documentPositionMappers = new Map<DocumentPositionMapper, DocumentPositionMapper>();
let nextDocumentPositionMapperId = 1;
const mapperToId = new Map<DocumentPositionMapper, number>();
const logger = service.logger as LoggerWithInMemoryLogs;
service.baseline = title => {
const currentMappers = new Set<DocumentPositionMapper>();
const projectLogs = baselineProjects(currentMappers);
const infoLogs = baselineScriptInfos(currentMappers);
currentMappers.delete(identitySourceMapConsumer);
const mapperLogs = baselineDocumentPositionMappers(currentMappers);
if (projectLogs?.length || infoLogs?.length || mapperLogs?.length) {
if (title) logger.log(title);
sendLogsToLogger("Projects::", projectLogs);
sendLogsToLogger("ScriptInfos::", infoLogs);
sendLogsToLogger("DocumentPositionMappers::", mapperLogs);
}
};
function sendLogsToLogger(title: string, logs: StateItemLog[] | undefined) {
if (!logs) return;
logger.log(title);
toSorted(logs, (a, b) => compareStringsCaseSensitive(a[0], b[0]))
.forEach(([title, propertyLogs]) => {
logger.log(title);
propertyLogs.forEach(p => isString(p) ? logger.log(p) : p.forEach(s => logger.log(s)));
});
logger.log("");
}
function baselineProjects(currentMappers: Set<DocumentPositionMapper>) {
const autoImportProviderProjects = [] as AutoImportProviderProject[];
const auxiliaryProjects = [] as AuxiliaryProject[];
const orphanConfiguredProjects = service.getOrphanConfiguredProjects(
/*toRetainConfiguredProjects*/ undefined,
/*openFilesWithRetainedConfiguredProject*/ undefined,
/*externalProjectsRetainingConfiguredProjects*/ undefined,
);
const noOpenRef = (project: Project) => isConfiguredProject(project) && (project.isClosed() || orphanConfiguredProjects.has(project));
return baselineState(
[service.externalProjects, service.configuredProjects, service.inferredProjects, autoImportProviderProjects, auxiliaryProjects],
projects,
(logs, project, data) => {
if (project.autoImportProviderHost) autoImportProviderProjects.push(project.autoImportProviderHost);
if (project.noDtsResolutionProject) auxiliaryProjects.push(project.noDtsResolutionProject);
let projectDiff = newOrDeleted(project, projects, data);
if (projectDiff !== Diff.Deleted) getSourceMapper(project)?.documentPositionMappers.forEach(mapper => currentMappers.add(mapper));
const projectPropertyLogs = [] as string[];
projectDiff = printProperty(PrintPropertyWhen.Always, data, "projectStateVersion", project.projectStateVersion, projectDiff, projectPropertyLogs);
projectDiff = printProperty(PrintPropertyWhen.Always, data, "projectProgramVersion", project.projectProgramVersion, projectDiff, projectPropertyLogs);
projectDiff = printProperty(PrintPropertyWhen.TruthyOrChangedOrNew, data, "dirty", project.dirty, projectDiff, projectPropertyLogs);
projectDiff = printProperty(PrintPropertyWhen.TruthyOrChangedOrNew, data, "initialLoadPending", project.initialLoadPending, projectDiff, projectPropertyLogs);
projectDiff = printProperty(PrintPropertyWhen.TruthyOrChangedOrNew, data, "isClosed", project.isClosed(), projectDiff, projectPropertyLogs);
projectDiff = printProperty(PrintPropertyWhen.TruthyOrChangedOrNew, data, "isOrphan", !isBackgroundProject(project) && project.isOrphan(), projectDiff, projectPropertyLogs);
projectDiff = printProperty(PrintPropertyWhen.TruthyOrChangedOrNew, data, "noOpenRef", noOpenRef(project), projectDiff, projectPropertyLogs);
projectDiff = printProperty(PrintPropertyWhen.TruthyOrChangedOrNew, data, "deferredClose", isConfiguredProject(project) && project.deferredClose, projectDiff, projectPropertyLogs);
projectDiff = printMapPropertyValue(
PrintPropertyWhen.Changed,
data?.documentPositionMappers,
"documentPositionMappers",
getSourceMapper(project)?.documentPositionMappers,
projectDiff,
projectPropertyLogs,
toStringDocumentPostionMapper,
);
projectDiff = printProperty(PrintPropertyWhen.DefinedOrChangedOrNew, data, "autoImportProviderHost", project.autoImportProviderHost, projectDiff, projectPropertyLogs, project.autoImportProviderHost ? project.autoImportProviderHost.projectName : project.autoImportProviderHost);
projectDiff = printProperty(PrintPropertyWhen.DefinedOrChangedOrNew, data, "noDtsResolutionProject", project.noDtsResolutionProject, projectDiff, projectPropertyLogs, project.noDtsResolutionProject ? project.noDtsResolutionProject.projectName : project.noDtsResolutionProject);
return printSetPropertyAndCreateStatementLog(
logs,
`${project.projectName} (${ProjectKind[project.projectKind]})`,
PrintPropertyWhen.DefinedOrChangedOrNew,
data?.originalConfiguredProjects,
"originalConfiguredProjects",
project.originalConfiguredProjects,
projectDiff,
projectPropertyLogs,
identity,
);
},
project => ({
projectStateVersion: project.projectStateVersion,
projectProgramVersion: project.projectProgramVersion,
dirty: project.dirty,
initialLoadPending: project.initialLoadPending,
isClosed: project.isClosed(),
isOrphan: !isBackgroundProject(project) && project.isOrphan(),
noOpenRef: noOpenRef(project),
deferredClose: isConfiguredProject(project) && project.deferredClose,
autoImportProviderHost: project.autoImportProviderHost,
noDtsResolutionProject: project.noDtsResolutionProject,
originalConfiguredProjects: project.originalConfiguredProjects && new Set(project.originalConfiguredProjects),
documentPositionMappers: new Map(getSourceMapper(project)?.documentPositionMappers),
}),
);
}
function baselineScriptInfos(currentMappers: Set<DocumentPositionMapper>) {
return baselineState(
[service.filenameToScriptInfo],
scriptInfos,
(logs, info, data) => {
let infoDiff = newOrDeleted(info, scriptInfos, data);
if (infoDiff !== Diff.Deleted && info.documentPositionMapper) currentMappers.add(info.documentPositionMapper);
const infoPropertyLogs = [] as string[];
const isOpen = info.isScriptOpen();
infoDiff = printProperty(PrintPropertyWhen.Changed, data, "open", isOpen, infoDiff, infoPropertyLogs);
infoDiff = printProperty(PrintPropertyWhen.Always, data, "version", info.textStorage.getVersion(), infoDiff, infoPropertyLogs);
infoDiff = printProperty(PrintPropertyWhen.TruthyOrChangedOrNew, data, "pendingReloadFromDisk", info.textStorage.pendingReloadFromDisk, infoDiff, infoPropertyLogs);
infoDiff = printProperty(PrintPropertyWhen.TruthyOrChangedOrNew, data, "deferredDelete", info.deferredDelete, infoDiff, infoPropertyLogs);
infoDiff = printScriptInfoSourceMapFilePath(data, info, infoDiff, infoPropertyLogs);
infoDiff = printProperty(PrintPropertyWhen.DefinedOrChangedOrNew, data, "declarationInfoPath", info.declarationInfoPath, infoDiff, infoPropertyLogs);
infoDiff = printSetPropertyValueWorker(PrintPropertyWhen.DefinedOrChangedOrNew, data?.sourceInfos, "sourceInfos", info.sourceInfos, infoDiff, infoPropertyLogs, identity);
infoDiff = printProperty(PrintPropertyWhen.DefinedOrChangedOrNew, data, "documentPositionMapper", info.documentPositionMapper, infoDiff, infoPropertyLogs, info.documentPositionMapper ? toStringDocumentPostionMapper(info.documentPositionMapper) : undefined);
let defaultProject: Project | undefined;
try {
if (isOpen) defaultProject = info.getDefaultProject();
}
catch (e) {
Debug.assert(e.message === "No Project.");
}
return printSetPropertyAndCreateStatementLog(
logs,
`${info.fileName}${info.isDynamic ? " (Dynamic)" : ""}${isOpen ? " (Open)" : ""}`,
PrintPropertyWhen.Always,
data?.containingProjects,
"containingProjects",
new Set(info.containingProjects),
infoDiff,
infoPropertyLogs,
project => `${project.projectName}${defaultProject === project ? " *default*" : ""}`,
);
},
info => ({
open: info.isScriptOpen(),
version: info.textStorage.getVersion(),
pendingReloadFromDisk: info.textStorage.pendingReloadFromDisk,
sourceMapFilePath: info.sourceMapFilePath && !isString(info.sourceMapFilePath) ?
{ original: info.sourceMapFilePath, sourceInfos: new Set(info.sourceMapFilePath.sourceInfos) } :
info.sourceMapFilePath,
declarationInfoPath: info.declarationInfoPath,
sourceInfos: info.sourceInfos && new Set(info.sourceInfos),
documentPositionMapper: info.documentPositionMapper,
containingProjects: new Set(info.containingProjects),
deferredDelete: info.deferredDelete,
}),
);
}
function baselineDocumentPositionMappers(currentMappers: Set<DocumentPositionMapper>) {
const result = baselineState(
[currentMappers],
documentPositionMappers,
(logs, mapper, data) => {
const mapperDiff = newOrDeleted(mapper, documentPositionMappers, data);
logs.push([
`${toStringDocumentPostionMapper(mapper)}${mapperDiff}`,
emptyArray,
]);
return mapperDiff;
},
identity,
);
mapperToId.forEach((_id, mapper) => {
if (!documentPositionMappers.has(mapper)) mapperToId.delete(mapper);
});
return result;
}
function baselineState<T, Data>(
currentCaches: (T[] | Map<any, T> | Set<T>)[],
dataMap: Map<T, Data>,
printWorker: (logs: StateItemLog[], current: T, data: Data | undefined) => Diff,
toData: (current: T) => Data,
) {
const logs = [] as StateItemLog[];
let hasChange = false;
const currentSet = new Set<T>();
currentCaches.forEach(currentCache => currentCache.forEach(printCurrent));
if (currentSet.size !== dataMap.size) {
for (const [key, data] of arrayFrom(dataMap.entries())) {
if (!currentSet.has(key)) {
dataMap.delete(key);
printWorker(logs, key, data);
}
}
hasChange = true;
}
return hasChange ? logs : undefined;
function printCurrent(current: T) {
if (!current) return;
currentSet.add(current);
if (printWorker(logs, current, dataMap.get(current))) {
hasChange = true;
dataMap.set(current, toData(current));
}
}
}
function newOrDeleted<T, Data>(current: T, dataMap: Map<T, Data>, data: Data | undefined) {
return !data ? Diff.New : !dataMap.has(current) ? Diff.Deleted : Diff.None;
}
enum PrintPropertyWhen {
Always,
TruthyOrChangedOrNew,
Changed,
DefinedOrChangedOrNew,
}
function printProperty<Data, Key extends keyof Data & string>(
printWhen: PrintPropertyWhen,
data: Data | undefined,
key: Key,
value: Data[Key],
dataDiff: Diff,
propertyLogs: StatePropertyLog[],
stringValue?: Data[Key] | string,
) {
return printPropertyWorker(
printWhen,
value,
!!data && data[key] !== value ? Diff.Change : Diff.None,
dataDiff,
propertyLogs,
`${key}: ${stringValue === undefined ? value : stringValue}`,
);
}
function printSetPropertyValueWorker<T>(
printWhen: PrintPropertyWhen.Always | PrintPropertyWhen.DefinedOrChangedOrNew,
dataValue: Set<T> | undefined,
propertyName: string,
propertyValue: Set<T> | undefined,
dataDiff: Diff,
propertyLogs: StatePropertyLog[],
toStringPropertyValue: (v: T) => string,
) {
return printMapOrSetPropertyValueWorker(
printWhen,
dataValue,
propertyName,
propertyValue,
dataDiff,
propertyLogs,
value => dataDiff !== Diff.New && !dataValue?.has(value) ? Diff.New : Diff.None,
toStringPropertyValue,
);
}
function printMapPropertyValue<T>(
printWhen: PrintPropertyWhen.Changed,
dataValue: Map<string, T> | undefined,
propertyName: string,
propertyValue: Map<string, T> | undefined,
dataDiff: Diff,
propertyLogs: StatePropertyLog[],
toStringPropertyValue: (v: T) => string,
) {
return printMapOrSetPropertyValueWorker(
printWhen,
dataValue,
propertyName,
propertyValue,
dataDiff,
propertyLogs,
(value, key) =>
dataDiff !== Diff.New && dataValue?.get(key) !== value ?
!dataValue?.has(key) ? Diff.New : Diff.Change : Diff.None,
(value, key) => `${key}: ${toStringPropertyValue(value)}`,
);
}
type MapOrSetPropertyKey<T> = T extends Map<infer U, any> ? U : T extends Set<infer U> ? U : never;
type MapOrSetPropertyValue<T> = T extends Map<any, infer U> ? U : T extends Set<infer U> ? U : never;
function printMapOrSetPropertyValueWorker<T extends Map<string, any> | Set<any>>(
printWhen: PrintPropertyWhen.Always | PrintPropertyWhen.DefinedOrChangedOrNew | PrintPropertyWhen.Changed,
dataValue: T | undefined,
propertyName: string,
propertyValue: T | undefined,
dataDiff: Diff,
propertyLogs: StatePropertyLog[],
getPropertyDiff: (value: MapOrSetPropertyValue<T>, key: MapOrSetPropertyKey<T>) => Diff,
toStringPropertyValue: (value: MapOrSetPropertyValue<T>, key: MapOrSetPropertyKey<T>) => string,
) {
// Print changes
const mapPropertyLogs = [] as string[];
let mapPropertyDiff = Diff.None;
propertyValue?.forEach((p, key) =>
mapPropertyDiff = printPropertyWorker(
PrintPropertyWhen.Always,
p,
getPropertyDiff(p, key),
mapPropertyDiff,
mapPropertyLogs,
` ${toStringPropertyValue(p, key)}`,
)
);
dataValue?.forEach((p, key) => {
if (propertyValue?.has(key)) return;
mapPropertyDiff = Diff.Change;
mapPropertyLogs.push(` ${toStringPropertyValue(p, key)}${Diff.Deleted}`);
});
dataDiff = printPropertyWorker(
printWhen,
propertyValue,
!!mapPropertyDiff ? Diff.Change : Diff.None,
dataDiff,
propertyLogs,
`${propertyName}: ${propertyValue?.size || 0}`,
);
if (printWhen !== PrintPropertyWhen.Changed || mapPropertyDiff) propertyLogs.push(mapPropertyLogs);
return dataDiff;
}
function printPropertyWorker(
printWhen: PrintPropertyWhen,
value: any,
propertyDiff: Diff,
dataDiff: Diff,
propertyLogs: StatePropertyLog[],
stringValue: string,
) {
const result = !dataDiff && propertyDiff ? Diff.Change : dataDiff;
switch (printWhen) {
case PrintPropertyWhen.Changed:
if (!propertyDiff) return result;
break;
case PrintPropertyWhen.TruthyOrChangedOrNew:
if (!value && !propertyDiff) return result;
break;
case PrintPropertyWhen.Always:
break;
case PrintPropertyWhen.DefinedOrChangedOrNew:
if (value === undefined && !propertyDiff) return result;
break;
default:
Debug.assertNever(printWhen);
}
propertyLogs.push(` ${stringValue}${propertyDiff}`);
return result;
}
function printSetPropertyAndCreateStatementLog<T>(
logs: StateItemLog[],
header: string,
printWhen: PrintPropertyWhen.Always | PrintPropertyWhen.DefinedOrChangedOrNew,
dataValue: Set<T> | undefined,
propertyName: string,
propertyValue: Set<T> | undefined,
dataDiff: Diff,
propertyLogs: StatePropertyLog[],
toStringPropertyValue: (v: T) => string,
) {
dataDiff = printSetPropertyValueWorker(
printWhen,
dataValue,
propertyName,
propertyValue,
dataDiff,
propertyLogs,
toStringPropertyValue,
);
logs.push([
`${header}${dataDiff}`,
propertyLogs,
]);
return dataDiff;
}
function printScriptInfoSourceMapFilePath(
data: ScriptInfoData | undefined,
info: ScriptInfo,
infoDiff: Diff,
infoPropertyLogs: StatePropertyLog[],
) {
return !info.sourceMapFilePath || isString(info.sourceMapFilePath) ?
printProperty(
PrintPropertyWhen.DefinedOrChangedOrNew,
data,
"sourceMapFilePath",
info.sourceMapFilePath,
infoDiff,
infoPropertyLogs,
) :
printSetPropertyValueWorker(
PrintPropertyWhen.DefinedOrChangedOrNew,
data?.sourceMapFilePath && !isString(data?.sourceMapFilePath) ? data.sourceMapFilePath.sourceInfos : undefined,
"sourceMapFilePath sourceInfos",
info.sourceMapFilePath.sourceInfos,
infoDiff,
infoPropertyLogs,
identity,
);
}
function toStringDocumentPostionMapper(documentPositionMapper: DocumentPositionMapper) {
if (documentPositionMapper === identitySourceMapConsumer) return "identitySourceMapConsumer";
let id = mapperToId.get(documentPositionMapper);
if (!id) mapperToId.set(documentPositionMapper, id = nextDocumentPositionMapperId++);
return `DocumentPositionMapper${id}`;
}
function getSourceMapper(project: Project) {
return project.projectKind !== ProjectKind.AutoImportProvider ?
project.getLanguageService(/*ensureSynchronized*/ false)?.getSourceMapper() :
undefined;
}
}