mirror of
https://github.com/microsoft/TypeScript.git
synced 2026-02-15 03:23:08 -06:00
Co-authored-by: Sheetal Nandi <shkamat@microsoft.com> Co-authored-by: Andrew Branch <andrew@wheream.io>
664 lines
31 KiB
TypeScript
664 lines
31 KiB
TypeScript
import * as ts from "./_namespaces/ts.js";
|
|
|
|
export function reportDocumentRegistryStats(documentRegistry: ts.DocumentRegistry): string[] {
|
|
const str: string[] = [];
|
|
documentRegistry.getBuckets().forEach((bucketEntries, key) => {
|
|
str.push(` Key:: ${key}`);
|
|
bucketEntries.forEach((entry, path) => {
|
|
if (ts.isDocumentRegistryEntry(entry)) {
|
|
str.push(` ${path}: ${ts.Debug.formatScriptKind(entry.sourceFile.scriptKind)} ${entry.languageServiceRefCount}`);
|
|
}
|
|
else {
|
|
entry.forEach((real, kind) => str.push(` ${path}: ${ts.Debug.formatScriptKind(kind)} ${real.languageServiceRefCount}`));
|
|
}
|
|
});
|
|
});
|
|
return str;
|
|
}
|
|
|
|
type DocumentRegistryExpectedStats = Map<ts.DocumentRegistryBucketKeyWithMode, Map<ts.Path, Map<ts.ScriptKind, number>>>;
|
|
function verifyDocumentRegistryStats(
|
|
documentRegistry: ts.DocumentRegistry,
|
|
stats: DocumentRegistryExpectedStats,
|
|
) {
|
|
documentRegistry.getBuckets().forEach((bucketEntries, key) => {
|
|
const statsByPath = stats.get(key);
|
|
bucketEntries.forEach((entry, path) => {
|
|
const expected = statsByPath?.get(path);
|
|
if (ts.isDocumentRegistryEntry(entry)) {
|
|
ts.Debug.assert(
|
|
expected?.size === 1 && expected.has(entry.sourceFile.scriptKind) && expected.get(entry.sourceFile.scriptKind) === entry.languageServiceRefCount,
|
|
`Document registry has unexpected language service ref count for ${key} ${path} ${ts.Debug.formatScriptKind(entry.sourceFile.scriptKind)} ${entry.languageServiceRefCount}`,
|
|
reportStats,
|
|
);
|
|
}
|
|
else {
|
|
entry.forEach((real, kind) =>
|
|
ts.Debug.assert(
|
|
real.languageServiceRefCount === expected?.get(kind),
|
|
`Document registry has unexpected language service ref count for ${key} ${path} ${ts.Debug.formatScriptKind(kind)} ${real.languageServiceRefCount}`,
|
|
reportStats,
|
|
)
|
|
);
|
|
expected?.forEach((value, kind) =>
|
|
ts.Debug.assert(
|
|
entry.has(kind),
|
|
`Document registry expected language service ref count for ${key} ${path} ${ts.Debug.formatScriptKind(kind)} ${value}`,
|
|
reportStats,
|
|
)
|
|
);
|
|
}
|
|
});
|
|
statsByPath?.forEach((_value, path) =>
|
|
ts.Debug.assert(
|
|
bucketEntries.has(path),
|
|
`Document registry does not contain entry for ${key}, ${path}`,
|
|
reportStats,
|
|
)
|
|
);
|
|
});
|
|
stats.forEach((_value, key) =>
|
|
ts.Debug.assert(
|
|
documentRegistry.getBuckets().has(key),
|
|
`Document registry does not contain entry for key: ${key}`,
|
|
reportStats,
|
|
)
|
|
);
|
|
|
|
function reportStats() {
|
|
const str: string[] = ["", "Actual::", ...reportDocumentRegistryStats(documentRegistry)];
|
|
str.push("Expected::");
|
|
stats?.forEach((statsByPath, key) => {
|
|
str.push(` Key:: ${key}`);
|
|
statsByPath.forEach((entry, path) => entry.forEach((refCount, kind) => str.push(` ${path}: ${ts.Debug.formatScriptKind(kind)} ${refCount}`)));
|
|
});
|
|
return str.join("\n");
|
|
}
|
|
}
|
|
|
|
function verifyDocumentRegistry(service: ts.server.ProjectService) {
|
|
const stats: DocumentRegistryExpectedStats = new Map();
|
|
const collectStats = (project: ts.server.Project) => {
|
|
if (project.autoImportProviderHost) collectStats(project.autoImportProviderHost);
|
|
if (project.noDtsResolutionProject) collectStats(project.noDtsResolutionProject);
|
|
const program = project.getCurrentProgram();
|
|
if (!program) return;
|
|
const key = service.documentRegistry.getKeyForCompilationSettings(program.getCompilerOptions());
|
|
program.getSourceFiles().forEach(f => {
|
|
const keyWithMode = service.documentRegistry.getDocumentRegistryBucketKeyWithMode(key, f.impliedNodeFormat);
|
|
let mapForKeyWithMode = stats.get(keyWithMode);
|
|
let result: Map<ts.ScriptKind, number> | undefined;
|
|
if (mapForKeyWithMode === undefined) {
|
|
stats.set(keyWithMode, mapForKeyWithMode = new Map());
|
|
mapForKeyWithMode.set(f.resolvedPath, result = new Map());
|
|
}
|
|
else {
|
|
result = mapForKeyWithMode.get(f.resolvedPath);
|
|
if (!result) mapForKeyWithMode.set(f.resolvedPath, result = new Map());
|
|
}
|
|
result.set(f.scriptKind, (result.get(f.scriptKind) || 0) + 1);
|
|
});
|
|
};
|
|
service.forEachProject(collectStats);
|
|
verifyDocumentRegistryStats(service.documentRegistry, stats);
|
|
}
|
|
interface ResolutionInfo {
|
|
cacheType: string;
|
|
fileName: string;
|
|
name: string;
|
|
mode: ts.ResolutionMode;
|
|
}
|
|
|
|
function getResolutionCacheDetails<File, T extends ts.ResolutionWithFailedLookupLocations>(
|
|
baseline: string[],
|
|
cacheType: string,
|
|
file: File,
|
|
forEach:
|
|
| ((
|
|
callback: (resolvedModule: T, moduleName: string, mode: ts.ResolutionMode) => void,
|
|
file: File,
|
|
) => void)
|
|
| undefined,
|
|
getResolvedFileName: (resolution: T) => string | undefined,
|
|
indent: string,
|
|
) {
|
|
let addedCacheType = false;
|
|
forEach?.((resolved, key, mode) => {
|
|
if (!addedCacheType) {
|
|
addedCacheType = true;
|
|
baseline.push(`${indent}${cacheType}:`);
|
|
}
|
|
baseline.push(`${indent} ${key}: ${mode ? ts.getNameOfCompilerOptionValue(mode, ts.moduleOptionDeclaration.type) + ":" : ""}${getResolvedFileName(resolved)}`);
|
|
}, file);
|
|
}
|
|
|
|
function getResolvedModuleFileName(r: ts.ResolvedModuleWithFailedLookupLocations) {
|
|
return r.resolvedModule?.resolvedFileName;
|
|
}
|
|
|
|
function getResolvedTypeRefFileName(r: ts.ResolvedTypeReferenceDirectiveWithFailedLookupLocations) {
|
|
return r.resolvedTypeReferenceDirective?.resolvedFileName;
|
|
}
|
|
|
|
function getLibResolutionCacheDetails(
|
|
baseline: string[],
|
|
cache: Map<string, ts.LibResolution> | undefined,
|
|
indent: string,
|
|
) {
|
|
let addedCacheType = false;
|
|
cache?.forEach((resolved, libFileName) => {
|
|
if (!addedCacheType) {
|
|
addedCacheType = true;
|
|
baseline.push(`${indent}Libs:`);
|
|
}
|
|
baseline.push(`${indent} ${libFileName}: Actual: ${resolved.actual} Resolution: ${getResolvedModuleFileName(resolved.resolution)}`);
|
|
});
|
|
}
|
|
|
|
function getProgramStructure(program: ts.Program | undefined) {
|
|
const baseline: string[] = [];
|
|
program?.getSourceFiles().slice().sort((f1, f2) => ts.comparePathsCaseSensitive(f1.path, f2.path)).forEach(f => {
|
|
baseline.push(` File: ${f.fileName} Path: ${f.path} ResolvedPath: ${f.resolvedPath} impliedNodeFormat: ${f.impliedNodeFormat}`);
|
|
baseline.push(f.text.split(/\r?\n/).map(l => l ? " " + l : "").join("\n"));
|
|
getResolutionCacheDetails(
|
|
baseline,
|
|
"Modules",
|
|
f,
|
|
program.forEachResolvedModule,
|
|
getResolvedModuleFileName,
|
|
" ",
|
|
);
|
|
getResolutionCacheDetails(
|
|
baseline,
|
|
"TypeRefs",
|
|
f,
|
|
program.forEachResolvedTypeReferenceDirective,
|
|
getResolvedTypeRefFileName,
|
|
" ",
|
|
);
|
|
});
|
|
getResolutionCacheDetails(
|
|
baseline,
|
|
"AutoTypeRefs",
|
|
/*file*/ undefined,
|
|
program?.getAutomaticTypeDirectiveResolutions().forEach,
|
|
getResolvedTypeRefFileName,
|
|
" ",
|
|
);
|
|
getLibResolutionCacheDetails(
|
|
baseline,
|
|
program?.resolvedLibReferences,
|
|
" ",
|
|
);
|
|
return baseline.join("\n");
|
|
}
|
|
|
|
export function verifyProgramStructure(expectedProgram: ts.Program, actualProgram: ts.Program, projectName: string): void {
|
|
const actual = getProgramStructure(actualProgram);
|
|
const expected = getProgramStructure(expectedProgram);
|
|
ts.Debug.assert(actual === expected, `Program verification:: ${projectName}`);
|
|
}
|
|
|
|
export function verifyResolutionCache(
|
|
actual: ts.ResolutionCache,
|
|
actualProgram: ts.Program,
|
|
resolutionHostCacheHost: ts.ResolutionCacheHost,
|
|
projectName: string,
|
|
): void {
|
|
const currentDirectory = resolutionHostCacheHost.getCurrentDirectory!();
|
|
const expected = ts.createResolutionCache(resolutionHostCacheHost, actual.rootDirForResolution, /*logChangesWhenResolvingModule*/ false);
|
|
expected.startCachingPerDirectoryResolution();
|
|
|
|
type ExpectedResolution = ts.CachedResolvedModuleWithFailedLookupLocations & ts.CachedResolvedTypeReferenceDirectiveWithFailedLookupLocations;
|
|
|
|
const expectedToResolution = new Map<ExpectedResolution, ts.ResolutionWithFailedLookupLocations>();
|
|
const resolutionToExpected = new Map<ts.ResolutionWithFailedLookupLocations, ExpectedResolution>();
|
|
const resolutionToRefs = new Map<ts.ResolutionWithFailedLookupLocations, ResolutionInfo[]>();
|
|
actual.resolvedModuleNames.forEach((resolutions, path) =>
|
|
collectResolutionToRefFromCache(
|
|
"Modules",
|
|
path,
|
|
resolutions,
|
|
getResolvedModuleFileName,
|
|
/*deferWatchingNonRelativeResolution*/ true,
|
|
expected.resolvedModuleNames,
|
|
)
|
|
);
|
|
actual.resolvedTypeReferenceDirectives.forEach((resolutions, path) =>
|
|
collectResolutionToRefFromCache(
|
|
"TypeRefs",
|
|
path,
|
|
resolutions,
|
|
getResolvedTypeRefFileName,
|
|
/*deferWatchingNonRelativeResolution*/ false,
|
|
expected.resolvedTypeReferenceDirectives,
|
|
)
|
|
);
|
|
actual.resolvedLibraries.forEach((resolved, libFileName) => {
|
|
const expectedResolution = collectResolution(
|
|
"Libs",
|
|
resolutionHostCacheHost.toPath(
|
|
ts.getInferredLibraryNameResolveFrom(actualProgram.getCompilerOptions(), currentDirectory, libFileName),
|
|
),
|
|
resolved,
|
|
getResolvedModuleFileName(resolved),
|
|
ts.getLibraryNameFromLibFileName(libFileName),
|
|
/*mode*/ undefined,
|
|
/*deferWatchingNonRelativeResolution*/ false,
|
|
);
|
|
expected.resolvedLibraries.set(libFileName, expectedResolution);
|
|
});
|
|
|
|
expected.finishCachingPerDirectoryResolution(actualProgram, /*oldProgram*/ undefined);
|
|
|
|
// Verify ref count
|
|
resolutionToRefs.forEach((info, resolution) => {
|
|
ts.Debug.assert(
|
|
resolution.files?.size === info.length,
|
|
`${projectName}:: Expected Resolution ref count ${info.length} but got ${resolution.files?.size}`,
|
|
() =>
|
|
`Expected from:: ${JSON.stringify(info, undefined, " ")}` +
|
|
`Actual from: ${resolution.files?.size}`,
|
|
);
|
|
verifySet(resolutionToExpected.get(resolution)!.files, resolution.files, `${projectName}:: Resolution files`);
|
|
});
|
|
verifyMapOfResolutionSet(expected.resolvedFileToResolution, actual.resolvedFileToResolution, `resolvedFileToResolution`);
|
|
verifyResolutionSet(expected.resolutionsWithFailedLookups, actual.resolutionsWithFailedLookups, `resolutionsWithFailedLookups`);
|
|
verifyResolutionSet(expected.resolutionsWithOnlyAffectingLocations, actual.resolutionsWithOnlyAffectingLocations, `resolutionsWithOnlyAffectingLocations`);
|
|
verifyDirectoryWatchesOfFailedLookups(expected.directoryWatchesOfFailedLookups, actual.directoryWatchesOfFailedLookups);
|
|
verifyFileWatchesOfAffectingLocations(expected.fileWatchesOfAffectingLocations, actual.fileWatchesOfAffectingLocations);
|
|
verifyPackageDirWatchers(expected.packageDirWatchers, actual.packageDirWatchers);
|
|
verifyDirPathToSymlinkPackageRefCount(expected.dirPathToSymlinkPackageRefCount, actual.dirPathToSymlinkPackageRefCount);
|
|
|
|
// Stop watching resolutions to verify everything gets closed.
|
|
expected.startCachingPerDirectoryResolution();
|
|
actual.resolvedModuleNames.forEach((_resolutions, path) => expected.removeResolutionsOfFile(path));
|
|
actual.resolvedTypeReferenceDirectives.forEach((_resolutions, path) => expected.removeResolutionsOfFile(path));
|
|
expected.finishCachingPerDirectoryResolution(/*newProgram*/ undefined, actualProgram);
|
|
|
|
resolutionToExpected.forEach(
|
|
expected => ts.Debug.assert(!expected.files?.size, `${projectName}:: Shouldnt ref to any files`),
|
|
);
|
|
ts.Debug.assert(expected.resolvedFileToResolution.size === 0, `${projectName}:: resolvedFileToResolution should be released`);
|
|
ts.Debug.assert(expected.resolutionsWithFailedLookups.size === 0, `${projectName}:: resolutionsWithFailedLookups should be released`);
|
|
ts.Debug.assert(expected.resolutionsWithOnlyAffectingLocations.size === 0, `${projectName}:: resolutionsWithOnlyAffectingLocations should be released`);
|
|
ts.Debug.assert(expected.directoryWatchesOfFailedLookups.size === 0, `${projectName}:: directoryWatchesOfFailedLookups should be released`);
|
|
ts.Debug.assert(expected.fileWatchesOfAffectingLocations.size === 0, `${projectName}:: fileWatchesOfAffectingLocations should be released`);
|
|
|
|
function collectResolutionToRefFromCache<T extends ts.ResolutionWithFailedLookupLocations>(
|
|
cacheType: string,
|
|
fileName: ts.Path,
|
|
cache: ts.ModeAwareCache<T> | undefined,
|
|
getResolvedFileName: (resolution: T) => string | undefined,
|
|
deferWatchingNonRelativeResolution: boolean,
|
|
storeExpcted: Map<ts.Path, ts.ModeAwareCache<ts.ResolutionWithFailedLookupLocations>>,
|
|
) {
|
|
ts.Debug.assert(
|
|
actualProgram.getSourceFileByPath(fileName) || ts.endsWith(fileName, ts.inferredTypesContainingFile),
|
|
`${projectName}:: ${cacheType} ${fileName} Expect cache for file in program or auto type ref`,
|
|
);
|
|
let expectedCache: ts.ModeAwareCache<ts.ResolutionWithFailedLookupLocations> | undefined;
|
|
cache?.forEach((resolved, name, mode) => {
|
|
const resolvedFileName = getResolvedFileName(resolved);
|
|
const expected = collectResolution(cacheType, fileName, resolved, resolvedFileName, name, mode, deferWatchingNonRelativeResolution);
|
|
if (!expectedCache) storeExpcted.set(fileName, expectedCache = ts.createModeAwareCache());
|
|
expectedCache.set(name, mode, expected);
|
|
});
|
|
}
|
|
|
|
function collectResolution<T extends ts.ResolutionWithFailedLookupLocations>(
|
|
cacheType: string,
|
|
fileName: ts.Path,
|
|
resolved: T,
|
|
resolvedFileName: string | undefined,
|
|
name: string,
|
|
mode: ts.ResolutionMode,
|
|
deferWatchingNonRelativeResolution: boolean,
|
|
): ExpectedResolution {
|
|
const existing = resolutionToRefs.get(resolved);
|
|
let expectedResolution: ExpectedResolution;
|
|
if (existing) {
|
|
existing.push({ cacheType, fileName, name, mode });
|
|
expectedResolution = resolutionToExpected.get(resolved)!;
|
|
}
|
|
else {
|
|
resolutionToRefs.set(resolved, [{ cacheType, fileName, name, mode }]);
|
|
expectedResolution = {
|
|
resolvedModule: (resolved as any).resolvedModule,
|
|
resolvedTypeReferenceDirective: (resolved as any).resolvedTypeReferenceDirective,
|
|
failedLookupLocations: resolved.failedLookupLocations,
|
|
affectingLocations: resolved.affectingLocations,
|
|
alternateResult: resolved.alternateResult,
|
|
};
|
|
expectedToResolution.set(expectedResolution, resolved);
|
|
resolutionToExpected.set(resolved, expectedResolution);
|
|
}
|
|
expected.watchFailedLookupLocationsOfExternalModuleResolutions(name, expectedResolution, fileName, () => ({ resolvedFileName }), deferWatchingNonRelativeResolution);
|
|
return expectedResolution;
|
|
}
|
|
|
|
function verifyMapOfResolutionSet(
|
|
expected: Map<ts.Path, Set<ts.ResolutionWithFailedLookupLocations>> | undefined,
|
|
actual: Map<ts.Path, Set<ts.ResolutionWithFailedLookupLocations>> | undefined,
|
|
caption: string,
|
|
) {
|
|
verifyMap(expected, actual, verifyResolutionSet, caption);
|
|
}
|
|
|
|
function verifyResolutionSet(
|
|
expected: Set<ts.ResolutionWithFailedLookupLocations> | undefined,
|
|
actual: Set<ts.ResolutionWithFailedLookupLocations> | undefined,
|
|
caption: string,
|
|
) {
|
|
expected?.forEach(resolution =>
|
|
ts.Debug.assert(
|
|
actual?.has(expectedToResolution.get(resolution as ExpectedResolution)!),
|
|
`${projectName}:: ${caption}:: Expected resolution should be present in actual resolutions`,
|
|
)
|
|
);
|
|
actual?.forEach(resolution =>
|
|
ts.Debug.assert(
|
|
expected?.has(resolutionToExpected.get(resolution)!),
|
|
`${projectName}:: ${caption}:: Actual resolution should be present in expected resolutions`,
|
|
)
|
|
);
|
|
}
|
|
|
|
function verifyDirectoryWatchesOfFailedLookups(expected: Map<string, ts.DirectoryWatchesOfFailedLookup>, actual: Map<string, ts.DirectoryWatchesOfFailedLookup>) {
|
|
verifyMap(expected, actual, verifyDirectoryWatchesOfFailedLookup, "directoryWatchesOfFailedLookups");
|
|
}
|
|
|
|
function verifyDirectoryWatchesOfFailedLookup(
|
|
expected: ts.DirectoryWatchesOfFailedLookup | undefined,
|
|
actual: ts.DirectoryWatchesOfFailedLookup | undefined,
|
|
caption: string,
|
|
) {
|
|
ts.Debug.assert(expected?.refCount === actual?.refCount, `${projectName}:: ${caption}:: refCount`);
|
|
ts.Debug.assert(!!expected?.refCount, `${projectName}:: ${caption}:: expected refCount to be non zero`);
|
|
ts.Debug.assert(expected?.nonRecursive === actual?.nonRecursive, `${projectName}:: ${caption}:: nonRecursive`);
|
|
}
|
|
|
|
function verifyFileWatchesOfAffectingLocations(
|
|
expected: Map<string, ts.FileWatcherOfAffectingLocation>,
|
|
actual: Map<string, ts.FileWatcherOfAffectingLocation>,
|
|
) {
|
|
verifyMap(expected, actual, verifyFileWatcherOfAffectingLocation, "fileWatchesOfAffectingLocations");
|
|
}
|
|
|
|
function verifyFileWatcherOfAffectingLocation(
|
|
expected: ts.FileWatcherOfAffectingLocation | undefined,
|
|
actual: ts.FileWatcherOfAffectingLocation | undefined,
|
|
caption: string,
|
|
) {
|
|
ts.Debug.assert(expected?.resolutions === actual?.resolutions, `${projectName}:: ${caption}:: resolutions`);
|
|
ts.Debug.assert(expected?.files === actual?.files, `${projectName}:: ${caption}:: files`);
|
|
verifySet(expected?.symlinks, actual?.symlinks, `${projectName}:: ${caption}:: symlinks`);
|
|
}
|
|
|
|
function verifyPackageDirWatchers(
|
|
expected: Map<ts.Path, ts.PackageDirWatcher>,
|
|
actual: Map<ts.Path, ts.PackageDirWatcher>,
|
|
) {
|
|
verifyMap(expected, actual, verifyPackageDirWatcher, "packageDirWatchers");
|
|
}
|
|
|
|
function verifyPackageDirWatcher(
|
|
expected: ts.PackageDirWatcher | undefined,
|
|
actual: ts.PackageDirWatcher | undefined,
|
|
caption: string,
|
|
) {
|
|
ts.Debug.assert(expected?.isSymlink === actual?.isSymlink, `${projectName}:: ${caption}:: isSymlink`);
|
|
verifyMap(expected?.dirPathToWatcher, actual?.dirPathToWatcher, verfiyDirPathToWatcherOfPackageDirWatcher, `${projectName}:: ${caption}:: dirPathToWatcher`);
|
|
}
|
|
|
|
function verfiyDirPathToWatcherOfPackageDirWatcher(
|
|
expected: ts.DirPathToWatcherOfPackageDirWatcher | undefined,
|
|
actual: ts.DirPathToWatcherOfPackageDirWatcher | undefined,
|
|
caption: string,
|
|
) {
|
|
ts.Debug.assert(expected?.refCount === actual?.refCount, `${projectName}:: ${caption}:: refCount`);
|
|
verifyDirectoryWatchesOfFailedLookup(expected?.watcher, actual?.watcher, `${projectName}:: ${caption}:: directoryWatchesOfFailedLookup`);
|
|
}
|
|
|
|
function verifyDirPathToSymlinkPackageRefCount(
|
|
expected: Map<ts.Path, number>,
|
|
actual: Map<ts.Path, number>,
|
|
) {
|
|
verifyMap(expected, actual, (expected, actual, caption) => {
|
|
ts.Debug.assert(expected === actual, `${projectName}:: ${caption}`);
|
|
}, "dirPathToSymlinkPackageRefCount");
|
|
}
|
|
}
|
|
|
|
function verifyMap<Key extends string, Expected, Actual>(
|
|
expected: Map<Key, Expected> | undefined,
|
|
actual: Map<Key, Actual> | undefined,
|
|
verifyValue: (expected: Expected | undefined, actual: Actual | undefined, key: string) => void,
|
|
caption: string,
|
|
) {
|
|
expected?.forEach((expected, path) => verifyValue(expected, actual?.get(path), `${caption}:: ${path}`));
|
|
actual?.forEach((actual, path) => verifyValue(expected?.get(path), actual, `${caption}:: ${path}`));
|
|
}
|
|
|
|
function verifySet(
|
|
expected: Set<string> | undefined,
|
|
actual: Set<string> | undefined,
|
|
caption: string,
|
|
) {
|
|
expected?.forEach(expected =>
|
|
ts.Debug.assert(
|
|
actual?.has(expected),
|
|
`${caption}:: Expected should be present in actual`,
|
|
)
|
|
);
|
|
actual?.forEach(actual =>
|
|
ts.Debug.assert(
|
|
expected?.has(actual),
|
|
`${caption}:: Actual should be present in expected`,
|
|
)
|
|
);
|
|
}
|
|
|
|
function verifyProgram(service: ts.server.ProjectService, project: ts.server.Project) {
|
|
if (service.serverMode === ts.LanguageServiceMode.Syntactic) return;
|
|
const options = project.getCompilerOptions();
|
|
const compilerHost = ts.createCompilerHostWorker(options, /*setParentNodes*/ undefined, service.host);
|
|
compilerHost.useSourceOfProjectReferenceRedirect = project.useSourceOfProjectReferenceRedirect?.bind(project);
|
|
compilerHost.getCurrentDirectory = project.getCurrentDirectory.bind(project);
|
|
const getDefaultLibLocation = compilerHost.getDefaultLibLocation!;
|
|
compilerHost.getDefaultLibLocation = () => ts.getNormalizedAbsolutePath(getDefaultLibLocation(), service.host.getCurrentDirectory());
|
|
compilerHost.getDefaultLibFileName = options => ts.combinePaths(compilerHost.getDefaultLibLocation!(), ts.getDefaultLibFileName(options));
|
|
compilerHost.trace = ts.noop; // We dont want to update host just because of trace
|
|
const readFile = compilerHost.readFile;
|
|
compilerHost.readFile = fileName => {
|
|
const path = project.toPath(fileName);
|
|
const info = project.projectService.filenameToScriptInfo.get(path);
|
|
if (info?.isDynamicOrHasMixedContent()) {
|
|
return ts.getSnapshotText(info.getSnapshot());
|
|
}
|
|
if (!ts.isAnySupportedFileExtension(path)) {
|
|
// Some external file
|
|
const snapshot = project.getScriptSnapshot(path);
|
|
return snapshot ? ts.getSnapshotText(snapshot) : undefined;
|
|
}
|
|
if (project.fileIsOpen(path)) {
|
|
return ts.getSnapshotText(info!.getSnapshot());
|
|
}
|
|
// Read only rooted disk paths from host similar to ProjectService
|
|
if (!ts.isRootedDiskPath(fileName) || !compilerHost.fileExists(fileName)) return undefined;
|
|
if (ts.hasTSFileExtension(fileName)) return readFile(fileName);
|
|
let text: string | undefined | false;
|
|
let fileSize: number;
|
|
if (service.host.getFileSize) fileSize = service.host.getFileSize(fileName);
|
|
else {
|
|
text = readFile(fileName);
|
|
fileSize = text?.length || 0;
|
|
text = text !== undefined ? text : false;
|
|
}
|
|
// Large js files like project service
|
|
if (fileSize > ts.server.maxFileSize) return "";
|
|
return text !== undefined ? text || undefined : readFile(fileName);
|
|
};
|
|
const getSourceFile = compilerHost.getSourceFile;
|
|
compilerHost.getSourceFile = (fileName, languageVersionOrOptions, onError, shouldCreateNewSourceFile) => {
|
|
const projectScriptKind = project.getScriptKind(fileName);
|
|
const scriptKind = ts.ensureScriptKind(fileName, /*scriptKind*/ undefined);
|
|
if (scriptKind === projectScriptKind) return getSourceFile(fileName, languageVersionOrOptions, onError, shouldCreateNewSourceFile);
|
|
|
|
let text: string | undefined;
|
|
try {
|
|
text = compilerHost.readFile(fileName);
|
|
}
|
|
catch (e) {
|
|
onError?.(e.message);
|
|
text = "";
|
|
}
|
|
return text !== undefined ?
|
|
ts.createSourceFile(fileName, text, languageVersionOrOptions, /*setParentNodes*/ undefined, projectScriptKind) :
|
|
undefined;
|
|
};
|
|
const resolutionHostCacheHost: ts.ResolutionCacheHost = {
|
|
...compilerHost,
|
|
|
|
getCompilerHost: () => compilerHost,
|
|
toPath: project.toPath.bind(project),
|
|
getCompilationSettings: project.getCompilationSettings.bind(project),
|
|
projectName: project.projectName,
|
|
getGlobalTypingsCacheLocation: project.getGlobalTypingsCacheLocation.bind(project),
|
|
globalCacheResolutionModuleName: project.globalCacheResolutionModuleName.bind(project),
|
|
fileIsOpen: project.fileIsOpen.bind(project),
|
|
getCurrentProgram: () => project.getCurrentProgram(),
|
|
|
|
preferNonRecursiveWatch: project.preferNonRecursiveWatch,
|
|
watchDirectoryOfFailedLookupLocation: ts.returnNoopFileWatcher,
|
|
watchAffectingFileLocation: ts.returnNoopFileWatcher,
|
|
onInvalidatedResolution: ts.noop,
|
|
watchTypeRootsDirectory: ts.returnNoopFileWatcher,
|
|
onChangedAutomaticTypeDirectiveNames: ts.noop,
|
|
scheduleInvalidateResolutionsOfFailedLookupLocations: ts.noop,
|
|
getCachedDirectoryStructureHost: ts.returnUndefined,
|
|
writeLog: ts.noop,
|
|
};
|
|
const moduleResolutionCache = ts.createModuleResolutionCache(compilerHost.getCurrentDirectory(), compilerHost.getCanonicalFileName, project.getCompilerOptions());
|
|
compilerHost.resolveModuleNameLiterals = (moduleNames, containingFile, redirectedReference, options, containingSourceFile) =>
|
|
ts.loadWithModeAwareCache(
|
|
moduleNames,
|
|
containingFile,
|
|
redirectedReference,
|
|
options,
|
|
containingSourceFile,
|
|
compilerHost,
|
|
moduleResolutionCache,
|
|
(containingFile, redirectedReference, options) =>
|
|
ts.createModuleResolutionLoaderUsingGlobalCache(
|
|
containingFile,
|
|
redirectedReference,
|
|
options,
|
|
resolutionHostCacheHost,
|
|
moduleResolutionCache,
|
|
),
|
|
);
|
|
compilerHost.getGlobalTypingsCacheLocation = resolutionHostCacheHost.getGlobalTypingsCacheLocation;
|
|
verifyProgramStructure(
|
|
ts.createProgram({
|
|
rootNames: project.getScriptFileNames(),
|
|
options: project.getCompilationSettings(),
|
|
projectReferences: project.getProjectReferences(),
|
|
host: compilerHost,
|
|
}),
|
|
project.getCurrentProgram()!,
|
|
project.projectName,
|
|
);
|
|
verifyResolutionCache(project.resolutionCache, project.getCurrentProgram()!, resolutionHostCacheHost, project.projectName);
|
|
}
|
|
|
|
interface ResolveSingleModuleNameWithoutWatchingData {
|
|
resolutionToData: Map<ts.ResolutionWithFailedLookupLocations, Pick<ts.ResolvedModuleWithFailedLookupLocations, "failedLookupLocations" | "affectingLocations" | "resolutionDiagnostics">>;
|
|
packageJsonMap: Map<ts.Path, ts.PackageJsonInfoCacheEntry> | undefined;
|
|
}
|
|
|
|
function beforeResolveSingleModuleNameWithoutWatching(
|
|
moduleResolutionCache: ts.ModuleResolutionCache,
|
|
): ResolveSingleModuleNameWithoutWatchingData {
|
|
const resolutionToData: ResolveSingleModuleNameWithoutWatchingData["resolutionToData"] = new Map();
|
|
// Currently it doesnt matter if moduleResolutionCache itself changes or not so just verify resolutions:
|
|
moduleResolutionCache.directoryToModuleNameMap.getOwnMap().forEach(cache => {
|
|
cache.forEach(resolution => {
|
|
if (resolutionToData.has(resolution)) return;
|
|
resolutionToData.set(resolution, {
|
|
failedLookupLocations: resolution.failedLookupLocations?.slice(),
|
|
affectingLocations: resolution.affectingLocations?.slice(),
|
|
resolutionDiagnostics: resolution.resolutionDiagnostics?.slice(),
|
|
});
|
|
});
|
|
});
|
|
|
|
// We also care about package json info cache
|
|
const packageJsonMap = moduleResolutionCache.getPackageJsonInfoCache().getInternalMap();
|
|
return {
|
|
resolutionToData,
|
|
packageJsonMap: packageJsonMap && new Map(packageJsonMap),
|
|
};
|
|
}
|
|
|
|
function afterResolveSingleModuleNameWithoutWatching(
|
|
moduleResolutionCache: ts.ModuleResolutionCache,
|
|
moduleName: string,
|
|
containingFile: string,
|
|
result: ts.ResolvedModuleWithFailedLookupLocations,
|
|
data: ResolveSingleModuleNameWithoutWatchingData,
|
|
) {
|
|
const existing = data.resolutionToData.get(result);
|
|
if (existing) {
|
|
verifyArrayLength(existing.failedLookupLocations, result.failedLookupLocations, "failedLookupLocations");
|
|
verifyArrayLength(existing.affectingLocations, result.affectingLocations, "affectingLocations");
|
|
verifyArrayLength(existing.resolutionDiagnostics, result.resolutionDiagnostics, "resolutionDiagnostics");
|
|
}
|
|
|
|
verifyMap(
|
|
data.packageJsonMap,
|
|
moduleResolutionCache.getPackageJsonInfoCache().getInternalMap(),
|
|
(expected, actual, caption) => ts.Debug.assert(expected === actual, caption),
|
|
`Expected packageJsonInfo to not change: ${moduleName} ${containingFile}`,
|
|
);
|
|
|
|
function verifyArrayLength<T>(expected: T[] | undefined, actual: T[] | undefined, caption: string) {
|
|
ts.Debug.assert(
|
|
expected?.length === actual?.length,
|
|
`Expected ${caption} to not change: ${moduleName} ${containingFile}`,
|
|
() =>
|
|
`Expected: ${JSON.stringify(expected, undefined, " ")}` +
|
|
`Actual: ${JSON.stringify(actual, undefined, " ")}`,
|
|
);
|
|
}
|
|
}
|
|
|
|
function onProjectCreation(project: ts.server.Project) {
|
|
if (project.projectKind !== ts.server.ProjectKind.Auxiliary) return;
|
|
|
|
(project as ts.ResolutionCacheHost).beforeResolveSingleModuleNameWithoutWatching = beforeResolveSingleModuleNameWithoutWatching;
|
|
(project as ts.ResolutionCacheHost).afterResolveSingleModuleNameWithoutWatching = afterResolveSingleModuleNameWithoutWatching;
|
|
}
|
|
|
|
export interface IncrementalVerifierCallbacks {
|
|
beforeVerification?(): any;
|
|
afterVerification?(dataFromBefore: any): void;
|
|
}
|
|
|
|
export function incrementalVerifier(service: ts.server.ProjectService): void {
|
|
service.verifyDocumentRegistry = withIncrementalVerifierCallbacks(service, verifyDocumentRegistry);
|
|
service.verifyProgram = withIncrementalVerifierCallbacks(service, verifyProgram);
|
|
service.onProjectCreation = onProjectCreation;
|
|
}
|
|
|
|
function withIncrementalVerifierCallbacks(
|
|
service: ts.server.ProjectService,
|
|
callback: (service: ts.server.ProjectService, ...args: any[]) => void,
|
|
): (...args: any[]) => void {
|
|
return (...args: any[]) => {
|
|
const data = (service.host as IncrementalVerifierCallbacks).beforeVerification?.();
|
|
callback(service, ...args);
|
|
(service.host as IncrementalVerifierCallbacks).afterVerification?.(data);
|
|
};
|
|
}
|