Implementation for invalidating source file containing possibly changed module resolution

This commit is contained in:
Sheetal Nandi 2017-08-05 05:01:33 -07:00
parent 8dc62484ec
commit 7474ba762c
11 changed files with 66 additions and 41 deletions

View File

@ -22,7 +22,7 @@ namespace ts {
/**
* This is the callback when file infos in the builder are updated
*/
onProgramUpdateGraph(program: Program): void;
onProgramUpdateGraph(program: Program, hasInvalidatedResolution: HasInvalidatedResolution): void;
getFilesAffectedBy(program: Program, path: Path): string[];
emitFile(program: Program, path: Path): EmitOutput;
emitChangedFiles(program: Program): EmitOutputDetailed[];
@ -84,7 +84,7 @@ namespace ts {
clear
};
function createProgramGraph(program: Program) {
function createProgramGraph(program: Program, hasInvalidatedResolution: HasInvalidatedResolution) {
const currentIsModuleEmit = program.getCompilerOptions().module !== ModuleKind.None;
if (isModuleEmit !== currentIsModuleEmit) {
isModuleEmit = currentIsModuleEmit;
@ -100,7 +100,7 @@ namespace ts {
// Remove existing file info
removeExistingFileInfo,
// We will update in place instead of deleting existing value and adding new one
(existingInfo, sourceFile) => updateExistingFileInfo(program, existingInfo, sourceFile)
(existingInfo, sourceFile) => updateExistingFileInfo(program, existingInfo, sourceFile, hasInvalidatedResolution)
);
}
@ -115,8 +115,8 @@ namespace ts {
emitHandler.removeScriptInfo(path);
}
function updateExistingFileInfo(program: Program, existingInfo: FileInfo, sourceFile: SourceFile) {
if (existingInfo.version !== sourceFile.version) {
function updateExistingFileInfo(program: Program, existingInfo: FileInfo, sourceFile: SourceFile, hasInvalidatedResolution: HasInvalidatedResolution) {
if (existingInfo.version !== sourceFile.version || hasInvalidatedResolution(sourceFile.path)) {
changedFilesSinceLastEmit.set(sourceFile.path, true);
existingInfo.version = sourceFile.version;
emitHandler.updateScriptInfo(program, sourceFile);
@ -125,13 +125,13 @@ namespace ts {
function ensureProgramGraph(program: Program) {
if (!emitHandler) {
createProgramGraph(program);
createProgramGraph(program, noop);
}
}
function onProgramUpdateGraph(program: Program) {
function onProgramUpdateGraph(program: Program, hasInvalidatedResolution: HasInvalidatedResolution) {
if (emitHandler) {
createProgramGraph(program);
createProgramGraph(program, hasInvalidatedResolution);
}
}
@ -298,8 +298,6 @@ namespace ts {
return result;
}
function noop() { }
function getNonModuleEmitHandler(): EmitHandler {
return {
addScriptInfo: noop,

View File

@ -1220,7 +1220,7 @@ namespace ts {
}
/** Does nothing. */
export function noop(): void {}
export function noop(): any {}
/** Throws an error because a function is not implemented. */
export function notImplemented(): never {

View File

@ -768,6 +768,8 @@ namespace ts {
return !host.directoryExists || host.directoryExists(directoryName);
}
export type HasInvalidatedResolution = (sourceFile: Path) => boolean;
/**
* @param {boolean} onlyRecordFailures - if true then function won't try to actually load files but instead record all attempts as failures. This flag is necessary
* in cases when we know upfront that all load attempts will fail (because containing folder does not exists) however we still need to record all failed lookup locations.

View File

@ -394,7 +394,7 @@ namespace ts {
}
export function isProgramUptoDate(program: Program, rootFileNames: string[], newOptions: CompilerOptions,
getSourceVersion: (path: Path) => string, fileExists: (fileName: string) => boolean): boolean {
getSourceVersion: (path: Path) => string, fileExists: (fileName: string) => boolean, hasInvalidatedResolution: HasInvalidatedResolution): boolean {
// If we haven't create a program yet, then it is not up-to-date
if (!program) {
return false;
@ -432,10 +432,9 @@ namespace ts {
return true;
function sourceFileUpToDate(sourceFile: SourceFile): boolean {
if (!sourceFile) {
return false;
}
return sourceFile.version === getSourceVersion(sourceFile.path);
return sourceFile &&
sourceFile.version === getSourceVersion(sourceFile.path) &&
!hasInvalidatedResolution(sourceFile.path);
}
}
@ -565,6 +564,7 @@ namespace ts {
let moduleResolutionCache: ModuleResolutionCache;
let resolveModuleNamesWorker: (moduleNames: string[], containingFile: string) => ResolvedModuleFull[];
const hasInvalidatedResolution = host.hasInvalidatedResolution || noop;
if (host.resolveModuleNames) {
resolveModuleNamesWorker = (moduleNames, containingFile) => host.resolveModuleNames(checkAllDefined(moduleNames), containingFile).map(resolved => {
// An older host may have omitted extension, in which case we should infer it from the file extension of resolvedFileName.
@ -803,7 +803,7 @@ namespace ts {
trace(host, Diagnostics.Module_0_was_resolved_as_locally_declared_ambient_module_in_file_1, moduleName, containingFile);
}
}
else {
else if (!hasInvalidatedResolution(oldProgramState.file.path)) {
resolvesToAmbientModuleInNonModifiedFile = moduleNameResolvesToAmbientModuleInNonModifiedFile(moduleName, oldProgramState);
}
@ -962,6 +962,13 @@ namespace ts {
// tentatively approve the file
modifiedSourceFiles.push({ oldFile: oldSourceFile, newFile: newSourceFile });
}
else if (hasInvalidatedResolution(oldSourceFile.path)) {
// 'module/types' references could have changed
oldProgram.structureIsReused = StructureIsReused.SafeModules;
// add file to the modified list so that we will resolve it later
modifiedSourceFiles.push({ oldFile: oldSourceFile, newFile: newSourceFile });
}
// if file has passed all checks it should be safe to reuse it
newSourceFiles.push(newSourceFile);

View File

@ -4,12 +4,18 @@
namespace ts {
export interface ResolutionCache {
setModuleResolutionHost(host: ModuleResolutionHost): void;
startRecordingFilesWithChangedResolutions(): void;
finishRecordingFilesWithChangedResolutions(): Path[];
resolveModuleNames(moduleNames: string[], containingFile: string, logChanges: boolean): ResolvedModuleFull[];
resolveTypeReferenceDirectives(typeDirectiveNames: string[], containingFile: string): ResolvedTypeReferenceDirective[];
invalidateResolutionOfDeletedFile(filePath: Path): void;
invalidateResolutionOfChangedFailedLookupLocation(failedLookupLocation: string): void;
createHasInvalidatedResolution(): HasInvalidatedResolution;
clear(): void;
}
@ -40,6 +46,7 @@ namespace ts {
let host: ModuleResolutionHost;
let filesWithChangedSetOfUnresolvedImports: Path[];
let filesWithInvalidatedResolutions: Map<true>;
const resolvedModuleNames = createMap<Map<ResolvedModuleWithFailedLookupLocations>>();
const resolvedTypeReferenceDirectives = createMap<Map<ResolvedTypeReferenceDirectiveWithFailedLookupLocations>>();
@ -55,6 +62,7 @@ namespace ts {
resolveTypeReferenceDirectives,
invalidateResolutionOfDeletedFile,
invalidateResolutionOfChangedFailedLookupLocation,
createHasInvalidatedResolution,
clear
};
@ -82,6 +90,12 @@ namespace ts {
return collected;
}
function createHasInvalidatedResolution(): HasInvalidatedResolution {
const collected = filesWithInvalidatedResolutions;
filesWithInvalidatedResolutions = undefined;
return path => collected && collected.has(path);
}
function resolveModuleName(moduleName: string, containingFile: string, compilerOptions: CompilerOptions, host: ModuleResolutionHost): ResolvedModuleWithFailedLookupLocations {
const primaryResult = ts.resolveModuleName(moduleName, containingFile, compilerOptions, host);
// return result immediately only if it is .ts, .tsx or .d.ts
@ -250,7 +264,7 @@ namespace ts {
cache: Map<Map<T>>,
getResult: (s: T) => R,
getResultFileName: (result: R) => string | undefined) {
cache.forEach((value, path) => {
cache.forEach((value, path: Path) => {
if (path === deletedFilePath) {
cache.delete(path);
value.forEach((resolution, name) => {
@ -264,6 +278,7 @@ namespace ts {
if (result) {
if (getResultFileName(result) === deletedFilePath) {
resolution.isInvalidated = true;
(filesWithInvalidatedResolutions || (filesWithInvalidatedResolutions = createMap<true>())).set(path, true);
}
}
}
@ -275,14 +290,13 @@ namespace ts {
function invalidateResolutionCacheOfChangedFailedLookupLocation<T extends NameResolutionWithFailedLookupLocations>(
failedLookupLocation: string,
cache: Map<Map<T>>) {
cache.forEach((value, _containingFilePath) => {
cache.forEach((value, containingFile: Path) => {
if (value) {
value.forEach((resolution, __name) => {
if (resolution && !resolution.isInvalidated && contains(resolution.failedLookupLocations, failedLookupLocation)) {
// TODO: mark the file as needing re-evaluation of module resolution instead of using it blindly.
// Note: Right now this invalidation path is not used at all as it doesnt matter as we are anyways clearing the program,
// which means all the resolutions will be discarded.
// Mark the file as needing re-evaluation of module resolution instead of using it blindly.
resolution.isInvalidated = true;
(filesWithInvalidatedResolutions || (filesWithInvalidatedResolutions = createMap<true>())).set(containingFile, true);
}
});
}

View File

@ -3979,6 +3979,7 @@ namespace ts {
resolveTypeReferenceDirectives?(typeReferenceDirectiveNames: string[], containingFile: string): ResolvedTypeReferenceDirective[];
getEnvironmentVariable?(name: string): string;
onReleaseOldSourceFile?(oldSourceFile: SourceFile, oldOptions: CompilerOptions): void;
hasInvalidatedResolution?: HasInvalidatedResolution;
}
/* @internal */

View File

@ -256,6 +256,7 @@ namespace ts {
const sourceFilesCache = createMap<HostFileInfo | string>(); // Cache that stores the source file and version info
let missingFilePathsRequestedForRelease: Path[]; // These paths are held temparirly so that we can remove the entry from source file cache if the file is not tracked by missing files
let hasInvalidatedResolution: HasInvalidatedResolution; // Passed along to see if source file has invalidated resolutions
watchingHost = watchingHost || createWatchingSystemHost(compilerOptions.pretty);
const { system, parseConfigFile, reportDiagnostic, reportWatchDiagnostic, beforeCompile, afterCompile } = watchingHost;
@ -292,7 +293,8 @@ namespace ts {
function synchronizeProgram() {
writeLog(`Synchronizing program`);
if (isProgramUptoDate(program, rootFileNames, compilerOptions, getSourceVersion, fileExists)) {
hasInvalidatedResolution = resolutionCache.createHasInvalidatedResolution();
if (isProgramUptoDate(program, rootFileNames, compilerOptions, getSourceVersion, fileExists, hasInvalidatedResolution)) {
return;
}
@ -306,7 +308,7 @@ namespace ts {
// Compile the program
program = createProgram(rootFileNames, compilerOptions, compilerHost, program);
builder.onProgramUpdateGraph(program);
builder.onProgramUpdateGraph(program, hasInvalidatedResolution);
// Update watches
missingFilesMap = updateMissingFilePathsWatch(program, missingFilesMap, watchMissingFilePath, closeMissingFilePathWatcher);
@ -351,7 +353,8 @@ namespace ts {
realpath,
resolveTypeReferenceDirectives: (typeDirectiveNames, containingFile) => resolutionCache.resolveTypeReferenceDirectives(typeDirectiveNames, containingFile),
resolveModuleNames: (moduleNames, containingFile) => resolutionCache.resolveModuleNames(moduleNames, containingFile, /*logChanges*/ false),
onReleaseOldSourceFile
onReleaseOldSourceFile,
hasInvalidatedResolution
};
}
@ -569,13 +572,7 @@ namespace ts {
writeLog(`Failed lookup location : ${failedLookupLocation} changed: ${FileWatcherEventKind[eventKind]}, fileName: ${fileName} containingFile: ${containingFile}, name: ${name}`);
const path = toPath(failedLookupLocation);
updateCachedSystem(failedLookupLocation, path);
// TODO: We need more intensive approach wherein we are able to comunicate to the program structure reuser that the even though the source file
// refering to this failed location hasnt changed, it needs to re-evaluate the module resolutions for the invalidated resolutions.
// For now just clear existing program, that should still reuse the source files but atleast compute the resolutions again.
// resolutionCache.invalidateResolutionOfChangedFailedLookupLocation(failedLookupLocation);
program = undefined;
resolutionCache.invalidateResolutionOfChangedFailedLookupLocation(failedLookupLocation);
scheduleProgramUpdate();
}

View File

@ -110,6 +110,10 @@ namespace ts.server {
readonly trace: (s: string) => void;
readonly realpath?: (path: string) => string;
/*@internal*/
hasInvalidatedResolution: HasInvalidatedResolution;
/**
* This is the host that is associated with the project. This is normally same as projectService's host
* except in Configured projects where it is CachedServerHost so that we can cache the results of the

View File

@ -241,12 +241,8 @@ namespace ts.server {
if (this.projectKind === ProjectKind.Configured) {
(this.lsHost.host as CachedServerHost).addOrDeleteFileOrFolder(toNormalizedPath(failedLookupLocation));
}
this.updateTypes();
// TODO: We need more intensive approach wherein we are able to comunicate to the program structure reuser that the even though the source file
// refering to this failed location hasnt changed, it needs to re-evaluate the module resolutions for the invalidated resolutions.
// For now just clear existing program, that should still reuse the source files but atleast compute the resolutions again.
// this.resolutionCache.invalidateResolutionOfChangedFailedLookupLocation(failedLookupLocation);
// this.markAsDirty();
this.resolutionCache.invalidateResolutionOfChangedFailedLookupLocation(failedLookupLocation);
this.markAsDirty();
this.projectService.delayUpdateProjectGraphAndInferredProjectsRefresh(this);
});
}
@ -605,6 +601,7 @@ namespace ts.server {
*/
updateGraph(): boolean {
this.resolutionCache.startRecordingFilesWithChangedResolutions();
this.lsHost.hasInvalidatedResolution = this.resolutionCache.createHasInvalidatedResolution();
let hasChanges = this.updateGraphWorker();
@ -640,7 +637,7 @@ namespace ts.server {
// otherwise tell it to drop its internal state
if (this.builder) {
if (this.languageServiceEnabled && this.compileOnSaveEnabled) {
this.builder.onProgramUpdateGraph(this.program);
this.builder.onProgramUpdateGraph(this.program, this.lsHost.hasInvalidatedResolution);
}
else {
this.builder.clear();

View File

@ -1115,8 +1115,11 @@ namespace ts {
// Get a fresh cache of the host information
let hostCache = new HostCache(host, getCanonicalFileName);
const rootFileNames = hostCache.getRootFileNames();
const hasInvalidatedResolution: HasInvalidatedResolution = host.hasInvalidatedResolution || noop;
// If the program is already up-to-date, we can reuse it
if (isProgramUptoDate(program, rootFileNames, hostCache.compilationSettings(), path => hostCache.getVersion(path), fileExists)) {
if (isProgramUptoDate(program, rootFileNames, hostCache.compilationSettings(), path => hostCache.getVersion(path), fileExists, hasInvalidatedResolution)) {
return;
}
@ -1155,7 +1158,8 @@ namespace ts {
getDirectories: path => {
return host.getDirectories ? host.getDirectories(path) : [];
},
onReleaseOldSourceFile
onReleaseOldSourceFile,
hasInvalidatedResolution
};
if (host.trace) {
compilerHost.trace = message => host.trace(message);

View File

@ -185,6 +185,7 @@ namespace ts {
*/
resolveModuleNames?(moduleNames: string[], containingFile: string): ResolvedModule[];
resolveTypeReferenceDirectives?(typeDirectiveNames: string[], containingFile: string): ResolvedTypeReferenceDirective[];
hasInvalidatedResolution?: HasInvalidatedResolution;
directoryExists?(directoryName: string): boolean;
/*