diff --git a/src/compiler/program.ts b/src/compiler/program.ts index 18f730cafea1d..ac57b9bac7239 100644 --- a/src/compiler/program.ts +++ b/src/compiler/program.ts @@ -554,11 +554,11 @@ namespace ts { getSourceVersion: (path: Path, fileName: string) => string | undefined, fileExists: (fileName: string) => boolean, hasInvalidatedResolution: HasInvalidatedResolution, - hasChangedAutomaticTypeDirectiveNames: boolean, + hasChangedAutomaticTypeDirectiveNames: HasChangedAutomaticTypeDirectiveNames | undefined, projectReferences: readonly ProjectReference[] | undefined ): boolean { // If we haven't created a program yet or have changed automatic type directives, then it is not up-to-date - if (!program || hasChangedAutomaticTypeDirectiveNames) { + if (!program || hasChangedAutomaticTypeDirectiveNames?.()) { return false; } @@ -1424,7 +1424,7 @@ namespace ts { return oldProgram.structureIsReused; } - if (host.hasChangedAutomaticTypeDirectiveNames) { + if (host.hasChangedAutomaticTypeDirectiveNames?.()) { return oldProgram.structureIsReused = StructureIsReused.SafeModules; } diff --git a/src/compiler/resolutionCache.ts b/src/compiler/resolutionCache.ts index 3b0695450e04c..547a8f4c9d243 100644 --- a/src/compiler/resolutionCache.ts +++ b/src/compiler/resolutionCache.ts @@ -9,11 +9,13 @@ namespace ts { getResolvedModuleWithFailedLookupLocationsFromCache(moduleName: string, containingFile: string): CachedResolvedModuleWithFailedLookupLocations | undefined; resolveTypeReferenceDirectives(typeDirectiveNames: string[], containingFile: string, redirectedReference?: ResolvedProjectReference): (ResolvedTypeReferenceDirective | undefined)[]; + invalidateResolutionsOfFailedLookupLocations(): boolean; invalidateResolutionOfFile(filePath: Path): void; removeResolutionsOfFile(filePath: Path): void; removeResolutionsFromProjectReferenceRedirects(filePath: Path): void; setFilesWithInvalidatedNonRelativeUnresolvedImports(filesWithUnresolvedImports: Map): void; createHasInvalidatedResolution(forceAllFilesAsInvalidated?: boolean): HasInvalidatedResolution; + hasChangedAutomaticTypeDirectiveNames(): boolean; startCachingPerDirectoryResolution(): void; finishCachingPerDirectoryResolution(): void; @@ -50,6 +52,7 @@ namespace ts { onInvalidatedResolution(): void; watchTypeRootsDirectory(directory: string, cb: DirectoryWatcherCallback, flags: WatchDirectoryFlags): FileWatcher; onChangedAutomaticTypeDirectiveNames(): void; + scheduleInvalidateResolutionsOfFailedLookupLocations(): void; getCachedDirectoryStructureHost(): CachedDirectoryStructureHost | undefined; projectName?: string; getGlobalCache?(): string | undefined; @@ -147,6 +150,11 @@ namespace ts { const resolutionsWithFailedLookups: ResolutionWithFailedLookupLocations[] = []; const resolvedFileToResolution = createMultiMap(); + let hasChangedAutomaticTypeDirectiveNames = false; + const failedLookupChecks: Path[] = []; + const startsWithPathChecks: Path[] = []; + const isInDirectoryChecks: Path[] = []; + const getCurrentDirectory = memoize(() => resolutionHost.getCurrentDirectory!()); // TODO: GH#18217 const cachedDirectoryStructureHost = resolutionHost.getCachedDirectoryStructureHost(); @@ -195,7 +203,9 @@ namespace ts { resolveTypeReferenceDirectives, removeResolutionsFromProjectReferenceRedirects, removeResolutionsOfFile, + hasChangedAutomaticTypeDirectiveNames: () => hasChangedAutomaticTypeDirectiveNames, invalidateResolutionOfFile, + invalidateResolutionsOfFailedLookupLocations, setFilesWithInvalidatedNonRelativeUnresolvedImports, createHasInvalidatedResolution, updateTypeRootsWatch, @@ -227,9 +237,13 @@ namespace ts { resolvedTypeReferenceDirectives.clear(); resolvedFileToResolution.clear(); resolutionsWithFailedLookups.length = 0; + failedLookupChecks.length = 0; + startsWithPathChecks.length = 0; + isInDirectoryChecks.length = 0; // perDirectoryResolvedModuleNames and perDirectoryResolvedTypeReferenceDirectives could be non empty if there was exception during program update // (between startCachingPerDirectoryResolution and finishCachingPerDirectoryResolution) clearPerDirectoryResolutions(); + hasChangedAutomaticTypeDirectiveNames = false; } function startRecordingFilesWithChangedResolutions() { @@ -253,6 +267,8 @@ namespace ts { } function createHasInvalidatedResolution(forceAllFilesAsInvalidated?: boolean): HasInvalidatedResolution { + // Ensure pending resolutions are applied + invalidateResolutionsOfFailedLookupLocations(); if (forceAllFilesAsInvalidated) { // Any file asked would have invalidated resolution filesWithInvalidatedResolutions = undefined; @@ -281,6 +297,7 @@ namespace ts { watcher.watcher.close(); } }); + hasChangedAutomaticTypeDirectiveNames = false; } function resolveModuleName(moduleName: string, containingFile: string, compilerOptions: CompilerOptions, host: ModuleResolutionHost, redirectedReference?: ResolvedProjectReference): CachedResolvedModuleWithFailedLookupLocations { @@ -662,9 +679,7 @@ namespace ts { cachedDirectoryStructureHost.addOrDeleteFileOrDirectory(fileOrDirectory, fileOrDirectoryPath); } - if (invalidateResolutionOfFailedLookupLocation(fileOrDirectoryPath, dirPath === fileOrDirectoryPath)) { - resolutionHost.onInvalidatedResolution(); - } + scheduleInvalidateResolutionOfFailedLookupLocation(fileOrDirectoryPath, dirPath === fileOrDirectoryPath); }, nonRecursive ? WatchDirectoryFlags.None : WatchDirectoryFlags.Recursive); } @@ -700,23 +715,30 @@ namespace ts { removeResolutionsOfFileFromCache(resolvedTypeReferenceDirectives, filePath, getResolvedTypeReferenceDirective); } - function invalidateResolution(resolution: ResolutionWithFailedLookupLocations) { - resolution.isInvalidated = true; - let changedInAutoTypeReferenced = false; - for (const containingFilePath of Debug.assertDefined(resolution.files)) { - (filesWithInvalidatedResolutions || (filesWithInvalidatedResolutions = createMap())).set(containingFilePath, true); - // When its a file with inferred types resolution, invalidate type reference directive resolution - changedInAutoTypeReferenced = changedInAutoTypeReferenced || containingFilePath.endsWith(inferredTypesContainingFile); - } - if (changedInAutoTypeReferenced) { - resolutionHost.onChangedAutomaticTypeDirectiveNames(); + function invalidateResolutions(resolutions: ResolutionWithFailedLookupLocations[] | undefined, canInvalidate: (resolution: ResolutionWithFailedLookupLocations) => boolean) { + if (!resolutions) return false; + let invalidated = false; + for (const resolution of resolutions) { + if (resolution.isInvalidated || !canInvalidate(resolution)) continue; + resolution.isInvalidated = invalidated = true; + for (const containingFilePath of Debug.assertDefined(resolution.files)) { + (filesWithInvalidatedResolutions || (filesWithInvalidatedResolutions = createMap())).set(containingFilePath, true); + // When its a file with inferred types resolution, invalidate type reference directive resolution + hasChangedAutomaticTypeDirectiveNames = hasChangedAutomaticTypeDirectiveNames || containingFilePath.endsWith(inferredTypesContainingFile); + } } + return invalidated; } function invalidateResolutionOfFile(filePath: Path) { removeResolutionsOfFile(filePath); // Resolution is invalidated if the resulting file name is same as the deleted file path - forEach(resolvedFileToResolution.get(filePath), invalidateResolution); + const prevHasChangedAutomaticTypeDirectiveNames = hasChangedAutomaticTypeDirectiveNames; + if (invalidateResolutions(resolvedFileToResolution.get(filePath), returnTrue) && + hasChangedAutomaticTypeDirectiveNames && + !prevHasChangedAutomaticTypeDirectiveNames) { + resolutionHost.onChangedAutomaticTypeDirectiveNames(); + } } function setFilesWithInvalidatedNonRelativeUnresolvedImports(filesMap: ReadonlyMap) { @@ -724,12 +746,11 @@ namespace ts { filesWithInvalidatedNonRelativeUnresolvedImports = filesMap; } - function invalidateResolutionOfFailedLookupLocation(fileOrDirectoryPath: Path, isCreatingWatchedDirectory: boolean) { - let isChangedFailedLookupLocation: (location: string) => boolean; + function scheduleInvalidateResolutionOfFailedLookupLocation(fileOrDirectoryPath: Path, isCreatingWatchedDirectory: boolean) { if (isCreatingWatchedDirectory) { // Watching directory is created // Invalidate any resolution has failed lookup in this directory - isChangedFailedLookupLocation = location => isInDirectoryPath(fileOrDirectoryPath, resolutionHost.toPath(location)); + isInDirectoryChecks.push(fileOrDirectoryPath); } else { // If something to do with folder/file starting with "." in node_modules folder, skip it @@ -748,10 +769,8 @@ namespace ts { if (isNodeModulesAtTypesDirectory(fileOrDirectoryPath) || isNodeModulesDirectory(fileOrDirectoryPath) || isNodeModulesAtTypesDirectory(dirOfFileOrDirectory) || isNodeModulesDirectory(dirOfFileOrDirectory)) { // Invalidate any resolution from this directory - isChangedFailedLookupLocation = location => { - const locationPath = resolutionHost.toPath(location); - return locationPath === fileOrDirectoryPath || startsWith(resolutionHost.toPath(location), fileOrDirectoryPath); - }; + failedLookupChecks.push(fileOrDirectoryPath); + startsWithPathChecks.push(fileOrDirectoryPath); } else { if (!isPathWithDefaultFailedLookupExtension(fileOrDirectoryPath) && !customFailedLookupPaths.has(fileOrDirectoryPath)) { @@ -762,20 +781,33 @@ namespace ts { return false; } // Resolution need to be invalidated if failed lookup location is same as the file or directory getting created - isChangedFailedLookupLocation = location => resolutionHost.toPath(location) === fileOrDirectoryPath; + failedLookupChecks.push(fileOrDirectoryPath); } } - let invalidated = false; - // Resolution is invalidated if the resulting file name is same as the deleted file path - for (const resolution of resolutionsWithFailedLookups) { - if (resolution.failedLookupLocations.some(isChangedFailedLookupLocation)) { - invalidateResolution(resolution); - invalidated = true; - } + resolutionHost.scheduleInvalidateResolutionsOfFailedLookupLocations(); + } + + function invalidateResolutionsOfFailedLookupLocations() { + if (!failedLookupChecks.length && !startsWithPathChecks.length && !isInDirectoryChecks.length) { + return false; } + + const invalidated = invalidateResolutions(resolutionsWithFailedLookups, canInvalidateFailedLookupResolution); + failedLookupChecks.length = 0; + startsWithPathChecks.length = 0; + isInDirectoryChecks.length = 0; return invalidated; } + function canInvalidateFailedLookupResolution(resolution: ResolutionWithFailedLookupLocations) { + return resolution.failedLookupLocations.some(location => { + const locationPath = resolutionHost.toPath(location); + return contains(failedLookupChecks, locationPath) || + startsWithPathChecks.some(fileOrDirectoryPath => startsWith(locationPath, fileOrDirectoryPath)) || + isInDirectoryChecks.some(fileOrDirectoryPath => isInDirectoryPath(fileOrDirectoryPath, locationPath)); + }); + } + function closeTypeRootsWatch() { clearMap(typeRootsWatches, closeFileWatcher); } @@ -800,13 +832,14 @@ namespace ts { // For now just recompile // We could potentially store more data here about whether it was/would be really be used or not // and with that determine to trigger compilation but for now this is enough + hasChangedAutomaticTypeDirectiveNames = true; resolutionHost.onChangedAutomaticTypeDirectiveNames(); // Since directory watchers invoked are flaky, the failed lookup location events might not be triggered // So handle to failed lookup locations here as well to ensure we are invalidating resolutions const dirPath = getDirectoryToWatchFailedLookupLocationFromTypeRoot(typeRoot, typeRootPath); - if (dirPath && invalidateResolutionOfFailedLookupLocation(fileOrDirectoryPath, dirPath === fileOrDirectoryPath)) { - resolutionHost.onInvalidatedResolution(); + if (dirPath) { + scheduleInvalidateResolutionOfFailedLookupLocation(fileOrDirectoryPath, dirPath === fileOrDirectoryPath); } }, WatchDirectoryFlags.Recursive); } diff --git a/src/compiler/types.ts b/src/compiler/types.ts index 0c45dca9529fc..274ee12590191 100644 --- a/src/compiler/types.ts +++ b/src/compiler/types.ts @@ -5688,6 +5688,8 @@ namespace ts { /* @internal */ export type HasInvalidatedResolution = (sourceFile: Path) => boolean; + /* @internal */ + export type HasChangedAutomaticTypeDirectiveNames = () => boolean; export interface CompilerHost extends ModuleResolutionHost { getSourceFile(fileName: string, languageVersion: ScriptTarget, onError?: (message: string) => void, shouldCreateNewSourceFile?: boolean): SourceFile | undefined; @@ -5717,7 +5719,7 @@ namespace ts { getEnvironmentVariable?(name: string): string | undefined; /* @internal */ onReleaseOldSourceFile?(oldSourceFile: SourceFile, oldOptions: CompilerOptions, hasSourceFileByPath: boolean): void; /* @internal */ hasInvalidatedResolution?: HasInvalidatedResolution; - /* @internal */ hasChangedAutomaticTypeDirectiveNames?: boolean; + /* @internal */ hasChangedAutomaticTypeDirectiveNames?: HasChangedAutomaticTypeDirectiveNames; createHash?(data: string): string; getParsedCommandLine?(fileName: string): ParsedCommandLine | undefined; /* @internal */ useSourceOfProjectReferenceRedirect?(): boolean; diff --git a/src/compiler/watchPublic.ts b/src/compiler/watchPublic.ts index 5b7e7c357ee7c..e4c30a1a87303 100644 --- a/src/compiler/watchPublic.ts +++ b/src/compiler/watchPublic.ts @@ -249,11 +249,12 @@ namespace ts { let missingFilesMap: Map; // Map of file watchers for the missing files let watchedWildcardDirectories: Map; // map of watchers for the wild card directories in the config file let timerToUpdateProgram: any; // timer callback to recompile the program + let timerToInvalidateFailedLookupResolutions: any; // timer callback to invalidate resolutions for changes in failed lookup locations + const sourceFilesCache = createMap(); // Cache that stores the source file and version info let missingFilePathsRequestedForRelease: Path[] | undefined; // 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 hasChangedCompilerOptions = false; // True if the compiler options have changed between compilations - let hasChangedAutomaticTypeDirectiveNames = false; // True if the automatic type directives have changed const useCaseSensitiveFileNames = host.useCaseSensitiveFileNames(); const currentDirectory = host.getCurrentDirectory(); @@ -307,11 +308,9 @@ namespace ts { compilerHost.watchDirectoryOfFailedLookupLocation = (dir, cb, flags) => watchDirectory(host, dir, cb, flags, watchOptions, WatchType.FailedLookupLocations); compilerHost.watchTypeRootsDirectory = (dir, cb, flags) => watchDirectory(host, dir, cb, flags, watchOptions, WatchType.TypeRoots); compilerHost.getCachedDirectoryStructureHost = () => cachedDirectoryStructureHost; + compilerHost.scheduleInvalidateResolutionsOfFailedLookupLocations = scheduleInvalidateResolutionsOfFailedLookupLocations; compilerHost.onInvalidatedResolution = scheduleProgramUpdate; - compilerHost.onChangedAutomaticTypeDirectiveNames = () => { - hasChangedAutomaticTypeDirectiveNames = true; - scheduleProgramUpdate(); - }; + compilerHost.onChangedAutomaticTypeDirectiveNames = scheduleProgramUpdate; compilerHost.fileIsOpen = returnFalse; compilerHost.getCurrentProgram = getCurrentProgram; compilerHost.writeLog = writeLog; @@ -343,6 +342,7 @@ namespace ts { { getCurrentProgram: getCurrentBuilderProgram, getProgram: updateProgram, updateRootFileNames, close }; function close() { + clearInvalidateResolutionsOfFailedLookupLocations(); resolutionCache.clear(); clearMap(sourceFilesCache, value => { if (value && value.fileWatcher) { @@ -374,6 +374,7 @@ namespace ts { function synchronizeProgram() { writeLog(`Synchronizing program`); + clearInvalidateResolutionsOfFailedLookupLocations(); const program = getCurrentBuilderProgram(); if (hasChangedCompilerOptions) { @@ -414,7 +415,6 @@ namespace ts { resolutionCache.startCachingPerDirectoryResolution(); compilerHost.hasInvalidatedResolution = hasInvalidatedResolution; compilerHost.hasChangedAutomaticTypeDirectiveNames = hasChangedAutomaticTypeDirectiveNames; - hasChangedAutomaticTypeDirectiveNames = false; builderProgram = createProgram(rootFileNames, compilerOptions, compilerHost, builderProgram, configFileParsingDiagnostics, projectReferences); resolutionCache.finishCachingPerDirectoryResolution(); @@ -560,6 +560,33 @@ namespace ts { } } + function hasChangedAutomaticTypeDirectiveNames() { + return resolutionCache.hasChangedAutomaticTypeDirectiveNames(); + } + + function clearInvalidateResolutionsOfFailedLookupLocations() { + if (!timerToInvalidateFailedLookupResolutions) return false; + host.clearTimeout!(timerToInvalidateFailedLookupResolutions); + timerToInvalidateFailedLookupResolutions = undefined; + return true; + } + + function scheduleInvalidateResolutionsOfFailedLookupLocations() { + if (!host.setTimeout || !host.clearTimeout) { + return resolutionCache.invalidateResolutionsOfFailedLookupLocations(); + } + const pending = clearInvalidateResolutionsOfFailedLookupLocations(); + writeLog(`Scheduling invalidateFailedLookup${pending ? ", Cancelled earlier one" : ""}`); + timerToInvalidateFailedLookupResolutions = host.setTimeout(invalidateResolutionsOfFailedLookup, 250); + } + + function invalidateResolutionsOfFailedLookup() { + timerToInvalidateFailedLookupResolutions = undefined; + if (resolutionCache.invalidateResolutionsOfFailedLookupLocations()) { + scheduleProgramUpdate(); + } + } + // Upon detecting a file change, wait for 250ms and then perform a recompilation. This gives batch // operations (such as saving all modified files in an editor) a chance to complete before we kick // off a new compilation. diff --git a/src/server/editorServices.ts b/src/server/editorServices.ts index f98f0dce2dbd0..51571a4c7ed23 100644 --- a/src/server/editorServices.ts +++ b/src/server/editorServices.ts @@ -540,6 +540,7 @@ namespace ts.server { /*@internal*/ export function updateProjectIfDirty(project: Project) { + project.invalidateResolutionsOfFailedLookupLocations(); return project.dirty && project.updateGraph(); } @@ -634,7 +635,7 @@ namespace ts.server { * In this case the exists property is always true */ private readonly configFileExistenceInfoCache = createMap(); - private readonly throttledOperations: ThrottledOperations; + /*@internal*/ readonly throttledOperations: ThrottledOperations; private readonly hostConfiguration: HostConfiguration; private safelist: SafeList = defaultTypeSafeList; @@ -823,7 +824,8 @@ namespace ts.server { this.delayUpdateProjectGraphAndEnsureProjectStructureForOpenFiles(project); } - private delayEnsureProjectForOpenFiles() { + /*@internal*/ + delayEnsureProjectForOpenFiles() { this.pendingEnsureProjectForOpenFiles = true; this.throttledOperations.schedule("*ensureProjectForOpenFiles*", /*delay*/ 2500, () => { if (this.pendingProjectUpdates.size !== 0) { diff --git a/src/server/project.ts b/src/server/project.ts index a3645fdddd1f3..3aa5b6a125a3c 100644 --- a/src/server/project.ts +++ b/src/server/project.ts @@ -196,9 +196,6 @@ namespace ts.server { /*@internal*/ dirty = false; - /*@internal*/ - hasChangedAutomaticTypeDirectiveNames = false; - /*@internal*/ typingFiles: SortedReadonlyArray = emptyArray; @@ -479,6 +476,29 @@ namespace ts.server { ); } + /*@internal*/ + clearInvalidateResolutionOfFailedLookupTimer() { + return this.projectService.throttledOperations.cancel(`${this.getProjectName()}FailedLookupInvalidation`); + } + + /*@internal*/ + scheduleInvalidateResolutionsOfFailedLookupLocations() { + this.projectService.throttledOperations.schedule(`${this.getProjectName()}FailedLookupInvalidation`, /*delay*/ 1000, () => { + if (this.resolutionCache.invalidateResolutionsOfFailedLookupLocations()) { + this.projectService.delayUpdateProjectGraphAndEnsureProjectStructureForOpenFiles(this); + } + }); + } + + /*@internal*/ + invalidateResolutionsOfFailedLookupLocations() { + if (this.clearInvalidateResolutionOfFailedLookupTimer() && + this.resolutionCache.invalidateResolutionsOfFailedLookupLocations()) { + this.markAsDirty(); + this.projectService.delayEnsureProjectForOpenFiles(); + } + } + /*@internal*/ onInvalidatedResolution() { this.projectService.delayUpdateProjectGraphAndEnsureProjectStructureForOpenFiles(this); @@ -497,9 +517,13 @@ namespace ts.server { ); } + /*@internal*/ + hasChangedAutomaticTypeDirectiveNames() { + return this.resolutionCache.hasChangedAutomaticTypeDirectiveNames(); + } + /*@internal*/ onChangedAutomaticTypeDirectiveNames() { - this.hasChangedAutomaticTypeDirectiveNames = true; this.projectService.delayUpdateProjectGraphAndEnsureProjectStructureForOpenFiles(this); } @@ -723,6 +747,7 @@ namespace ts.server { this.packageJsonFilesMap = undefined; } this.clearGeneratedFileWatch(); + this.clearInvalidateResolutionOfFailedLookupTimer(); // signal language service to release source files acquired from document registry this.languageService.dispose(); @@ -1007,7 +1032,6 @@ namespace ts.server { // - oldProgram is not set - this is a first time updateGraph is called // - newProgram is different from the old program and structure of the old program was not reused. const hasNewProgram = this.program && (!oldProgram || (this.program !== oldProgram && !(oldProgram.structureIsReused! & StructureIsReused.Completely))); - this.hasChangedAutomaticTypeDirectiveNames = false; if (hasNewProgram) { if (oldProgram) { for (const f of oldProgram.getSourceFiles()) { diff --git a/src/server/utilities.ts b/src/server/utilities.ts index 23f44685500ec..4d6cd9dbf5dd2 100644 --- a/src/server/utilities.ts +++ b/src/server/utilities.ts @@ -26,6 +26,13 @@ namespace ts.server { } } + public cancel(operationId: string) { + const pendingTimeout = this.pendingTimeouts.get(operationId); + if (!pendingTimeout) return false; + this.host.clearTimeout(pendingTimeout); + return this.pendingTimeouts.delete(operationId); + } + private static run(self: ThrottledOperations, operationId: string, cb: () => void) { perfLogger.logStartScheduledOperation(operationId); self.pendingTimeouts.delete(operationId); diff --git a/src/services/services.ts b/src/services/services.ts index 2002af73df9c2..3e4db282263d5 100644 --- a/src/services/services.ts +++ b/src/services/services.ts @@ -1230,7 +1230,7 @@ namespace ts { if (host.getProjectVersion) { const hostProjectVersion = host.getProjectVersion(); if (hostProjectVersion) { - if (lastProjectVersion === hostProjectVersion && !host.hasChangedAutomaticTypeDirectiveNames) { + if (lastProjectVersion === hostProjectVersion && !host.hasChangedAutomaticTypeDirectiveNames?.()) { return; } @@ -1250,10 +1250,11 @@ namespace ts { const rootFileNames = hostCache.getRootFileNames(); const hasInvalidatedResolution: HasInvalidatedResolution = host.hasInvalidatedResolution || returnFalse; + const hasChangedAutomaticTypeDirectiveNames = maybeBind(host, host.hasChangedAutomaticTypeDirectiveNames); const projectReferences = hostCache.getProjectReferences(); // If the program is already up-to-date, we can reuse it - if (isProgramUptoDate(program, rootFileNames, hostCache.compilationSettings(), (_path, fileName) => host.getScriptVersion(fileName), fileExists, hasInvalidatedResolution, !!host.hasChangedAutomaticTypeDirectiveNames, projectReferences)) { + if (isProgramUptoDate(program, rootFileNames, hostCache.compilationSettings(), (_path, fileName) => host.getScriptVersion(fileName), fileExists, hasInvalidatedResolution, hasChangedAutomaticTypeDirectiveNames, projectReferences)) { return; } @@ -1291,7 +1292,7 @@ namespace ts { }, onReleaseOldSourceFile, hasInvalidatedResolution, - hasChangedAutomaticTypeDirectiveNames: host.hasChangedAutomaticTypeDirectiveNames + hasChangedAutomaticTypeDirectiveNames }; if (host.trace) { compilerHost.trace = message => host.trace!(message); diff --git a/src/services/types.ts b/src/services/types.ts index d970e5a69e2b8..47a6d96ac8724 100644 --- a/src/services/types.ts +++ b/src/services/types.ts @@ -254,7 +254,7 @@ namespace ts { getResolvedModuleWithFailedLookupLocationsFromCache?(modulename: string, containingFile: string): ResolvedModuleWithFailedLookupLocations | undefined; resolveTypeReferenceDirectives?(typeDirectiveNames: string[], containingFile: string, redirectedReference: ResolvedProjectReference | undefined, options: CompilerOptions): (ResolvedTypeReferenceDirective | undefined)[]; /* @internal */ hasInvalidatedResolution?: HasInvalidatedResolution; - /* @internal */ hasChangedAutomaticTypeDirectiveNames?: boolean; + /* @internal */ hasChangedAutomaticTypeDirectiveNames?: HasChangedAutomaticTypeDirectiveNames; /* @internal */ getGlobalTypingsCacheLocation?(): string | undefined; /* @internal */ diff --git a/src/testRunner/unittests/printer.ts b/src/testRunner/unittests/printer.ts index e96aba3674948..6c5449c97943d 100644 --- a/src/testRunner/unittests/printer.ts +++ b/src/testRunner/unittests/printer.ts @@ -83,7 +83,6 @@ namespace ts { // https://github.com/microsoft/TypeScript/issues/35054 printsCorrectly("jsx attribute escaping", {}, printer => { - debugger; return printer.printFile(createSourceFile( "source.ts", String.raw``, diff --git a/src/testRunner/unittests/reuseProgramStructure.ts b/src/testRunner/unittests/reuseProgramStructure.ts index 562e76c181b8b..cc4f6fadb785a 100644 --- a/src/testRunner/unittests/reuseProgramStructure.ts +++ b/src/testRunner/unittests/reuseProgramStructure.ts @@ -915,7 +915,7 @@ namespace ts { program, newRootFileNames, newOptions, path => program.getSourceFileByPath(path)!.version, /*fileExists*/ returnFalse, /*hasInvalidatedResolution*/ returnFalse, - /*hasChangedAutomaticTypeDirectiveNames*/ false, + /*hasChangedAutomaticTypeDirectiveNames*/ undefined, /*projectReferences*/ undefined ); } diff --git a/src/testRunner/unittests/tsbuild/watchMode.ts b/src/testRunner/unittests/tsbuild/watchMode.ts index 3c17690a4eb1b..bbf1dde7864c8 100644 --- a/src/testRunner/unittests/tsbuild/watchMode.ts +++ b/src/testRunner/unittests/tsbuild/watchMode.ts @@ -875,6 +875,7 @@ export function gfoo() { interface VerifyScenario { edit: (host: TsBuildWatchSystem, solutionBuilder: SolutionBuilder) => void; + schedulesFailedWatchUpdate?: boolean; expectedEditErrors: readonly string[]; expectedProgramFiles: readonly string[]; expectedProjectFiles: readonly string[]; @@ -886,20 +887,20 @@ export function gfoo() { orphanInfosAfterEdit?: readonly string[]; orphanInfosAfterRevert?: readonly string[]; } - function verifyScenario({ edit, expectedEditErrors, expectedProgramFiles, expectedProjectFiles, expectedWatchedFiles, expectedProjectWatchedFiles, expectedWatchedDirectoriesRecursive, dependencies, revert, orphanInfosAfterEdit, orphanInfosAfterRevert }: VerifyScenario) { + function verifyScenario({ edit, schedulesFailedWatchUpdate, expectedEditErrors, expectedProgramFiles, expectedProjectFiles, expectedWatchedFiles, expectedProjectWatchedFiles, expectedWatchedDirectoriesRecursive, dependencies, revert, orphanInfosAfterEdit, orphanInfosAfterRevert }: VerifyScenario) { it("with tsc-watch", () => { const { host, solutionBuilder, watch } = createSolutionAndWatchMode(); edit(host, solutionBuilder); - host.checkTimeoutQueueLengthAndRun(1); + host.checkTimeoutQueueLengthAndRun(schedulesFailedWatchUpdate ? 2 : 1); checkOutputErrorsIncremental(host, expectedEditErrors); verifyWatchState(host, watch, expectedProgramFiles, expectedWatchedFiles, expectedWatchedDirectoriesRecursive, dependencies, expectedWatchedDirectories); if (revert) { revert(host); - host.checkTimeoutQueueLengthAndRun(1); + host.checkTimeoutQueueLengthAndRun(schedulesFailedWatchUpdate ? 2 : 1); checkOutputErrorsIncremental(host, emptyArray); verifyProgram(host, watch); } @@ -912,13 +913,13 @@ export function gfoo() { edit(host, solutionBuilder); - host.checkTimeoutQueueLengthAndRun(2); + host.checkTimeoutQueueLengthAndRun(schedulesFailedWatchUpdate ? 3 : 2); verifyServerState({ host, service, expectedProjectFiles, expectedProjectWatchedFiles, expectedWatchedDirectoriesRecursive, orphanInfos: orphanInfosAfterEdit }); if (revert) { revert(host); - host.checkTimeoutQueueLengthAndRun(2); + host.checkTimeoutQueueLengthAndRun(schedulesFailedWatchUpdate ? 3 : 2); verifyProject(host, service, orphanInfosAfterRevert); } }); @@ -1033,6 +1034,7 @@ export function gfoo() { // Should map to b.ts instead with options from our own config verifyScenario({ edit: host => host.deleteFile(bTsconfig.path), + schedulesFailedWatchUpdate: multiFolder, expectedEditErrors: [ `${multiFolder ? "c/tsconfig.json" : "tsconfig.c.json"}(9,21): error TS6053: File '/user/username/projects/transitiveReferences/${multiFolder ? "b" : "tsconfig.b.json"}' not found.\n` ], @@ -1056,6 +1058,7 @@ export function gfoo() { describe("deleting transitively referenced config file", () => { verifyScenario({ edit: host => host.deleteFile(aTsconfig.path), + schedulesFailedWatchUpdate: multiFolder, expectedEditErrors: [ `${multiFolder ? "b/tsconfig.json" : "tsconfig.b.json"}(10,21): error TS6053: File '/user/username/projects/transitiveReferences/${multiFolder ? "a" : "tsconfig.a.json"}' not found.\n` ], diff --git a/src/testRunner/unittests/tscWatch/programUpdates.ts b/src/testRunner/unittests/tscWatch/programUpdates.ts index 343c8221bf30d..fb3e008c7d962 100644 --- a/src/testRunner/unittests/tscWatch/programUpdates.ts +++ b/src/testRunner/unittests/tscWatch/programUpdates.ts @@ -707,18 +707,23 @@ declare const eval: any` changes: emptyArray }); + function runQueuedTimeoutCallbacksTwice(sys: WatchedSystem) { + sys.runQueuedTimeoutCallbacks(); // Scheduled invalidation of resolutions + sys.runQueuedTimeoutCallbacks(); // Actual update + } + const changeModuleFileToModuleFile1: TscWatchCompileChange = { caption: "Rename moduleFile to moduleFile1", change: sys => { sys.renameFile("/a/b/moduleFile.ts", "/a/b/moduleFile1.ts"); sys.deleteFile("/a/b/moduleFile.js"); }, - timeouts: runQueuedTimeoutCallbacks + timeouts: runQueuedTimeoutCallbacksTwice }; const changeModuleFile1ToModuleFile: TscWatchCompileChange = { caption: "Rename moduleFile1 back to moduleFile", change: sys => sys.renameFile("/a/b/moduleFile1.ts", "/a/b/moduleFile.ts"), - timeouts: runQueuedTimeoutCallbacks, + timeouts: runQueuedTimeoutCallbacksTwice, }; verifyTscWatch({ @@ -803,7 +808,7 @@ declare const eval: any` { caption: "Create module file", change: sys => sys.writeFile("/a/b/moduleFile.ts", "export function bar() { }"), - timeouts: runQueuedTimeoutCallbacks, + timeouts: runQueuedTimeoutCallbacksTwice, } ] }); diff --git a/src/testRunner/unittests/tscWatch/resolutionCache.ts b/src/testRunner/unittests/tscWatch/resolutionCache.ts index a3f498ddce4b8..03cc81752a554 100644 --- a/src/testRunner/unittests/tscWatch/resolutionCache.ts +++ b/src/testRunner/unittests/tscWatch/resolutionCache.ts @@ -170,7 +170,8 @@ namespace ts.tscWatch { fileExistsCalledForBar = false; host.writeFile(imported.path, imported.content); - host.checkTimeoutQueueLengthAndRun(1); + host.checkTimeoutQueueLengthAndRun(1); // Scheduled invalidation of resolutions + host.checkTimeoutQueueLengthAndRun(1); // Actual update checkOutputErrorsIncremental(host, emptyArray); assert.isTrue(fileExistsCalledForBar, "'fileExists' should be called."); }); @@ -391,7 +392,10 @@ declare namespace myapp { }` }); }, - timeouts: checkSingleTimeoutQueueLengthAndRun, + timeouts: sys => { + sys.checkTimeoutQueueLengthAndRun(2); // Scheduled invalidation of resolutions, update that gets cancelled and rescheduled by actual invalidation of resolution + sys.checkTimeoutQueueLengthAndRun(1); // Actual update + }, }, { caption: "No change, just check program", diff --git a/src/testRunner/unittests/tscWatch/watchEnvironment.ts b/src/testRunner/unittests/tscWatch/watchEnvironment.ts index 3e61757125387..2e266ef682a2c 100644 --- a/src/testRunner/unittests/tscWatch/watchEnvironment.ts +++ b/src/testRunner/unittests/tscWatch/watchEnvironment.ts @@ -189,7 +189,8 @@ namespace ts.tscWatch { change: noop, timeouts: sys => { sys.checkTimeoutQueueLengthAndRun(1); // To update directory callbacks for file1.js output - sys.checkTimeoutQueueLengthAndRun(1); // Update program again + sys.checkTimeoutQueueLengthAndRun(2); // Update program again and Failed lookup update + sys.checkTimeoutQueueLengthAndRun(1); // Actual program update sys.checkTimeoutQueueLength(0); }, }, @@ -198,7 +199,7 @@ namespace ts.tscWatch { // Remove directory node_modules change: sys => sys.deleteFolder(`${projectRoot}/node_modules`, /*recursive*/ true), timeouts: sys => { - sys.checkTimeoutQueueLength(2); // 1. For updating program and 2. for updating child watches + sys.checkTimeoutQueueLength(3); // 1. Failed lookup invalidation 2. For updating program and 3. for updating child watches sys.runQueuedTimeoutCallbacks(sys.getNextTimeoutId() - 2); // Update program }, }, @@ -207,7 +208,8 @@ namespace ts.tscWatch { change: noop, timeouts: sys => { sys.checkTimeoutQueueLengthAndRun(1); // To update directory watchers - sys.checkTimeoutQueueLengthAndRun(1); // To Update program + sys.checkTimeoutQueueLengthAndRun(2); // To Update program and failed lookup update + sys.checkTimeoutQueueLengthAndRun(1); // Actual program update sys.checkTimeoutQueueLength(0); }, }, @@ -232,7 +234,15 @@ namespace ts.tscWatch { change: noop, timeouts: sys => { sys.runQueuedTimeoutCallbacks(); - sys.checkTimeoutQueueLength(1); // To Update the program + sys.checkTimeoutQueueLength(2); // To Update program and failed lookup update + }, + }, + { + caption: "Invalidates module resolution cache", + change: noop, + timeouts: sys => { + sys.runQueuedTimeoutCallbacks(); + sys.checkTimeoutQueueLength(1); // To Update program }, }, { diff --git a/src/testRunner/unittests/tsserver/cachingFileSystemInformation.ts b/src/testRunner/unittests/tsserver/cachingFileSystemInformation.ts index 7187be8c13946..ed15145cf9cce 100644 --- a/src/testRunner/unittests/tsserver/cachingFileSystemInformation.ts +++ b/src/testRunner/unittests/tsserver/cachingFileSystemInformation.ts @@ -483,7 +483,8 @@ namespace ts.projectSystem { }; files.push(debugTypesFile); host.writeFile(debugTypesFile.path, debugTypesFile.content); - host.runQueuedTimeoutCallbacks(); + host.runQueuedTimeoutCallbacks(); // Scheduled invalidation of resolutions + host.runQueuedTimeoutCallbacks(); // Actual update checkProjectActualFiles(project, files.map(f => f.path)); assert.deepEqual(project.getLanguageService().getSemanticDiagnostics(file1.path).map(diag => diag.messageText), []); assert.deepEqual(project.getLanguageService().getSemanticDiagnostics(file2.path).map(diag => diag.messageText), []); @@ -636,10 +637,17 @@ namespace ts.projectSystem { function verifyAfterPartialOrCompleteNpmInstall(timeoutQueueLengthWhenRunningTimeouts: number) { filesAndFoldersToAdd.forEach(f => host.ensureFileOrFolder(f)); if (npmInstallComplete || timeoutDuringPartialInstallation) { - host.checkTimeoutQueueLengthAndRun(timeoutQueueLengthWhenRunningTimeouts); + if (timeoutQueueLengthWhenRunningTimeouts) { + // Expected project update + host.checkTimeoutQueueLengthAndRun(timeoutQueueLengthWhenRunningTimeouts + 1); // Scheduled invalidation of resolutions + host.runQueuedTimeoutCallbacks(); // Actual update + } + else { + host.checkTimeoutQueueLengthAndRun(timeoutQueueLengthWhenRunningTimeouts); + } } else { - host.checkTimeoutQueueLength(2); + host.checkTimeoutQueueLength(3); } verifyProject(); } diff --git a/src/testRunner/unittests/tsserver/configuredProjects.ts b/src/testRunner/unittests/tsserver/configuredProjects.ts index 7e53d229a175c..b09b2bf10d629 100644 --- a/src/testRunner/unittests/tsserver/configuredProjects.ts +++ b/src/testRunner/unittests/tsserver/configuredProjects.ts @@ -1205,7 +1205,8 @@ foo();` files.push(file2); host.writeFile(file2.path, file2.content); - host.runQueuedTimeoutCallbacks(); + host.runQueuedTimeoutCallbacks(); // Scheduled invalidation of resolutions + host.runQueuedTimeoutCallbacks(); // Actual update checkNumberOfProjects(projectService, { configuredProjects: 1 }); assert.strictEqual(projectService.configuredProjects.get(configFile.path), project); checkProjectActualFiles(project, mapDefined(files, file => file === file2a ? undefined : file.path)); diff --git a/src/testRunner/unittests/tsserver/events/projectUpdatedInBackground.ts b/src/testRunner/unittests/tsserver/events/projectUpdatedInBackground.ts index f6bf4e4a970c2..918c817857ce0 100644 --- a/src/testRunner/unittests/tsserver/events/projectUpdatedInBackground.ts +++ b/src/testRunner/unittests/tsserver/events/projectUpdatedInBackground.ts @@ -185,6 +185,7 @@ namespace ts.projectSystem { // Since this is first event, it will have all the files filesToReload.forEach(f => host.ensureFileOrFolder(f)); + if (!firstReloadFileList) host.runQueuedTimeoutCallbacks(); // Invalidated module resolutions to schedule project update verifyProjectsUpdatedInBackgroundEvent(); return { @@ -463,7 +464,8 @@ namespace ts.projectSystem { projectFiles.push(file2); host.writeFile(file2.path, file2.content); - host.runQueuedTimeoutCallbacks(); + host.runQueuedTimeoutCallbacks(); // For invalidation + host.runQueuedTimeoutCallbacks(); // For actual update if (useSlashRootAsSomeNotRootFolderInUserDirectory) { watchedRecursiveDirectories.length = 3; } diff --git a/src/testRunner/unittests/tsserver/projectErrors.ts b/src/testRunner/unittests/tsserver/projectErrors.ts index b5386939807eb..d542c86959d69 100644 --- a/src/testRunner/unittests/tsserver/projectErrors.ts +++ b/src/testRunner/unittests/tsserver/projectErrors.ts @@ -1018,7 +1018,7 @@ console.log(blabla);` { path: `${tscWatch.projectRoot}/node_modules/.staging/@babel/helper-plugin-utils-a06c629f` }, { path: `${tscWatch.projectRoot}/node_modules/.staging/core-js-db53158d` }, ]; - verifyWhileNpmInstall({ timeouts: 2, semantic: moduleNotFoundErr }); + verifyWhileNpmInstall({ timeouts: 3, semantic: moduleNotFoundErr }); filesAndFoldersToAdd = [ { path: `${tscWatch.projectRoot}/node_modules/.staging/@angular/platform-browser-dynamic-5efaaa1a` }, @@ -1039,18 +1039,21 @@ console.log(blabla);` projectFiles.push(moduleFile); // Additional watch for watching script infos from node_modules expectedRecursiveWatches.set(`${tscWatch.projectRoot}/node_modules`, 2); - verifyWhileNpmInstall({ timeouts: 2, semantic: [] }); + verifyWhileNpmInstall({ timeouts: 3, semantic: [] }); function verifyWhileNpmInstall({ timeouts, semantic }: { timeouts: number; semantic: protocol.Diagnostic[] }) { filesAndFoldersToAdd.forEach(f => host.ensureFileOrFolder(f)); if (npmInstallComplete || timeoutDuringPartialInstallation) { - host.checkTimeoutQueueLengthAndRun(timeouts); + host.checkTimeoutQueueLengthAndRun(timeouts); // Invalidation of failed lookups + if (timeouts) { + host.checkTimeoutQueueLengthAndRun(timeouts - 1); // Actual update + } } else { - host.checkTimeoutQueueLength(2); + host.checkTimeoutQueueLength(timeouts ? 3 : 2); } verifyProject(); - verifyErrors(semantic, !npmInstallComplete && !timeoutDuringPartialInstallation ? 2 : undefined); + verifyErrors(semantic, !npmInstallComplete && !timeoutDuringPartialInstallation ? timeouts ? 3 : 2 : undefined); } function verifyProject() { diff --git a/src/testRunner/unittests/tsserver/resolutionCache.ts b/src/testRunner/unittests/tsserver/resolutionCache.ts index 8a4beb47dc92b..73a2467aa21fe 100644 --- a/src/testRunner/unittests/tsserver/resolutionCache.ts +++ b/src/testRunner/unittests/tsserver/resolutionCache.ts @@ -168,7 +168,8 @@ namespace ts.projectSystem { content: "export = pad;declare function pad(length: number, text: string, char ?: string): string;" }; host.ensureFileOrFolder(padIndex, /*ignoreWatchInvokedWithTriggerAsFileCreate*/ true); - host.runQueuedTimeoutCallbacks(); + host.runQueuedTimeoutCallbacks(); // Scheduled invalidation of resolutions + host.runQueuedTimeoutCallbacks(); // Actual update checkProjectUpdatedInBackgroundEvent(session, [file1.path]); session.clearMessages(); diff --git a/src/testRunner/unittests/tsserver/symLinks.ts b/src/testRunner/unittests/tsserver/symLinks.ts index 59fb744f0e78e..3b8eb07d1ee69 100644 --- a/src/testRunner/unittests/tsserver/symLinks.ts +++ b/src/testRunner/unittests/tsserver/symLinks.ts @@ -221,7 +221,8 @@ new C();` host.ensureFileOrFolder(nodeModulesRecorgnizersText); host.writeFile(recongnizerTextDistTypingFile.path, recongnizerTextDistTypingFile.content); - host.runQueuedTimeoutCallbacks(); + host.runQueuedTimeoutCallbacks(); // Scheduled invalidation of resolutions + host.runQueuedTimeoutCallbacks(); // Actual update verifyProjectWithResolvedModule(session); }); @@ -232,7 +233,8 @@ new C();` verifyProjectWithUnresolvedModule(session); host.writeFile(recongnizerTextDistTypingFile.path, recongnizerTextDistTypingFile.content); - host.runQueuedTimeoutCallbacks(); + host.runQueuedTimeoutCallbacks(); // Scheduled invalidation of resolutions + host.runQueuedTimeoutCallbacks(); // Actual update if (withPathMapping) { verifyProjectWithResolvedModule(session); @@ -250,12 +252,14 @@ new C();` verifyProjectWithResolvedModule(session); host.deleteFolder(recognizersTextDist, /*recursive*/ true); - host.runQueuedTimeoutCallbacks(); + host.runQueuedTimeoutCallbacks(); // Scheduled invalidation of resolutions + host.runQueuedTimeoutCallbacks(); // Actual update verifyProjectWithUnresolvedModule(session); host.ensureFileOrFolder(recongnizerTextDistTypingFile); - host.runQueuedTimeoutCallbacks(); + host.runQueuedTimeoutCallbacks(); // Scheduled invalidation of resolutions + host.runQueuedTimeoutCallbacks(); // Actual update if (withPathMapping) { verifyProjectWithResolvedModule(session); diff --git a/tests/baselines/reference/api/tsserverlibrary.d.ts b/tests/baselines/reference/api/tsserverlibrary.d.ts index 37ec6e753ab22..06a5090f60114 100644 --- a/tests/baselines/reference/api/tsserverlibrary.d.ts +++ b/tests/baselines/reference/api/tsserverlibrary.d.ts @@ -9298,7 +9298,6 @@ declare namespace ts.server { * In this case the exists property is always true */ private readonly configFileExistenceInfoCache; - private readonly throttledOperations; private readonly hostConfiguration; private safelist; private readonly legacySafelist; @@ -9328,7 +9327,6 @@ declare namespace ts.server { toPath(fileName: string): Path; private loadTypesMap; updateTypingsForProject(response: SetTypings | InvalidateCachedTypings | PackageInstalledResponse): void; - private delayEnsureProjectForOpenFiles; private delayUpdateProjectGraph; private delayUpdateProjectGraphs; setCompilerOptionsForInferredProjects(projectCompilerOptions: protocol.ExternalProjectCompilerOptions, projectRootPath?: string): void; diff --git a/tests/baselines/reference/tscWatch/watchEnvironment/watchDirectories/with-non-synchronous-watch-directory.js b/tests/baselines/reference/tscWatch/watchEnvironment/watchDirectories/with-non-synchronous-watch-directory.js index 2a49142932e80..ae257f71dac10 100644 --- a/tests/baselines/reference/tscWatch/watchEnvironment/watchDirectories/with-non-synchronous-watch-directory.js +++ b/tests/baselines/reference/tscWatch/watchEnvironment/watchDirectories/with-non-synchronous-watch-directory.js @@ -349,6 +349,37 @@ FsWatchesRecursive:: exitCode:: ExitStatus.undefined +Change:: Invalidates module resolution cache + +Input:: + +Output:: + +WatchedFiles:: +/user/username/projects/myproject/tsconfig.json: + {"fileName":"/user/username/projects/myproject/tsconfig.json","pollingInterval":250} +/user/username/projects/myproject/src/file1.ts: + {"fileName":"/user/username/projects/myproject/src/file1.ts","pollingInterval":250} +/a/lib/lib.d.ts: + {"fileName":"/a/lib/lib.d.ts","pollingInterval":250} + +FsWatches:: +/user/username/projects/myproject/src: + {"directoryName":"/user/username/projects/myproject/src","fallbackPollingInterval":500,"fallbackOptions":{"watchFile":"PriorityPollingInterval"}} +/user/username/projects/myproject/node_modules: + {"directoryName":"/user/username/projects/myproject/node_modules","fallbackPollingInterval":500,"fallbackOptions":{"watchFile":"PriorityPollingInterval"}} +/user/username/projects/myproject/node_modules/@types: + {"directoryName":"/user/username/projects/myproject/node_modules/@types","fallbackPollingInterval":500,"fallbackOptions":{"watchFile":"PriorityPollingInterval"}} +/user/username/projects/myproject: + {"directoryName":"/user/username/projects/myproject","fallbackPollingInterval":500,"fallbackOptions":{"watchFile":"PriorityPollingInterval"}} +/user/username/projects/myproject/node_modules/file2: + {"directoryName":"/user/username/projects/myproject/node_modules/file2","fallbackPollingInterval":500,"fallbackOptions":{"watchFile":"PriorityPollingInterval"}} + +FsWatchesRecursive:: + +exitCode:: ExitStatus.undefined + + Change:: Pending updates Input::