Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Use changeCompilerHostLikeToUseCache in synchronizeHostData #48980

Merged
merged 6 commits into from
May 19, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 9 additions & 6 deletions src/compiler/program.ts
Original file line number Diff line number Diff line change
Expand Up @@ -282,7 +282,7 @@ namespace ts {
}

// directoryExists
if (originalDirectoryExists && originalCreateDirectory) {
if (originalDirectoryExists) {
host.directoryExists = directory => {
const key = toPath(directory);
const value = directoryExistsCache.get(key);
Expand All @@ -291,11 +291,14 @@ namespace ts {
directoryExistsCache.set(key, !!newValue);
return newValue;
};
host.createDirectory = directory => {
const key = toPath(directory);
directoryExistsCache.delete(key);
originalCreateDirectory.call(host, directory);
};

if (originalCreateDirectory) {
host.createDirectory = directory => {
const key = toPath(directory);
directoryExistsCache.delete(key);
originalCreateDirectory.call(host, directory);
};
}
}

return {
Expand Down
196 changes: 53 additions & 143 deletions src/services/services.ts
Original file line number Diff line number Diff line change
Expand Up @@ -926,14 +926,6 @@ namespace ts {

/// Language Service

// Information about a specific host file.
interface HostFileInformation {
hostFileName: string;
version: string;
scriptSnapshot: IScriptSnapshot;
scriptKind: ScriptKind;
}

/* @internal */
export interface DisplayPartsSymbolWriter extends EmitTextWriter {
displayParts(): SymbolDisplayPart[];
Expand Down Expand Up @@ -987,82 +979,6 @@ namespace ts {
return codefix.getSupportedErrorCodes();
}

// Either it will be file name if host doesnt have file or it will be the host's file information
type CachedHostFileInformation = HostFileInformation | string;

// Cache host information about script Should be refreshed
// at each language service public entry point, since we don't know when
// the set of scripts handled by the host changes.
class HostCache {
private fileNameToEntry: ESMap<Path, CachedHostFileInformation>;
private currentDirectory: string;

constructor(private host: LanguageServiceHost, getCanonicalFileName: GetCanonicalFileName) {
// script id => script index
this.currentDirectory = host.getCurrentDirectory();
this.fileNameToEntry = new Map();

// Initialize the list with the root file names
const rootFileNames = host.getScriptFileNames();
tracing?.push(tracing.Phase.Session, "initializeHostCache", { count: rootFileNames.length });
for (const fileName of rootFileNames) {
this.createEntry(fileName, toPath(fileName, this.currentDirectory, getCanonicalFileName));
}
tracing?.pop();
}

private createEntry(fileName: string, path: Path) {
let entry: CachedHostFileInformation;
const scriptSnapshot = this.host.getScriptSnapshot(fileName);
if (scriptSnapshot) {
entry = {
hostFileName: fileName,
version: this.host.getScriptVersion(fileName),
scriptSnapshot,
scriptKind: getScriptKind(fileName, this.host)
};
}
else {
entry = fileName;
}

this.fileNameToEntry.set(path, entry);
return entry;
}

public getEntryByPath(path: Path): CachedHostFileInformation | undefined {
return this.fileNameToEntry.get(path);
}

public getHostFileInformation(path: Path): HostFileInformation | undefined {
const entry = this.fileNameToEntry.get(path);
return !isString(entry) ? entry : undefined;
}

public getOrCreateEntryByPath(fileName: string, path: Path): HostFileInformation {
const info = this.getEntryByPath(path) || this.createEntry(fileName, path);
return isString(info) ? undefined! : info; // TODO: GH#18217
}

public getRootFileNames(): string[] {
const names: string[] = [];
this.fileNameToEntry.forEach(entry => {
if (isString(entry)) {
names.push(entry);
}
else {
names.push(entry.hostFileName);
}
});
return names;
}

public getScriptSnapshot(path: Path): IScriptSnapshot {
const file = this.getHostFileInformation(path);
return (file && file.scriptSnapshot)!; // TODO: GH#18217
}
}

class SyntaxTreeCache {
// For our syntactic only features, we also keep a cache of the syntax tree for the
// currently edited file.
Expand Down Expand Up @@ -1366,37 +1282,17 @@ namespace ts {
lastTypesRootVersion = typeRootsVersion;
}

const rootFileNames = host.getScriptFileNames();

// Get a fresh cache of the host information
let hostCache: HostCache | undefined = new HostCache(host, getCanonicalFileName);
const rootFileNames = hostCache.getRootFileNames();
const newSettings = host.getCompilationSettings() || getDefaultCompilerOptions();
const hasInvalidatedResolution: HasInvalidatedResolution = host.hasInvalidatedResolution || returnFalse;
const hasChangedAutomaticTypeDirectiveNames = maybeBind(host, host.hasChangedAutomaticTypeDirectiveNames);
const projectReferences = host.getProjectReferences?.();
let parsedCommandLines: ESMap<Path, ParsedCommandLine | false> | undefined;
const parseConfigHost: ParseConfigFileHost = {
useCaseSensitiveFileNames,
fileExists,
readFile,
readDirectory,
trace: maybeBind(host, host.trace),
getCurrentDirectory: () => currentDirectory,
onUnRecoverableConfigFileDiagnostic: noop,
};

// If the program is already up-to-date, we can reuse it
if (isProgramUptoDate(program, rootFileNames, newSettings, (_path, fileName) => host.getScriptVersion(fileName), fileExists, hasInvalidatedResolution, hasChangedAutomaticTypeDirectiveNames, getParsedCommandLine, projectReferences)) {
return;
}

// IMPORTANT - It is critical from this moment onward that we do not check
// cancellation tokens. We are about to mutate source files from a previous program
// instance. If we cancel midway through, we may end up in an inconsistent state where
// the program points to old source files that have been invalidated because of
// incremental parsing.

// Now create a new compiler
const compilerHost: CompilerHost = {
let compilerHost: CompilerHost | undefined = {
getSourceFile: getOrCreateSourceFile,
getSourceFileByPath: getOrCreateSourceFileByPath,
getCancellationToken: () => cancellationToken,
Expand All @@ -1406,8 +1302,8 @@ namespace ts {
getDefaultLibFileName: options => host.getDefaultLibFileName(options),
writeFile: noop,
getCurrentDirectory: () => currentDirectory,
fileExists,
readFile,
fileExists: fileName => host.fileExists(fileName),
readFile: fileName => host.readFile && host.readFile(fileName),
getSymlinkCache: maybeBind(host, host.getSymlinkCache),
realpath: maybeBind(host, host.realpath),
directoryExists: directoryName => {
Expand All @@ -1416,20 +1312,54 @@ namespace ts {
getDirectories: path => {
return host.getDirectories ? host.getDirectories(path) : [];
},
readDirectory,
readDirectory: (path: string, extensions?: readonly string[], exclude?: readonly string[], include?: readonly string[], depth?: number) => {
Debug.checkDefined(host.readDirectory, "'LanguageServiceHost.readDirectory' must be implemented to correctly process 'projectReferences'");
return host.readDirectory!(path, extensions, exclude, include, depth);
},
onReleaseOldSourceFile,
onReleaseParsedCommandLine,
hasInvalidatedResolution,
hasChangedAutomaticTypeDirectiveNames,
trace: parseConfigHost.trace,
trace: maybeBind(host, host.trace),
resolveModuleNames: maybeBind(host, host.resolveModuleNames),
getModuleResolutionCache: maybeBind(host, host.getModuleResolutionCache),
resolveTypeReferenceDirectives: maybeBind(host, host.resolveTypeReferenceDirectives),
useSourceOfProjectReferenceRedirect: maybeBind(host, host.useSourceOfProjectReferenceRedirect),
getParsedCommandLine,
};

const originalGetSourceFile = compilerHost.getSourceFile;

const { getSourceFileWithCache } = changeCompilerHostLikeToUseCache(
compilerHost,
fileName => toPath(fileName, currentDirectory, getCanonicalFileName),
(...args) => originalGetSourceFile.call(compilerHost, ...args)
);
compilerHost.getSourceFile = getSourceFileWithCache!;

host.setCompilerHost?.(compilerHost);

const parseConfigHost: ParseConfigFileHost = {
useCaseSensitiveFileNames,
fileExists: fileName => compilerHost!.fileExists(fileName),
readFile: fileName => compilerHost!.readFile(fileName),
readDirectory: (...args) => compilerHost!.readDirectory!(...args),
trace: compilerHost.trace,
getCurrentDirectory: compilerHost.getCurrentDirectory,
onUnRecoverableConfigFileDiagnostic: noop,
};

// If the program is already up-to-date, we can reuse it
if (isProgramUptoDate(program, rootFileNames, newSettings, (_path, fileName) => host.getScriptVersion(fileName), fileName => compilerHost!.fileExists(fileName), hasInvalidatedResolution, hasChangedAutomaticTypeDirectiveNames, getParsedCommandLine, projectReferences)) {
return;
}

// IMPORTANT - It is critical from this moment onward that we do not check
// cancellation tokens. We are about to mutate source files from a previous program
// instance. If we cancel midway through, we may end up in an inconsistent state where
// the program points to old source files that have been invalidated because of
// incremental parsing.

const documentRegistryBucketKey = documentRegistry.getKeyForCompilationSettings(newSettings);
const options: CreateProgramOptions = {
rootNames: rootFileNames,
Expand All @@ -1440,9 +1370,9 @@ namespace ts {
};
program = createProgram(options);

// hostCache is captured in the closure for 'getOrCreateSourceFile' but it should not be used past this point.
// It needs to be cleared to allow all collected snapshots to be released
hostCache = undefined;
// 'getOrCreateSourceFile' depends on caching but should be used past this point.
// After this point, the cache needs to be cleared to allow all collected snapshots to be released
compilerHost = undefined;
parsedCommandLines = undefined;

// We reset this cache on structure invalidation so we don't hold on to outdated files for long; however we can't use the `compilerHost` above,
Expand Down Expand Up @@ -1491,29 +1421,6 @@ namespace ts {
}
}

function fileExists(fileName: string): boolean {
const path = toPath(fileName, currentDirectory, getCanonicalFileName);
const entry = hostCache && hostCache.getEntryByPath(path);
return entry ?
!isString(entry) :
(!!host.fileExists && host.fileExists(fileName));
}

function readFile(fileName: string) {
// stub missing host functionality
const path = toPath(fileName, currentDirectory, getCanonicalFileName);
const entry = hostCache && hostCache.getEntryByPath(path);
if (entry) {
return isString(entry) ? undefined : getSnapshotText(entry.scriptSnapshot);
}
return host.readFile && host.readFile(fileName);
}

function readDirectory(path: string, extensions?: readonly string[], exclude?: readonly string[], include?: readonly string[], depth?: number) {
Debug.checkDefined(host.readDirectory, "'LanguageServiceHost.readDirectory' must be implemented to correctly process 'projectReferences'");
return host.readDirectory!(path, extensions, exclude, include, depth);
}

// Release any files we have acquired in the old program but are
// not part of the new program.
function onReleaseOldSourceFile(oldSourceFile: SourceFile, oldOptions: CompilerOptions) {
Expand All @@ -1526,15 +1433,18 @@ namespace ts {
}

function getOrCreateSourceFileByPath(fileName: string, path: Path, _languageVersion: ScriptTarget, _onError?: (message: string) => void, shouldCreateNewSourceFile?: boolean): SourceFile | undefined {
Debug.assert(hostCache !== undefined, "getOrCreateSourceFileByPath called after typical CompilerHost lifetime, check the callstack something with a reference to an old host.");
Debug.assert(compilerHost, "getOrCreateSourceFileByPath called after typical CompilerHost lifetime, check the callstack something with a reference to an old host.");
// The program is asking for this file, check first if the host can locate it.
// If the host can not locate the file, then it does not exist. return undefined
// to the program to allow reporting of errors for missing files.
const hostFileInformation = hostCache && hostCache.getOrCreateEntryByPath(fileName, path);
if (!hostFileInformation) {
const scriptSnapshot = host.getScriptSnapshot(fileName);
if (!scriptSnapshot) {
return undefined;
}

const scriptKind = getScriptKind(fileName, host);
const scriptVersion = host.getScriptVersion(fileName);

// Check if the language version has changed since we last created a program; if they are the same,
// it is safe to reuse the sourceFiles; if not, then the shape of the AST can change, and the oldSourceFile
// can not be reused. we have to dump all syntax trees and create new ones.
Expand Down Expand Up @@ -1567,8 +1477,8 @@ namespace ts {
// We do not support the scenario where a host can modify a registered
// file's script kind, i.e. in one project some file is treated as ".ts"
// and in another as ".js"
if (hostFileInformation.scriptKind === oldSourceFile.scriptKind) {
return documentRegistry.updateDocumentWithKey(fileName, path, host, documentRegistryBucketKey, hostFileInformation.scriptSnapshot, hostFileInformation.version, hostFileInformation.scriptKind);
if (scriptKind === oldSourceFile.scriptKind) {
return documentRegistry.updateDocumentWithKey(fileName, path, host, documentRegistryBucketKey, scriptSnapshot, scriptVersion, scriptKind);
}
else {
// Release old source file and fall through to aquire new file with new script kind
Expand All @@ -1580,7 +1490,7 @@ namespace ts {
}

// Could not find this file in the old program, create a new SourceFile for it.
return documentRegistry.acquireDocumentWithKey(fileName, path, host, documentRegistryBucketKey, hostFileInformation.scriptSnapshot, hostFileInformation.version, hostFileInformation.scriptKind);
return documentRegistry.acquireDocumentWithKey(fileName, path, host, documentRegistryBucketKey, scriptSnapshot, scriptVersion, scriptKind);
}
}

Expand Down