From 21ad26b6fffb93b8c83d517cbd49837d402bab0f Mon Sep 17 00:00:00 2001 From: Sheetal Nandi Date: Fri, 30 Jun 2017 12:59:34 -0700 Subject: [PATCH 001/109] When opening a file, if it is using existing project, there is no need to update the project by re-reading the config file This will improve the opening file perf for file opens from same config project --- src/harness/unittests/projectErrors.ts | 2 +- src/server/editorServices.ts | 156 +++++++++++++------------ src/server/project.ts | 4 + 3 files changed, 88 insertions(+), 74 deletions(-) diff --git a/src/harness/unittests/projectErrors.ts b/src/harness/unittests/projectErrors.ts index f250d4d9b36f4..142aa3bf1b1ba 100644 --- a/src/harness/unittests/projectErrors.ts +++ b/src/harness/unittests/projectErrors.ts @@ -94,8 +94,8 @@ namespace ts.projectSystem { checkProjectErrors(projectService.synchronizeProjectList([])[0], ["File '/a/b/lib.ts' not found."]); host.reloadFS([file1, file2, config]); + host.triggerFileWatcherCallback(file2.path, FileWatcherEventKind.Created); - projectService.openClientFile(file1.path); projectService.checkNumberOfProjects({ configuredProjects: 1 }); checkProjectErrors(projectService.synchronizeProjectList([])[0], []); }); diff --git a/src/server/editorServices.ts b/src/server/editorServices.ts index 3edb880f62093..2eb1d73ad78e9 100644 --- a/src/server/editorServices.ts +++ b/src/server/editorServices.ts @@ -211,20 +211,6 @@ namespace ts.server { extraFileExtensions?: JsFileExtensionInfo[]; } - interface ConfigFileConversionResult { - success: boolean; - configFileErrors?: Diagnostic[]; - - projectOptions?: ProjectOptions; - } - - interface OpenConfigFileResult { - success: boolean; - errors?: Diagnostic[]; - - project?: ConfiguredProject; - } - export interface OpenConfiguredProjectResult { configFileName?: NormalizedPath; configFileErrors?: Diagnostic[]; @@ -862,42 +848,44 @@ namespace ts.server { } /** - * This function tries to search for a tsconfig.json for the given file. If we found it, - * we first detect if there is already a configured project created for it: if so, we re-read - * the tsconfig file content and update the project; otherwise we create a new one. + * This function tries to search for a tsconfig.json for the given file. + * This is different from the method the compiler uses because + * the compiler can assume it will always start searching in the + * current directory (the directory in which tsc was invoked). + * The server must start searching from the directory containing + * the newly opened file. */ - private openOrUpdateConfiguredProjectForFile(fileName: NormalizedPath, projectRootPath?: NormalizedPath): OpenConfiguredProjectResult { + private getConfigFileNameForFile(fileName: NormalizedPath, projectRootPath?: NormalizedPath) { const searchPath = getDirectoryPath(fileName); this.logger.info(`Search path: ${searchPath}`); // check if this file is already included in one of external projects const configFileName = this.findConfigFile(asNormalizedPath(searchPath), projectRootPath); - if (!configFileName) { + if (configFileName) { + this.logger.info(`Config file name: ${configFileName}`); + } + else { this.logger.info("No config files found."); - return {}; } + return configFileName; + } - this.logger.info(`Config file name: ${configFileName}`); - - const project = this.findConfiguredProjectByProjectName(configFileName); - if (!project) { - const { success, errors } = this.openConfigFile(configFileName, fileName); - if (!success) { - return { configFileName, configFileErrors: errors }; + /** + * This function tries to search for a tsconfig.json for the given file. If we found it, + * we first detect if there is already a configured project created for it: if so, we re-read + * the tsconfig file content and update the project; otherwise we create a new one. + */ + private openOrUpdateConfiguredProjectForFile(fileName: NormalizedPath) { + const configFileName = this.getConfigFileNameForFile(fileName); + if (configFileName) { + const project = this.findConfiguredProjectByProjectName(configFileName); + if (!project) { + this.openConfigFile(configFileName, fileName); } - - // even if opening config file was successful, it could still - // contain errors that were tolerated. - this.logger.info(`Opened configuration file ${configFileName}`); - if (errors && errors.length > 0) { - return { configFileName, configFileErrors: errors }; + else { + this.updateConfiguredProject(project); } } - else { - this.updateConfiguredProject(project); - } - - return { configFileName }; } // This is different from the method the compiler uses because @@ -971,7 +959,7 @@ namespace ts.server { return findProjectByName(projectFileName, this.externalProjects); } - private convertConfigFileContentToProjectOptions(configFilename: string): ConfigFileConversionResult { + private convertConfigFileContentToProjectOptions(configFilename: string) { configFilename = normalizePath(configFilename); const configFileContent = this.host.readFile(configFilename); @@ -996,23 +984,38 @@ namespace ts.server { Debug.assert(!!parsedCommandLine.fileNames); + let success: boolean; + let projectOptions: ProjectOptions; + if (parsedCommandLine.fileNames.length === 0) { errors.push(createCompilerDiagnostic(Diagnostics.The_config_file_0_found_doesn_t_contain_any_source_files, configFilename)); - return { success: false, configFileErrors: errors }; - } - - const projectOptions: ProjectOptions = { - files: parsedCommandLine.fileNames, - compilerOptions: parsedCommandLine.options, - configHasExtendsProperty: parsedCommandLine.raw["extends"] !== undefined, - configHasFilesProperty: parsedCommandLine.raw["files"] !== undefined, - configHasIncludeProperty: parsedCommandLine.raw["include"] !== undefined, - configHasExcludeProperty: parsedCommandLine.raw["exclude"] !== undefined, - wildcardDirectories: createMapFromTemplate(parsedCommandLine.wildcardDirectories), - typeAcquisition: parsedCommandLine.typeAcquisition, - compileOnSave: parsedCommandLine.compileOnSave - }; - return { success: true, projectOptions, configFileErrors: errors }; + success = false; + projectOptions = { + files: [], + compilerOptions: {}, + configHasExtendsProperty: false, + configHasFilesProperty: false, + configHasIncludeProperty: false, + configHasExcludeProperty: false, + typeAcquisition: { enable: false } + }; + } + else { + success = true; + projectOptions = { + files: parsedCommandLine.fileNames, + compilerOptions: parsedCommandLine.options, + configHasExtendsProperty: parsedCommandLine.raw["extends"] !== undefined, + configHasFilesProperty: parsedCommandLine.raw["files"] !== undefined, + configHasIncludeProperty: parsedCommandLine.raw["include"] !== undefined, + configHasExcludeProperty: parsedCommandLine.raw["exclude"] !== undefined, + wildcardDirectories: createMapFromTemplate(parsedCommandLine.wildcardDirectories), + typeAcquisition: parsedCommandLine.typeAcquisition, + compileOnSave: parsedCommandLine.compileOnSave + }; + } + + return { success, projectOptions, configFileErrors: errors }; } private exceededTotalSizeLimitForNonTsFiles(name: string, options: CompilerOptions, fileNames: T[], propertyReader: FilePropertyReader) { @@ -1149,8 +1152,7 @@ namespace ts.server { } private addFilesToProjectAndUpdateGraph(project: ConfiguredProject | ExternalProject, files: T[], propertyReader: FilePropertyReader, clientFileName: string, typeAcquisition: TypeAcquisition, configFileErrors: Diagnostic[]): void { - let errors: Diagnostic[]; - for (const f of files) { + for (const f of files) { const rootFilename = propertyReader.getFileName(f); const scriptKind = propertyReader.getScriptKind(f); const hasMixedContent = propertyReader.hasMixedContent(f, this.hostConfiguration.extraFileExtensions); @@ -1159,25 +1161,21 @@ namespace ts.server { project.addRoot(info); } else { - (errors || (errors = [])).push(createFileNotFoundDiagnostic(rootFilename)); + (configFileErrors || (configFileErrors = [])).push(createFileNotFoundDiagnostic(rootFilename)); } } - project.setProjectErrors(concatenate(configFileErrors, errors)); + project.setProjectErrors(configFileErrors); project.setTypeAcquisition(typeAcquisition); project.updateGraph(); } - private openConfigFile(configFileName: NormalizedPath, clientFileName?: string): OpenConfigFileResult { - const conversionResult = this.convertConfigFileContentToProjectOptions(configFileName); - const projectOptions: ProjectOptions = conversionResult.success - ? conversionResult.projectOptions - : { files: [], compilerOptions: {}, configHasExtendsProperty: false, configHasFilesProperty: false, configHasIncludeProperty: false, configHasExcludeProperty: false, typeAcquisition: { enable: false } }; - const project = this.createAndAddConfiguredProject(configFileName, projectOptions, conversionResult.configFileErrors, clientFileName); - return { - success: conversionResult.success, - project, - errors: project.getGlobalProjectErrors() - }; + private openConfigFile(configFileName: NormalizedPath, clientFileName?: string) { + const { success, projectOptions, configFileErrors } = this.convertConfigFileContentToProjectOptions(configFileName); + if (success) { + this.logger.info(`Opened configuration file ${configFileName}`); + } + + return this.createAndAddConfiguredProject(configFileName, projectOptions, configFileErrors, clientFileName); } private updateNonInferredProject(project: ExternalProject | ConfiguredProject, newUncheckedFiles: T[], propertyReader: FilePropertyReader, newOptions: CompilerOptions, newTypeAcquisition: TypeAcquisition, compileOnSave: boolean, configFileErrors: Diagnostic[]) { @@ -1392,6 +1390,8 @@ namespace ts.server { } if (args.extraFileExtensions) { this.hostConfiguration.extraFileExtensions = args.extraFileExtensions; + // TODO: (sheetalkamat) We need to update the projects because of we might interprete more/less files + // depending on whether extra files extenstions are either added or removed this.logger.info("Host file extension mappings updated"); } } @@ -1465,9 +1465,20 @@ namespace ts.server { let project: ConfiguredProject | ExternalProject = this.findContainingExternalProject(fileName); if (!project) { - ({ configFileName, configFileErrors } = this.openOrUpdateConfiguredProjectForFile(fileName, projectRootPath)); + configFileName = this.getConfigFileNameForFile(fileName, projectRootPath); if (configFileName) { project = this.findConfiguredProjectByProjectName(configFileName); + if (!project) { + project = this.openConfigFile(configFileName, fileName); + + // even if opening config file was successful, it could still + // contain errors that were tolerated. + const errors = project.getGlobalProjectErrors(); + if (errors && errors.length > 0) { + // set configFileErrors only when the errors array is non-empty + configFileErrors = errors; + } + } } } if (project && !project.languageServiceEnabled) { @@ -1792,9 +1803,8 @@ namespace ts.server { for (const tsconfigFile of tsConfigFiles) { let project = this.findConfiguredProjectByProjectName(tsconfigFile); if (!project) { - const result = this.openConfigFile(tsconfigFile); - // TODO: save errors - project = result.success && result.project; + // errors are stored in the project + project = this.openConfigFile(tsconfigFile); } if (project && !contains(exisingConfigFiles, tsconfigFile)) { // keep project alive even if no documents are opened - its lifetime is bound to the lifetime of containing external project diff --git a/src/server/project.ts b/src/server/project.ts index 778f62947ff2d..74d4f2c907c36 100644 --- a/src/server/project.ts +++ b/src/server/project.ts @@ -626,6 +626,10 @@ namespace ts.server { for (const missingFilePath of missingFilePaths) { if (!this.missingFilesMap.has(missingFilePath)) { const fileWatcher = this.projectService.host.watchFile(missingFilePath, (_filename: string, eventKind: FileWatcherEventKind) => { + // TODO: (sheetalkamat) This needs to be fixed because of the way we create the projects + // Eg. ConfiguredProject and ExternalProject add roots only for existing files + // What this means is that if the file is not present when creating the project + // the program structure will not change because we created wrong program. if (eventKind === FileWatcherEventKind.Created && this.missingFilesMap.has(missingFilePath)) { fileWatcher.close(); this.missingFilesMap.delete(missingFilePath); From ae33ae894d290092bb36a5a2415d0214092ddc9d Mon Sep 17 00:00:00 2001 From: Sheetal Nandi Date: Fri, 30 Jun 2017 18:04:33 -0700 Subject: [PATCH 002/109] Restructure updating the configured project from disk and actual project update Also reload the projects when extra extension in the host change --- .../unittests/tsserverProjectSystem.ts | 18 +++-- src/server/editorServices.ts | 75 ++++++++++++------- src/server/project.ts | 4 +- 3 files changed, 63 insertions(+), 34 deletions(-) diff --git a/src/harness/unittests/tsserverProjectSystem.ts b/src/harness/unittests/tsserverProjectSystem.ts index b34669c26907a..83a9b58a0048f 100644 --- a/src/harness/unittests/tsserverProjectSystem.ts +++ b/src/harness/unittests/tsserverProjectSystem.ts @@ -770,7 +770,7 @@ namespace ts.projectSystem { // remove the tsconfig file host.reloadFS(filesWithoutConfig); - host.triggerFileWatcherCallback(configFile.path, FileWatcherEventKind.Changed); + host.triggerFileWatcherCallback(configFile.path, FileWatcherEventKind.Deleted); checkNumberOfInferredProjects(projectService, 2); checkNumberOfConfiguredProjects(projectService, 0); @@ -1837,16 +1837,18 @@ namespace ts.projectSystem { // HTML file will not be included in any projects yet checkNumberOfProjects(projectService, { configuredProjects: 1 }); - checkProjectActualFiles(projectService.configuredProjects[0], [file1.path, config.path]); + const configuredProj = projectService.configuredProjects[0]; + checkProjectActualFiles(configuredProj, [file1.path, config.path]); // Specify .html extension as mixed content const extraFileExtensions = [{ extension: ".html", scriptKind: ScriptKind.JS, isMixedContent: true }]; const configureHostRequest = makeSessionRequest(CommandNames.Configure, { extraFileExtensions }); session.executeCommand(configureHostRequest).response; - // HTML file still not included in the project as it is closed + // The configured project should now be updated to include html file checkNumberOfProjects(projectService, { configuredProjects: 1 }); - checkProjectActualFiles(projectService.configuredProjects[0], [file1.path, config.path]); + assert.strictEqual(projectService.configuredProjects[0], configuredProj, "Same configured project should be updated"); + checkProjectActualFiles(projectService.configuredProjects[0], [file1.path, file2.path, config.path]); // Open HTML file projectService.applyChangesInOpenFiles( @@ -2859,7 +2861,7 @@ namespace ts.projectSystem { const moduleFileNewPath = "/a/b/moduleFile1.ts"; moduleFile.path = moduleFileNewPath; host.reloadFS([moduleFile, file1]); - host.triggerFileWatcherCallback(moduleFileOldPath, FileWatcherEventKind.Changed); + host.triggerFileWatcherCallback(moduleFileOldPath, FileWatcherEventKind.Deleted); host.triggerDirectoryWatcherCallback("/a/b", moduleFile.path); host.runQueuedTimeoutCallbacks(); diags = session.executeCommand(getErrRequest).response; @@ -2911,7 +2913,8 @@ namespace ts.projectSystem { const moduleFileNewPath = "/a/b/moduleFile1.ts"; moduleFile.path = moduleFileNewPath; host.reloadFS([moduleFile, file1, configFile]); - host.triggerFileWatcherCallback(moduleFileOldPath, FileWatcherEventKind.Changed); + host.triggerFileWatcherCallback(moduleFileOldPath, FileWatcherEventKind.Deleted); + host.triggerFileWatcherCallback(moduleFileNewPath, FileWatcherEventKind.Created); host.triggerDirectoryWatcherCallback("/a/b", moduleFile.path); host.runQueuedTimeoutCallbacks(); diags = session.executeCommand(getErrRequest).response; @@ -2919,7 +2922,8 @@ namespace ts.projectSystem { moduleFile.path = moduleFileOldPath; host.reloadFS([moduleFile, file1, configFile]); - host.triggerFileWatcherCallback(moduleFileNewPath, FileWatcherEventKind.Changed); + host.triggerFileWatcherCallback(moduleFileNewPath, FileWatcherEventKind.Deleted); + host.triggerFileWatcherCallback(moduleFileOldPath, FileWatcherEventKind.Created); host.triggerDirectoryWatcherCallback("/a/b", moduleFile.path); host.runQueuedTimeoutCallbacks(); diags = session.executeCommand(getErrRequest).response; diff --git a/src/server/editorServices.ts b/src/server/editorServices.ts index 2eb1d73ad78e9..852b4be78d6ab 100644 --- a/src/server/editorServices.ts +++ b/src/server/editorServices.ts @@ -538,21 +538,21 @@ namespace ts.server { } } - private onSourceFileChanged(fileName: NormalizedPath) { + private onSourceFileChanged(fileName: NormalizedPath, eventKind: FileWatcherEventKind) { const info = this.getScriptInfoForNormalizedPath(fileName); if (!info) { this.logger.info(`Error: got watch notification for unknown file: ${fileName}`); return; } - if (!this.host.fileExists(fileName)) { + if (eventKind === FileWatcherEventKind.Deleted) { // File was deleted this.handleDeletedFile(info); } else { - if (info && (!info.isScriptOpen())) { + if (!info.isScriptOpen()) { if (info.containingProjects.length === 0) { - // Orphan script info, remove it as we can always reload it on next open + // Orphan script info, remove it as we can always reload it on next open file request info.stopWatcher(); this.filenameToScriptInfo.delete(info.path); } @@ -605,7 +605,7 @@ namespace ts.server { this.logger.info(`Type root file ${fileName} changed`); this.throttledOperations.schedule(project.getConfigFilePath() + " * type root", /*delay*/ 250, () => { project.updateTypes(); - this.updateConfiguredProject(project); // TODO: Figure out why this is needed (should be redundant?) + this.reloadConfiguredProject(project); // TODO: Figure out why this is needed (should be redundant?) this.refreshInferredProjects(); }); } @@ -644,7 +644,7 @@ namespace ts.server { // just update the current project. this.logger.info("Updating configured project"); - this.updateConfiguredProject(project); + this.updateConfiguredProject(project, projectOptions, configFileErrors); // Call refreshInferredProjects to clean up inferred projects we may have // created for the new files @@ -652,11 +652,19 @@ namespace ts.server { } } - private onConfigChangedForConfiguredProject(project: ConfiguredProject) { + private onConfigChangedForConfiguredProject(project: ConfiguredProject, eventKind: FileWatcherEventKind) { const configFileName = project.getConfigFilePath(); - this.logger.info(`Config file changed: ${configFileName}`); - const configFileErrors = this.updateConfiguredProject(project); - this.reportConfigFileDiagnostics(configFileName, configFileErrors, /*triggerFile*/ configFileName); + if (eventKind === FileWatcherEventKind.Deleted) { + this.logger.info(`Config file deleted: ${configFileName}`); + // TODO: (sheetalkamat) Get the list of open files from this project + // and update the projects + this.removeProject(project); + } + else { + this.logger.info(`Config file changed: ${configFileName}`); + this.reloadConfiguredProject(project); + this.reportConfigFileDiagnostics(configFileName, project.getGlobalProjectErrors(), /*triggerFile*/ configFileName); + } this.refreshInferredProjects(); } @@ -670,6 +678,8 @@ namespace ts.server { return; } + // TODO: (sheetalkamat) Technically we shouldnt have to do this: + // We can report these errors in reload projects or after reload project? const { configFileErrors } = this.convertConfigFileContentToProjectOptions(fileName); this.reportConfigFileDiagnostics(fileName, configFileErrors, fileName); @@ -883,7 +893,7 @@ namespace ts.server { this.openConfigFile(configFileName, fileName); } else { - this.updateConfiguredProject(project); + this.reloadConfiguredProject(project); } } } @@ -1133,7 +1143,7 @@ namespace ts.server { this.addFilesToProjectAndUpdateGraph(project, projectOptions.files, fileNamePropertyReader, clientFileName, projectOptions.typeAcquisition, configFileErrors); - project.watchConfigFile(project => this.onConfigChangedForConfiguredProject(project)); + project.watchConfigFile((project, eventKind) => this.onConfigChangedForConfiguredProject(project, eventKind)); if (!sizeLimitExceeded) { this.watchConfigDirectoryForProject(project, projectOptions); } @@ -1161,6 +1171,9 @@ namespace ts.server { project.addRoot(info); } else { + // TODO: (sheetalkamat) because the files are not added as a root, we wont have these available in + // missing files unless someone recreates the project or it was also refrenced in existing sourcefile + // Also these errors wouldnt show correct errors (configFileErrors || (configFileErrors = [])).push(createFileNotFoundDiagnostic(rootFilename)); } } @@ -1183,12 +1196,11 @@ namespace ts.server { const newRootScriptInfos: ScriptInfo[] = []; const newRootScriptInfoMap: NormalizedPathMap = createNormalizedPathMap(); - let projectErrors: Diagnostic[]; let rootFilesChanged = false; for (const f of newUncheckedFiles) { const newRootFile = propertyReader.getFileName(f); if (!this.host.fileExists(newRootFile)) { - (projectErrors || (projectErrors = [])).push(createFileNotFoundDiagnostic(newRootFile)); + (configFileErrors || (configFileErrors = [])).push(createFileNotFoundDiagnostic(newRootFile)); continue; } const normalizedPath = toNormalizedPath(newRootFile); @@ -1247,17 +1259,20 @@ namespace ts.server { if (compileOnSave !== undefined) { project.compileOnSaveEnabled = compileOnSave; } - project.setProjectErrors(concatenate(configFileErrors, projectErrors)); + project.setProjectErrors(configFileErrors); project.updateGraph(); } - private updateConfiguredProject(project: ConfiguredProject) { - if (!this.host.fileExists(project.getConfigFilePath())) { - this.logger.info("Config file deleted"); - this.removeProject(project); - return; - } + /** + * Read the config file of the project again and update the project + * @param project + */ + private reloadConfiguredProject(project: ConfiguredProject) { + // At this point, there is no reason to not have configFile in the host + + // TODO: (sheetalkamat) configErrors should always be in project and there shouldnt be + // any need to return these errors // note: the returned "success" is true does not mean the "configFileErrors" is empty. // because we might have tolerated the errors and kept going. So always return the configFileErrors @@ -1266,9 +1281,16 @@ namespace ts.server { if (!success) { // reset project settings to default this.updateNonInferredProject(project, [], fileNamePropertyReader, {}, {}, /*compileOnSave*/ false, configFileErrors); - return configFileErrors; } + return this.updateConfiguredProject(project, projectOptions, configFileErrors); + } + + /** + * Updates the configured project with updated config file contents + * @param project + */ + private updateConfiguredProject(project: ConfiguredProject, projectOptions: ProjectOptions, configFileErrors: Diagnostic[]) { if (this.exceededTotalSizeLimitForNonTsFiles(project.canonicalConfigFilePath, projectOptions.compilerOptions, projectOptions.files, fileNamePropertyReader)) { project.setCompilerOptions(projectOptions.compilerOptions); if (!project.languageServiceEnabled) { @@ -1277,13 +1299,13 @@ namespace ts.server { } project.disableLanguageService(); project.stopWatchingDirectory(); + project.setProjectErrors(configFileErrors); } else { project.enableLanguageService(); this.watchConfigDirectoryForProject(project, projectOptions); this.updateNonInferredProject(project, projectOptions.files, fileNamePropertyReader, projectOptions.compilerOptions, projectOptions.typeAcquisition, projectOptions.compileOnSave, configFileErrors); } - return configFileErrors; } createInferredProjectWithRootFileIfNecessary(root: ScriptInfo) { @@ -1324,7 +1346,7 @@ namespace ts.server { // do not watch files with mixed content - server doesn't know how to interpret it if (!info.hasMixedContent) { const { fileName } = info; - info.setWatcher(this.host.watchFile(fileName, _ => this.onSourceFileChanged(fileName))); + info.setWatcher(this.host.watchFile(fileName, (_fileName, eventKind) => this.onSourceFileChanged(fileName, eventKind))); } } @@ -1390,8 +1412,9 @@ namespace ts.server { } if (args.extraFileExtensions) { this.hostConfiguration.extraFileExtensions = args.extraFileExtensions; - // TODO: (sheetalkamat) We need to update the projects because of we might interprete more/less files + // We need to update the projects because of we might interprete more/less files // depending on whether extra files extenstions are either added or removed + this.reloadProjects(); this.logger.info("Host file extension mappings updated"); } } @@ -1408,6 +1431,8 @@ namespace ts.server { this.logger.info("reload projects."); // try to reload config file for all open files for (const info of this.openFiles) { + // TODO: (sheetalkamat) batch these = multiple open files from the same project will + // end up updating project that many times this.openOrUpdateConfiguredProjectForFile(info.fileName); } this.refreshInferredProjects(); diff --git a/src/server/project.ts b/src/server/project.ts index 74d4f2c907c36..6788081176ce9 100644 --- a/src/server/project.ts +++ b/src/server/project.ts @@ -1072,8 +1072,8 @@ namespace ts.server { })); } - watchConfigFile(callback: (project: ConfiguredProject) => void) { - this.projectFileWatcher = this.projectService.host.watchFile(this.getConfigFilePath(), _ => callback(this)); + watchConfigFile(callback: (project: ConfiguredProject, eventKind: FileWatcherEventKind) => void) { + this.projectFileWatcher = this.projectService.host.watchFile(this.getConfigFilePath(), (_fileName, eventKind) => callback(this, eventKind)); } watchTypeRoots(callback: (project: ConfiguredProject, path: string) => void) { From 75698a893f4ba7a0c3399a9082b484ff12e06767 Mon Sep 17 00:00:00 2001 From: Sheetal Nandi Date: Wed, 5 Jul 2017 18:03:11 -0700 Subject: [PATCH 003/109] Add project roots in the list of root files even if they arent present on the disk This helps in reporting errors as well as syncing of the configured/external project when the files are created --- src/harness/unittests/projectErrors.ts | 89 ++++++++++------- .../unittests/tsserverProjectSystem.ts | 37 ++++++- src/server/editorServices.ts | 97 +++++++++---------- src/server/project.ts | 45 +++++++-- src/server/scriptInfo.ts | 8 +- src/services/services.ts | 10 +- 6 files changed, 179 insertions(+), 107 deletions(-) diff --git a/src/harness/unittests/projectErrors.ts b/src/harness/unittests/projectErrors.ts index 142aa3bf1b1ba..7c694ec429653 100644 --- a/src/harness/unittests/projectErrors.ts +++ b/src/harness/unittests/projectErrors.ts @@ -20,19 +20,37 @@ namespace ts.projectSystem { } } + function checkDiagnosticsWithLinePos(errors: server.protocol.DiagnosticWithLinePosition[], expectedErrors: string[]) { + assert.equal(errors ? errors.length : 0, expectedErrors.length, `expected ${expectedErrors.length} error in the list`); + if (expectedErrors.length) { + for (let i = 0; i < errors.length; i++) { + const actualMessage = errors[i].message; + const expectedMessage = expectedErrors[i]; + assert.isTrue(actualMessage.indexOf(errors[i].message) === 0, `error message does not match, expected ${actualMessage} to start with ${expectedMessage}`); + } + } + } + it("external project - diagnostics for missing files", () => { const file1 = { path: "/a/b/app.ts", content: "" }; const file2 = { - path: "/a/b/lib.ts", + path: "/a/b/applib.ts", content: "" }; // only file1 exists - expect error - const host = createServerHost([file1]); - const projectService = createProjectService(host); + const host = createServerHost([file1, libFile]); + const session = createSession(host); + const projectService = session.getProjectService(); const projectFileName = "/a/b/test.csproj"; + const compilerOptionsRequest = { + type: "request", + command: server.CommandNames.CompilerOptionsDiagnosticsFull, + seq: 2, + arguments: { projectFileName } + }; { projectService.openExternalProject({ @@ -41,35 +59,27 @@ namespace ts.projectSystem { rootFiles: toExternalFiles([file1.path, file2.path]) }); - projectService.checkNumberOfProjects({ externalProjects: 1 }); - const knownProjects = projectService.synchronizeProjectList([]); - checkProjectErrors(knownProjects[0], ["File '/a/b/lib.ts' not found."]); + checkNumberOfProjects(projectService, { externalProjects: 1 }); + const diags = session.executeCommand(compilerOptionsRequest).response; + checkDiagnosticsWithLinePos(diags, ["File '/a/b/applib.ts' not found."]); } // only file2 exists - expect error - host.reloadFS([file2]); + host.reloadFS([file2, libFile]); { - projectService.openExternalProject({ - projectFileName, - options: {}, - rootFiles: toExternalFiles([file1.path, file2.path]) - }); - projectService.checkNumberOfProjects({ externalProjects: 1 }); - const knownProjects = projectService.synchronizeProjectList([]); - checkProjectErrors(knownProjects[0], ["File '/a/b/app.ts' not found."]); + host.triggerFileWatcherCallback(file1.path, FileWatcherEventKind.Deleted); + host.triggerFileWatcherCallback(file2.path, FileWatcherEventKind.Created); + checkNumberOfProjects(projectService, { externalProjects: 1 }); + const diags = session.executeCommand(compilerOptionsRequest).response; + checkDiagnosticsWithLinePos(diags, ["File '/a/b/app.ts' not found."]); } // both files exist - expect no errors - host.reloadFS([file1, file2]); + host.reloadFS([file1, file2, libFile]); { - projectService.openExternalProject({ - projectFileName, - options: {}, - rootFiles: toExternalFiles([file1.path, file2.path]) - }); - - projectService.checkNumberOfProjects({ externalProjects: 1 }); - const knownProjects = projectService.synchronizeProjectList([]); - checkProjectErrors(knownProjects[0], []); + host.triggerFileWatcherCallback(file1.path, FileWatcherEventKind.Created); + checkNumberOfProjects(projectService, { externalProjects: 1 }); + const diags = session.executeCommand(compilerOptionsRequest).response; + checkDiagnosticsWithLinePos(diags, []); } }); @@ -79,25 +89,34 @@ namespace ts.projectSystem { content: "" }; const file2 = { - path: "/a/b/lib.ts", + path: "/a/b/applib.ts", content: "" }; const config = { path: "/a/b/tsconfig.json", content: JSON.stringify({ files: [file1, file2].map(f => getBaseFileName(f.path)) }) }; - const host = createServerHost([file1, config]); - const projectService = createProjectService(host); - - projectService.openClientFile(file1.path); - projectService.checkNumberOfProjects({ configuredProjects: 1 }); - checkProjectErrors(projectService.synchronizeProjectList([])[0], ["File '/a/b/lib.ts' not found."]); + const host = createServerHost([file1, config, libFile]); + const session = createSession(host); + const projectService = session.getProjectService(); + openFilesForSession([file1], session); + checkNumberOfProjects(projectService, { configuredProjects: 1 }); + const project = projectService.configuredProjects[0]; + const compilerOptionsRequest = { + type: "request", + command: server.CommandNames.CompilerOptionsDiagnosticsFull, + seq: 2, + arguments: { projectFileName: project.getProjectName() } + }; + let diags = session.executeCommand(compilerOptionsRequest).response; + checkDiagnosticsWithLinePos(diags, ["File '/a/b/applib.ts' not found."]); - host.reloadFS([file1, file2, config]); + host.reloadFS([file1, file2, config, libFile]); host.triggerFileWatcherCallback(file2.path, FileWatcherEventKind.Created); - projectService.checkNumberOfProjects({ configuredProjects: 1 }); - checkProjectErrors(projectService.synchronizeProjectList([])[0], []); + checkNumberOfProjects(projectService, { configuredProjects: 1 }); + diags = session.executeCommand(compilerOptionsRequest).response; + checkDiagnosticsWithLinePos(diags, []); }); it("configured projects - diagnostics for corrupted config 1", () => { diff --git a/src/harness/unittests/tsserverProjectSystem.ts b/src/harness/unittests/tsserverProjectSystem.ts index 83a9b58a0048f..6ffc636dc7f20 100644 --- a/src/harness/unittests/tsserverProjectSystem.ts +++ b/src/harness/unittests/tsserverProjectSystem.ts @@ -907,6 +907,41 @@ namespace ts.projectSystem { checkProjectRootFiles(project, [commonFile1.path, commonFile2.path]); }); + it("handles the missing files - that were added to program because they were added with /// { + const file1: FileOrFolder = { + path: "/a/b/commonFile1.ts", + content: `/// + let x = y` + }; + const host = createServerHost([file1, libFile]); + const session = createSession(host); + openFilesForSession([file1], session); + const projectService = session.getProjectService(); + + checkNumberOfInferredProjects(projectService, 1); + const project = projectService.inferredProjects[0]; + checkProjectRootFiles(project, [file1.path]); + checkProjectActualFiles(project, [file1.path, libFile.path]); + const getErrRequest = makeSessionRequest( + server.CommandNames.SemanticDiagnosticsSync, + { file: file1.path } + ); + let diags = session.executeCommand(getErrRequest).response; + + // Two errors: CommonFile2 not found and cannot find name y + assert.equal(diags.length, 2, diags.map(diag => flattenDiagnosticMessageText(diag.text, "\n")).join("\n")); + + host.reloadFS([file1, commonFile2, libFile]); + host.triggerFileWatcherCallback(commonFile2.path, FileWatcherEventKind.Created); + host.runQueuedTimeoutCallbacks(); + checkNumberOfInferredProjects(projectService, 1); + assert.strictEqual(projectService.inferredProjects[0], project, "Inferred project should be same"); + checkProjectRootFiles(project, [file1.path]); + checkProjectActualFiles(project, [file1.path, libFile.path, commonFile2.path]); + diags = session.executeCommand(getErrRequest).response; + assert.equal(diags.length, 0); + }); + it("should create new inferred projects for files excluded from a configured project", () => { const configFile: FileOrFolder = { path: "/a/b/tsconfig.json", @@ -2869,7 +2904,7 @@ namespace ts.projectSystem { moduleFile.path = moduleFileOldPath; host.reloadFS([moduleFile, file1]); - host.triggerFileWatcherCallback(moduleFileNewPath, FileWatcherEventKind.Changed); + host.triggerFileWatcherCallback(moduleFileNewPath, FileWatcherEventKind.Deleted); host.triggerDirectoryWatcherCallback("/a/b", moduleFile.path); host.runQueuedTimeoutCallbacks(); diff --git a/src/server/editorServices.ts b/src/server/editorServices.ts index 852b4be78d6ab..7d43a1e67a67e 100644 --- a/src/server/editorServices.ts +++ b/src/server/editorServices.ts @@ -245,10 +245,6 @@ namespace ts.server { } } - function createFileNotFoundDiagnostic(fileName: string) { - return createCompilerDiagnostic(Diagnostics.File_0_not_found, fileName); - } - /** * TODO: enforce invariants: * - script info can be never migrate to state - root file in inferred project, this is only a starting point @@ -1166,15 +1162,14 @@ namespace ts.server { const rootFilename = propertyReader.getFileName(f); const scriptKind = propertyReader.getScriptKind(f); const hasMixedContent = propertyReader.hasMixedContent(f, this.hostConfiguration.extraFileExtensions); + const fileName = toNormalizedPath(rootFilename); if (this.host.fileExists(rootFilename)) { - const info = this.getOrCreateScriptInfoForNormalizedPath(toNormalizedPath(rootFilename), /*openedByClient*/ clientFileName === rootFilename, /*fileContent*/ undefined, scriptKind, hasMixedContent); + const info = this.getOrCreateScriptInfoForNormalizedPath(fileName, /*openedByClient*/ clientFileName === rootFilename, /*fileContent*/ undefined, scriptKind, hasMixedContent); project.addRoot(info); } else { - // TODO: (sheetalkamat) because the files are not added as a root, we wont have these available in - // missing files unless someone recreates the project or it was also refrenced in existing sourcefile - // Also these errors wouldnt show correct errors - (configFileErrors || (configFileErrors = [])).push(createFileNotFoundDiagnostic(rootFilename)); + // Create the file root with just the filename so that LS will have correct set of roots + project.addMissingFileRoot(fileName); } } project.setProjectErrors(configFileErrors); @@ -1192,67 +1187,63 @@ namespace ts.server { } private updateNonInferredProject(project: ExternalProject | ConfiguredProject, newUncheckedFiles: T[], propertyReader: FilePropertyReader, newOptions: CompilerOptions, newTypeAcquisition: TypeAcquisition, compileOnSave: boolean, configFileErrors: Diagnostic[]) { - const oldRootScriptInfos = project.getRootScriptInfos(); - const newRootScriptInfos: ScriptInfo[] = []; - const newRootScriptInfoMap: NormalizedPathMap = createNormalizedPathMap(); + const projectRootFilesMap = project.getRootFilesMap(); + const newRootScriptInfoMap: Map = createMap(); - let rootFilesChanged = false; for (const f of newUncheckedFiles) { const newRootFile = propertyReader.getFileName(f); - if (!this.host.fileExists(newRootFile)) { - (configFileErrors || (configFileErrors = [])).push(createFileNotFoundDiagnostic(newRootFile)); - continue; - } const normalizedPath = toNormalizedPath(newRootFile); - let scriptInfo = this.getScriptInfoForNormalizedPath(normalizedPath); - if (!scriptInfo || !project.isRoot(scriptInfo)) { - rootFilesChanged = true; - if (!scriptInfo) { - const scriptKind = propertyReader.getScriptKind(f); - const hasMixedContent = propertyReader.hasMixedContent(f, this.hostConfiguration.extraFileExtensions); - scriptInfo = this.getOrCreateScriptInfoForNormalizedPath(normalizedPath, /*openedByClient*/ false, /*fileContent*/ undefined, scriptKind, hasMixedContent); - } - } - newRootScriptInfos.push(scriptInfo); - newRootScriptInfoMap.set(scriptInfo.fileName, scriptInfo); - } - - if (rootFilesChanged || newRootScriptInfos.length !== oldRootScriptInfos.length) { - let toAdd: ScriptInfo[]; - let toRemove: ScriptInfo[]; - for (const oldFile of oldRootScriptInfos) { - if (!newRootScriptInfoMap.contains(oldFile.fileName)) { - (toRemove || (toRemove = [])).push(oldFile); - } - } - for (const newFile of newRootScriptInfos) { - if (!project.isRoot(newFile)) { - (toAdd || (toAdd = [])).push(newFile); - } - } - if (toRemove) { - for (const f of toRemove) { - project.removeFile(f); + let scriptInfo: ScriptInfo | NormalizedPath; + let path: Path; + if (!this.host.fileExists(newRootFile)) { + path = normalizedPathToPath(normalizedPath, this.host.getCurrentDirectory(), this.toCanonicalFileName); + const existingValue = projectRootFilesMap.get(path); + if (isScriptInfo(existingValue)) { + project.removeFile(existingValue); + projectRootFilesMap.set(path, normalizedPath); } + scriptInfo = normalizedPath; } - if (toAdd) { - for (const f of toAdd) { - if (f.isScriptOpen() && isRootFileInInferredProject(f)) { + else { + const scriptKind = propertyReader.getScriptKind(f); + const hasMixedContent = propertyReader.hasMixedContent(f, this.hostConfiguration.extraFileExtensions); + scriptInfo = this.getOrCreateScriptInfoForNormalizedPath(normalizedPath, /*openedByClient*/ false, /*fileContent*/ undefined, scriptKind, hasMixedContent); + path = scriptInfo.path; + // If this script info is not already a root add it + if (!project.isRoot(scriptInfo)) { + if (scriptInfo.isScriptOpen() && isRootFileInInferredProject(scriptInfo)) { // if file is already root in some inferred project // - remove the file from that project and delete the project if necessary - const inferredProject = f.containingProjects[0]; - inferredProject.removeFile(f); + const inferredProject = scriptInfo.containingProjects[0]; + inferredProject.removeFile(scriptInfo); if (!inferredProject.hasRoots()) { this.removeProject(inferredProject); } } - project.addRoot(f); + project.addRoot(scriptInfo); } } + newRootScriptInfoMap.set(path, scriptInfo); + } + + // project's root file map size is always going to be larger than new roots map + // as we have already all the new files to the project + if (projectRootFilesMap.size > newRootScriptInfoMap.size) { + projectRootFilesMap.forEach((value, path) => { + if (!newRootScriptInfoMap.has(path)) { + if (isScriptInfo(value)) { + project.removeFile(value); + } + else { + projectRootFilesMap.delete(path); + project.markAsDirty(); + } + } + }); } project.setCompilerOptions(newOptions); - (project).setTypeAcquisition(newTypeAcquisition); + project.setTypeAcquisition(newTypeAcquisition); // VS only set the CompileOnSaveEnabled option in the request if the option was changed recently // therefore if it is undefined, it should not be updated. diff --git a/src/server/project.ts b/src/server/project.ts index 6788081176ce9..7e94de2afac12 100644 --- a/src/server/project.ts +++ b/src/server/project.ts @@ -102,9 +102,19 @@ namespace ts.server { (mod: { typescript: typeof ts }): PluginModule; } + /** + * The project root can be script info - if root is present, + * or it could be just normalized path if root wasnt present on the host(only for non inferred project) + */ + export type ProjectRoot = ScriptInfo | NormalizedPath; + /* @internal */ + export function isScriptInfo(value: ProjectRoot): value is ScriptInfo { + return value instanceof ScriptInfo; + } + export abstract class Project { private rootFiles: ScriptInfo[] = []; - private rootFilesMap: Map = createMap(); + private rootFilesMap: Map = createMap(); private program: ts.Program; private externalFiles: SortedReadonlyArray; private missingFilesMap: Map = createMap(); @@ -335,12 +345,13 @@ namespace ts.server { getRootFilesLSHost() { const result: string[] = []; if (this.rootFiles) { - for (const f of this.rootFiles) { - if (this.languageServiceEnabled || f.isScriptOpen()) { + this.rootFilesMap.forEach((value, _path) => { + const f: ScriptInfo = isScriptInfo(value) && value; + if (this.languageServiceEnabled || (f && f.isScriptOpen())) { // if language service is disabled - process only files that are open - result.push(f.fileName); + result.push(f ? f.fileName : value as NormalizedPath); } - } + }); if (this.typingFiles) { for (const f of this.typingFiles) { result.push(f); @@ -350,6 +361,10 @@ namespace ts.server { return result; } + getRootFilesMap() { + return this.rootFilesMap; + } + getRootScriptInfos() { return this.rootFiles; } @@ -458,7 +473,7 @@ namespace ts.server { } isRoot(info: ScriptInfo) { - return this.rootFilesMap && this.rootFilesMap.has(info.path); + return this.rootFilesMap && this.rootFilesMap.get(info.path) === info; } // add a root file to project @@ -472,6 +487,14 @@ namespace ts.server { } } + // add a root file to project + addMissingFileRoot(fileName: NormalizedPath) { + const path = toPath(fileName, this.projectService.host.getCurrentDirectory(), + createGetCanonicalFileName(this.projectService.host.useCaseSensitiveFileNames)); + this.rootFilesMap.set(path, fileName); + this.markAsDirty(); + } + removeFile(info: ScriptInfo, detachFromProject = true) { if (this.isRoot(info)) { this.removeRoot(info); @@ -671,6 +694,12 @@ namespace ts.server { getScriptInfoLSHost(fileName: string) { const scriptInfo = this.projectService.getOrCreateScriptInfo(fileName, /*openedByClient*/ false); if (scriptInfo) { + const existingValue = this.rootFilesMap.get(scriptInfo.path); + if (existingValue !== undefined && existingValue !== scriptInfo) { + // This was missing path earlier but now the file exists. Update the root + this.rootFiles.push(scriptInfo); + this.rootFilesMap.set(scriptInfo.path, scriptInfo); + } scriptInfo.attachToProject(this); } return scriptInfo; @@ -898,12 +927,12 @@ namespace ts.server { } removeRoot(info: ScriptInfo) { + super.removeRoot(info); if (this._isJsInferredProject && info.isJavaScript()) { - if (filter(this.getRootScriptInfos(), info => info.isJavaScript()).length === 0) { + if (!some(this.getRootScriptInfos(), info => info.isJavaScript())) { this.toggleJsInferredProject(/*isJsInferredProject*/ false); } } - super.removeRoot(info); } getProjectRootPath() { diff --git a/src/server/scriptInfo.ts b/src/server/scriptInfo.ts index 534d73a641018..ee5e45c7c4753 100644 --- a/src/server/scriptInfo.ts +++ b/src/server/scriptInfo.ts @@ -254,8 +254,14 @@ namespace ts.server { detachAllProjects() { for (const p of this.containingProjects) { + const isInfoRoot = p.isRoot(this); // detach is unnecessary since we'll clean the list of containing projects anyways p.removeFile(this, /*detachFromProjects*/ false); + // If the info was for the external or configured project's root, + // add missing file as the root + if (isInfoRoot && p.projectKind !== ProjectKind.Inferred) { + p.addMissingFileRoot(this.fileName); + } } this.containingProjects.length = 0; } @@ -376,4 +382,4 @@ namespace ts.server { return this.scriptKind === ScriptKind.JS || this.scriptKind === ScriptKind.JSX; } } -} \ No newline at end of file +} diff --git a/src/services/services.ts b/src/services/services.ts index 759de900b848a..28a29352b80c2 100644 --- a/src/services/services.ts +++ b/src/services/services.ts @@ -862,15 +862,7 @@ namespace ts { } public getRootFileNames(): string[] { - const fileNames: string[] = []; - - this.fileNameToEntry.forEach(value => { - if (value) { - fileNames.push(value.hostFileName); - } - }); - - return fileNames; + return this.host.getScriptFileNames(); } public getVersion(path: Path): string { From f15491046c2a3d5597546953d424fa34affc8fcb Mon Sep 17 00:00:00 2001 From: Sheetal Nandi Date: Thu, 6 Jul 2017 17:50:59 -0700 Subject: [PATCH 004/109] Move the server file system to handle watches on file/folder create, update and delete This will ensure that the calling of watches doesnt rely on writing test correctly --- .../unittests/tsserverProjectSystem.ts | 324 +++++++++++++----- src/harness/unittests/typingsInstaller.ts | 2 +- 2 files changed, 235 insertions(+), 91 deletions(-) diff --git a/src/harness/unittests/tsserverProjectSystem.ts b/src/harness/unittests/tsserverProjectSystem.ts index 6ffc636dc7f20..fda10cae6c9d5 100644 --- a/src/harness/unittests/tsserverProjectSystem.ts +++ b/src/harness/unittests/tsserverProjectSystem.ts @@ -254,29 +254,29 @@ namespace ts.projectSystem { } export function isFolder(s: FSEntry): s is Folder { - return isArray((s).entries); + return s && isArray((s).entries); } export function isFile(s: FSEntry): s is File { - return typeof (s).content === "string"; + return s && typeof (s).content === "string"; } - export function addFolder(fullPath: string, toPath: (s: string) => Path, fs: Map): Folder { - const path = toPath(fullPath); - if (fs.has(path)) { - Debug.assert(isFolder(fs.get(path))); - return (fs.get(path)); + function invokeDirectoryWatcher(callbacks: DirectoryWatcherCallback[], fileName: string) { + if (callbacks) { + const cbs = callbacks.slice(); + for (const cb of cbs) { + cb(fileName); + } } + } - const entry: Folder = { path, entries: [], fullPath }; - fs.set(path, entry); - - const baseFullPath = getDirectoryPath(fullPath); - if (fullPath !== baseFullPath) { - addFolder(baseFullPath, toPath, fs).entries.push(entry); + function invokeFileWatcher(callbacks: FileWatcherCallback[], fileName: string, eventId: FileWatcherEventKind) { + if (callbacks) { + const cbs = callbacks.slice(); + for (const cb of cbs) { + cb(fileName, eventId); + } } - - return entry; } export function checkMapKeys(caption: string, map: Map, expectedKeys: string[]) { @@ -315,8 +315,8 @@ namespace ts.projectSystem { checkMapKeys("watchedFiles", host.watchedFiles, expectedFiles); } - export function checkWatchedDirectories(host: TestServerHost, expectedDirectories: string[]) { - checkMapKeys("watchedDirectories", host.watchedDirectories, expectedDirectories); + export function checkWatchedDirectories(host: TestServerHost, expectedDirectories: string[], recursive = false) { + checkMapKeys("watchedDirectories", recursive ? host.watchedDirectoriesRecursive : host.watchedDirectories, expectedDirectories); } export function checkProjectActualFiles(project: server.Project, expectedFiles: string[]) { @@ -370,17 +370,16 @@ namespace ts.projectSystem { private readonly output: string[] = []; - private fs: Map; + private fs: Map = createMap(); private getCanonicalFileName: (s: string) => string; private toPath: (f: string) => Path; private timeoutCallbacks = new Callbacks(); private immediateCallbacks = new Callbacks(); - readonly watchedDirectories = createMultiMap<{ cb: DirectoryWatcherCallback, recursive: boolean }>(); + readonly watchedDirectories = createMultiMap(); + readonly watchedDirectoriesRecursive = createMultiMap(); readonly watchedFiles = createMultiMap(); - private filesOrFolders: FileOrFolder[]; - constructor(public useCaseSensitiveFileNames: boolean, private executingFilePath: string, private currentDirectory: string, fileOrFolderList: FileOrFolder[], public readonly newLine = "\n") { this.getCanonicalFileName = createGetCanonicalFileName(useCaseSensitiveFileNames); this.toPath = s => toPath(s, currentDirectory, this.getCanonicalFileName); @@ -388,58 +387,201 @@ namespace ts.projectSystem { this.reloadFS(fileOrFolderList); } - reloadFS(filesOrFolders: FileOrFolder[]) { - this.filesOrFolders = filesOrFolders; - this.fs = createMap(); + private toFullPath(s: string) { + const fullPath = getNormalizedAbsolutePath(s, this.currentDirectory); + return this.toPath(fullPath); + } + + reloadFS(fileOrFolderList: FileOrFolder[]) { + const mapNewLeaves = createMap(); + const isNewFs = this.fs.size === 0; // always inject safelist file in the list of files - for (const fileOrFolder of filesOrFolders.concat(safeList)) { - const path = this.toPath(fileOrFolder.path); + for (const fileOrFolder of fileOrFolderList.concat(safeList)) { + const path = this.toFullPath(fileOrFolder.path); + mapNewLeaves.set(path, true); + // If its a change + const currentEntry = this.fs.get(path); + if (currentEntry) { + if (isFile(currentEntry)) { + if (typeof fileOrFolder.content === "string") { + // Update file + if (currentEntry.content !== fileOrFolder.content) { + currentEntry.content = fileOrFolder.content; + this.invokeFileWatcher(currentEntry.path, FileWatcherEventKind.Changed); + } + } + else { + // TODO: Changing from file => folder + } + } + else { + // Folder + if (typeof fileOrFolder.content === "string") { + // TODO: Changing from folder => file + } + else { + // Folder update: Nothing to do. + } + } + } + else { + this.ensureFileOrFolder(fileOrFolder); + } + } + + if (!isNewFs) { + this.fs.forEach((fileOrFolder, path) => { + // If this entry is not from the new file or folder + if (!mapNewLeaves.get(path)) { + // Leaf entries that arent in new list => remove these + if (isFile(fileOrFolder) || isFolder(fileOrFolder) && fileOrFolder.entries.length === 0) { + this.removeFileOrFolder(fileOrFolder, folder => !mapNewLeaves.get(folder.path)); + } + } + }); + } + } + + ensureFileOrFolder(fileOrFolder: FileOrFolder) { + if (typeof fileOrFolder.content === "string") { + const file = this.toFile(fileOrFolder); + Debug.assert(!this.fs.get(file.path)); + const baseFolder = this.ensureFolder(getDirectoryPath(file.fullPath)); + this.addFileOrFolderInFolder(baseFolder, file); + } + else { const fullPath = getNormalizedAbsolutePath(fileOrFolder.path, this.currentDirectory); - if (typeof fileOrFolder.content === "string") { - const entry = { path, content: fileOrFolder.content, fullPath, fileSize: fileOrFolder.fileSize }; - this.fs.set(path, entry); - addFolder(getDirectoryPath(fullPath), this.toPath, this.fs).entries.push(entry); + this.ensureFolder(fullPath); + } + } + + private ensureFolder(fullPath: string): Folder { + const path = this.toPath(fullPath); + let folder = this.fs.get(path) as Folder; + if (!folder) { + folder = this.toFolder(fullPath); + const baseFullPath = getDirectoryPath(fullPath); + if (fullPath !== baseFullPath) { + // Add folder in the base folder + const baseFolder = this.ensureFolder(baseFullPath); + this.addFileOrFolderInFolder(baseFolder, folder); } else { - addFolder(fullPath, this.toPath, this.fs); + // root folder + Debug.assert(this.fs.size === 0); + this.fs.set(path, folder); } } + Debug.assert(isFolder(folder)); + return folder; + } + + private addFileOrFolderInFolder(folder: Folder, fileOrFolder: File | Folder) { + folder.entries.push(fileOrFolder); + this.fs.set(fileOrFolder.path, fileOrFolder); + + if (isFile(fileOrFolder)) { + this.invokeFileWatcher(fileOrFolder.path, FileWatcherEventKind.Created); + } + this.invokeDirectoryWatcher(folder.path, fileOrFolder.path); + } + + private removeFileOrFolder(fileOrFolder: File | Folder, isRemovableLeafFolder: (folder: Folder) => boolean) { + const basePath = getDirectoryPath(fileOrFolder.path); + const baseFolder = this.fs.get(basePath) as Folder; + if (basePath !== fileOrFolder.path) { + Debug.assert(!!baseFolder); + filterMutate(baseFolder.entries, entry => entry !== fileOrFolder); + } + this.fs.delete(fileOrFolder.path); + + if (isFile(fileOrFolder)) { + this.invokeFileWatcher(fileOrFolder.path, FileWatcherEventKind.Deleted); + } + else { + Debug.assert(fileOrFolder.entries.length === 0); + invokeDirectoryWatcher(this.watchedDirectories.get(fileOrFolder.path), fileOrFolder.path); + invokeDirectoryWatcher(this.watchedDirectoriesRecursive.get(fileOrFolder.path), fileOrFolder.path); + } + + if (basePath !== fileOrFolder.path) { + if (baseFolder.entries.length === 0 && isRemovableLeafFolder(baseFolder)) { + this.removeFileOrFolder(baseFolder, isRemovableLeafFolder); + } + else { + this.invokeRecursiveDirectoryWatcher(baseFolder.path, fileOrFolder.path); + } + } + } + + private invokeFileWatcher(filePath: Path, eventId: FileWatcherEventKind) { + const callbacks = this.watchedFiles.get(filePath); + invokeFileWatcher(callbacks, filePath, eventId); + } + + private invokeDirectoryWatcher(folderPath: Path, fileName: string) { + invokeDirectoryWatcher(this.watchedDirectories.get(folderPath), fileName); + this.invokeRecursiveDirectoryWatcher(folderPath, fileName); + } + + private invokeRecursiveDirectoryWatcher(path: Path, fileName: string) { + invokeDirectoryWatcher(this.watchedDirectoriesRecursive.get(path), fileName); + const basePath = getDirectoryPath(path); + if (path !== basePath) { + this.invokeRecursiveDirectoryWatcher(basePath, fileName); + } + } + + private toFile(fileOrFolder: FileOrFolder): File { + const fullPath = getNormalizedAbsolutePath(fileOrFolder.path, this.currentDirectory); + return { + path: this.toPath(fullPath), + content: fileOrFolder.content, + fullPath, + fileSize: fileOrFolder.fileSize + }; + } + + private toFolder(path: string): Folder { + const fullPath = getNormalizedAbsolutePath(path, this.currentDirectory); + return { + path: this.toPath(fullPath), + entries: [], + fullPath + }; } fileExists(s: string) { - const path = this.toPath(s); - return this.fs.has(path) && isFile(this.fs.get(path)); + const path = this.toFullPath(s); + return isFile(this.fs.get(path)); } getFileSize(s: string) { - const path = this.toPath(s); - if (this.fs.has(path)) { - const entry = this.fs.get(path); - if (isFile(entry)) { - return entry.fileSize ? entry.fileSize : entry.content.length; - } + const path = this.toFullPath(s); + const entry = this.fs.get(path); + if (isFile(entry)) { + return entry.fileSize ? entry.fileSize : entry.content.length; } return undefined; } directoryExists(s: string) { - const path = this.toPath(s); - return this.fs.has(path) && isFolder(this.fs.get(path)); + const path = this.toFullPath(s); + return isFolder(this.fs.get(path)); } getDirectories(s: string) { - const path = this.toPath(s); - if (!this.fs.has(path)) { - return []; - } - else { - const entry = this.fs.get(path); - return isFolder(entry) ? map(entry.entries, x => getBaseFileName(x.fullPath)) : []; + const path = this.toFullPath(s); + const folder = this.fs.get(path); + if (isFolder(folder)) { + return map(folder.entries, x => getBaseFileName(x.fullPath)); } + Debug.fail(folder ? "getDirectories called on file" : "getDirectories called on missing folder"); + return []; } readDirectory(path: string, extensions?: string[], exclude?: string[], include?: string[], depth?: number): string[] { - return ts.matchFiles(path, extensions, exclude, include, this.useCaseSensitiveFileNames, this.getCurrentDirectory(), depth, (dir) => { + return ts.matchFiles(this.toFullPath(path), extensions, exclude, include, this.useCaseSensitiveFileNames, this.getCurrentDirectory(), depth, (dir) => { const result: FileSystemEntries = { directories: [], files: [] @@ -453,6 +595,9 @@ namespace ts.projectSystem { else if (isFile(entry)) { result.files.push(entry.fullPath); } + else { + Debug.fail("Unknown entry"); + } }); } return result; @@ -460,13 +605,13 @@ namespace ts.projectSystem { } watchDirectory(directoryName: string, callback: DirectoryWatcherCallback, recursive: boolean): DirectoryWatcher { - const path = this.toPath(directoryName); - const cbWithRecursive = { cb: callback, recursive }; - this.watchedDirectories.add(path, cbWithRecursive); + const path = this.toFullPath(directoryName); + const map = recursive ? this.watchedDirectoriesRecursive : this.watchedDirectories; + map.add(path, callback); return { referenceCount: 0, directoryName, - close: () => this.watchedDirectories.remove(path, cbWithRecursive) + close: () => map.remove(path, callback) }; } @@ -474,28 +619,28 @@ namespace ts.projectSystem { return Harness.LanguageService.mockHash(s); } - triggerDirectoryWatcherCallback(directoryName: string, fileName: string): void { - const path = this.toPath(directoryName); - const callbacks = this.watchedDirectories.get(path); - if (callbacks) { - for (const callback of callbacks) { - callback.cb(fileName); - } - } + triggerDirectoryWatcherCallback(_directoryName: string, _fileName: string): void { + //const path = this.toPath(directoryName); + //const callbacks = this.watchedDirectories.get(path); + //if (callbacks) { + // for (const callback of callbacks) { + // callback.cb(fileName); + // } + //} } - triggerFileWatcherCallback(fileName: string, eventKind: FileWatcherEventKind): void { - const path = this.toPath(fileName); - const callbacks = this.watchedFiles.get(path); - if (callbacks) { - for (const callback of callbacks) { - callback(path, eventKind); - } - } + triggerFileWatcherCallback(_fileName: string, _eventKind: FileWatcherEventKind): void { + //const path = this.toPath(fileName); + //const callbacks = this.watchedFiles.get(path); + //if (callbacks) { + // for (const callback of callbacks) { + // callback(path, eventKind); + // } + //} } watchFile(fileName: string, callback: FileWatcherCallback) { - const path = this.toPath(fileName); + const path = this.toFullPath(fileName); this.watchedFiles.add(path, callback); return { close: () => this.watchedFiles.remove(path, callback) }; } @@ -531,27 +676,26 @@ namespace ts.projectSystem { } createDirectory(directoryName: string): void { - this.createFileOrFolder({ path: directoryName }); + const folder = this.toFolder(directoryName); + + // base folder has to be present + const base = getDirectoryPath(folder.fullPath); + const baseFolder = this.fs.get(base) as Folder; + Debug.assert(isFolder(baseFolder)); + + Debug.assert(!this.fs.get(folder.path), isFile(this.fs.get(folder.path)) ? `Found the file ${folder.path}` : `Found the folder ${folder.path}`); + this.addFileOrFolderInFolder(baseFolder, folder); } writeFile(path: string, content: string): void { - this.createFileOrFolder({ path, content, fileSize: content.length }); - } + const file = this.toFile({ path, content }); - createFileOrFolder(f: FileOrFolder, createParentDirectory = false): void { - const base = getDirectoryPath(f.path); - if (base !== f.path && !this.directoryExists(base)) { - if (createParentDirectory) { - // TODO: avoid reloading FS on every creation - this.createFileOrFolder({ path: base }, createParentDirectory); - } - else { - throw new Error(`directory ${base} does not exist`); - } - } - const filesOrFolders = this.filesOrFolders.slice(0); - filesOrFolders.push(f); - this.reloadFS(filesOrFolders); + // base folder has to be present + const base = getDirectoryPath(file.fullPath); + const folder = this.fs.get(base) as Folder; + Debug.assert(isFolder(folder)); + + this.addFileOrFolderInFolder(folder, file); } write(message: string) { @@ -566,7 +710,7 @@ namespace ts.projectSystem { this.output.length = 0; } - readonly readFile = (s: string) => (this.fs.get(this.toPath(s))).content; + readonly readFile = (s: string) => (this.fs.get(this.toFullPath(s))).content; readonly resolvePath = (s: string) => s; readonly getExecutingFilePath = () => this.executingFilePath; readonly getCurrentDirectory = () => this.currentDirectory; @@ -738,7 +882,7 @@ namespace ts.projectSystem { checkProjectRootFiles(project, [file1.path, file2.path]); // watching all files except one that was open checkWatchedFiles(host, [configFile.path, file2.path, libFile.path]); - checkWatchedDirectories(host, [getDirectoryPath(configFile.path)]); + checkWatchedDirectories(host, [getDirectoryPath(configFile.path)], /*recursive*/ true); }); it("add and then remove a config file in a folder with loose files", () => { @@ -785,7 +929,7 @@ namespace ts.projectSystem { const host = createServerHost([commonFile1, libFile, configFile]); const projectService = createProjectService(host); projectService.openClientFile(commonFile1.path); - checkWatchedDirectories(host, ["/a/b"]); + checkWatchedDirectories(host, ["/a/b"], /*recursive*/ true); checkNumberOfConfiguredProjects(projectService, 1); const project = projectService.configuredProjects[0]; diff --git a/src/harness/unittests/typingsInstaller.ts b/src/harness/unittests/typingsInstaller.ts index c23e152998470..8c201c84a1355 100644 --- a/src/harness/unittests/typingsInstaller.ts +++ b/src/harness/unittests/typingsInstaller.ts @@ -38,7 +38,7 @@ namespace ts.projectSystem { function executeCommand(self: Installer, host: TestServerHost, installedTypings: string[] | string, typingFiles: FileOrFolder[], cb: TI.RequestCompletedAction): void { self.addPostExecAction(installedTypings, success => { for (const file of typingFiles) { - host.createFileOrFolder(file, /*createParentDirectory*/ true); + host.ensureFileOrFolder(file); } cb(success); }); From 0e4436774d79875d20d04a3deb3232e1ba23e906 Mon Sep 17 00:00:00 2001 From: Sheetal Nandi Date: Thu, 6 Jul 2017 18:11:00 -0700 Subject: [PATCH 005/109] Remove the functions to trigger watch callbacks now that it is auto on reloading FS --- src/harness/unittests/compileOnSave.ts | 6 +- src/harness/unittests/projectErrors.ts | 6 -- .../unittests/tsserverProjectSystem.ts | 55 ------------------- src/harness/unittests/typingsInstaller.ts | 1 - 4 files changed, 1 insertion(+), 67 deletions(-) diff --git a/src/harness/unittests/compileOnSave.ts b/src/harness/unittests/compileOnSave.ts index 79e1f921dcb82..184e4804cb806 100644 --- a/src/harness/unittests/compileOnSave.ts +++ b/src/harness/unittests/compileOnSave.ts @@ -208,7 +208,6 @@ namespace ts.projectSystem { file1Consumer1.content = `let y = 10;`; host.reloadFS([moduleFile1, file1Consumer1, file1Consumer2, configFile, libFile]); - host.triggerFileWatcherCallback(file1Consumer1.path, FileWatcherEventKind.Changed); session.executeCommand(changeModuleFile1ShapeRequest1); sendAffectedFileRequestAndCheckResult(session, moduleFile1FileListRequest, [{ projectFileName: configFile.path, files: [moduleFile1, file1Consumer2] }]); @@ -225,7 +224,6 @@ namespace ts.projectSystem { session.executeCommand(changeModuleFile1ShapeRequest1); // Delete file1Consumer2 host.reloadFS([moduleFile1, file1Consumer1, configFile, libFile]); - host.triggerFileWatcherCallback(file1Consumer2.path, FileWatcherEventKind.Deleted); sendAffectedFileRequestAndCheckResult(session, moduleFile1FileListRequest, [{ projectFileName: configFile.path, files: [moduleFile1, file1Consumer1] }]); }); @@ -242,7 +240,6 @@ namespace ts.projectSystem { content: `import {Foo} from "./moduleFile1"; let y = Foo();` }; host.reloadFS([moduleFile1, file1Consumer1, file1Consumer2, file1Consumer3, globalFile3, configFile, libFile]); - host.triggerDirectoryWatcherCallback(ts.getDirectoryPath(file1Consumer3.path), file1Consumer3.path); host.runQueuedTimeoutCallbacks(); session.executeCommand(changeModuleFile1ShapeRequest1); sendAffectedFileRequestAndCheckResult(session, moduleFile1FileListRequest, [{ projectFileName: configFile.path, files: [moduleFile1, file1Consumer1, file1Consumer2, file1Consumer3] }]); @@ -475,7 +472,6 @@ namespace ts.projectSystem { openFilesForSession([referenceFile1], session); host.reloadFS([referenceFile1, configFile]); - host.triggerFileWatcherCallback(moduleFile1.path, FileWatcherEventKind.Deleted); const request = makeSessionRequest(CommandNames.CompileOnSaveAffectedFileList, { file: referenceFile1.path }); sendAffectedFileRequestAndCheckResult(session, request, [ @@ -601,4 +597,4 @@ namespace ts.projectSystem { assert.isTrue(outFileContent.indexOf(file3.content) === -1); }); }); -} \ No newline at end of file +} diff --git a/src/harness/unittests/projectErrors.ts b/src/harness/unittests/projectErrors.ts index 7c694ec429653..3d093300cbcf5 100644 --- a/src/harness/unittests/projectErrors.ts +++ b/src/harness/unittests/projectErrors.ts @@ -66,8 +66,6 @@ namespace ts.projectSystem { // only file2 exists - expect error host.reloadFS([file2, libFile]); { - host.triggerFileWatcherCallback(file1.path, FileWatcherEventKind.Deleted); - host.triggerFileWatcherCallback(file2.path, FileWatcherEventKind.Created); checkNumberOfProjects(projectService, { externalProjects: 1 }); const diags = session.executeCommand(compilerOptionsRequest).response; checkDiagnosticsWithLinePos(diags, ["File '/a/b/app.ts' not found."]); @@ -76,7 +74,6 @@ namespace ts.projectSystem { // both files exist - expect no errors host.reloadFS([file1, file2, libFile]); { - host.triggerFileWatcherCallback(file1.path, FileWatcherEventKind.Created); checkNumberOfProjects(projectService, { externalProjects: 1 }); const diags = session.executeCommand(compilerOptionsRequest).response; checkDiagnosticsWithLinePos(diags, []); @@ -112,7 +109,6 @@ namespace ts.projectSystem { checkDiagnosticsWithLinePos(diags, ["File '/a/b/applib.ts' not found."]); host.reloadFS([file1, file2, config, libFile]); - host.triggerFileWatcherCallback(file2.path, FileWatcherEventKind.Created); checkNumberOfProjects(projectService, { configuredProjects: 1 }); diags = session.executeCommand(compilerOptionsRequest).response; @@ -154,7 +150,6 @@ namespace ts.projectSystem { } // fix config and trigger watcher host.reloadFS([file1, file2, correctConfig]); - host.triggerFileWatcherCallback(correctConfig.path, FileWatcherEventKind.Changed); { projectService.checkNumberOfProjects({ configuredProjects: 1 }); const configuredProject = forEach(projectService.synchronizeProjectList([]), f => f.info.projectName === corruptedConfig.path && f); @@ -196,7 +191,6 @@ namespace ts.projectSystem { } // break config and trigger watcher host.reloadFS([file1, file2, corruptedConfig]); - host.triggerFileWatcherCallback(corruptedConfig.path, FileWatcherEventKind.Changed); { projectService.checkNumberOfProjects({ configuredProjects: 1 }); const configuredProject = forEach(projectService.synchronizeProjectList([]), f => f.info.projectName === corruptedConfig.path && f); diff --git a/src/harness/unittests/tsserverProjectSystem.ts b/src/harness/unittests/tsserverProjectSystem.ts index fda10cae6c9d5..51eea84f49b85 100644 --- a/src/harness/unittests/tsserverProjectSystem.ts +++ b/src/harness/unittests/tsserverProjectSystem.ts @@ -619,26 +619,6 @@ namespace ts.projectSystem { return Harness.LanguageService.mockHash(s); } - triggerDirectoryWatcherCallback(_directoryName: string, _fileName: string): void { - //const path = this.toPath(directoryName); - //const callbacks = this.watchedDirectories.get(path); - //if (callbacks) { - // for (const callback of callbacks) { - // callback.cb(fileName); - // } - //} - } - - triggerFileWatcherCallback(_fileName: string, _eventKind: FileWatcherEventKind): void { - //const path = this.toPath(fileName); - //const callbacks = this.watchedFiles.get(path); - //if (callbacks) { - // for (const callback of callbacks) { - // callback(path, eventKind); - // } - //} - } - watchFile(fileName: string, callback: FileWatcherCallback) { const path = this.toFullPath(fileName); this.watchedFiles.add(path, callback); @@ -905,7 +885,6 @@ namespace ts.projectSystem { // Add a tsconfig file host.reloadFS(filesWithConfig); - host.triggerDirectoryWatcherCallback("/a/b", configFile.path); checkNumberOfInferredProjects(projectService, 1); checkNumberOfConfiguredProjects(projectService, 1); @@ -914,7 +893,6 @@ namespace ts.projectSystem { // remove the tsconfig file host.reloadFS(filesWithoutConfig); - host.triggerFileWatcherCallback(configFile.path, FileWatcherEventKind.Deleted); checkNumberOfInferredProjects(projectService, 2); checkNumberOfConfiguredProjects(projectService, 0); @@ -937,7 +915,6 @@ namespace ts.projectSystem { // add a new ts file host.reloadFS([commonFile1, commonFile2, libFile, configFile]); - host.triggerDirectoryWatcherCallback("/a/b", commonFile2.path); host.runQueuedTimeoutCallbacks(); // project service waits for 250ms to update the project structure, therefore the assertion needs to wait longer. checkProjectRootFiles(project, [commonFile1.path, commonFile2.path]); @@ -1040,13 +1017,11 @@ namespace ts.projectSystem { // delete commonFile2 host.reloadFS([commonFile1, configFile]); - host.triggerDirectoryWatcherCallback("/a/b", commonFile2.path); host.runQueuedTimeoutCallbacks(); checkProjectRootFiles(project, [commonFile1.path]); // re-add commonFile2 host.reloadFS([commonFile1, commonFile2, configFile]); - host.triggerDirectoryWatcherCallback("/a/b", commonFile2.path); host.runQueuedTimeoutCallbacks(); checkProjectRootFiles(project, [commonFile1.path, commonFile2.path]); }); @@ -1076,7 +1051,6 @@ namespace ts.projectSystem { assert.equal(diags.length, 2, diags.map(diag => flattenDiagnosticMessageText(diag.text, "\n")).join("\n")); host.reloadFS([file1, commonFile2, libFile]); - host.triggerFileWatcherCallback(commonFile2.path, FileWatcherEventKind.Created); host.runQueuedTimeoutCallbacks(); checkNumberOfInferredProjects(projectService, 1); assert.strictEqual(projectService.inferredProjects[0], project, "Inferred project should be same"); @@ -1106,7 +1080,6 @@ namespace ts.projectSystem { "files": ["${commonFile1.path}"] }`; host.reloadFS(files); - host.triggerFileWatcherCallback(configFile.path, FileWatcherEventKind.Changed); checkNumberOfConfiguredProjects(projectService, 1); checkProjectRootFiles(project, [commonFile1.path]); @@ -1180,7 +1153,6 @@ namespace ts.projectSystem { "files": ["${file1.path}"] }`; host.reloadFS(files); - host.triggerFileWatcherCallback(configFile.path, FileWatcherEventKind.Changed); checkProjectActualFiles(project, [file1.path, classicModuleFile.path, configFile.path]); checkNumberOfInferredProjects(projectService, 1); }); @@ -1327,7 +1299,6 @@ namespace ts.projectSystem { host.reloadFS([file1, configFile, file2, file3, libFile]); - host.triggerDirectoryWatcherCallback(getDirectoryPath(configFile.path), configFile.path); checkNumberOfConfiguredProjects(projectService, 1); checkNumberOfInferredProjects(projectService, 1); @@ -1611,7 +1582,6 @@ namespace ts.projectSystem { }; host.reloadFS([file1, modifiedFile2, file3]); - host.triggerFileWatcherCallback(modifiedFile2.path, FileWatcherEventKind.Changed); checkNumberOfInferredProjects(projectService, 1); checkProjectActualFiles(projectService.inferredProjects[0], [file1.path, modifiedFile2.path, file3.path]); @@ -1643,7 +1613,6 @@ namespace ts.projectSystem { checkNumberOfProjects(projectService, { inferredProjects: 1 }); host.reloadFS([file1, file3]); - host.triggerFileWatcherCallback(file2.path, FileWatcherEventKind.Deleted); checkNumberOfProjects(projectService, { inferredProjects: 2 }); @@ -1742,7 +1711,6 @@ namespace ts.projectSystem { checkProjectActualFiles(projectService.inferredProjects[1], [file3.path]); host.reloadFS([file1, file2, file3, configFile]); - host.triggerDirectoryWatcherCallback(getDirectoryPath(configFile.path), configFile.path); checkNumberOfProjects(projectService, { configuredProjects: 1 }); checkProjectActualFiles(projectService.configuredProjects[0], [file1.path, file2.path, file3.path, configFile.path]); }); @@ -1806,7 +1774,6 @@ namespace ts.projectSystem { host.reloadFS([file1, file2, configFile]); - host.triggerDirectoryWatcherCallback(getDirectoryPath(file2.path), file2.path); host.checkTimeoutQueueLength(1); host.runQueuedTimeoutCallbacks(); // to execute throttled requests @@ -1841,7 +1808,6 @@ namespace ts.projectSystem { }; host.reloadFS([file1, file2, modifiedConfigFile]); - host.triggerFileWatcherCallback(configFile.path, FileWatcherEventKind.Changed); checkNumberOfProjects(projectService, { configuredProjects: 1 }); checkProjectRootFiles(projectService.configuredProjects[0], [file1.path, file2.path]); @@ -1874,7 +1840,6 @@ namespace ts.projectSystem { }; host.reloadFS([file1, file2, modifiedConfigFile]); - host.triggerFileWatcherCallback(configFile.path, FileWatcherEventKind.Changed); checkNumberOfProjects(projectService, { configuredProjects: 1 }); checkProjectRootFiles(projectService.configuredProjects[0], [file1.path, file2.path]); @@ -1954,7 +1919,6 @@ namespace ts.projectSystem { checkProjectActualFiles(projectService.configuredProjects[0], [file1.path, file2.path, config.path]); host.reloadFS([file1, file2]); - host.triggerFileWatcherCallback(config.path, FileWatcherEventKind.Deleted); checkNumberOfProjects(projectService, { inferredProjects: 2 }); checkProjectActualFiles(projectService.inferredProjects[0], [file1.path]); @@ -2438,7 +2402,6 @@ namespace ts.projectSystem { assert.isFalse(lastEvent.data.languageServiceEnabled, "Language service state"); host.reloadFS([f1, f2, configWithExclude]); - host.triggerFileWatcherCallback(config.path, FileWatcherEventKind.Changed); checkNumberOfProjects(projectService, { configuredProjects: 1 }); assert.isTrue(project.languageServiceEnabled, "Language service enabled"); @@ -2672,7 +2635,6 @@ namespace ts.projectSystem { // rename tsconfig.json back to lib.ts host.reloadFS([f1, f2]); - host.triggerFileWatcherCallback(tsconfig.path, FileWatcherEventKind.Deleted); projectService.openExternalProject({ projectFileName: projectName, rootFiles: toExternalFiles([f1.path, f2.path]), @@ -2817,7 +2779,6 @@ namespace ts.projectSystem { checkProjectActualFiles(projectService.configuredProjects[0], [libES5.path, app.path, config1.path]); host.reloadFS([libES5, libES2015Promise, app, config2]); - host.triggerFileWatcherCallback(config1.path, FileWatcherEventKind.Changed); projectService.checkNumberOfProjects({ configuredProjects: 1 }); checkProjectActualFiles(projectService.configuredProjects[0], [libES5.path, libES2015Promise.path, app.path, config2.path]); @@ -2950,7 +2911,6 @@ namespace ts.projectSystem { // delete t1 host.reloadFS([f1, tsconfig]); - host.triggerDirectoryWatcherCallback("/a/b/node_modules/@types", "lib1"); // run throttled operation host.runQueuedTimeoutCallbacks(); @@ -2959,7 +2919,6 @@ namespace ts.projectSystem { // create t2 host.reloadFS([f1, tsconfig, t2]); - host.triggerDirectoryWatcherCallback("/a/b/node_modules/@types", "lib2"); // run throttled operation host.runQueuedTimeoutCallbacks(); @@ -3040,16 +2999,12 @@ namespace ts.projectSystem { const moduleFileNewPath = "/a/b/moduleFile1.ts"; moduleFile.path = moduleFileNewPath; host.reloadFS([moduleFile, file1]); - host.triggerFileWatcherCallback(moduleFileOldPath, FileWatcherEventKind.Deleted); - host.triggerDirectoryWatcherCallback("/a/b", moduleFile.path); host.runQueuedTimeoutCallbacks(); diags = session.executeCommand(getErrRequest).response; assert.equal(diags.length, 1); moduleFile.path = moduleFileOldPath; host.reloadFS([moduleFile, file1]); - host.triggerFileWatcherCallback(moduleFileNewPath, FileWatcherEventKind.Deleted); - host.triggerDirectoryWatcherCallback("/a/b", moduleFile.path); host.runQueuedTimeoutCallbacks(); // Make a change to trigger the program rebuild @@ -3092,18 +3047,12 @@ namespace ts.projectSystem { const moduleFileNewPath = "/a/b/moduleFile1.ts"; moduleFile.path = moduleFileNewPath; host.reloadFS([moduleFile, file1, configFile]); - host.triggerFileWatcherCallback(moduleFileOldPath, FileWatcherEventKind.Deleted); - host.triggerFileWatcherCallback(moduleFileNewPath, FileWatcherEventKind.Created); - host.triggerDirectoryWatcherCallback("/a/b", moduleFile.path); host.runQueuedTimeoutCallbacks(); diags = session.executeCommand(getErrRequest).response; assert.equal(diags.length, 1); moduleFile.path = moduleFileOldPath; host.reloadFS([moduleFile, file1, configFile]); - host.triggerFileWatcherCallback(moduleFileNewPath, FileWatcherEventKind.Deleted); - host.triggerFileWatcherCallback(moduleFileOldPath, FileWatcherEventKind.Created); - host.triggerDirectoryWatcherCallback("/a/b", moduleFile.path); host.runQueuedTimeoutCallbacks(); diags = session.executeCommand(getErrRequest).response; assert.equal(diags.length, 0); @@ -3176,7 +3125,6 @@ namespace ts.projectSystem { assert.equal(diags.length, 1); host.reloadFS([file1, moduleFile]); - host.triggerDirectoryWatcherCallback(getDirectoryPath(file1.path), moduleFile.path); host.runQueuedTimeoutCallbacks(); // Make a change to trigger the program rebuild @@ -3267,7 +3215,6 @@ namespace ts.projectSystem { } }`; host.reloadFS([file, configFile]); - host.triggerFileWatcherCallback(configFile.path, FileWatcherEventKind.Changed); host.runQueuedTimeoutCallbacks(); serverEventManager.checkEventCountOfType("configFileDiag", 2); @@ -3275,7 +3222,6 @@ namespace ts.projectSystem { "compilerOptions": {} }`; host.reloadFS([file, configFile]); - host.triggerFileWatcherCallback(configFile.path, FileWatcherEventKind.Changed); host.runQueuedTimeoutCallbacks(); serverEventManager.checkEventCountOfType("configFileDiag", 3); }); @@ -4203,7 +4149,6 @@ namespace ts.projectSystem { configFile.content = configFileContentWithoutCommentLine; host.reloadFS([file, configFile]); - host.triggerFileWatcherCallback(configFile.path, FileWatcherEventKind.Changed); const diagsAfterEdit = session.executeCommand({ type: "request", diff --git a/src/harness/unittests/typingsInstaller.ts b/src/harness/unittests/typingsInstaller.ts index 8c201c84a1355..ceae7582ace4d 100644 --- a/src/harness/unittests/typingsInstaller.ts +++ b/src/harness/unittests/typingsInstaller.ts @@ -826,7 +826,6 @@ namespace ts.projectSystem { installer.checkPendingCommands(/*expectedCount*/ 0); host.reloadFS([f, fixedPackageJson]); - host.triggerFileWatcherCallback(fixedPackageJson.path, FileWatcherEventKind.Changed); // expected install request installer.installAll(/*expectedCount*/ 1); From 2a638278911097fa2135348ce2d89c31c16562c2 Mon Sep 17 00:00:00 2001 From: Sheetal Nandi Date: Fri, 7 Jul 2017 10:43:21 -0700 Subject: [PATCH 006/109] Update the todo list --- src/server/editorServices.ts | 15 ++++----------- src/server/project.ts | 4 ---- 2 files changed, 4 insertions(+), 15 deletions(-) diff --git a/src/server/editorServices.ts b/src/server/editorServices.ts index 7d43a1e67a67e..5974dde2ba96c 100644 --- a/src/server/editorServices.ts +++ b/src/server/editorServices.ts @@ -674,11 +674,6 @@ namespace ts.server { return; } - // TODO: (sheetalkamat) Technically we shouldnt have to do this: - // We can report these errors in reload projects or after reload project? - const { configFileErrors } = this.convertConfigFileContentToProjectOptions(fileName); - this.reportConfigFileDiagnostics(fileName, configFileErrors, fileName); - this.logger.info(`Detected newly added tsconfig file: ${fileName}`); this.reloadProjects(); } @@ -1262,9 +1257,6 @@ namespace ts.server { private reloadConfiguredProject(project: ConfiguredProject) { // At this point, there is no reason to not have configFile in the host - // TODO: (sheetalkamat) configErrors should always be in project and there shouldnt be - // any need to return these errors - // note: the returned "success" is true does not mean the "configFileErrors" is empty. // because we might have tolerated the errors and kept going. So always return the configFileErrors // regardless the "success" here is true or not. @@ -1273,8 +1265,9 @@ namespace ts.server { // reset project settings to default this.updateNonInferredProject(project, [], fileNamePropertyReader, {}, {}, /*compileOnSave*/ false, configFileErrors); } - - return this.updateConfiguredProject(project, projectOptions, configFileErrors); + else { + this.updateConfiguredProject(project, projectOptions, configFileErrors); + } } /** @@ -1286,7 +1279,7 @@ namespace ts.server { project.setCompilerOptions(projectOptions.compilerOptions); if (!project.languageServiceEnabled) { // language service is already disabled - return configFileErrors; + project.setProjectErrors(configFileErrors); } project.disableLanguageService(); project.stopWatchingDirectory(); diff --git a/src/server/project.ts b/src/server/project.ts index 7e94de2afac12..ebafad825a968 100644 --- a/src/server/project.ts +++ b/src/server/project.ts @@ -649,10 +649,6 @@ namespace ts.server { for (const missingFilePath of missingFilePaths) { if (!this.missingFilesMap.has(missingFilePath)) { const fileWatcher = this.projectService.host.watchFile(missingFilePath, (_filename: string, eventKind: FileWatcherEventKind) => { - // TODO: (sheetalkamat) This needs to be fixed because of the way we create the projects - // Eg. ConfiguredProject and ExternalProject add roots only for existing files - // What this means is that if the file is not present when creating the project - // the program structure will not change because we created wrong program. if (eventKind === FileWatcherEventKind.Created && this.missingFilesMap.has(missingFilePath)) { fileWatcher.close(); this.missingFilesMap.delete(missingFilePath); From 96ffd53c2d3e7bebb3a0391b806f38e05157ba9d Mon Sep 17 00:00:00 2001 From: Sheetal Nandi Date: Fri, 7 Jul 2017 10:58:31 -0700 Subject: [PATCH 007/109] Reload the configured project only once even though there are multiple files open from that project --- src/server/editorServices.ts | 38 ++++++++++++++++-------------------- 1 file changed, 17 insertions(+), 21 deletions(-) diff --git a/src/server/editorServices.ts b/src/server/editorServices.ts index 5974dde2ba96c..cc1eea5c4fe05 100644 --- a/src/server/editorServices.ts +++ b/src/server/editorServices.ts @@ -871,24 +871,6 @@ namespace ts.server { return configFileName; } - /** - * This function tries to search for a tsconfig.json for the given file. If we found it, - * we first detect if there is already a configured project created for it: if so, we re-read - * the tsconfig file content and update the project; otherwise we create a new one. - */ - private openOrUpdateConfiguredProjectForFile(fileName: NormalizedPath) { - const configFileName = this.getConfigFileNameForFile(fileName); - if (configFileName) { - const project = this.findConfiguredProjectByProjectName(configFileName); - if (!project) { - this.openConfigFile(configFileName, fileName); - } - else { - this.reloadConfiguredProject(project); - } - } - } - // This is different from the method the compiler uses because // the compiler can assume it will always start searching in the // current directory (the directory in which tsc was invoked). @@ -1413,11 +1395,25 @@ namespace ts.server { */ reloadProjects() { this.logger.info("reload projects."); + const mapUpdatedProjects = createMap(); // try to reload config file for all open files for (const info of this.openFiles) { - // TODO: (sheetalkamat) batch these = multiple open files from the same project will - // end up updating project that many times - this.openOrUpdateConfiguredProjectForFile(info.fileName); + // This tries to search for a tsconfig.json for the given file. If we found it, + // we first detect if there is already a configured project created for it: if so, + // we re- read the tsconfig file content and update the project only if we havent already done so + // otherwise we create a new one. + const configFileName = this.getConfigFileNameForFile(info.fileName); + if (configFileName) { + let project = this.findConfiguredProjectByProjectName(configFileName); + if (!project) { + project = this.openConfigFile(configFileName, info.fileName); + mapUpdatedProjects.set(configFileName, true); + } + else if (!mapUpdatedProjects.has(configFileName)) { + this.reloadConfiguredProject(project); + mapUpdatedProjects.set(configFileName, true); + } + } } this.refreshInferredProjects(); } From 6bd42b81ee49accb376fc3da560b4cec9e5cb2d6 Mon Sep 17 00:00:00 2001 From: Sheetal Nandi Date: Fri, 7 Jul 2017 11:20:53 -0700 Subject: [PATCH 008/109] When config file is deleted, apart from removing the projecty, reload the configured projects for open files from that project to ensure to pick them by another config file that can be present in parent directory --- src/server/editorServices.ts | 35 +++++++++++++++++++++++------------ 1 file changed, 23 insertions(+), 12 deletions(-) diff --git a/src/server/editorServices.ts b/src/server/editorServices.ts index cc1eea5c4fe05..328876e9c6f66 100644 --- a/src/server/editorServices.ts +++ b/src/server/editorServices.ts @@ -652,9 +652,11 @@ namespace ts.server { const configFileName = project.getConfigFilePath(); if (eventKind === FileWatcherEventKind.Deleted) { this.logger.info(`Config file deleted: ${configFileName}`); - // TODO: (sheetalkamat) Get the list of open files from this project - // and update the projects this.removeProject(project); + // Reload the configured projects for these open files in the project as + // they could be held up by another config file somewhere in the parent directory + const openFilesInProject = this.getOrphanFiles(); + this.reloadConfiguredProjectForFiles(openFilesInProject); } else { this.logger.info(`Config file changed: ${configFileName}`); @@ -767,6 +769,17 @@ namespace ts.server { } } + private getOrphanFiles() { + let orphanFiles: ScriptInfo[]; + // for all open files + for (const f of this.openFiles) { + if (f.containingProjects.length === 0) { + (orphanFiles || (orphanFiles = [])).push(f); + } + } + return orphanFiles; + } + /** * Remove this file from the set of open, non-configured files. * @param info The file that has been closed or newly configured @@ -808,14 +821,8 @@ namespace ts.server { this.removeProject(project); } - let orphanFiles: ScriptInfo[]; - // for all open files - for (const f of this.openFiles) { - // collect orphanted files and try to re-add them as newly opened - if (f.containingProjects.length === 0) { - (orphanFiles || (orphanFiles = [])).push(f); - } - } + // collect orphanted files and try to re-add them as newly opened + const orphanFiles = this.getOrphanFiles(); // treat orphaned files as newly opened if (orphanFiles) { @@ -1395,9 +1402,14 @@ namespace ts.server { */ reloadProjects() { this.logger.info("reload projects."); + this.reloadConfiguredProjectForFiles(this.openFiles); + this.refreshInferredProjects(); + } + + reloadConfiguredProjectForFiles(openFiles: ScriptInfo[]) { const mapUpdatedProjects = createMap(); // try to reload config file for all open files - for (const info of this.openFiles) { + for (const info of openFiles) { // This tries to search for a tsconfig.json for the given file. If we found it, // we first detect if there is already a configured project created for it: if so, // we re- read the tsconfig file content and update the project only if we havent already done so @@ -1415,7 +1427,6 @@ namespace ts.server { } } } - this.refreshInferredProjects(); } /** From df6f75bc705b297ebec01ffeabaf3b2f4361d371 Mon Sep 17 00:00:00 2001 From: Sheetal Nandi Date: Fri, 7 Jul 2017 14:43:00 -0700 Subject: [PATCH 009/109] Optimize wildcard watchers and config directory watching as now we have missing file watching as well We dont need to explicitly watch config file directory as it will be watched: - if there was no files specified, in wild card directories - if there were files specified as missing file (if the file wasnt present) --- src/server/editorServices.ts | 30 +++++------ src/server/project.ts | 100 ++++++++++++++++++++--------------- 2 files changed, 72 insertions(+), 58 deletions(-) diff --git a/src/server/editorServices.ts b/src/server/editorServices.ts index 328876e9c6f66..e4e62e7dd68d5 100644 --- a/src/server/editorServices.ts +++ b/src/server/editorServices.ts @@ -611,7 +611,8 @@ namespace ts.server { * @param project the project that associates with this directory watcher * @param fileName the absolute file name that changed in watched directory */ - private onSourceFileInDirectoryChangedForConfiguredProject(project: ConfiguredProject, fileName: string) { + /* @internal */ + onFileAddOrRemoveInWatchedDirectoryOfProject(project: ConfiguredProject, fileName: string) { // If a change was made inside "folder/file", node will trigger the callback twice: // one with the fileName being "folder/file", and the other one with "folder". // We don't respond to the second one. @@ -623,10 +624,13 @@ namespace ts.server { this.throttledOperations.schedule( project.getConfigFilePath(), /*delay*/250, - () => this.handleChangeInSourceFileForConfiguredProject(project, fileName)); + () => this.handleFileAddOrRemoveInWatchedDirectoryOfProject(project, fileName)); } - private handleChangeInSourceFileForConfiguredProject(project: ConfiguredProject, triggerFile: string) { + private handleFileAddOrRemoveInWatchedDirectoryOfProject(project: ConfiguredProject, triggerFile: string) { + // TODO: (sheetalkamat) this actually doesnt need to re-read the config file from the disk + // it just needs to update the file list of file names + // We might be able to do that by caching the info from first parse and add reusing this with the change in the file path const { projectOptions, configFileErrors } = this.convertConfigFileContentToProjectOptions(project.getConfigFilePath()); this.reportConfigFileDiagnostics(project.getProjectName(), configFileErrors, triggerFile); @@ -676,6 +680,10 @@ namespace ts.server { return; } + // TODO: (sheetalkamat) + // 1. We should only watch tsconfig/jsconfig file here instead of watching directory + // 2. We should try reloading projects with open files in Inferred project only + // 3. We should use this watcher to answer questions to findConfigFile rather than calling host everytime this.logger.info(`Detected newly added tsconfig file: ${fileName}`); this.reloadProjects(); } @@ -1117,17 +1125,13 @@ namespace ts.server { this.documentRegistry, projectOptions.configHasFilesProperty, projectOptions.compilerOptions, - projectOptions.wildcardDirectories, /*languageServiceEnabled*/ !sizeLimitExceeded, projectOptions.compileOnSave === undefined ? false : projectOptions.compileOnSave); this.addFilesToProjectAndUpdateGraph(project, projectOptions.files, fileNamePropertyReader, clientFileName, projectOptions.typeAcquisition, configFileErrors); project.watchConfigFile((project, eventKind) => this.onConfigChangedForConfiguredProject(project, eventKind)); - if (!sizeLimitExceeded) { - this.watchConfigDirectoryForProject(project, projectOptions); - } - project.watchWildcards((project, path) => this.onSourceFileInDirectoryChangedForConfiguredProject(project, path)); + project.watchWildcards(projectOptions.wildcardDirectories); project.watchTypeRoots((project, path) => this.onTypeRootFileChanged(project, path)); this.configuredProjects.push(project); @@ -1135,12 +1139,6 @@ namespace ts.server { return project; } - private watchConfigDirectoryForProject(project: ConfiguredProject, options: ProjectOptions): void { - if (!options.configHasFilesProperty) { - project.watchConfigDirectory((project, path) => this.onSourceFileInDirectoryChangedForConfiguredProject(project, path)); - } - } - private addFilesToProjectAndUpdateGraph(project: ConfiguredProject | ExternalProject, files: T[], propertyReader: FilePropertyReader, clientFileName: string, typeAcquisition: TypeAcquisition, configFileErrors: Diagnostic[]): void { for (const f of files) { const rootFilename = propertyReader.getFileName(f); @@ -1271,12 +1269,12 @@ namespace ts.server { project.setProjectErrors(configFileErrors); } project.disableLanguageService(); - project.stopWatchingDirectory(); + project.stopWatchingWildCards(); project.setProjectErrors(configFileErrors); } else { project.enableLanguageService(); - this.watchConfigDirectoryForProject(project, projectOptions); + project.watchWildcards(projectOptions.wildcardDirectories); this.updateNonInferredProject(project, projectOptions.files, fileNamePropertyReader, projectOptions.compilerOptions, projectOptions.typeAcquisition, projectOptions.compileOnSave, configFileErrors); } } diff --git a/src/server/project.ts b/src/server/project.ts index ebafad825a968..0851b4c64b64d 100644 --- a/src/server/project.ts +++ b/src/server/project.ts @@ -957,6 +957,8 @@ namespace ts.server { } } + type WildCardDirectoryWatchers = { watcher: FileWatcher, recursive: boolean }; + /** * If a file is opened, the server will look for a tsconfig (or jsconfig) * and if successfull create a ConfiguredProject for it. @@ -964,9 +966,8 @@ namespace ts.server { */ export class ConfiguredProject extends Project { private typeAcquisition: TypeAcquisition; - private projectFileWatcher: FileWatcher; - private directoryWatcher: FileWatcher; - private directoriesWatchedForWildcards: Map; + private configFileWatcher: FileWatcher; + private directoriesWatchedForWildcards: Map; private typeRootsWatchers: FileWatcher[]; readonly canonicalConfigFilePath: NormalizedPath; @@ -980,7 +981,6 @@ namespace ts.server { documentRegistry: ts.DocumentRegistry, hasExplicitListOfFiles: boolean, compilerOptions: CompilerOptions, - private wildcardDirectories: Map, languageServiceEnabled: boolean, public compileOnSaveEnabled: boolean) { super(configFileName, ProjectKind.Configured, projectService, documentRegistry, hasExplicitListOfFiles, languageServiceEnabled, compilerOptions, compileOnSaveEnabled); @@ -1098,7 +1098,7 @@ namespace ts.server { } watchConfigFile(callback: (project: ConfiguredProject, eventKind: FileWatcherEventKind) => void) { - this.projectFileWatcher = this.projectService.host.watchFile(this.getConfigFilePath(), (_fileName, eventKind) => callback(this, eventKind)); + this.configFileWatcher = this.projectService.host.watchFile(this.getConfigFilePath(), (_fileName, eventKind) => callback(this, eventKind)); } watchTypeRoots(callback: (project: ConfiguredProject, path: string) => void) { @@ -1111,49 +1111,70 @@ namespace ts.server { this.typeRootsWatchers = watchers; } - watchConfigDirectory(callback: (project: ConfiguredProject, path: string) => void) { - if (this.directoryWatcher) { - return; + private addWatcherForDirectory(flag: WatchDirectoryFlags, directory: string, replaceExisting: boolean) { + if (replaceExisting || !this.directoriesWatchedForWildcards.has(directory)) { + const recursive = (flag & WatchDirectoryFlags.Recursive) !== 0; + this.projectService.logger.info(`Add ${recursive ? "recursive " : ""} watcher for: ${directory}`); + this.directoriesWatchedForWildcards.set(directory, { + watcher: this.projectService.host.watchDirectory( + directory, + path => this.projectService.onFileAddOrRemoveInWatchedDirectoryOfProject(this, path), + recursive + ), + recursive + }); } - - const directoryToWatch = getDirectoryPath(this.getConfigFilePath()); - this.projectService.logger.info(`Add recursive watcher for: ${directoryToWatch}`); - this.directoryWatcher = this.projectService.host.watchDirectory(directoryToWatch, path => callback(this, path), /*recursive*/ true); } - watchWildcards(callback: (project: ConfiguredProject, path: string) => void) { - if (!this.wildcardDirectories) { - return; - } - const configDirectoryPath = getDirectoryPath(this.getConfigFilePath()); - - this.directoriesWatchedForWildcards = createMap(); - this.wildcardDirectories.forEach((flag, directory) => { - if (comparePaths(configDirectoryPath, directory, ".", !this.projectService.host.useCaseSensitiveFileNames) !== Comparison.EqualTo) { - const recursive = (flag & WatchDirectoryFlags.Recursive) !== 0; - this.projectService.logger.info(`Add ${recursive ? "recursive " : ""}watcher for: ${directory}`); - this.directoriesWatchedForWildcards.set(directory, this.projectService.host.watchDirectory( - directory, - path => callback(this, path), - recursive - )); + watchWildcards(wildcardDirectories: Map) { + if (wildcardDirectories) { + if (this.directoriesWatchedForWildcards) { + this.directoriesWatchedForWildcards.forEach(({ watcher, recursive }, directory) => { + const currentFlag = wildcardDirectories.get(directory); + // Remove already watching wild card if it isnt in updated map + if (currentFlag === undefined) { + this.projectService.logger.info(`Removing ${recursive ? "recursive " : ""} watcher for: ${directory}`); + watcher.close(); + this.directoriesWatchedForWildcards.delete(directory); + } + // Or if the recursive doesnt match (add the updated one here) + else { + const currentRecursive = (currentFlag & WatchDirectoryFlags.Recursive) !== 0; + if (currentRecursive !== recursive) { + this.projectService.logger.info(`Removing ${recursive ? "recursive " : ""} watcher for: ${directory}`); + watcher.close(); + this.addWatcherForDirectory(currentFlag, directory, /*replaceExisting*/ true); + } + } + }); } - }); + else { + this.directoriesWatchedForWildcards = createMap(); + } + wildcardDirectories.forEach((flag, directory) => + this.addWatcherForDirectory(flag, directory, /*replaceExisting*/ false)); + } + else { + this.stopWatchingWildCards(); + } } - stopWatchingDirectory() { - if (this.directoryWatcher) { - this.directoryWatcher.close(); - this.directoryWatcher = undefined; + stopWatchingWildCards() { + if (this.directoriesWatchedForWildcards) { + this.directoriesWatchedForWildcards.forEach(({ watcher, recursive }, directory) => { + this.projectService.logger.info(`Removing ${recursive ? "recursive " : ""} watcher for: ${directory}`); + watcher.close(); + }); + this.directoriesWatchedForWildcards = undefined; } } close() { super.close(); - if (this.projectFileWatcher) { - this.projectFileWatcher.close(); - this.projectFileWatcher = undefined; + if (this.configFileWatcher) { + this.configFileWatcher.close(); + this.configFileWatcher = undefined; } if (this.typeRootsWatchers) { @@ -1163,12 +1184,7 @@ namespace ts.server { this.typeRootsWatchers = undefined; } - this.directoriesWatchedForWildcards.forEach(watcher => { - watcher.close(); - }); - this.directoriesWatchedForWildcards = undefined; - - this.stopWatchingDirectory(); + this.stopWatchingWildCards(); } addOpenRef() { From 9ff9476b4aed078d44cb5446dd2fd59c1ab1341b Mon Sep 17 00:00:00 2001 From: Sheetal Nandi Date: Mon, 10 Jul 2017 14:32:36 -0700 Subject: [PATCH 010/109] Handle the deleted file in update graph better, so that in next update it is possible to schedule the update graph of project --- src/compiler/types.ts | 4 ++++ src/server/editorServices.ts | 4 ---- src/server/lsHost.ts | 40 +++++++++++++++++++++++++++++------- 3 files changed, 37 insertions(+), 11 deletions(-) diff --git a/src/compiler/types.ts b/src/compiler/types.ts index 14b80325aa3b7..b1610c76088ad 100644 --- a/src/compiler/types.ts +++ b/src/compiler/types.ts @@ -3975,6 +3975,8 @@ namespace ts { resolvedModule: ResolvedModuleFull | undefined; /* @internal */ failedLookupLocations: string[]; + /*@internal*/ + isInvalidated?: boolean; } export interface ResolvedTypeReferenceDirective { @@ -3987,6 +3989,8 @@ namespace ts { export interface ResolvedTypeReferenceDirectiveWithFailedLookupLocations { resolvedTypeReferenceDirective: ResolvedTypeReferenceDirective; failedLookupLocations: string[]; + /*@internal*/ + isInvalidated?: boolean; } export interface CompilerHost extends ModuleResolutionHost { diff --git a/src/server/editorServices.ts b/src/server/editorServices.ts index e4e62e7dd68d5..e5fb2006379c6 100644 --- a/src/server/editorServices.ts +++ b/src/server/editorServices.ts @@ -361,8 +361,6 @@ namespace ts.server { readonly toCanonicalFileName: (f: string) => string; - public lastDeletedFile: ScriptInfo; - public readonly host: ServerHost; public readonly logger: Logger; public readonly cancellationToken: HostCancellationToken; @@ -571,7 +569,6 @@ namespace ts.server { if (!info.isScriptOpen()) { this.filenameToScriptInfo.delete(info.path); - this.lastDeletedFile = info; // capture list of projects since detachAllProjects will wipe out original list const containingProjects = info.containingProjects.slice(); @@ -580,7 +577,6 @@ namespace ts.server { // update projects to make sure that set of referenced files is correct this.updateProjectGraphs(containingProjects); - this.lastDeletedFile = undefined; if (!this.eventHandler) { return; diff --git a/src/server/lsHost.ts b/src/server/lsHost.ts index 55513f1a28505..c8ab7d3c0f37c 100644 --- a/src/server/lsHost.ts +++ b/src/server/lsHost.ts @@ -3,6 +3,7 @@ /// namespace ts.server { + type NameResolutionWithFailedLookupLocations = { failedLookupLocations: string[], isInvalidated?: boolean }; export class LSHost implements ts.LanguageServiceHost, ModuleResolutionHost { private compilationSettings: ts.CompilerOptions; private readonly resolvedModuleNames = createMap>(); @@ -62,7 +63,7 @@ namespace ts.server { return collected; } - private resolveNamesWithLocalCache( + private resolveNamesWithLocalCache( names: string[], containingFile: string, cache: Map>, @@ -77,7 +78,6 @@ namespace ts.server { const newResolutions: Map = createMap(); const resolvedModules: R[] = []; const compilerOptions = this.getCompilationSettings(); - const lastDeletedFileName = this.project.projectService.lastDeletedFile && this.project.projectService.lastDeletedFile.fileName; for (const name of names) { // check if this is a duplicate entry in the list @@ -112,7 +112,7 @@ namespace ts.server { if (oldResolution === newResolution) { return true; } - if (!oldResolution || !newResolution) { + if (!oldResolution || !newResolution || oldResolution.isInvalidated) { return false; } const oldResult = getResult(oldResolution); @@ -127,13 +127,13 @@ namespace ts.server { } function moduleResolutionIsValid(resolution: T): boolean { - if (!resolution) { + if (!resolution || resolution.isInvalidated) { return false; } const result = getResult(resolution); if (result) { - return getResultFileName(result) !== lastDeletedFileName; + return true; } // consider situation if we have no candidate locations as valid resolution. @@ -234,8 +234,34 @@ namespace ts.server { } notifyFileRemoved(info: ScriptInfo) { - this.resolvedModuleNames.delete(info.path); - this.resolvedTypeReferenceDirectives.delete(info.path); + this.invalidateResolutionOfDeletedFile(info, this.resolvedModuleNames, + m => m.resolvedModule, r => r.resolvedFileName); + this.invalidateResolutionOfDeletedFile(info, this.resolvedTypeReferenceDirectives, + m => m.resolvedTypeReferenceDirective, r => r.resolvedFileName); + } + + private invalidateResolutionOfDeletedFile( + deletedInfo: ScriptInfo, + cache: Map>, + getResult: (s: T) => R, + getResultFileName: (result: R) => string | undefined) { + cache.forEach((value, path) => { + if (path === deletedInfo.path) { + cache.delete(path); + } + else if (value) { + value.forEach((resolution) => { + if (resolution && !resolution.isInvalidated) { + const result = getResult(resolution); + if (result) { + if (getResultFileName(result) === deletedInfo.path) { + resolution.isInvalidated = true; + } + } + } + }); + } + }); } setCompilationSettings(opt: ts.CompilerOptions) { From 62871cc0f9a87344bc6b6e6f90a7b3885209a0d5 Mon Sep 17 00:00:00 2001 From: Sheetal Nandi Date: Tue, 11 Jul 2017 13:38:12 -0700 Subject: [PATCH 011/109] Only update file list when there are changes in the watched directories --- src/compiler/commandLineParser.ts | 90 ++++++++++++------- src/compiler/types.ts | 11 +++ .../unittests/tsserverProjectSystem.ts | 3 +- src/server/editorServices.ts | 71 +++++++-------- src/server/project.ts | 3 + 5 files changed, 107 insertions(+), 71 deletions(-) diff --git a/src/compiler/commandLineParser.ts b/src/compiler/commandLineParser.ts index 422789f420fea..94b6540f16a4b 100644 --- a/src/compiler/commandLineParser.ts +++ b/src/compiler/commandLineParser.ts @@ -1421,7 +1421,7 @@ namespace ts { const options = extend(existingOptions, parsedConfig.options || {}); options.configFilePath = configFileName; setConfigFileInOptions(options, sourceFile); - const { fileNames, wildcardDirectories } = getFileNames(); + const { fileNames, wildcardDirectories, spec } = getFileNames(); return { options, fileNames, @@ -1429,15 +1429,16 @@ namespace ts { raw, errors, wildcardDirectories, - compileOnSave: !!raw.compileOnSave + compileOnSave: !!raw.compileOnSave, + configFileSpecs: spec }; function getFileNames(): ExpandResult { - let fileNames: string[]; + let filesSpecs: string[]; if (hasProperty(raw, "files")) { if (isArray(raw["files"])) { - fileNames = raw["files"]; - if (fileNames.length === 0) { + filesSpecs = raw["files"]; + if (filesSpecs.length === 0) { createCompilerDiagnosticOnlyIfJson(Diagnostics.The_files_list_in_config_file_0_is_empty, configFileName || "tsconfig.json"); } } @@ -1475,19 +1476,14 @@ namespace ts { } } - if (fileNames === undefined && includeSpecs === undefined) { + if (filesSpecs === undefined && includeSpecs === undefined) { includeSpecs = ["**/*"]; } - const result = matchFileNames(fileNames, includeSpecs, excludeSpecs, basePath, options, host, errors, extraFileExtensions, sourceFile); + const result = matchFileNames(filesSpecs, includeSpecs, excludeSpecs, basePath, options, host, errors, extraFileExtensions, sourceFile); if (result.fileNames.length === 0 && !hasProperty(raw, "files") && resolutionStack.length === 0) { - errors.push( - createCompilerDiagnostic( - Diagnostics.No_inputs_were_found_in_config_file_0_Specified_include_paths_were_1_and_exclude_paths_were_2, - configFileName || "tsconfig.json", - JSON.stringify(includeSpecs || []), - JSON.stringify(excludeSpecs || []))); + errors.push(getErrorForNoInputFiles(result.spec, configFileName)); } return result; @@ -1500,6 +1496,14 @@ namespace ts { } } + export function getErrorForNoInputFiles({ includeSpecs, excludeSpecs }: ConfigFileSpecs, configFileName?: string) { + return createCompilerDiagnostic( + Diagnostics.No_inputs_were_found_in_config_file_0_Specified_include_paths_were_1_and_exclude_paths_were_2, + configFileName || "tsconfig.json", + JSON.stringify(includeSpecs || []), + JSON.stringify(excludeSpecs || [])); + } + interface ParsedTsconfig { raw: any; options?: CompilerOptions; @@ -1946,7 +1950,40 @@ namespace ts { * @param host The host used to resolve files and directories. * @param errors An array for diagnostic reporting. */ - function matchFileNames(fileNames: string[], include: string[], exclude: string[], basePath: string, options: CompilerOptions, host: ParseConfigHost, errors: Diagnostic[], extraFileExtensions: JsFileExtensionInfo[], jsonSourceFile: JsonSourceFile): ExpandResult { + function matchFileNames(filesSpecs: string[], includeSpecs: string[], excludeSpecs: string[], basePath: string, options: CompilerOptions, host: ParseConfigHost, errors: Diagnostic[], extraFileExtensions: JsFileExtensionInfo[], jsonSourceFile: JsonSourceFile): ExpandResult { + basePath = normalizePath(basePath); + let validatedIncludeSpecs: string[], validatedExcludeSpecs: string[]; + + if (includeSpecs) { + validatedIncludeSpecs = validateSpecs(includeSpecs, errors, /*allowTrailingRecursion*/ false, jsonSourceFile, "include"); + } + + if (excludeSpecs) { + validatedExcludeSpecs = validateSpecs(excludeSpecs, errors, /*allowTrailingRecursion*/ true, jsonSourceFile, "exclude"); + } + + // Wildcard directories (provided as part of a wildcard path) are stored in a + // file map that marks whether it was a regular wildcard match (with a `*` or `?` token), + // or a recursive directory. This information is used by filesystem watchers to monitor for + // new entries in these paths. + const wildcardDirectories = getWildcardDirectories(validatedIncludeSpecs, validatedExcludeSpecs, basePath, host.useCaseSensitiveFileNames); + + const spec: ConfigFileSpecs = { filesSpecs, includeSpecs, excludeSpecs, validatedIncludeSpecs, validatedExcludeSpecs, wildcardDirectories }; + return getFileNamesFromConfigSpecs(spec, basePath, options, host, extraFileExtensions); + } + + /** + * Expands an array of file specifications. + * + * @param fileNames The literal file names to include. + * @param include The wildcard file specifications to include. + * @param exclude The wildcard file specifications to exclude. + * @param basePath The base path for any relative file specifications. + * @param options Compiler options. + * @param host The host used to resolve files and directories. + * @param errors An array for diagnostic reporting. + */ + export function getFileNamesFromConfigSpecs(spec: ConfigFileSpecs, basePath: string, options: CompilerOptions, host: ParseConfigHost, extraFileExtensions: JsFileExtensionInfo[]): ExpandResult { basePath = normalizePath(basePath); // The exclude spec list is converted into a regular expression, which allows us to quickly @@ -1964,19 +2001,7 @@ namespace ts { // via wildcard, and to handle extension priority. const wildcardFileMap = createMap(); - if (include) { - include = validateSpecs(include, errors, /*allowTrailingRecursion*/ false, jsonSourceFile, "include"); - } - - if (exclude) { - exclude = validateSpecs(exclude, errors, /*allowTrailingRecursion*/ true, jsonSourceFile, "exclude"); - } - - // Wildcard directories (provided as part of a wildcard path) are stored in a - // file map that marks whether it was a regular wildcard match (with a `*` or `?` token), - // or a recursive directory. This information is used by filesystem watchers to monitor for - // new entries in these paths. - const wildcardDirectories = getWildcardDirectories(include, exclude, basePath, host.useCaseSensitiveFileNames); + const { filesSpecs, validatedIncludeSpecs, validatedExcludeSpecs, wildcardDirectories } = spec; // Rather than requery this for each file and filespec, we query the supported extensions // once and store it on the expansion context. @@ -1984,15 +2009,15 @@ namespace ts { // Literal files are always included verbatim. An "include" or "exclude" specification cannot // remove a literal file. - if (fileNames) { - for (const fileName of fileNames) { + if (filesSpecs) { + for (const fileName of filesSpecs) { const file = combinePaths(basePath, fileName); literalFileMap.set(keyMapper(file), file); } } - if (include && include.length > 0) { - for (const file of host.readDirectory(basePath, supportedExtensions, exclude, include, /*depth*/ undefined)) { + if (validatedIncludeSpecs && validatedIncludeSpecs.length > 0) { + for (const file of host.readDirectory(basePath, supportedExtensions, validatedExcludeSpecs, validatedIncludeSpecs, /*depth*/ undefined)) { // If we have already included a literal or wildcard path with a // higher priority extension, we should skip this file. // @@ -2020,7 +2045,8 @@ namespace ts { const wildcardFiles = arrayFrom(wildcardFileMap.values()); return { fileNames: literalFiles.concat(wildcardFiles), - wildcardDirectories + wildcardDirectories, + spec }; } diff --git a/src/compiler/types.ts b/src/compiler/types.ts index b1610c76088ad..88bfff872d596 100644 --- a/src/compiler/types.ts +++ b/src/compiler/types.ts @@ -3731,6 +3731,7 @@ namespace ts { errors: Diagnostic[]; wildcardDirectories?: MapLike; compileOnSave?: boolean; + configFileSpecs?: ConfigFileSpecs; } export const enum WatchDirectoryFlags { @@ -3738,9 +3739,19 @@ namespace ts { Recursive = 1 << 0, } + export interface ConfigFileSpecs { + filesSpecs: string[]; + includeSpecs: string[]; + excludeSpecs: string[]; + validatedIncludeSpecs: string[]; + validatedExcludeSpecs: string[]; + wildcardDirectories: MapLike; + } + export interface ExpandResult { fileNames: string[]; wildcardDirectories: MapLike; + spec: ConfigFileSpecs; } /* @internal */ diff --git a/src/harness/unittests/tsserverProjectSystem.ts b/src/harness/unittests/tsserverProjectSystem.ts index 51eea84f49b85..bd5bfb1841dbd 100644 --- a/src/harness/unittests/tsserverProjectSystem.ts +++ b/src/harness/unittests/tsserverProjectSystem.ts @@ -1774,8 +1774,7 @@ namespace ts.projectSystem { host.reloadFS([file1, file2, configFile]); - host.checkTimeoutQueueLength(1); - host.runQueuedTimeoutCallbacks(); // to execute throttled requests + host.checkTimeoutQueueLength(0); // TODO: update graph scheduling (instead of instant update graph) checkNumberOfProjects(projectService, { configuredProjects: 1 }); checkProjectRootFiles(projectService.configuredProjects[0], [file1.path, file2.path]); diff --git a/src/server/editorServices.ts b/src/server/editorServices.ts index e5fb2006379c6..a19ea125ebf49 100644 --- a/src/server/editorServices.ts +++ b/src/server/editorServices.ts @@ -617,33 +617,30 @@ namespace ts.server { } this.logger.info(`Detected source file changes: ${fileName}`); - this.throttledOperations.schedule( - project.getConfigFilePath(), - /*delay*/250, - () => this.handleFileAddOrRemoveInWatchedDirectoryOfProject(project, fileName)); - } - - private handleFileAddOrRemoveInWatchedDirectoryOfProject(project: ConfiguredProject, triggerFile: string) { - // TODO: (sheetalkamat) this actually doesnt need to re-read the config file from the disk - // it just needs to update the file list of file names - // We might be able to do that by caching the info from first parse and add reusing this with the change in the file path - const { projectOptions, configFileErrors } = this.convertConfigFileContentToProjectOptions(project.getConfigFilePath()); - this.reportConfigFileDiagnostics(project.getProjectName(), configFileErrors, triggerFile); - - const newRootFiles = projectOptions.files.map((f => this.getCanonicalFileName(f))); - const currentRootFiles = project.getRootFiles().map((f => this.getCanonicalFileName(f))); - // We check if the project file list has changed. If so, we update the project. - if (!arrayIsEqualTo(currentRootFiles.sort(), newRootFiles.sort())) { - // For configured projects, the change is made outside the tsconfig file, and - // it is not likely to affect the project for other files opened by the client. We can - // just update the current project. - - this.logger.info("Updating configured project"); - this.updateConfiguredProject(project, projectOptions, configFileErrors); - - // Call refreshInferredProjects to clean up inferred projects we may have - // created for the new files + const configFileSpecs = project.configFileSpecs; + const configFilename = normalizePath(project.getConfigFilePath()); + // TODO: (sheetalkamat) use the host that caches - so we dont do file exists and read directory call + const result = getFileNamesFromConfigSpecs(configFileSpecs, getDirectoryPath(configFilename), project.getCompilerOptions(), this.host, this.hostConfiguration.extraFileExtensions); + const errors = project.getAllProjectErrors(); + if (result.fileNames.length === 0) { + if (!configFileSpecs.filesSpecs) { + if (!some(errors, error => error.code === Diagnostics.No_inputs_were_found_in_config_file_0_Specified_include_paths_were_1_and_exclude_paths_were_2.code)) { + errors.push(getErrorForNoInputFiles(configFileSpecs, configFilename)); + } + if (!some(errors, error => error.code === Diagnostics.The_config_file_0_found_doesn_t_contain_any_source_files.code)) { + errors.push(createCompilerDiagnostic(Diagnostics.The_config_file_0_found_doesn_t_contain_any_source_files, configFilename)); + } + } + } + else { + filterMutate(errors, error => + error.code !== Diagnostics.No_inputs_were_found_in_config_file_0_Specified_include_paths_were_1_and_exclude_paths_were_2.code && + error.code !== Diagnostics.The_config_file_0_found_doesn_t_contain_any_source_files.code); + } + this.updateNonInferredProjectFiles(project, result.fileNames, fileNamePropertyReader); + // TODO: (sheetalkamat) schedule the update graph + if (!project.updateGraph()) { this.refreshInferredProjects(); } } @@ -684,11 +681,6 @@ namespace ts.server { this.reloadProjects(); } - private getCanonicalFileName(fileName: string) { - const name = this.host.useCaseSensitiveFileNames ? fileName : fileName.toLowerCase(); - return normalizePath(name); - } - private removeProject(project: Project) { this.logger.info(`remove project: ${project.getRootFiles().toString()}`); @@ -1009,7 +1001,7 @@ namespace ts.server { }; } - return { success, projectOptions, configFileErrors: errors }; + return { success, projectOptions, configFileErrors: errors, configFileSpecs: parsedCommandLine.configFileSpecs }; } private exceededTotalSizeLimitForNonTsFiles(name: string, options: CompilerOptions, fileNames: T[], propertyReader: FilePropertyReader) { @@ -1113,7 +1105,7 @@ namespace ts.server { }); } - private createAndAddConfiguredProject(configFileName: NormalizedPath, projectOptions: ProjectOptions, configFileErrors: Diagnostic[], clientFileName?: string) { + private createAndAddConfiguredProject(configFileName: NormalizedPath, projectOptions: ProjectOptions, configFileErrors: Diagnostic[], configFileSpecs: ConfigFileSpecs, clientFileName?: string) { const sizeLimitExceeded = this.exceededTotalSizeLimitForNonTsFiles(configFileName, projectOptions.compilerOptions, projectOptions.files, fileNamePropertyReader); const project = new ConfiguredProject( configFileName, @@ -1126,6 +1118,7 @@ namespace ts.server { this.addFilesToProjectAndUpdateGraph(project, projectOptions.files, fileNamePropertyReader, clientFileName, projectOptions.typeAcquisition, configFileErrors); + project.configFileSpecs = configFileSpecs; project.watchConfigFile((project, eventKind) => this.onConfigChangedForConfiguredProject(project, eventKind)); project.watchWildcards(projectOptions.wildcardDirectories); project.watchTypeRoots((project, path) => this.onTypeRootFileChanged(project, path)); @@ -1156,15 +1149,15 @@ namespace ts.server { } private openConfigFile(configFileName: NormalizedPath, clientFileName?: string) { - const { success, projectOptions, configFileErrors } = this.convertConfigFileContentToProjectOptions(configFileName); + const { success, projectOptions, configFileErrors, configFileSpecs } = this.convertConfigFileContentToProjectOptions(configFileName); if (success) { this.logger.info(`Opened configuration file ${configFileName}`); } - return this.createAndAddConfiguredProject(configFileName, projectOptions, configFileErrors, clientFileName); + return this.createAndAddConfiguredProject(configFileName, projectOptions, configFileErrors, configFileSpecs, clientFileName); } - private updateNonInferredProject(project: ExternalProject | ConfiguredProject, newUncheckedFiles: T[], propertyReader: FilePropertyReader, newOptions: CompilerOptions, newTypeAcquisition: TypeAcquisition, compileOnSave: boolean, configFileErrors: Diagnostic[]) { + private updateNonInferredProjectFiles(project: ExternalProject | ConfiguredProject, newUncheckedFiles: T[], propertyReader: FilePropertyReader) { const projectRootFilesMap = project.getRootFilesMap(); const newRootScriptInfoMap: Map = createMap(); @@ -1219,7 +1212,10 @@ namespace ts.server { } }); } + } + private updateNonInferredProject(project: ExternalProject | ConfiguredProject, newUncheckedFiles: T[], propertyReader: FilePropertyReader, newOptions: CompilerOptions, newTypeAcquisition: TypeAcquisition, compileOnSave: boolean, configFileErrors: Diagnostic[]) { + this.updateNonInferredProjectFiles(project, newUncheckedFiles, propertyReader); project.setCompilerOptions(newOptions); project.setTypeAcquisition(newTypeAcquisition); @@ -1243,7 +1239,8 @@ namespace ts.server { // note: the returned "success" is true does not mean the "configFileErrors" is empty. // because we might have tolerated the errors and kept going. So always return the configFileErrors // regardless the "success" here is true or not. - const { success, projectOptions, configFileErrors } = this.convertConfigFileContentToProjectOptions(project.getConfigFilePath()); + const { success, projectOptions, configFileErrors, configFileSpecs } = this.convertConfigFileContentToProjectOptions(project.getConfigFilePath()); + project.configFileSpecs = configFileSpecs; if (!success) { // reset project settings to default this.updateNonInferredProject(project, [], fileNamePropertyReader, {}, {}, /*compileOnSave*/ false, configFileErrors); diff --git a/src/server/project.ts b/src/server/project.ts index 0851b4c64b64d..acc826c773053 100644 --- a/src/server/project.ts +++ b/src/server/project.ts @@ -971,6 +971,9 @@ namespace ts.server { private typeRootsWatchers: FileWatcher[]; readonly canonicalConfigFilePath: NormalizedPath; + /*@internal*/ + configFileSpecs: ConfigFileSpecs; + private plugins: PluginModule[] = []; /** Used for configured projects which may have multiple open roots */ From 48c651317eeeb3c26a05f5d86dc48dd924c9f833 Mon Sep 17 00:00:00 2001 From: Sheetal Nandi Date: Tue, 11 Jul 2017 14:18:56 -0700 Subject: [PATCH 012/109] Update types instantly when the type root changes. --- src/server/editorServices.ts | 2 +- src/server/project.ts | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/src/server/editorServices.ts b/src/server/editorServices.ts index a19ea125ebf49..5060238521f19 100644 --- a/src/server/editorServices.ts +++ b/src/server/editorServices.ts @@ -595,8 +595,8 @@ namespace ts.server { private onTypeRootFileChanged(project: ConfiguredProject, fileName: string) { this.logger.info(`Type root file ${fileName} changed`); + project.updateTypes(); this.throttledOperations.schedule(project.getConfigFilePath() + " * type root", /*delay*/ 250, () => { - project.updateTypes(); this.reloadConfiguredProject(project); // TODO: Figure out why this is needed (should be redundant?) this.refreshInferredProjects(); }); diff --git a/src/server/project.ts b/src/server/project.ts index acc826c773053..91687faa0b968 100644 --- a/src/server/project.ts +++ b/src/server/project.ts @@ -294,7 +294,6 @@ namespace ts.server { updateTypes() { this.typesVersion++; this.markAsDirty(); - this.updateGraph(); } close() { From 19a6a003f59075049870d33f4201478a0fd15055 Mon Sep 17 00:00:00 2001 From: Sheetal Nandi Date: Tue, 11 Jul 2017 16:10:44 -0700 Subject: [PATCH 013/109] Cache the read directory results so that it doesnt end up reading it all the time --- .../unittests/tsserverProjectSystem.ts | 6 +-- src/server/editorServices.ts | 22 ++++---- src/server/lsHost.ts | 52 +++++++++++++++++++ src/server/project.ts | 10 ++-- 4 files changed, 74 insertions(+), 16 deletions(-) diff --git a/src/harness/unittests/tsserverProjectSystem.ts b/src/harness/unittests/tsserverProjectSystem.ts index bd5bfb1841dbd..1c368cad69c5b 100644 --- a/src/harness/unittests/tsserverProjectSystem.ts +++ b/src/harness/unittests/tsserverProjectSystem.ts @@ -574,7 +574,7 @@ namespace ts.projectSystem { const path = this.toFullPath(s); const folder = this.fs.get(path); if (isFolder(folder)) { - return map(folder.entries, x => getBaseFileName(x.fullPath)); + return mapDefined(folder.entries, entry => isFolder(entry) ? getBaseFileName(entry.fullPath) : undefined); } Debug.fail(folder ? "getDirectories called on file" : "getDirectories called on missing folder"); return []; @@ -590,10 +590,10 @@ namespace ts.projectSystem { if (isFolder(dirEntry)) { dirEntry.entries.forEach((entry) => { if (isFolder(entry)) { - result.directories.push(entry.fullPath); + result.directories.push(getBaseFileName(entry.fullPath)); } else if (isFile(entry)) { - result.files.push(entry.fullPath); + result.files.push(getBaseFileName(entry.fullPath)); } else { Debug.fail("Unknown entry"); diff --git a/src/server/editorServices.ts b/src/server/editorServices.ts index 5060238521f19..30da6631a4920 100644 --- a/src/server/editorServices.ts +++ b/src/server/editorServices.ts @@ -608,7 +608,9 @@ namespace ts.server { * @param fileName the absolute file name that changed in watched directory */ /* @internal */ - onFileAddOrRemoveInWatchedDirectoryOfProject(project: ConfiguredProject, fileName: string) { + onFileAddOrRemoveInWatchedDirectoryOfProject(project: ConfiguredProject, fileName: Path) { + project.cachedParseConfigHost.clearCacheForFile(fileName); + // If a change was made inside "folder/file", node will trigger the callback twice: // one with the fileName being "folder/file", and the other one with "folder". // We don't respond to the second one. @@ -621,7 +623,7 @@ namespace ts.server { const configFileSpecs = project.configFileSpecs; const configFilename = normalizePath(project.getConfigFilePath()); // TODO: (sheetalkamat) use the host that caches - so we dont do file exists and read directory call - const result = getFileNamesFromConfigSpecs(configFileSpecs, getDirectoryPath(configFilename), project.getCompilerOptions(), this.host, this.hostConfiguration.extraFileExtensions); + const result = getFileNamesFromConfigSpecs(configFileSpecs, getDirectoryPath(configFilename), project.getCompilerOptions(), project.cachedParseConfigHost, this.hostConfiguration.extraFileExtensions); const errors = project.getAllProjectErrors(); if (result.fileNames.length === 0) { if (!configFileSpecs.filesSpecs) { @@ -945,7 +947,7 @@ namespace ts.server { return findProjectByName(projectFileName, this.externalProjects); } - private convertConfigFileContentToProjectOptions(configFilename: string) { + private convertConfigFileContentToProjectOptions(configFilename: string, cachedParseConfigHost: CachedParseConfigHost) { configFilename = normalizePath(configFilename); const configFileContent = this.host.readFile(configFilename); @@ -957,7 +959,7 @@ namespace ts.server { const errors = result.parseDiagnostics; const parsedCommandLine = parseJsonSourceFileConfigFileContent( result, - this.host, + cachedParseConfigHost, getDirectoryPath(configFilename), /*existingOptions*/ {}, configFilename, @@ -1105,7 +1107,7 @@ namespace ts.server { }); } - private createAndAddConfiguredProject(configFileName: NormalizedPath, projectOptions: ProjectOptions, configFileErrors: Diagnostic[], configFileSpecs: ConfigFileSpecs, clientFileName?: string) { + private createAndAddConfiguredProject(configFileName: NormalizedPath, projectOptions: ProjectOptions, configFileErrors: Diagnostic[], configFileSpecs: ConfigFileSpecs, cachedParseConfigHost: CachedParseConfigHost, clientFileName?: string) { const sizeLimitExceeded = this.exceededTotalSizeLimitForNonTsFiles(configFileName, projectOptions.compilerOptions, projectOptions.files, fileNamePropertyReader); const project = new ConfiguredProject( configFileName, @@ -1115,7 +1117,7 @@ namespace ts.server { projectOptions.compilerOptions, /*languageServiceEnabled*/ !sizeLimitExceeded, projectOptions.compileOnSave === undefined ? false : projectOptions.compileOnSave); - + project.cachedParseConfigHost = cachedParseConfigHost; this.addFilesToProjectAndUpdateGraph(project, projectOptions.files, fileNamePropertyReader, clientFileName, projectOptions.typeAcquisition, configFileErrors); project.configFileSpecs = configFileSpecs; @@ -1149,12 +1151,13 @@ namespace ts.server { } private openConfigFile(configFileName: NormalizedPath, clientFileName?: string) { - const { success, projectOptions, configFileErrors, configFileSpecs } = this.convertConfigFileContentToProjectOptions(configFileName); + const cachedParseConfigHost = new CachedParseConfigHost(this.host); + const { success, projectOptions, configFileErrors, configFileSpecs } = this.convertConfigFileContentToProjectOptions(configFileName, cachedParseConfigHost); if (success) { this.logger.info(`Opened configuration file ${configFileName}`); } - return this.createAndAddConfiguredProject(configFileName, projectOptions, configFileErrors, configFileSpecs, clientFileName); + return this.createAndAddConfiguredProject(configFileName, projectOptions, configFileErrors, configFileSpecs, cachedParseConfigHost, clientFileName); } private updateNonInferredProjectFiles(project: ExternalProject | ConfiguredProject, newUncheckedFiles: T[], propertyReader: FilePropertyReader) { @@ -1239,7 +1242,8 @@ namespace ts.server { // note: the returned "success" is true does not mean the "configFileErrors" is empty. // because we might have tolerated the errors and kept going. So always return the configFileErrors // regardless the "success" here is true or not. - const { success, projectOptions, configFileErrors, configFileSpecs } = this.convertConfigFileContentToProjectOptions(project.getConfigFilePath()); + project.cachedParseConfigHost.clearCache(); + const { success, projectOptions, configFileErrors, configFileSpecs } = this.convertConfigFileContentToProjectOptions(project.getConfigFilePath(), project.cachedParseConfigHost); project.configFileSpecs = configFileSpecs; if (!success) { // reset project settings to default diff --git a/src/server/lsHost.ts b/src/server/lsHost.ts index c8ab7d3c0f37c..720748eb5d0b6 100644 --- a/src/server/lsHost.ts +++ b/src/server/lsHost.ts @@ -4,6 +4,58 @@ namespace ts.server { type NameResolutionWithFailedLookupLocations = { failedLookupLocations: string[], isInvalidated?: boolean }; + + export class CachedParseConfigHost implements ParseConfigHost { + useCaseSensitiveFileNames: boolean; + private getCanonicalFileName: (fileName: string) => string; + private cachedReadDirectoryResult = createMap(); + constructor(private readonly host: ServerHost) { + this.useCaseSensitiveFileNames = host.useCaseSensitiveFileNames; + this.getCanonicalFileName = createGetCanonicalFileName(this.useCaseSensitiveFileNames); + } + + private getFileSystemEntries(rootDir: string) { + const path = ts.toPath(rootDir, this.host.getCurrentDirectory(), this.getCanonicalFileName); + const cachedResult = this.cachedReadDirectoryResult.get(path); + if (cachedResult) { + return cachedResult; + } + + const resultFromHost: FileSystemEntries = { + files: this.host.readDirectory(rootDir, /*extensions*/ undefined, /*exclude*/ undefined, /*include*/["*.*"]) || [], + directories: this.host.getDirectories(rootDir) + }; + + this.cachedReadDirectoryResult.set(path, resultFromHost); + return resultFromHost; + } + + readDirectory(rootDir: string, extensions: string[], excludes: string[], includes: string[], depth: number): string[] { + return matchFiles(rootDir, extensions, excludes, includes, this.useCaseSensitiveFileNames, this.host.getCurrentDirectory(), depth, path => this.getFileSystemEntries(path)); + } + + fileExists(fileName: string): boolean { + const path = ts.toPath(fileName, this.host.getCurrentDirectory(), this.getCanonicalFileName); + const result = this.getFileSystemEntries(getDirectoryPath(path)); + return contains(result.files, fileName); + } + + readFile(path: string): string { + return this.host.readFile(path); + } + + clearCacheForFile(fileName: Path) { + this.cachedReadDirectoryResult.delete(fileName); + this.cachedReadDirectoryResult.delete(getDirectoryPath(fileName)); + } + + clearCache() { + this.cachedReadDirectoryResult = createMap(); + } + + // TODO: (sheetalkamat) to cache getFileSize as well as fileExists and readFile + } + export class LSHost implements ts.LanguageServiceHost, ModuleResolutionHost { private compilationSettings: ts.CompilerOptions; private readonly resolvedModuleNames = createMap>(); diff --git a/src/server/project.ts b/src/server/project.ts index 91687faa0b968..cfd234b64ea66 100644 --- a/src/server/project.ts +++ b/src/server/project.ts @@ -972,6 +972,7 @@ namespace ts.server { /*@internal*/ configFileSpecs: ConfigFileSpecs; + cachedParseConfigHost: CachedParseConfigHost; private plugins: PluginModule[] = []; @@ -1113,14 +1114,14 @@ namespace ts.server { this.typeRootsWatchers = watchers; } - private addWatcherForDirectory(flag: WatchDirectoryFlags, directory: string, replaceExisting: boolean) { + private addWatcherForDirectory(flag: WatchDirectoryFlags, directory: string, getCanonicalFileName: (fileName: string) => string, replaceExisting: boolean) { if (replaceExisting || !this.directoriesWatchedForWildcards.has(directory)) { const recursive = (flag & WatchDirectoryFlags.Recursive) !== 0; this.projectService.logger.info(`Add ${recursive ? "recursive " : ""} watcher for: ${directory}`); this.directoriesWatchedForWildcards.set(directory, { watcher: this.projectService.host.watchDirectory( directory, - path => this.projectService.onFileAddOrRemoveInWatchedDirectoryOfProject(this, path), + path => this.projectService.onFileAddOrRemoveInWatchedDirectoryOfProject(this, toPath(path, directory, getCanonicalFileName)), recursive ), recursive @@ -1129,6 +1130,7 @@ namespace ts.server { } watchWildcards(wildcardDirectories: Map) { + const getCanonicalFileName = createGetCanonicalFileName(this.projectService.host.useCaseSensitiveFileNames); if (wildcardDirectories) { if (this.directoriesWatchedForWildcards) { this.directoriesWatchedForWildcards.forEach(({ watcher, recursive }, directory) => { @@ -1145,7 +1147,7 @@ namespace ts.server { if (currentRecursive !== recursive) { this.projectService.logger.info(`Removing ${recursive ? "recursive " : ""} watcher for: ${directory}`); watcher.close(); - this.addWatcherForDirectory(currentFlag, directory, /*replaceExisting*/ true); + this.addWatcherForDirectory(currentFlag, directory, getCanonicalFileName, /*replaceExisting*/ true); } } }); @@ -1154,7 +1156,7 @@ namespace ts.server { this.directoriesWatchedForWildcards = createMap(); } wildcardDirectories.forEach((flag, directory) => - this.addWatcherForDirectory(flag, directory, /*replaceExisting*/ false)); + this.addWatcherForDirectory(flag, directory, getCanonicalFileName, /*replaceExisting*/ false)); } else { this.stopWatchingWildCards(); From 68def1b1f31eb1b45a83023b32f6d292839a4152 Mon Sep 17 00:00:00 2001 From: Sheetal Nandi Date: Tue, 11 Jul 2017 16:10:44 -0700 Subject: [PATCH 014/109] Use the cached file exists/directory exists for configured project since we watch the needed files --- .../unittests/tsserverProjectSystem.ts | 41 ++--- src/server/editorServices.ts | 34 +++-- src/server/lsHost.ts | 144 ++++++++++++++++-- src/server/project.ts | 47 +++--- src/server/scriptInfo.ts | 3 + src/server/utilities.ts | 4 +- 6 files changed, 204 insertions(+), 69 deletions(-) diff --git a/src/harness/unittests/tsserverProjectSystem.ts b/src/harness/unittests/tsserverProjectSystem.ts index 1c368cad69c5b..4426e52ab66e5 100644 --- a/src/harness/unittests/tsserverProjectSystem.ts +++ b/src/harness/unittests/tsserverProjectSystem.ts @@ -261,10 +261,11 @@ namespace ts.projectSystem { return s && typeof (s).content === "string"; } - function invokeDirectoryWatcher(callbacks: DirectoryWatcherCallback[], fileName: string) { + function invokeDirectoryWatcher(callbacks: DirectoryWatcherCallback[], getRelativeFilePath: () => string) { if (callbacks) { const cbs = callbacks.slice(); for (const cb of cbs) { + const fileName = getRelativeFilePath(); cb(fileName); } } @@ -407,7 +408,7 @@ namespace ts.projectSystem { // Update file if (currentEntry.content !== fileOrFolder.content) { currentEntry.content = fileOrFolder.content; - this.invokeFileWatcher(currentEntry.path, FileWatcherEventKind.Changed); + this.invokeFileWatcher(currentEntry.fullPath, FileWatcherEventKind.Changed); } } else { @@ -481,9 +482,9 @@ namespace ts.projectSystem { this.fs.set(fileOrFolder.path, fileOrFolder); if (isFile(fileOrFolder)) { - this.invokeFileWatcher(fileOrFolder.path, FileWatcherEventKind.Created); + this.invokeFileWatcher(fileOrFolder.fullPath, FileWatcherEventKind.Created); } - this.invokeDirectoryWatcher(folder.path, fileOrFolder.path); + this.invokeDirectoryWatcher(folder.fullPath, fileOrFolder.fullPath); } private removeFileOrFolder(fileOrFolder: File | Folder, isRemovableLeafFolder: (folder: Folder) => boolean) { @@ -496,12 +497,12 @@ namespace ts.projectSystem { this.fs.delete(fileOrFolder.path); if (isFile(fileOrFolder)) { - this.invokeFileWatcher(fileOrFolder.path, FileWatcherEventKind.Deleted); + this.invokeFileWatcher(fileOrFolder.fullPath, FileWatcherEventKind.Deleted); } else { Debug.assert(fileOrFolder.entries.length === 0); - invokeDirectoryWatcher(this.watchedDirectories.get(fileOrFolder.path), fileOrFolder.path); - invokeDirectoryWatcher(this.watchedDirectoriesRecursive.get(fileOrFolder.path), fileOrFolder.path); + invokeDirectoryWatcher(this.watchedDirectories.get(fileOrFolder.path), () => this.getRelativePathToDirectory(fileOrFolder.fullPath, fileOrFolder.fullPath)); + invokeDirectoryWatcher(this.watchedDirectoriesRecursive.get(fileOrFolder.path), () => this.getRelativePathToDirectory(fileOrFolder.fullPath, fileOrFolder.fullPath)); } if (basePath !== fileOrFolder.path) { @@ -509,25 +510,29 @@ namespace ts.projectSystem { this.removeFileOrFolder(baseFolder, isRemovableLeafFolder); } else { - this.invokeRecursiveDirectoryWatcher(baseFolder.path, fileOrFolder.path); + this.invokeRecursiveDirectoryWatcher(baseFolder.fullPath, fileOrFolder.fullPath); } } } - private invokeFileWatcher(filePath: Path, eventId: FileWatcherEventKind) { - const callbacks = this.watchedFiles.get(filePath); - invokeFileWatcher(callbacks, filePath, eventId); + private invokeFileWatcher(fileFullPath: string, eventId: FileWatcherEventKind) { + const callbacks = this.watchedFiles.get(this.toPath(fileFullPath)); + invokeFileWatcher(callbacks, getBaseFileName(fileFullPath), eventId); } - private invokeDirectoryWatcher(folderPath: Path, fileName: string) { - invokeDirectoryWatcher(this.watchedDirectories.get(folderPath), fileName); - this.invokeRecursiveDirectoryWatcher(folderPath, fileName); + private getRelativePathToDirectory(directoryFullPath: string, fileFullPath: string) { + return getRelativePathToDirectoryOrUrl(directoryFullPath, fileFullPath, this.currentDirectory, this.getCanonicalFileName, /*isAbsolutePathAnUrl*/ false); } - private invokeRecursiveDirectoryWatcher(path: Path, fileName: string) { - invokeDirectoryWatcher(this.watchedDirectoriesRecursive.get(path), fileName); - const basePath = getDirectoryPath(path); - if (path !== basePath) { + private invokeDirectoryWatcher(folderFullPath: string, fileName: string) { + invokeDirectoryWatcher(this.watchedDirectories.get(this.toPath(folderFullPath)), () => this.getRelativePathToDirectory(folderFullPath, fileName)); + this.invokeRecursiveDirectoryWatcher(folderFullPath, fileName); + } + + private invokeRecursiveDirectoryWatcher(fullPath: string, fileName: string) { + invokeDirectoryWatcher(this.watchedDirectoriesRecursive.get(this.toPath(fullPath)), () => this.getRelativePathToDirectory(fullPath, fileName)); + const basePath = getDirectoryPath(fullPath); + if (this.getCanonicalFileName(fullPath) !== this.getCanonicalFileName(basePath)) { this.invokeRecursiveDirectoryWatcher(basePath, fileName); } } diff --git a/src/server/editorServices.ts b/src/server/editorServices.ts index 30da6631a4920..ed2f50acd3905 100644 --- a/src/server/editorServices.ts +++ b/src/server/editorServices.ts @@ -593,8 +593,9 @@ namespace ts.server { this.printProjects(); } - private onTypeRootFileChanged(project: ConfiguredProject, fileName: string) { + private onTypeRootFileChanged(project: ConfiguredProject, fileName: NormalizedPath) { this.logger.info(`Type root file ${fileName} changed`); + project.getCachedServerHost().addOrDeleteFileOrFolder(fileName); project.updateTypes(); this.throttledOperations.schedule(project.getConfigFilePath() + " * type root", /*delay*/ 250, () => { this.reloadConfiguredProject(project); // TODO: Figure out why this is needed (should be redundant?) @@ -608,8 +609,8 @@ namespace ts.server { * @param fileName the absolute file name that changed in watched directory */ /* @internal */ - onFileAddOrRemoveInWatchedDirectoryOfProject(project: ConfiguredProject, fileName: Path) { - project.cachedParseConfigHost.clearCacheForFile(fileName); + onFileAddOrRemoveInWatchedDirectoryOfProject(project: ConfiguredProject, fileName: NormalizedPath) { + project.getCachedServerHost().addOrDeleteFileOrFolder(fileName); // If a change was made inside "folder/file", node will trigger the callback twice: // one with the fileName being "folder/file", and the other one with "folder". @@ -623,7 +624,7 @@ namespace ts.server { const configFileSpecs = project.configFileSpecs; const configFilename = normalizePath(project.getConfigFilePath()); // TODO: (sheetalkamat) use the host that caches - so we dont do file exists and read directory call - const result = getFileNamesFromConfigSpecs(configFileSpecs, getDirectoryPath(configFilename), project.getCompilerOptions(), project.cachedParseConfigHost, this.hostConfiguration.extraFileExtensions); + const result = getFileNamesFromConfigSpecs(configFileSpecs, getDirectoryPath(configFilename), project.getCompilerOptions(), project.getCachedServerHost(), this.hostConfiguration.extraFileExtensions); const errors = project.getAllProjectErrors(); if (result.fileNames.length === 0) { if (!configFileSpecs.filesSpecs) { @@ -947,7 +948,7 @@ namespace ts.server { return findProjectByName(projectFileName, this.externalProjects); } - private convertConfigFileContentToProjectOptions(configFilename: string, cachedParseConfigHost: CachedParseConfigHost) { + private convertConfigFileContentToProjectOptions(configFilename: string, cachedServerHost: CachedServerHost) { configFilename = normalizePath(configFilename); const configFileContent = this.host.readFile(configFilename); @@ -959,7 +960,7 @@ namespace ts.server { const errors = result.parseDiagnostics; const parsedCommandLine = parseJsonSourceFileConfigFileContent( result, - cachedParseConfigHost, + cachedServerHost, getDirectoryPath(configFilename), /*existingOptions*/ {}, configFilename, @@ -1107,7 +1108,7 @@ namespace ts.server { }); } - private createAndAddConfiguredProject(configFileName: NormalizedPath, projectOptions: ProjectOptions, configFileErrors: Diagnostic[], configFileSpecs: ConfigFileSpecs, cachedParseConfigHost: CachedParseConfigHost, clientFileName?: string) { + private createAndAddConfiguredProject(configFileName: NormalizedPath, projectOptions: ProjectOptions, configFileErrors: Diagnostic[], configFileSpecs: ConfigFileSpecs, cachedServerHost: CachedServerHost, clientFileName?: string) { const sizeLimitExceeded = this.exceededTotalSizeLimitForNonTsFiles(configFileName, projectOptions.compilerOptions, projectOptions.files, fileNamePropertyReader); const project = new ConfiguredProject( configFileName, @@ -1116,8 +1117,8 @@ namespace ts.server { projectOptions.configHasFilesProperty, projectOptions.compilerOptions, /*languageServiceEnabled*/ !sizeLimitExceeded, - projectOptions.compileOnSave === undefined ? false : projectOptions.compileOnSave); - project.cachedParseConfigHost = cachedParseConfigHost; + projectOptions.compileOnSave === undefined ? false : projectOptions.compileOnSave, + cachedServerHost); this.addFilesToProjectAndUpdateGraph(project, projectOptions.files, fileNamePropertyReader, clientFileName, projectOptions.typeAcquisition, configFileErrors); project.configFileSpecs = configFileSpecs; @@ -1136,7 +1137,7 @@ namespace ts.server { const scriptKind = propertyReader.getScriptKind(f); const hasMixedContent = propertyReader.hasMixedContent(f, this.hostConfiguration.extraFileExtensions); const fileName = toNormalizedPath(rootFilename); - if (this.host.fileExists(rootFilename)) { + if (project.lsHost.host.fileExists(rootFilename)) { const info = this.getOrCreateScriptInfoForNormalizedPath(fileName, /*openedByClient*/ clientFileName === rootFilename, /*fileContent*/ undefined, scriptKind, hasMixedContent); project.addRoot(info); } @@ -1151,13 +1152,13 @@ namespace ts.server { } private openConfigFile(configFileName: NormalizedPath, clientFileName?: string) { - const cachedParseConfigHost = new CachedParseConfigHost(this.host); - const { success, projectOptions, configFileErrors, configFileSpecs } = this.convertConfigFileContentToProjectOptions(configFileName, cachedParseConfigHost); + const cachedServerHost = new CachedServerHost(this.host); + const { success, projectOptions, configFileErrors, configFileSpecs } = this.convertConfigFileContentToProjectOptions(configFileName, cachedServerHost); if (success) { this.logger.info(`Opened configuration file ${configFileName}`); } - return this.createAndAddConfiguredProject(configFileName, projectOptions, configFileErrors, configFileSpecs, cachedParseConfigHost, clientFileName); + return this.createAndAddConfiguredProject(configFileName, projectOptions, configFileErrors, configFileSpecs, cachedServerHost, clientFileName); } private updateNonInferredProjectFiles(project: ExternalProject | ConfiguredProject, newUncheckedFiles: T[], propertyReader: FilePropertyReader) { @@ -1169,7 +1170,7 @@ namespace ts.server { const normalizedPath = toNormalizedPath(newRootFile); let scriptInfo: ScriptInfo | NormalizedPath; let path: Path; - if (!this.host.fileExists(newRootFile)) { + if (!project.lsHost.fileExists(newRootFile)) { path = normalizedPathToPath(normalizedPath, this.host.getCurrentDirectory(), this.toCanonicalFileName); const existingValue = projectRootFilesMap.get(path); if (isScriptInfo(existingValue)) { @@ -1242,8 +1243,9 @@ namespace ts.server { // note: the returned "success" is true does not mean the "configFileErrors" is empty. // because we might have tolerated the errors and kept going. So always return the configFileErrors // regardless the "success" here is true or not. - project.cachedParseConfigHost.clearCache(); - const { success, projectOptions, configFileErrors, configFileSpecs } = this.convertConfigFileContentToProjectOptions(project.getConfigFilePath(), project.cachedParseConfigHost); + const host = project.getCachedServerHost(); + host.clearCache(); + const { success, projectOptions, configFileErrors, configFileSpecs } = this.convertConfigFileContentToProjectOptions(project.getConfigFilePath(), host); project.configFileSpecs = configFileSpecs; if (!success) { // reset project settings to default diff --git a/src/server/lsHost.ts b/src/server/lsHost.ts index 720748eb5d0b6..b050a7d26d900 100644 --- a/src/server/lsHost.ts +++ b/src/server/lsHost.ts @@ -5,17 +5,34 @@ namespace ts.server { type NameResolutionWithFailedLookupLocations = { failedLookupLocations: string[], isInvalidated?: boolean }; - export class CachedParseConfigHost implements ParseConfigHost { + export class CachedServerHost implements ServerHost { + args: string[]; + newLine: string; useCaseSensitiveFileNames: boolean; + + readonly trace: (s: string) => void; + readonly realpath?: (path: string) => string; + private getCanonicalFileName: (fileName: string) => string; private cachedReadDirectoryResult = createMap(); + private readonly currentDirectory: string; + constructor(private readonly host: ServerHost) { + this.args = host.args; + this.newLine = host.newLine; this.useCaseSensitiveFileNames = host.useCaseSensitiveFileNames; this.getCanonicalFileName = createGetCanonicalFileName(this.useCaseSensitiveFileNames); + if (host.trace) { + this.trace = s => host.trace(s); + } + if (this.host.realpath) { + this.realpath = path => this.host.realpath(path); + } + this.currentDirectory = this.host.getCurrentDirectory(); } private getFileSystemEntries(rootDir: string) { - const path = ts.toPath(rootDir, this.host.getCurrentDirectory(), this.getCanonicalFileName); + const path = ts.toPath(rootDir, this.currentDirectory, this.getCanonicalFileName); const cachedResult = this.cachedReadDirectoryResult.get(path); if (cachedResult) { return cachedResult; @@ -23,52 +40,148 @@ namespace ts.server { const resultFromHost: FileSystemEntries = { files: this.host.readDirectory(rootDir, /*extensions*/ undefined, /*exclude*/ undefined, /*include*/["*.*"]) || [], - directories: this.host.getDirectories(rootDir) + directories: this.host.getDirectories(rootDir) || [] }; this.cachedReadDirectoryResult.set(path, resultFromHost); return resultFromHost; } + write(s: string) { + return this.host.write(s); + } + + writeFile(fileName: string, data: string, writeByteOrderMark?: boolean) { + const path = ts.toPath(fileName, this.currentDirectory, this.getCanonicalFileName); + const result = this.cachedReadDirectoryResult.get(getDirectoryPath(path)); + const baseFileName = getBaseFileName(toNormalizedPath(fileName)); + if (result && !some(result.files, file => this.fileNameEqual(file, baseFileName))) { + result.files.push(baseFileName); + } + return this.host.writeFile(fileName, data, writeByteOrderMark); + } + + resolvePath(path: string) { + return this.host.resolvePath(path); + } + + createDirectory(path: string) { + Debug.fail(`Why is createDirectory called on the cached server for ${path}`); + } + + getExecutingFilePath() { + return this.host.getExecutingFilePath(); + } + + getCurrentDirectory() { + return this.currentDirectory; + } + + exit(exitCode?: number) { + Debug.fail(`Why is exit called on the cached server: ${exitCode}`); + } + + getEnvironmentVariable(name: string) { + Debug.fail(`Why is getEnvironmentVariable called on the cached server: ${name}`); + return this.host.getEnvironmentVariable(name); + } + + getDirectories(path: string) { + return this.getFileSystemEntries(path).directories; + } + readDirectory(rootDir: string, extensions: string[], excludes: string[], includes: string[], depth: number): string[] { - return matchFiles(rootDir, extensions, excludes, includes, this.useCaseSensitiveFileNames, this.host.getCurrentDirectory(), depth, path => this.getFileSystemEntries(path)); + return matchFiles(rootDir, extensions, excludes, includes, this.useCaseSensitiveFileNames, this.currentDirectory, depth, path => this.getFileSystemEntries(path)); } fileExists(fileName: string): boolean { - const path = ts.toPath(fileName, this.host.getCurrentDirectory(), this.getCanonicalFileName); - const result = this.getFileSystemEntries(getDirectoryPath(path)); - return contains(result.files, fileName); + const path = ts.toPath(fileName, this.currentDirectory, this.getCanonicalFileName); + const result = this.cachedReadDirectoryResult.get(getDirectoryPath(path)); + const baseName = getBaseFileName(toNormalizedPath(fileName)); + return (result && some(result.files, file => this.fileNameEqual(file, baseName))) || this.host.fileExists(fileName); } - readFile(path: string): string { - return this.host.readFile(path); + directoryExists(dirPath: string) { + const path = ts.toPath(dirPath, this.currentDirectory, this.getCanonicalFileName); + return this.cachedReadDirectoryResult.has(path) || this.host.directoryExists(dirPath); } - clearCacheForFile(fileName: Path) { - this.cachedReadDirectoryResult.delete(fileName); - this.cachedReadDirectoryResult.delete(getDirectoryPath(fileName)); + readFile(path: string, encoding?: string): string { + return this.host.readFile(path, encoding); + } + + private fileNameEqual(name1: string, name2: string) { + return this.getCanonicalFileName(name1) === this.getCanonicalFileName(name2); + } + + private updateFileSystemEntry(entries: string[], baseName: string, isValid: boolean) { + if (some(entries, entry => this.fileNameEqual(entry, baseName))) { + if (!isValid) { + filterMutate(entries, entry => !this.fileNameEqual(entry, baseName)); + } + } + else if (isValid) { + entries.push(baseName); + } + } + + addOrDeleteFileOrFolder(fileOrFolder: NormalizedPath) { + const path = toPath(fileOrFolder, this.currentDirectory, this.getCanonicalFileName); + const existingResult = this.cachedReadDirectoryResult.get(path); + if (existingResult) { + if (!this.host.directoryExists(fileOrFolder)) { + this.cachedReadDirectoryResult.delete(path); + } + } + else { + // Was this earlier file + const parentResult = this.cachedReadDirectoryResult.get(getDirectoryPath(path)); + if (parentResult) { + const baseName = getBaseFileName(fileOrFolder); + if (parentResult) { + this.updateFileSystemEntry(parentResult.files, baseName, this.host.fileExists(path)); + this.updateFileSystemEntry(parentResult.directories, baseName, this.host.directoryExists(path)); + } + } + } } clearCache() { this.cachedReadDirectoryResult = createMap(); } - // TODO: (sheetalkamat) to cache getFileSize as well as fileExists and readFile + setTimeout(callback: (...args: any[]) => void, ms: number, ...args: any[]) { + return this.host.setTimeout(callback, ms, ...args); + } + clearTimeout(timeoutId: any) { + return this.host.clearTimeout(timeoutId); + } + setImmediate(callback: (...args: any[]) => void, ...args: any[]) { + this.host.setImmediate(callback, ...args); + } + clearImmediate(timeoutId: any) { + this.host.clearImmediate(timeoutId); + } + } export class LSHost implements ts.LanguageServiceHost, ModuleResolutionHost { private compilationSettings: ts.CompilerOptions; private readonly resolvedModuleNames = createMap>(); private readonly resolvedTypeReferenceDirectives = createMap>(); - private readonly getCanonicalFileName: (fileName: string) => string; + /* @internal */ + readonly getCanonicalFileName: (fileName: string) => string; private filesWithChangedSetOfUnresolvedImports: Path[]; private resolveModuleName: typeof resolveModuleName; readonly trace: (s: string) => void; readonly realpath?: (path: string) => string; + /*@internal*/ + host: ServerHost; - constructor(private readonly host: ServerHost, private project: Project, private readonly cancellationToken: HostCancellationToken) { + constructor(host: ServerHost, private project: Project, private readonly cancellationToken: HostCancellationToken) { + this.host = host; this.cancellationToken = new ThrottledCancellationToken(cancellationToken, project.projectService.throttleWaitMilliseconds); this.getCanonicalFileName = ts.createGetCanonicalFileName(this.host.useCaseSensitiveFileNames); @@ -103,6 +216,7 @@ namespace ts.server { dispose() { this.project = undefined; this.resolveModuleName = undefined; + this.host = undefined; } public startRecordingFilesWithChangedResolutions() { diff --git a/src/server/project.ts b/src/server/project.ts index cfd234b64ea66..b6f040b5a2632 100644 --- a/src/server/project.ts +++ b/src/server/project.ts @@ -127,7 +127,8 @@ namespace ts.server { public languageServiceEnabled = true; - protected lsHost: LSHost; + /*@internal*/ + lsHost: LSHost; builder: Builder; /** @@ -194,7 +195,8 @@ namespace ts.server { hasExplicitListOfFiles: boolean, languageServiceEnabled: boolean, private compilerOptions: CompilerOptions, - public compileOnSaveEnabled: boolean) { + public compileOnSaveEnabled: boolean, + host: ServerHost) { if (!this.compilerOptions) { this.compilerOptions = ts.getDefaultCompilerOptions(); @@ -208,7 +210,7 @@ namespace ts.server { this.setInternalCompilerOptionsForEmittingJsFiles(); - this.lsHost = new LSHost(this.projectService.host, this, this.projectService.cancellationToken); + this.lsHost = new LSHost(host, this, this.projectService.cancellationToken); this.lsHost.setCompilationSettings(this.compilerOptions); this.languageService = ts.createLanguageService(this.lsHost, this.documentRegistry); @@ -647,11 +649,16 @@ namespace ts.server { // Missing files that are not yet watched should be added to the map. for (const missingFilePath of missingFilePaths) { if (!this.missingFilesMap.has(missingFilePath)) { - const fileWatcher = this.projectService.host.watchFile(missingFilePath, (_filename: string, eventKind: FileWatcherEventKind) => { + const fileWatcher = this.projectService.host.watchFile(missingFilePath, (filename: string, eventKind: FileWatcherEventKind) => { if (eventKind === FileWatcherEventKind.Created && this.missingFilesMap.has(missingFilePath)) { fileWatcher.close(); this.missingFilesMap.delete(missingFilePath); + if (this.projectKind === ProjectKind.Configured) { + const absoluteNormalizedPath = getNormalizedAbsolutePath(filename, getDirectoryPath(missingFilePath)); + (this.lsHost.host as CachedServerHost).addOrDeleteFileOrFolder(toNormalizedPath(absoluteNormalizedPath)); + } + // When a missing file is created, we should update the graph. this.markAsDirty(); this.updateGraph(); @@ -849,7 +856,7 @@ namespace ts.server { } const allFileNames = arrayFrom(referencedFiles.keys()) as Path[]; - return filter(allFileNames, file => this.projectService.host.fileExists(file)); + return filter(allFileNames, file => this.lsHost.host.fileExists(file)); } // remove a root file from project @@ -911,7 +918,8 @@ namespace ts.server { /*files*/ undefined, /*languageServiceEnabled*/ true, compilerOptions, - /*compileOnSaveEnabled*/ false); + /*compileOnSaveEnabled*/ false, + projectService.host); } addRoot(info: ScriptInfo) { @@ -972,7 +980,6 @@ namespace ts.server { /*@internal*/ configFileSpecs: ConfigFileSpecs; - cachedParseConfigHost: CachedParseConfigHost; private plugins: PluginModule[] = []; @@ -985,12 +992,17 @@ namespace ts.server { hasExplicitListOfFiles: boolean, compilerOptions: CompilerOptions, languageServiceEnabled: boolean, - public compileOnSaveEnabled: boolean) { - super(configFileName, ProjectKind.Configured, projectService, documentRegistry, hasExplicitListOfFiles, languageServiceEnabled, compilerOptions, compileOnSaveEnabled); + public compileOnSaveEnabled: boolean, + cachedServerHost: CachedServerHost) { + super(configFileName, ProjectKind.Configured, projectService, documentRegistry, hasExplicitListOfFiles, languageServiceEnabled, compilerOptions, compileOnSaveEnabled, cachedServerHost); this.canonicalConfigFilePath = asNormalizedPath(projectService.toCanonicalFileName(configFileName)); this.enablePlugins(); } + getCachedServerHost() { + return this.lsHost.host as CachedServerHost; + } + getConfigFilePath() { return this.getProjectName(); } @@ -1104,24 +1116,24 @@ namespace ts.server { this.configFileWatcher = this.projectService.host.watchFile(this.getConfigFilePath(), (_fileName, eventKind) => callback(this, eventKind)); } - watchTypeRoots(callback: (project: ConfiguredProject, path: string) => void) { + watchTypeRoots(callback: (project: ConfiguredProject, path: NormalizedPath) => void) { const roots = this.getEffectiveTypeRoots(); const watchers: FileWatcher[] = []; for (const root of roots) { this.projectService.logger.info(`Add type root watcher for: ${root}`); - watchers.push(this.projectService.host.watchDirectory(root, path => callback(this, path), /*recursive*/ false)); + watchers.push(this.projectService.host.watchDirectory(root, path => callback(this, toNormalizedPath(getNormalizedAbsolutePath(path, root))), /*recursive*/ false)); } this.typeRootsWatchers = watchers; } - private addWatcherForDirectory(flag: WatchDirectoryFlags, directory: string, getCanonicalFileName: (fileName: string) => string, replaceExisting: boolean) { + private addWatcherForDirectory(flag: WatchDirectoryFlags, directory: string, replaceExisting: boolean) { if (replaceExisting || !this.directoriesWatchedForWildcards.has(directory)) { const recursive = (flag & WatchDirectoryFlags.Recursive) !== 0; this.projectService.logger.info(`Add ${recursive ? "recursive " : ""} watcher for: ${directory}`); this.directoriesWatchedForWildcards.set(directory, { watcher: this.projectService.host.watchDirectory( directory, - path => this.projectService.onFileAddOrRemoveInWatchedDirectoryOfProject(this, toPath(path, directory, getCanonicalFileName)), + path => this.projectService.onFileAddOrRemoveInWatchedDirectoryOfProject(this, toNormalizedPath(getNormalizedAbsolutePath(path, directory))), recursive ), recursive @@ -1130,7 +1142,6 @@ namespace ts.server { } watchWildcards(wildcardDirectories: Map) { - const getCanonicalFileName = createGetCanonicalFileName(this.projectService.host.useCaseSensitiveFileNames); if (wildcardDirectories) { if (this.directoriesWatchedForWildcards) { this.directoriesWatchedForWildcards.forEach(({ watcher, recursive }, directory) => { @@ -1147,7 +1158,7 @@ namespace ts.server { if (currentRecursive !== recursive) { this.projectService.logger.info(`Removing ${recursive ? "recursive " : ""} watcher for: ${directory}`); watcher.close(); - this.addWatcherForDirectory(currentFlag, directory, getCanonicalFileName, /*replaceExisting*/ true); + this.addWatcherForDirectory(currentFlag, directory, /*replaceExisting*/ true); } } }); @@ -1156,7 +1167,7 @@ namespace ts.server { this.directoriesWatchedForWildcards = createMap(); } wildcardDirectories.forEach((flag, directory) => - this.addWatcherForDirectory(flag, directory, getCanonicalFileName, /*replaceExisting*/ false)); + this.addWatcherForDirectory(flag, directory, /*replaceExisting*/ false)); } else { this.stopWatchingWildCards(); @@ -1201,7 +1212,7 @@ namespace ts.server { } getEffectiveTypeRoots() { - return ts.getEffectiveTypeRoots(this.getCompilerOptions(), this.projectService.host) || []; + return ts.getEffectiveTypeRoots(this.getCompilerOptions(), this.lsHost.host) || []; } } @@ -1218,7 +1229,7 @@ namespace ts.server { languageServiceEnabled: boolean, public compileOnSaveEnabled: boolean, private readonly projectFilePath?: string) { - super(externalProjectName, ProjectKind.External, projectService, documentRegistry, /*hasExplicitListOfFiles*/ true, languageServiceEnabled, compilerOptions, compileOnSaveEnabled); + super(externalProjectName, ProjectKind.External, projectService, documentRegistry, /*hasExplicitListOfFiles*/ true, languageServiceEnabled, compilerOptions, compileOnSaveEnabled, projectService.host); } getProjectRootPath() { diff --git a/src/server/scriptInfo.ts b/src/server/scriptInfo.ts index ee5e45c7c4753..90853124815b1 100644 --- a/src/server/scriptInfo.ts +++ b/src/server/scriptInfo.ts @@ -254,6 +254,9 @@ namespace ts.server { detachAllProjects() { for (const p of this.containingProjects) { + if (p.projectKind === ProjectKind.Configured) { + (p.lsHost.host as CachedServerHost).addOrDeleteFileOrFolder(this.fileName); + } const isInfoRoot = p.isRoot(this); // detach is unnecessary since we'll clean the list of containing projects anyways p.removeFile(this, /*detachFromProjects*/ false); diff --git a/src/server/utilities.ts b/src/server/utilities.ts index 10290fe864da2..c2126d4ce4515 100644 --- a/src/server/utilities.ts +++ b/src/server/utilities.ts @@ -42,7 +42,7 @@ namespace ts.server { return ""; case ProjectKind.External: const projectName = normalizeSlashes(project.getProjectName()); - return project.projectService.host.fileExists(projectName) ? getDirectoryPath(projectName) : projectName; + return project.lsHost.host.fileExists(projectName) ? getDirectoryPath(projectName) : projectName; } } @@ -273,4 +273,4 @@ namespace ts.server { } } } -} \ No newline at end of file +} From 029b1f25e442f0982cb103f47a03881bb0176019 Mon Sep 17 00:00:00 2001 From: Sheetal Nandi Date: Wed, 12 Jul 2017 12:50:52 -0700 Subject: [PATCH 015/109] Fixes the fourslash runner tests by handling hosts that cannot support read directory or getDirectories --- src/server/lsHost.ts | 26 +++++++++++++++++++++++--- 1 file changed, 23 insertions(+), 3 deletions(-) diff --git a/src/server/lsHost.ts b/src/server/lsHost.ts index b050a7d26d900..133db8cac2cb2 100644 --- a/src/server/lsHost.ts +++ b/src/server/lsHost.ts @@ -47,6 +47,20 @@ namespace ts.server { return resultFromHost; } + private canWorkWithCacheForDir(rootDir: string) { + // Some of the hosts might not be able to handle read directory or getDirectories + const path = ts.toPath(rootDir, this.currentDirectory, this.getCanonicalFileName); + if (this.cachedReadDirectoryResult.get(path)) { + return true; + } + try { + return this.getFileSystemEntries(rootDir); + } + catch (_e) { + return false; + } + } + write(s: string) { return this.host.write(s); } @@ -86,12 +100,18 @@ namespace ts.server { return this.host.getEnvironmentVariable(name); } - getDirectories(path: string) { - return this.getFileSystemEntries(path).directories; + getDirectories(rootDir: string) { + if (this.canWorkWithCacheForDir(rootDir)) { + return this.getFileSystemEntries(rootDir).directories; + } + return this.host.getDirectories(rootDir); } readDirectory(rootDir: string, extensions: string[], excludes: string[], includes: string[], depth: number): string[] { - return matchFiles(rootDir, extensions, excludes, includes, this.useCaseSensitiveFileNames, this.currentDirectory, depth, path => this.getFileSystemEntries(path)); + if (this.canWorkWithCacheForDir(rootDir)) { + return matchFiles(rootDir, extensions, excludes, includes, this.useCaseSensitiveFileNames, this.currentDirectory, depth, path => this.getFileSystemEntries(path)); + } + return this.host.readDirectory(rootDir, extensions, excludes, includes, depth); } fileExists(fileName: string): boolean { From f338a7025721f78d9dba9c1f9d3f506976a811dd Mon Sep 17 00:00:00 2001 From: Sheetal Nandi Date: Wed, 12 Jul 2017 13:39:11 -0700 Subject: [PATCH 016/109] Remove the done TODO --- src/server/editorServices.ts | 3 +-- src/server/utilities.ts | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/server/editorServices.ts b/src/server/editorServices.ts index ed2f50acd3905..5b2e428485754 100644 --- a/src/server/editorServices.ts +++ b/src/server/editorServices.ts @@ -623,7 +623,6 @@ namespace ts.server { const configFileSpecs = project.configFileSpecs; const configFilename = normalizePath(project.getConfigFilePath()); - // TODO: (sheetalkamat) use the host that caches - so we dont do file exists and read directory call const result = getFileNamesFromConfigSpecs(configFileSpecs, getDirectoryPath(configFilename), project.getCompilerOptions(), project.getCachedServerHost(), this.hostConfiguration.extraFileExtensions); const errors = project.getAllProjectErrors(); if (result.fileNames.length === 0) { @@ -1137,7 +1136,7 @@ namespace ts.server { const scriptKind = propertyReader.getScriptKind(f); const hasMixedContent = propertyReader.hasMixedContent(f, this.hostConfiguration.extraFileExtensions); const fileName = toNormalizedPath(rootFilename); - if (project.lsHost.host.fileExists(rootFilename)) { + if (project.lsHost.fileExists(rootFilename)) { const info = this.getOrCreateScriptInfoForNormalizedPath(fileName, /*openedByClient*/ clientFileName === rootFilename, /*fileContent*/ undefined, scriptKind, hasMixedContent); project.addRoot(info); } diff --git a/src/server/utilities.ts b/src/server/utilities.ts index c2126d4ce4515..416b148fc0d06 100644 --- a/src/server/utilities.ts +++ b/src/server/utilities.ts @@ -42,7 +42,7 @@ namespace ts.server { return ""; case ProjectKind.External: const projectName = normalizeSlashes(project.getProjectName()); - return project.lsHost.host.fileExists(projectName) ? getDirectoryPath(projectName) : projectName; + return project.projectService.host.fileExists(projectName) ? getDirectoryPath(projectName) : projectName; } } From 8fedcf78c780004f2fd1f4c0eb4ce81263c97c1c Mon Sep 17 00:00:00 2001 From: Sheetal Nandi Date: Wed, 12 Jul 2017 19:34:01 -0700 Subject: [PATCH 017/109] TODOs for the scheduling update graph and referesh projects --- src/server/editorServices.ts | 57 +++++++++++++++++++++++------------- src/server/project.ts | 2 ++ src/server/session.ts | 1 + 3 files changed, 39 insertions(+), 21 deletions(-) diff --git a/src/server/editorServices.ts b/src/server/editorServices.ts index 5b2e428485754..9aa627e75a13e 100644 --- a/src/server/editorServices.ts +++ b/src/server/editorServices.ts @@ -443,6 +443,8 @@ namespace ts.server { this.typingsCache.deleteTypingsForProject(response.projectName); break; } + project.markAsDirty(); + // TODO: (sheetalkamat) Schedule Update graph instead of immediate project.updateGraph(); } @@ -456,6 +458,7 @@ namespace ts.server { proj.setCompilerOptions(this.compilerOptionsForInferredProjects); proj.compileOnSaveEnabled = projectCompilerOptions.compileOnSave; } + // TODO: (sheetalkamat) (ensure updated projects) this.updateProjectGraphs(this.inferredProjects); } @@ -476,6 +479,7 @@ namespace ts.server { getDefaultProjectForFile(fileName: NormalizedPath, refreshInferredProjects: boolean) { if (refreshInferredProjects) { + // TODO: (sheetalkamat) needs to update inferred projects too ? this.ensureInferredProjectsUpToDate(); } const scriptInfo = this.getScriptInfoForNormalizedPath(fileName); @@ -498,6 +502,7 @@ namespace ts.server { this.updateProjectGraphs(projectsToUpdate); this.changedFiles = undefined; } + // TODO: (sheetalkamat) should also refresh inferred projects here since we are postponing the changes in closed files } private findContainingExternalProject(fileName: NormalizedPath): ExternalProject { @@ -554,7 +559,7 @@ namespace ts.server { // file has been changed which might affect the set of referenced files in projects that include // this file and set of inferred projects info.reloadFromFile(); - this.updateProjectGraphs(info.containingProjects); + this.recordChangedFile(info); } } } @@ -575,6 +580,7 @@ namespace ts.server { info.detachAllProjects(); + // TODO: (sheetalkamat) Schedule this too ? // update projects to make sure that set of referenced files is correct this.updateProjectGraphs(containingProjects); @@ -597,10 +603,13 @@ namespace ts.server { this.logger.info(`Type root file ${fileName} changed`); project.getCachedServerHost().addOrDeleteFileOrFolder(fileName); project.updateTypes(); - this.throttledOperations.schedule(project.getConfigFilePath() + " * type root", /*delay*/ 250, () => { - this.reloadConfiguredProject(project); // TODO: Figure out why this is needed (should be redundant?) + // TODO: (sheetalkamat) schedule project update and referesh inferred projects instead + + //this.throttledOperations.schedule(project.getConfigFilePath() + " * type root", /*delay*/ 250, () => { + // this.reloadConfiguredProject(project); // TODO: Figure out why this is needed (should be redundant?) + project.updateGraph(); this.refreshInferredProjects(); - }); + //}); } /** @@ -655,10 +664,12 @@ namespace ts.server { // Reload the configured projects for these open files in the project as // they could be held up by another config file somewhere in the parent directory const openFilesInProject = this.getOrphanFiles(); + // TODO: (sheetalkamat) can this be scheduled too this.reloadConfiguredProjectForFiles(openFilesInProject); } else { this.logger.info(`Config file changed: ${configFileName}`); + // TODO: (sheetalkamat) This should be scheduled and marked for reload of configured project since we dont want to updating this on every change this.reloadConfiguredProject(project); this.reportConfigFileDiagnostics(configFileName, project.getGlobalProjectErrors(), /*triggerFile*/ configFileName); } @@ -923,7 +934,7 @@ namespace ts.server { function printProjects(logger: Logger, projects: Project[], counter: number) { for (const project of projects) { - project.updateGraph(); + // Print shouldnt update the graph. It should emit whatever state the project is currently in logger.info(`Project '${project.getProjectName()}' (${ProjectKind[project.projectKind]}) ${counter}`); logger.info(project.filesToString()); logger.info("-----------------------------------------------"); @@ -1215,6 +1226,7 @@ namespace ts.server { } }); } + project.markAsDirty(); // Just to ensure that even if root files dont change, the changes to the non root file are picked up } private updateNonInferredProject(project: ExternalProject | ConfiguredProject, newUncheckedFiles: T[], propertyReader: FilePropertyReader, newOptions: CompilerOptions, newTypeAcquisition: TypeAcquisition, compileOnSave: boolean, configFileErrors: Diagnostic[]) { @@ -1229,6 +1241,7 @@ namespace ts.server { } project.setProjectErrors(configFileErrors); + // This doesnt need scheduling since its either creation or reload of the project project.updateGraph(); } @@ -1261,20 +1274,14 @@ namespace ts.server { */ private updateConfiguredProject(project: ConfiguredProject, projectOptions: ProjectOptions, configFileErrors: Diagnostic[]) { if (this.exceededTotalSizeLimitForNonTsFiles(project.canonicalConfigFilePath, projectOptions.compilerOptions, projectOptions.files, fileNamePropertyReader)) { - project.setCompilerOptions(projectOptions.compilerOptions); - if (!project.languageServiceEnabled) { - // language service is already disabled - project.setProjectErrors(configFileErrors); - } project.disableLanguageService(); project.stopWatchingWildCards(); - project.setProjectErrors(configFileErrors); } else { project.enableLanguageService(); project.watchWildcards(projectOptions.wildcardDirectories); - this.updateNonInferredProject(project, projectOptions.files, fileNamePropertyReader, projectOptions.compilerOptions, projectOptions.typeAcquisition, projectOptions.compileOnSave, configFileErrors); } + this.updateNonInferredProject(project, projectOptions.files, fileNamePropertyReader, projectOptions.compilerOptions, projectOptions.typeAcquisition, projectOptions.compileOnSave, configFileErrors); } createInferredProjectWithRootFileIfNecessary(root: ScriptInfo) { @@ -1540,9 +1547,19 @@ namespace ts.server { return files; } + private recordChangedFile(scriptInfo: ScriptInfo) { + if (!this.changedFiles) { + this.changedFiles = [scriptInfo]; + } + else if (this.changedFiles.indexOf(scriptInfo) < 0) { + this.changedFiles.push(scriptInfo); + } + // TODO: (sheetalkamat) Schedule the project update graphs + this.updateProjectGraphs(scriptInfo.containingProjects); + } + /* @internal */ applyChangesInOpenFiles(openFiles: protocol.ExternalFile[], changedFiles: protocol.ChangedOpenFile[], closedFiles: string[]): void { - const recordChangedFiles = changedFiles && !openFiles && !closedFiles; if (openFiles) { for (const file of openFiles) { const scriptInfo = this.getScriptInfo(file.fileName); @@ -1561,14 +1578,7 @@ namespace ts.server { const change = file.changes[i]; scriptInfo.editContent(change.span.start, change.span.start + change.span.length, change.newText); } - if (recordChangedFiles) { - if (!this.changedFiles) { - this.changedFiles = [scriptInfo]; - } - else if (this.changedFiles.indexOf(scriptInfo) < 0) { - this.changedFiles.push(scriptInfo); - } - } + this.recordChangedFile(scriptInfo); } } @@ -1580,6 +1590,7 @@ namespace ts.server { // if files were open or closed then explicitly refresh list of inferred projects // otherwise if there were only changes in files - record changed files in `changedFiles` and defer the update if (openFiles || closedFiles) { + // TODO: (sheetalkamat) is this ensureRefereshedProjects instead ? this.refreshInferredProjects(); } } @@ -1603,6 +1614,7 @@ namespace ts.server { } this.externalProjectToConfiguredProjectMap.delete(fileName); if (shouldRefreshInferredProjects && !suppressRefresh) { + // TODO: (sheetalkamat) is this ensureRefereshedProjects instead ? this.refreshInferredProjects(); } } @@ -1612,6 +1624,7 @@ namespace ts.server { if (externalProject) { this.removeProject(externalProject); if (!suppressRefresh) { + // TODO: (sheetalkamat) is this ensureRefereshedProjects instead ? this.refreshInferredProjects(); } } @@ -1636,6 +1649,7 @@ namespace ts.server { this.closeExternalProject(externalProjectName, /*suppressRefresh*/ true); }); + // TODO: (sheetalkamat) is this ensureRefereshedProjects instead ? this.refreshInferredProjects(); } @@ -1830,6 +1844,7 @@ namespace ts.server { this.createAndAddExternalProject(proj.projectFileName, rootFiles, proj.options, proj.typeAcquisition); } if (!suppressRefreshOfInferredProjects) { + // TODO: (sheetalkamat) is this ensureRefereshedProjects instead ? this.refreshInferredProjects(); } } diff --git a/src/server/project.ts b/src/server/project.ts index b6f040b5a2632..e3e7de78eefc2 100644 --- a/src/server/project.ts +++ b/src/server/project.ts @@ -555,6 +555,7 @@ namespace ts.server { * @returns: true if set of files in the project stays the same and false - otherwise. */ updateGraph(): boolean { + // TODO: (sheetalkamat) If reload scheduled do that before updating the graph this.lsHost.startRecordingFilesWithChangedResolutions(); let hasChanges = this.updateGraphWorker(); @@ -661,6 +662,7 @@ namespace ts.server { // When a missing file is created, we should update the graph. this.markAsDirty(); + // TODO: (sheetalkamat) schedule the update graph instead of doing it right away this.updateGraph(); } }); diff --git a/src/server/session.ts b/src/server/session.ts index 4b4d0a02265d4..2656b6a20ba18 100644 --- a/src/server/session.ts +++ b/src/server/session.ts @@ -462,6 +462,7 @@ namespace ts.server { private updateProjectStructure(seq: number, matchSeq: (seq: number) => boolean, ms = 1500) { this.host.setTimeout(() => { if (matchSeq(seq)) { + // TODO: (sheetalkamat) is this ensureRefereshedProjects instead ? this.projectService.refreshInferredProjects(); } }, ms); From e568976239e104e6b678449f9369d2e1ae4bed50 Mon Sep 17 00:00:00 2001 From: Sheetal Nandi Date: Wed, 12 Jul 2017 23:15:03 -0700 Subject: [PATCH 018/109] Allows the delayed update graph and project structure which helps in batching the updates when there are multiple files added/removed/changed --- .../unittests/tsserverProjectSystem.ts | 30 ++- src/harness/unittests/typingsInstaller.ts | 20 +- src/server/editorServices.ts | 172 ++++++++++-------- src/server/project.ts | 26 ++- src/server/session.ts | 11 +- 5 files changed, 161 insertions(+), 98 deletions(-) diff --git a/src/harness/unittests/tsserverProjectSystem.ts b/src/harness/unittests/tsserverProjectSystem.ts index 4426e52ab66e5..83e92c2018de5 100644 --- a/src/harness/unittests/tsserverProjectSystem.ts +++ b/src/harness/unittests/tsserverProjectSystem.ts @@ -639,6 +639,11 @@ namespace ts.projectSystem { this.timeoutCallbacks.unregister(timeoutId); } + checkTimeoutQueueLengthAndRun(expected: number) { + this.checkTimeoutQueueLength(expected); + this.runQueuedTimeoutCallbacks(); + } + checkTimeoutQueueLength(expected: number) { const callbacksCount = this.timeoutCallbacks.count(); assert.equal(callbacksCount, expected, `expected ${expected} timeout callbacks queued but found ${callbacksCount}.`); @@ -899,6 +904,9 @@ namespace ts.projectSystem { // remove the tsconfig file host.reloadFS(filesWithoutConfig); + checkNumberOfInferredProjects(projectService, 1); + host.checkTimeoutQueueLengthAndRun(1); // Refresh inferred projects + checkNumberOfInferredProjects(projectService, 2); checkNumberOfConfiguredProjects(projectService, 0); checkWatchedDirectories(host, ["/a/b", "/a"]); @@ -920,7 +928,7 @@ namespace ts.projectSystem { // add a new ts file host.reloadFS([commonFile1, commonFile2, libFile, configFile]); - host.runQueuedTimeoutCallbacks(); + host.checkTimeoutQueueLengthAndRun(2); // project service waits for 250ms to update the project structure, therefore the assertion needs to wait longer. checkProjectRootFiles(project, [commonFile1.path, commonFile2.path]); }); @@ -1022,12 +1030,12 @@ namespace ts.projectSystem { // delete commonFile2 host.reloadFS([commonFile1, configFile]); - host.runQueuedTimeoutCallbacks(); + host.checkTimeoutQueueLengthAndRun(2); checkProjectRootFiles(project, [commonFile1.path]); // re-add commonFile2 host.reloadFS([commonFile1, commonFile2, configFile]); - host.runQueuedTimeoutCallbacks(); + host.checkTimeoutQueueLengthAndRun(2); checkProjectRootFiles(project, [commonFile1.path, commonFile2.path]); }); @@ -1086,6 +1094,9 @@ namespace ts.projectSystem { }`; host.reloadFS(files); + checkNumberOfConfiguredProjects(projectService, 1); + checkProjectRootFiles(project, [commonFile1.path, commonFile2.path]); + host.checkTimeoutQueueLengthAndRun(2); // Update the configured project + refresh inferred projects checkNumberOfConfiguredProjects(projectService, 1); checkProjectRootFiles(project, [commonFile1.path]); @@ -1158,6 +1169,7 @@ namespace ts.projectSystem { "files": ["${file1.path}"] }`; host.reloadFS(files); + host.checkTimeoutQueueLengthAndRun(2); checkProjectActualFiles(project, [file1.path, classicModuleFile.path, configFile.path]); checkNumberOfInferredProjects(projectService, 1); }); @@ -1587,7 +1599,7 @@ namespace ts.projectSystem { }; host.reloadFS([file1, modifiedFile2, file3]); - + host.checkTimeoutQueueLengthAndRun(2); checkNumberOfInferredProjects(projectService, 1); checkProjectActualFiles(projectService.inferredProjects[0], [file1.path, modifiedFile2.path, file3.path]); }); @@ -1618,6 +1630,7 @@ namespace ts.projectSystem { checkNumberOfProjects(projectService, { inferredProjects: 1 }); host.reloadFS([file1, file3]); + host.checkTimeoutQueueLengthAndRun(2); checkNumberOfProjects(projectService, { inferredProjects: 2 }); @@ -1779,7 +1792,7 @@ namespace ts.projectSystem { host.reloadFS([file1, file2, configFile]); - host.checkTimeoutQueueLength(0); // TODO: update graph scheduling (instead of instant update graph) + host.checkTimeoutQueueLengthAndRun(2); checkNumberOfProjects(projectService, { configuredProjects: 1 }); checkProjectRootFiles(projectService.configuredProjects[0], [file1.path, file2.path]); @@ -1814,6 +1827,7 @@ namespace ts.projectSystem { host.reloadFS([file1, file2, modifiedConfigFile]); checkNumberOfProjects(projectService, { configuredProjects: 1 }); + host.checkTimeoutQueueLengthAndRun(2); checkProjectRootFiles(projectService.configuredProjects[0], [file1.path, file2.path]); }); @@ -1923,7 +1937,7 @@ namespace ts.projectSystem { checkProjectActualFiles(projectService.configuredProjects[0], [file1.path, file2.path, config.path]); host.reloadFS([file1, file2]); - + host.checkTimeoutQueueLengthAndRun(1); checkNumberOfProjects(projectService, { inferredProjects: 2 }); checkProjectActualFiles(projectService.inferredProjects[0], [file1.path]); checkProjectActualFiles(projectService.inferredProjects[1], [file2.path]); @@ -2217,6 +2231,7 @@ namespace ts.projectSystem { checkNumberOfProjects(projectService, { inferredProjects: 2 }); projectService.setCompilerOptionsForInferredProjects({ moduleResolution: ModuleResolutionKind.Classic }); + host.checkTimeoutQueueLengthAndRun(3); checkNumberOfProjects(projectService, { inferredProjects: 1 }); }); @@ -2406,7 +2421,7 @@ namespace ts.projectSystem { assert.isFalse(lastEvent.data.languageServiceEnabled, "Language service state"); host.reloadFS([f1, f2, configWithExclude]); - + host.checkTimeoutQueueLengthAndRun(2); checkNumberOfProjects(projectService, { configuredProjects: 1 }); assert.isTrue(project.languageServiceEnabled, "Language service enabled"); assert.equal(lastEvent.data.project, project, "project"); @@ -2783,6 +2798,7 @@ namespace ts.projectSystem { checkProjectActualFiles(projectService.configuredProjects[0], [libES5.path, app.path, config1.path]); host.reloadFS([libES5, libES2015Promise, app, config2]); + host.checkTimeoutQueueLengthAndRun(2); projectService.checkNumberOfProjects({ configuredProjects: 1 }); checkProjectActualFiles(projectService.configuredProjects[0], [libES5.path, libES2015Promise.path, app.path, config2.path]); diff --git a/src/harness/unittests/typingsInstaller.ts b/src/harness/unittests/typingsInstaller.ts index ceae7582ace4d..b29a619b1b481 100644 --- a/src/harness/unittests/typingsInstaller.ts +++ b/src/harness/unittests/typingsInstaller.ts @@ -138,6 +138,7 @@ namespace ts.projectSystem { installer.installAll(/*expectedCount*/ 1); checkNumberOfProjects(projectService, { configuredProjects: 1 }); + host.checkTimeoutQueueLengthAndRun(2); checkProjectActualFiles(p, [file1.path, jquery.path, tsconfig.path]); }); @@ -337,6 +338,8 @@ namespace ts.projectSystem { installer.installAll(/*expectedCount*/ 1); + checkNumberOfProjects(projectService, { externalProjects: 1 }); + host.checkTimeoutQueueLengthAndRun(2); checkNumberOfProjects(projectService, { externalProjects: 1 }); checkProjectActualFiles(p, [file1.path, file2.path, file3.path, lodash.path, react.path]); }); @@ -460,6 +463,8 @@ namespace ts.projectSystem { installer.installAll(/*expectedCount*/ 1); + checkNumberOfProjects(projectService, { externalProjects: 1 }); + host.checkTimeoutQueueLengthAndRun(2); checkNumberOfProjects(projectService, { externalProjects: 1 }); checkProjectActualFiles(p, [file1.path, file2.path, file3.path, commander.path, express.path, jquery.path, moment.path]); }); @@ -538,7 +543,7 @@ namespace ts.projectSystem { for (const f of typingFiles) { assert.isTrue(host.fileExists(f.path), `expected file ${f.path} to exist`); } - + host.checkTimeoutQueueLengthAndRun(2); checkNumberOfProjects(projectService, { externalProjects: 1 }); checkProjectActualFiles(p, [lodashJs.path, commanderJs.path, file3.path, commander.path, express.path, jquery.path, moment.path, lodash.path]); }); @@ -641,7 +646,7 @@ namespace ts.projectSystem { assert.equal(installer.pendingRunRequests.length, 0, "expected no throttled requests"); installer.executePendingCommands(); - + host.checkTimeoutQueueLengthAndRun(3); // for 2 projects and 1 refreshing inferred project checkProjectActualFiles(p1, [lodashJs.path, commanderJs.path, file3.path, commander.path, jquery.path, lodash.path, cordova.path]); checkProjectActualFiles(p2, [file3.path, grunt.path, gulp.path]); }); @@ -695,6 +700,7 @@ namespace ts.projectSystem { installer.installAll(/*expectedCount*/ 1); checkNumberOfProjects(projectService, { configuredProjects: 1 }); + host.checkTimeoutQueueLengthAndRun(2); checkProjectActualFiles(p, [app.path, jqueryDTS.path, jsconfig.path]); }); @@ -742,6 +748,7 @@ namespace ts.projectSystem { installer.installAll(/*expectedCount*/ 1); checkNumberOfProjects(projectService, { configuredProjects: 1 }); + host.checkTimeoutQueueLengthAndRun(2); checkProjectActualFiles(p, [app.path, jqueryDTS.path, jsconfig.path]); }); @@ -788,6 +795,7 @@ namespace ts.projectSystem { installer.installAll(/*expectedCount*/ 1); checkNumberOfProjects(projectService, { configuredProjects: 1 }); + host.checkTimeoutQueueLengthAndRun(2); checkProjectActualFiles(p, [app.path, jqueryDTS.path, jsconfig.path]); }); @@ -826,9 +834,10 @@ namespace ts.projectSystem { installer.checkPendingCommands(/*expectedCount*/ 0); host.reloadFS([f, fixedPackageJson]); + host.checkTimeoutQueueLengthAndRun(2); // To refresh the project and refresh inferred projects // expected install request installer.installAll(/*expectedCount*/ 1); - + host.checkTimeoutQueueLengthAndRun(2); service.checkNumberOfProjects({ inferredProjects: 1 }); checkProjectActualFiles(service.inferredProjects[0], [f.path, commander.path]); }); @@ -950,8 +959,7 @@ namespace ts.projectSystem { endOffset: 0 } }); - host.checkTimeoutQueueLength(1); - host.runQueuedTimeoutCallbacks(); + host.checkTimeoutQueueLengthAndRun(2); // This enqueues the updategraph and refresh inferred projects const version2 = proj.getCachedUnresolvedImportsPerFile_TestOnly().getVersion(); assert.equal(version1, version2, "set of unresolved imports should not change"); }); @@ -1132,6 +1140,7 @@ namespace ts.projectSystem { installer.installAll(/*expectedCount*/ 1); assert.isTrue(seenTelemetryEvent); + host.checkTimeoutQueueLengthAndRun(2); checkNumberOfProjects(projectService, { inferredProjects: 1 }); checkProjectActualFiles(projectService.inferredProjects[0], [f1.path, commander.path]); }); @@ -1185,6 +1194,7 @@ namespace ts.projectSystem { assert.isTrue(!!endEvent); assert.isTrue(beginEvent.eventId === endEvent.eventId); assert.isTrue(endEvent.installSuccess); + host.checkTimeoutQueueLengthAndRun(2); checkNumberOfProjects(projectService, { inferredProjects: 1 }); checkProjectActualFiles(projectService.inferredProjects[0], [f1.path, commander.path]); }); diff --git a/src/server/editorServices.ts b/src/server/editorServices.ts index 9aa627e75a13e..6d9be7a6520f0 100644 --- a/src/server/editorServices.ts +++ b/src/server/editorServices.ts @@ -358,6 +358,8 @@ namespace ts.server { private static safelist: SafeList = defaultTypeSafeList; private changedFiles: ScriptInfo[]; + private pendingProjectUpdates = createMap(); + private pendingInferredProjectUpdate: boolean; readonly toCanonicalFileName: (f: string) => string; @@ -444,8 +446,44 @@ namespace ts.server { break; } project.markAsDirty(); - // TODO: (sheetalkamat) Schedule Update graph instead of immediate - project.updateGraph(); + this.delayUpdateProjectGraphAndInferredProjectsRefresh(project); + } + + private delayInferredProjectsRefresh() { + this.pendingInferredProjectUpdate = true; + this.throttledOperations.schedule("*refreshInferredProjects*", /*delay*/ 250, () => { + if (this.pendingProjectUpdates.size !== 0) { + this.delayInferredProjectsRefresh(); + } + else if (this.pendingInferredProjectUpdate) { + this.pendingInferredProjectUpdate = false; + this.refreshInferredProjects(); + } + }); + } + + private delayUpdateProjectGraph(project: Project) { + const projectName = project.getProjectName(); + this.pendingProjectUpdates.set(projectName, project); + this.throttledOperations.schedule(projectName, /*delay*/ 250, () => { + const project = this.pendingProjectUpdates.get(projectName); + if (project) { + this.pendingProjectUpdates.delete(projectName); + project.updateGraph(); + } + }); + } + + delayUpdateProjectGraphAndInferredProjectsRefresh(project: Project) { + this.delayUpdateProjectGraph(project); + this.delayInferredProjectsRefresh(); + } + + private delayUpdateProjectGraphs(projects: Project[]) { + for (const project of projects) { + this.delayUpdateProjectGraph(project); + } + this.delayInferredProjectsRefresh(); } setCompilerOptionsForInferredProjects(projectCompilerOptions: protocol.ExternalProjectCompilerOptions): void { @@ -457,9 +495,9 @@ namespace ts.server { for (const proj of this.inferredProjects) { proj.setCompilerOptions(this.compilerOptionsForInferredProjects); proj.compileOnSaveEnabled = projectCompilerOptions.compileOnSave; + proj.markAsDirty(); } - // TODO: (sheetalkamat) (ensure updated projects) - this.updateProjectGraphs(this.inferredProjects); + this.delayUpdateProjectGraphs(this.inferredProjects); } stopWatchingDirectory(directory: string) { @@ -479,14 +517,17 @@ namespace ts.server { getDefaultProjectForFile(fileName: NormalizedPath, refreshInferredProjects: boolean) { if (refreshInferredProjects) { - // TODO: (sheetalkamat) needs to update inferred projects too ? this.ensureInferredProjectsUpToDate(); } const scriptInfo = this.getScriptInfoForNormalizedPath(fileName); return scriptInfo && scriptInfo.getDefaultProject(); } - private ensureInferredProjectsUpToDate() { + /** + * Ensures the project structures are upto date + * @param refreshInferredProjects when true updates the inferred projects even if there is no pending work + */ + private ensureInferredProjectsUpToDate(refreshInferredProjects?: boolean) { if (this.changedFiles) { let projectsToUpdate: Project[]; if (this.changedFiles.length === 1) { @@ -499,10 +540,20 @@ namespace ts.server { projectsToUpdate = projectsToUpdate.concat(f.containingProjects); } } - this.updateProjectGraphs(projectsToUpdate); this.changedFiles = undefined; + this.updateProjectGraphs(projectsToUpdate); + } + + if (this.pendingProjectUpdates.size !== 0) { + const projectsToUpdate = arrayFrom(this.pendingProjectUpdates.values()); + this.pendingProjectUpdates.clear(); + this.updateProjectGraphs(projectsToUpdate); + } + + if (this.pendingInferredProjectUpdate || refreshInferredProjects) { + this.pendingInferredProjectUpdate = false; + this.refreshInferredProjects(); } - // TODO: (sheetalkamat) should also refresh inferred projects here since we are postponing the changes in closed files } private findContainingExternalProject(fileName: NormalizedPath): ExternalProject { @@ -526,15 +577,11 @@ namespace ts.server { } private updateProjectGraphs(projects: Project[]) { - let shouldRefreshInferredProjects = false; for (const p of projects) { if (!p.updateGraph()) { - shouldRefreshInferredProjects = true; + this.pendingInferredProjectUpdate = true; } } - if (shouldRefreshInferredProjects) { - this.refreshInferredProjects(); - } } private onSourceFileChanged(fileName: NormalizedPath, eventKind: FileWatcherEventKind) { @@ -559,7 +606,7 @@ namespace ts.server { // file has been changed which might affect the set of referenced files in projects that include // this file and set of inferred projects info.reloadFromFile(); - this.recordChangedFile(info); + this.delayUpdateProjectGraphs(info.containingProjects); } } } @@ -580,36 +627,28 @@ namespace ts.server { info.detachAllProjects(); - // TODO: (sheetalkamat) Schedule this too ? // update projects to make sure that set of referenced files is correct - this.updateProjectGraphs(containingProjects); + this.delayUpdateProjectGraphs(containingProjects); - if (!this.eventHandler) { - return; - } + // TODO: (sheetalkamat) Someway to send this event so that error checks are updated? + // if (!this.eventHandler) { + // return; + // } - for (const openFile of this.openFiles) { - this.eventHandler({ - eventName: ContextEvent, - data: { project: openFile.getDefaultProject(), fileName: openFile.fileName } - }); - } + // for (const openFile of this.openFiles) { + // this.eventHandler({ + // eventName: ContextEvent, + // data: { project: openFile.getDefaultProject(), fileName: openFile.fileName } + // }); + // } } - - this.printProjects(); } private onTypeRootFileChanged(project: ConfiguredProject, fileName: NormalizedPath) { this.logger.info(`Type root file ${fileName} changed`); project.getCachedServerHost().addOrDeleteFileOrFolder(fileName); project.updateTypes(); - // TODO: (sheetalkamat) schedule project update and referesh inferred projects instead - - //this.throttledOperations.schedule(project.getConfigFilePath() + " * type root", /*delay*/ 250, () => { - // this.reloadConfiguredProject(project); // TODO: Figure out why this is needed (should be redundant?) - project.updateGraph(); - this.refreshInferredProjects(); - //}); + this.delayUpdateProjectGraphAndInferredProjectsRefresh(project); } /** @@ -650,10 +689,7 @@ namespace ts.server { error.code !== Diagnostics.The_config_file_0_found_doesn_t_contain_any_source_files.code); } this.updateNonInferredProjectFiles(project, result.fileNames, fileNamePropertyReader); - // TODO: (sheetalkamat) schedule the update graph - if (!project.updateGraph()) { - this.refreshInferredProjects(); - } + this.delayUpdateProjectGraphAndInferredProjectsRefresh(project); } private onConfigChangedForConfiguredProject(project: ConfiguredProject, eventKind: FileWatcherEventKind) { @@ -664,16 +700,14 @@ namespace ts.server { // Reload the configured projects for these open files in the project as // they could be held up by another config file somewhere in the parent directory const openFilesInProject = this.getOrphanFiles(); - // TODO: (sheetalkamat) can this be scheduled too - this.reloadConfiguredProjectForFiles(openFilesInProject); + this.reloadConfiguredProjectForFiles(openFilesInProject, project => { project.pendingReload = true; this.delayUpdateProjectGraph(project); }); + this.delayInferredProjectsRefresh(); } else { this.logger.info(`Config file changed: ${configFileName}`); - // TODO: (sheetalkamat) This should be scheduled and marked for reload of configured project since we dont want to updating this on every change - this.reloadConfiguredProject(project); - this.reportConfigFileDiagnostics(configFileName, project.getGlobalProjectErrors(), /*triggerFile*/ configFileName); + project.pendingReload = true; + this.delayUpdateProjectGraphAndInferredProjectsRefresh(project); } - this.refreshInferredProjects(); } /** @@ -698,6 +732,8 @@ namespace ts.server { this.logger.info(`remove project: ${project.getRootFiles().toString()}`); project.close(); + // Remove the project from pending project updates + this.pendingProjectUpdates.delete(project.getProjectName()); switch (project.projectKind) { case ProjectKind.External: @@ -1249,7 +1285,8 @@ namespace ts.server { * Read the config file of the project again and update the project * @param project */ - private reloadConfiguredProject(project: ConfiguredProject) { + /* @internal */ + reloadConfiguredProject(project: ConfiguredProject) { // At this point, there is no reason to not have configFile in the host // note: the returned "success" is true does not mean the "configFileErrors" is empty. @@ -1257,7 +1294,8 @@ namespace ts.server { // regardless the "success" here is true or not. const host = project.getCachedServerHost(); host.clearCache(); - const { success, projectOptions, configFileErrors, configFileSpecs } = this.convertConfigFileContentToProjectOptions(project.getConfigFilePath(), host); + const configFileName = project.getConfigFilePath(); + const { success, projectOptions, configFileErrors, configFileSpecs } = this.convertConfigFileContentToProjectOptions(configFileName, host); project.configFileSpecs = configFileSpecs; if (!success) { // reset project settings to default @@ -1266,6 +1304,7 @@ namespace ts.server { else { this.updateConfiguredProject(project, projectOptions, configFileErrors); } + this.reportConfigFileDiagnostics(configFileName, project.getGlobalProjectErrors(), /*triggerFile*/ configFileName); } /** @@ -1405,11 +1444,11 @@ namespace ts.server { */ reloadProjects() { this.logger.info("reload projects."); - this.reloadConfiguredProjectForFiles(this.openFiles); + this.reloadConfiguredProjectForFiles(this.openFiles, project => this.reloadConfiguredProject(project)); this.refreshInferredProjects(); } - reloadConfiguredProjectForFiles(openFiles: ScriptInfo[]) { + reloadConfiguredProjectForFiles(openFiles: ScriptInfo[], reload: (project: ConfiguredProject) => void) { const mapUpdatedProjects = createMap(); // try to reload config file for all open files for (const info of openFiles) { @@ -1425,7 +1464,7 @@ namespace ts.server { mapUpdatedProjects.set(configFileName, true); } else if (!mapUpdatedProjects.has(configFileName)) { - this.reloadConfiguredProject(project); + reload(project); mapUpdatedProjects.set(configFileName, true); } } @@ -1437,7 +1476,7 @@ namespace ts.server { * It is called on the premise that all the configured projects are * up to date. */ - refreshInferredProjects() { + private refreshInferredProjects() { this.logger.info("updating project structure from ..."); this.printProjects(); @@ -1547,17 +1586,6 @@ namespace ts.server { return files; } - private recordChangedFile(scriptInfo: ScriptInfo) { - if (!this.changedFiles) { - this.changedFiles = [scriptInfo]; - } - else if (this.changedFiles.indexOf(scriptInfo) < 0) { - this.changedFiles.push(scriptInfo); - } - // TODO: (sheetalkamat) Schedule the project update graphs - this.updateProjectGraphs(scriptInfo.containingProjects); - } - /* @internal */ applyChangesInOpenFiles(openFiles: protocol.ExternalFile[], changedFiles: protocol.ChangedOpenFile[], closedFiles: string[]): void { if (openFiles) { @@ -1578,7 +1606,12 @@ namespace ts.server { const change = file.changes[i]; scriptInfo.editContent(change.span.start, change.span.start + change.span.length, change.newText); } - this.recordChangedFile(scriptInfo); + if (!this.changedFiles) { + this.changedFiles = [scriptInfo]; + } + else if (this.changedFiles.indexOf(scriptInfo) < 0) { + this.changedFiles.push(scriptInfo); + } } } @@ -1590,8 +1623,7 @@ namespace ts.server { // if files were open or closed then explicitly refresh list of inferred projects // otherwise if there were only changes in files - record changed files in `changedFiles` and defer the update if (openFiles || closedFiles) { - // TODO: (sheetalkamat) is this ensureRefereshedProjects instead ? - this.refreshInferredProjects(); + this.ensureInferredProjectsUpToDate(/*refreshInferredProjects*/ true); } } @@ -1614,8 +1646,7 @@ namespace ts.server { } this.externalProjectToConfiguredProjectMap.delete(fileName); if (shouldRefreshInferredProjects && !suppressRefresh) { - // TODO: (sheetalkamat) is this ensureRefereshedProjects instead ? - this.refreshInferredProjects(); + this.ensureInferredProjectsUpToDate(/*refreshInferredProjects*/ true); } } else { @@ -1624,8 +1655,7 @@ namespace ts.server { if (externalProject) { this.removeProject(externalProject); if (!suppressRefresh) { - // TODO: (sheetalkamat) is this ensureRefereshedProjects instead ? - this.refreshInferredProjects(); + this.ensureInferredProjectsUpToDate(/*refreshInferredProjects*/ true); } } } @@ -1649,8 +1679,7 @@ namespace ts.server { this.closeExternalProject(externalProjectName, /*suppressRefresh*/ true); }); - // TODO: (sheetalkamat) is this ensureRefereshedProjects instead ? - this.refreshInferredProjects(); + this.ensureInferredProjectsUpToDate(/*refreshInferredProjects*/ true); } /** Makes a filename safe to insert in a RegExp */ @@ -1844,8 +1873,7 @@ namespace ts.server { this.createAndAddExternalProject(proj.projectFileName, rootFiles, proj.options, proj.typeAcquisition); } if (!suppressRefreshOfInferredProjects) { - // TODO: (sheetalkamat) is this ensureRefereshedProjects instead ? - this.refreshInferredProjects(); + this.ensureInferredProjectsUpToDate(/*refreshInferredProjects*/ true); } } } diff --git a/src/server/project.ts b/src/server/project.ts index e3e7de78eefc2..5b78f1f6630d7 100644 --- a/src/server/project.ts +++ b/src/server/project.ts @@ -303,7 +303,11 @@ namespace ts.server { // if we have a program - release all files that are enlisted in program for (const f of this.program.getSourceFiles()) { const info = this.projectService.getScriptInfo(f.fileName); - info.detachFromProject(this); + // We might not find the script info in case its not associated with the project any more + // and project graph was not updated (eg delayed update graph in case of files changed/deleted on the disk) + if (info) { + info.detachFromProject(this); + } } } if (!this.program || !this.languageServiceEnabled) { @@ -555,7 +559,6 @@ namespace ts.server { * @returns: true if set of files in the project stays the same and false - otherwise. */ updateGraph(): boolean { - // TODO: (sheetalkamat) If reload scheduled do that before updating the graph this.lsHost.startRecordingFilesWithChangedResolutions(); let hasChanges = this.updateGraphWorker(); @@ -662,8 +665,7 @@ namespace ts.server { // When a missing file is created, we should update the graph. this.markAsDirty(); - // TODO: (sheetalkamat) schedule the update graph instead of doing it right away - this.updateGraph(); + this.projectService.delayUpdateProjectGraphAndInferredProjectsRefresh(this); } }); this.missingFilesMap.set(missingFilePath, fileWatcher); @@ -980,6 +982,9 @@ namespace ts.server { private typeRootsWatchers: FileWatcher[]; readonly canonicalConfigFilePath: NormalizedPath; + /* @internal */ + pendingReload: boolean; + /*@internal*/ configFileSpecs: ConfigFileSpecs; @@ -1001,6 +1006,19 @@ namespace ts.server { this.enablePlugins(); } + /** + * Checks if the project has reload from disk pending, if thats pending, it reloads (and then updates graph as part of that) instead of just updating the graph + * @returns: true if set of files in the project stays the same and false - otherwise. + */ + updateGraph(): boolean { + if (this.pendingReload) { + this.pendingReload = false; + this.projectService.reloadConfiguredProject(this); + return true; + } + return super.updateGraph(); + } + getCachedServerHost() { return this.lsHost.host as CachedServerHost; } diff --git a/src/server/session.ts b/src/server/session.ts index 2656b6a20ba18..a51e17cde6576 100644 --- a/src/server/session.ts +++ b/src/server/session.ts @@ -459,15 +459,6 @@ namespace ts.server { } } - private updateProjectStructure(seq: number, matchSeq: (seq: number) => boolean, ms = 1500) { - this.host.setTimeout(() => { - if (matchSeq(seq)) { - // TODO: (sheetalkamat) is this ensureRefereshedProjects instead ? - this.projectService.refreshInferredProjects(); - } - }, ms); - } - private updateErrorCheck(next: NextStep, checkList: PendingErrorCheck[], seq: number, matchSeq: (seq: number) => boolean, ms = 1500, followMs = 200, requireOpen = true) { if (followMs > ms) { followMs = ms; @@ -1302,7 +1293,7 @@ namespace ts.server { scriptInfo.editContent(start, end, args.insertString); this.changeSeq++; } - this.updateProjectStructure(this.changeSeq, n => n === this.changeSeq); + this.projectService.delayUpdateProjectGraphAndInferredProjectsRefresh(project); } } From 036590138115509c12f1845711e3034995259cb2 Mon Sep 17 00:00:00 2001 From: Sheetal Nandi Date: Thu, 13 Jul 2017 15:04:49 -0700 Subject: [PATCH 019/109] use single instance of getCanonicalFileName --- src/compiler/commandLineParser.ts | 14 ++++++++------ src/server/editorServices.ts | 7 ++++--- src/server/lsHost.ts | 16 ++++++++-------- src/server/project.ts | 7 +++---- src/server/scriptInfo.ts | 5 ++--- 5 files changed, 25 insertions(+), 24 deletions(-) diff --git a/src/compiler/commandLineParser.ts b/src/compiler/commandLineParser.ts index 159dc27bb47e8..3b3127f8a41c9 100644 --- a/src/compiler/commandLineParser.ts +++ b/src/compiler/commandLineParser.ts @@ -1416,7 +1416,8 @@ namespace ts { Debug.assert((json === undefined && sourceFile !== undefined) || (json !== undefined && sourceFile === undefined)); const errors: Diagnostic[] = []; - const parsedConfig = parseConfig(json, sourceFile, host, basePath, configFileName, resolutionStack, errors); + const getCanonicalFileName = createGetCanonicalFileName(host.useCaseSensitiveFileNames); + const parsedConfig = parseConfig(json, sourceFile, host, basePath, configFileName, getCanonicalFileName, resolutionStack, errors); const { raw } = parsedConfig; const options = extend(existingOptions, parsedConfig.options || {}); options.configFilePath = configFileName; @@ -1525,11 +1526,11 @@ namespace ts { host: ParseConfigHost, basePath: string, configFileName: string, + getCanonicalFileName: (fileName: string) => string, resolutionStack: Path[], errors: Diagnostic[], ): ParsedTsconfig { basePath = normalizeSlashes(basePath); - const getCanonicalFileName = createGetCanonicalFileName(host.useCaseSensitiveFileNames); const resolvedPath = toPath(configFileName || "", basePath, getCanonicalFileName); if (resolutionStack.indexOf(resolvedPath) >= 0) { @@ -1711,7 +1712,7 @@ namespace ts { const extendedDirname = getDirectoryPath(extendedConfigPath); const extendedConfig = parseConfig(/*json*/ undefined, extendedResult, host, extendedDirname, - getBaseFileName(extendedConfigPath), resolutionStack, errors); + getBaseFileName(extendedConfigPath), getCanonicalFileName, resolutionStack, errors); if (sourceFile) { sourceFile.extendedSourceFiles.push(...extendedResult.extendedSourceFiles); } @@ -1954,6 +1955,10 @@ namespace ts { basePath = normalizePath(basePath); let validatedIncludeSpecs: string[], validatedExcludeSpecs: string[]; + // The exclude spec list is converted into a regular expression, which allows us to quickly + // test whether a file or directory should be excluded before recursively traversing the + // file system. + if (includeSpecs) { validatedIncludeSpecs = validateSpecs(includeSpecs, errors, /*allowTrailingRecursion*/ false, jsonSourceFile, "include"); } @@ -1986,9 +1991,6 @@ namespace ts { export function getFileNamesFromConfigSpecs(spec: ConfigFileSpecs, basePath: string, options: CompilerOptions, host: ParseConfigHost, extraFileExtensions: JsFileExtensionInfo[]): ExpandResult { basePath = normalizePath(basePath); - // The exclude spec list is converted into a regular expression, which allows us to quickly - // test whether a file or directory should be excluded before recursively traversing the - // file system. const keyMapper = host.useCaseSensitiveFileNames ? caseSensitiveKeyMapper : caseInsensitiveKeyMapper; // Literal file names (provided via the "files" array in tsconfig.json) are stored in a diff --git a/src/server/editorServices.ts b/src/server/editorServices.ts index 1b2793e5f2f3b..db7234c8824c9 100644 --- a/src/server/editorServices.ts +++ b/src/server/editorServices.ts @@ -1198,7 +1198,7 @@ namespace ts.server { } private openConfigFile(configFileName: NormalizedPath, clientFileName?: string) { - const cachedServerHost = new CachedServerHost(this.host); + const cachedServerHost = new CachedServerHost(this.host, this.toCanonicalFileName); const { success, projectOptions, configFileErrors, configFileSpecs } = this.convertConfigFileContentToProjectOptions(configFileName, cachedServerHost); if (success) { this.logger.info(`Opened configuration file ${configFileName}`); @@ -1366,10 +1366,11 @@ namespace ts.server { } getOrCreateScriptInfoForNormalizedPath(fileName: NormalizedPath, openedByClient: boolean, fileContent?: string, scriptKind?: ScriptKind, hasMixedContent?: boolean) { - let info = this.getScriptInfoForNormalizedPath(fileName); + const path = toPath(fileName, this.host.getCurrentDirectory(), this.toCanonicalFileName); + let info = this.getScriptInfoForPath(path); if (!info) { if (openedByClient || this.host.fileExists(fileName)) { - info = new ScriptInfo(this.host, fileName, scriptKind, hasMixedContent); + info = new ScriptInfo(this.host, fileName, scriptKind, hasMixedContent, path); this.filenameToScriptInfo.set(info.path, info); diff --git a/src/server/lsHost.ts b/src/server/lsHost.ts index b54f493be1b21..a7e9c5e231cca 100644 --- a/src/server/lsHost.ts +++ b/src/server/lsHost.ts @@ -11,15 +11,13 @@ namespace ts.server { readonly trace: (s: string) => void; readonly realpath?: (path: string) => string; - private getCanonicalFileName: (fileName: string) => string; private cachedReadDirectoryResult = createMap(); private readonly currentDirectory: string; - constructor(private readonly host: ServerHost) { + constructor(private readonly host: ServerHost, private getCanonicalFileName: (fileName: string) => string) { this.args = host.args; this.newLine = host.newLine; this.useCaseSensitiveFileNames = host.useCaseSensitiveFileNames; - this.getCanonicalFileName = createGetCanonicalFileName(this.useCaseSensitiveFileNames); if (host.trace) { this.trace = s => host.trace(s); } @@ -193,21 +191,23 @@ namespace ts.server { private compilationSettings: CompilerOptions; private readonly resolvedModuleNames = createMap>(); private readonly resolvedTypeReferenceDirectives = createMap>(); - /* @internal */ - readonly getCanonicalFileName: (fileName: string) => string; private filesWithChangedSetOfUnresolvedImports: Path[]; private resolveModuleName: typeof resolveModuleName; readonly trace: (s: string) => void; readonly realpath?: (path: string) => string; + /** + * 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 + * file system entries as we would anyways be watching files in the project (so safe to cache) + */ /*@internal*/ host: ServerHost; constructor(host: ServerHost, private project: Project, private readonly cancellationToken: HostCancellationToken) { this.host = host; this.cancellationToken = new ThrottledCancellationToken(cancellationToken, project.projectService.throttleWaitMilliseconds); - this.getCanonicalFileName = createGetCanonicalFileName(this.host.useCaseSensitiveFileNames); if (host.trace) { this.trace = s => host.trace(s); @@ -262,7 +262,7 @@ namespace ts.server { getResultFileName: (result: R) => string | undefined, logChanges: boolean): R[] { - const path = toPath(containingFile, this.host.getCurrentDirectory(), this.getCanonicalFileName); + const path = toPath(containingFile, this.host.getCurrentDirectory(), this.project.projectService.toCanonicalFileName); const currentResolutionsInFile = cache.get(path); const newResolutions: Map = createMap(); @@ -403,7 +403,7 @@ namespace ts.server { fileExists(file: string): boolean { // As an optimization, don't hit the disks for files we already know don't exist // (because we're watching for their creation). - const path = toPath(file, this.host.getCurrentDirectory(), this.getCanonicalFileName); + const path = toPath(file, this.host.getCurrentDirectory(), this.project.projectService.toCanonicalFileName); return !this.project.isWatchedMissingFile(path) && this.host.fileExists(file); } diff --git a/src/server/project.ts b/src/server/project.ts index 9db74efeb6b99..5a397bdf3a4ad 100644 --- a/src/server/project.ts +++ b/src/server/project.ts @@ -495,7 +495,7 @@ namespace ts.server { // add a root file to project addMissingFileRoot(fileName: NormalizedPath) { const path = toPath(fileName, this.projectService.host.getCurrentDirectory(), - createGetCanonicalFileName(this.projectService.host.useCaseSensitiveFileNames)); + this.projectService.toCanonicalFileName); this.rootFilesMap.set(path, fileName); this.markAsDirty(); } @@ -837,11 +837,10 @@ namespace ts.server { } const currentDirectory = getDirectoryPath(path); - const getCanonicalFileName = createGetCanonicalFileName(this.projectService.host.useCaseSensitiveFileNames); // Handle triple slash references if (sourceFile.referencedFiles && sourceFile.referencedFiles.length > 0) { for (const referencedFile of sourceFile.referencedFiles) { - const referencedPath = toPath(referencedFile.fileName, currentDirectory, getCanonicalFileName); + const referencedPath = toPath(referencedFile.fileName, currentDirectory, this.projectService.toCanonicalFileName); referencedFiles.set(referencedPath, true); } } @@ -854,7 +853,7 @@ namespace ts.server { } const fileName = resolvedTypeReferenceDirective.resolvedFileName; - const typeFilePath = toPath(fileName, currentDirectory, getCanonicalFileName); + const typeFilePath = toPath(fileName, currentDirectory, this.projectService.toCanonicalFileName); referencedFiles.set(typeFilePath, true); }); } diff --git a/src/server/scriptInfo.ts b/src/server/scriptInfo.ts index 3da4cdb14af62..46008e77340c1 100644 --- a/src/server/scriptInfo.ts +++ b/src/server/scriptInfo.ts @@ -148,7 +148,6 @@ namespace ts.server { */ readonly containingProjects: Project[] = []; private formatCodeSettings: FormatCodeSettings; - readonly path: Path; private fileWatcher: FileWatcher; private textStorage: TextStorage; @@ -159,9 +158,9 @@ namespace ts.server { private readonly host: ServerHost, readonly fileName: NormalizedPath, readonly scriptKind: ScriptKind, - public hasMixedContent = false) { + public hasMixedContent: boolean, + readonly path: Path) { - this.path = toPath(fileName, host.getCurrentDirectory(), createGetCanonicalFileName(host.useCaseSensitiveFileNames)); this.textStorage = new TextStorage(host, fileName); if (hasMixedContent) { this.textStorage.reload(""); From 404aa8f0be1fdc29fc34d5913628407f402e9af9 Mon Sep 17 00:00:00 2001 From: Sheetal Nandi Date: Thu, 13 Jul 2017 20:08:57 -0700 Subject: [PATCH 020/109] Logging of the watch add/remove/event --- src/server/editorServices.ts | 109 ++++++++++++++---- src/server/project.ts | 218 +++++++++++++++++------------------ src/server/scriptInfo.ts | 15 +-- src/server/utilities.ts | 73 ++++++++++++ 4 files changed, 268 insertions(+), 147 deletions(-) diff --git a/src/server/editorServices.ts b/src/server/editorServices.ts index db7234c8824c9..0aed0ab99fac5 100644 --- a/src/server/editorServices.ts +++ b/src/server/editorServices.ts @@ -303,6 +303,31 @@ namespace ts.server { } } + /* @internal */ + export const enum WatchType { + ConfigFilePath = "Config file for the program", + MissingFilePath = "Missing file from program", + WildCardDirectories = "Wild card directory", + TypeRoot = "Type root of the project", + ClosedScriptInfo = "Closed Script info" + } + + /* @internal */ + export const enum WatcherCloseReason { + ProjectClose = "Project close", + NotNeeded = "After project update isnt required any more", + FileCreated = "File got created", + RecursiveChanged = "Recursive changed for the watch", + ProjectReloadHitMaxSize = "Project reloaded and hit the max file size capacity", + OrphanScriptInfoWithChange = "Orphan script info, Detected change in file thats not needed any more", + OrphanScriptInfo = "Removing Orphan script info as part of cleanup", + FileDeleted = "File was deleted", + FileOpened = "File opened" + } + + /* @internal */ + export type ServerDirectoryWatcherCallback = (path: NormalizedPath) => void; + export interface ProjectServiceOptions { host: ServerHost; logger: Logger; @@ -599,7 +624,7 @@ namespace ts.server { if (!info.isScriptOpen()) { if (info.containingProjects.length === 0) { // Orphan script info, remove it as we can always reload it on next open file request - info.stopWatcher(); + this.stopWatchingScriptInfo(info, WatcherCloseReason.OrphanScriptInfoWithChange); this.filenameToScriptInfo.delete(info.path); } else { @@ -613,9 +638,7 @@ namespace ts.server { } private handleDeletedFile(info: ScriptInfo) { - this.logger.info(`${info.fileName} deleted`); - - info.stopWatcher(); + this.stopWatchingScriptInfo(info, WatcherCloseReason.FileDeleted); // TODO: handle isOpen = true case @@ -644,7 +667,8 @@ namespace ts.server { } } - private onTypeRootFileChanged(project: ConfiguredProject, fileName: NormalizedPath) { + /* @internal */ + onTypeRootFileChanged(project: ConfiguredProject, fileName: NormalizedPath) { this.logger.info(`Type root file ${fileName} changed`); project.getCachedServerHost().addOrDeleteFileOrFolder(fileName); project.updateTypes(); @@ -667,10 +691,10 @@ namespace ts.server { return; } - this.logger.info(`Detected source file changes: ${fileName}`); + const configFilename = project.getConfigFilePath(); + this.logger.info(`Project: ${configFilename} Detected source file add/remove: ${fileName}`); const configFileSpecs = project.configFileSpecs; - const configFilename = normalizePath(project.getConfigFilePath()); const result = getFileNamesFromConfigSpecs(configFileSpecs, getDirectoryPath(configFilename), project.getCompilerOptions(), project.getCachedServerHost(), this.hostConfiguration.extraFileExtensions); const errors = project.getAllProjectErrors(); if (result.fileNames.length === 0) { @@ -693,9 +717,7 @@ namespace ts.server { } private onConfigChangedForConfiguredProject(project: ConfiguredProject, eventKind: FileWatcherEventKind) { - const configFileName = project.getConfigFilePath(); if (eventKind === FileWatcherEventKind.Deleted) { - this.logger.info(`Config file deleted: ${configFileName}`); this.removeProject(project); // Reload the configured projects for these open files in the project as // they could be held up by another config file somewhere in the parent directory @@ -704,7 +726,6 @@ namespace ts.server { this.delayInferredProjectsRefresh(); } else { - this.logger.info(`Config file changed: ${configFileName}`); project.pendingReload = true; this.delayUpdateProjectGraphAndInferredProjectsRefresh(project); } @@ -894,7 +915,7 @@ namespace ts.server { this.filenameToScriptInfo.forEach(info => { if (!info.isScriptOpen() && info.containingProjects.length === 0) { // if there are not projects that include this script info - delete it - info.stopWatcher(); + this.stopWatchingScriptInfo(info, WatcherCloseReason.OrphanScriptInfo); this.filenameToScriptInfo.delete(info.path); } }); @@ -1155,22 +1176,26 @@ namespace ts.server { } private createAndAddConfiguredProject(configFileName: NormalizedPath, projectOptions: ProjectOptions, configFileErrors: Diagnostic[], configFileSpecs: ConfigFileSpecs, cachedServerHost: CachedServerHost, clientFileName?: string) { - const sizeLimitExceeded = this.exceededTotalSizeLimitForNonTsFiles(configFileName, projectOptions.compilerOptions, projectOptions.files, fileNamePropertyReader); + const languageServiceEnabled = !this.exceededTotalSizeLimitForNonTsFiles(configFileName, projectOptions.compilerOptions, projectOptions.files, fileNamePropertyReader); const project = new ConfiguredProject( configFileName, this, this.documentRegistry, projectOptions.configHasFilesProperty, projectOptions.compilerOptions, - /*languageServiceEnabled*/ !sizeLimitExceeded, + languageServiceEnabled, projectOptions.compileOnSave === undefined ? false : projectOptions.compileOnSave, cachedServerHost); this.addFilesToProjectAndUpdateGraph(project, projectOptions.files, fileNamePropertyReader, clientFileName, projectOptions.typeAcquisition, configFileErrors); project.configFileSpecs = configFileSpecs; - project.watchConfigFile((project, eventKind) => this.onConfigChangedForConfiguredProject(project, eventKind)); - project.watchWildcards(projectOptions.wildcardDirectories); - project.watchTypeRoots((project, path) => this.onTypeRootFileChanged(project, path)); + project.configFileWatcher = this.addFileWatcher(WatchType.ConfigFilePath, project, + configFileName, (_fileName, eventKind) => this.onConfigChangedForConfiguredProject(project, eventKind) + ); + if (languageServiceEnabled) { + project.watchWildcards(projectOptions.wildcardDirectories); + project.watchTypeRoots(); + } this.configuredProjects.push(project); this.sendProjectTelemetry(project.getConfigFilePath(), project, projectOptions); @@ -1314,11 +1339,13 @@ namespace ts.server { private updateConfiguredProject(project: ConfiguredProject, projectOptions: ProjectOptions, configFileErrors: Diagnostic[]) { if (this.exceededTotalSizeLimitForNonTsFiles(project.canonicalConfigFilePath, projectOptions.compilerOptions, projectOptions.files, fileNamePropertyReader)) { project.disableLanguageService(); - project.stopWatchingWildCards(); + project.stopWatchingWildCards(WatcherCloseReason.ProjectReloadHitMaxSize); + project.stopWatchingTypeRoots(WatcherCloseReason.ProjectReloadHitMaxSize); } else { project.enableLanguageService(); project.watchWildcards(projectOptions.wildcardDirectories); + project.watchTypeRoots(); } this.updateNonInferredProject(project, projectOptions.files, fileNamePropertyReader, projectOptions.compilerOptions, projectOptions.typeAcquisition, projectOptions.compileOnSave, configFileErrors); } @@ -1357,11 +1384,21 @@ namespace ts.server { return this.getScriptInfoForNormalizedPath(toNormalizedPath(uncheckedFileName)); } - watchClosedScriptInfo(info: ScriptInfo) { + private watchClosedScriptInfo(info: ScriptInfo) { + Debug.assert(!info.fileWatcher); // do not watch files with mixed content - server doesn't know how to interpret it if (!info.hasMixedContent) { const { fileName } = info; - info.setWatcher(this.host.watchFile(fileName, (_fileName, eventKind) => this.onSourceFileChanged(fileName, eventKind))); + info.fileWatcher = this.addFileWatcher(WatchType.ClosedScriptInfo, /*project*/ undefined, fileName, + (_fileName, eventKind) => this.onSourceFileChanged(fileName, eventKind) + ); + } + } + + private stopWatchingScriptInfo(info: ScriptInfo, reason: WatcherCloseReason) { + if (info.fileWatcher) { + this.closeFileWatcher(WatchType.ClosedScriptInfo, /*project*/ undefined, info.fileName, info.fileWatcher, reason); + info.fileWatcher = undefined; } } @@ -1387,7 +1424,7 @@ namespace ts.server { } if (info) { if (openedByClient && !info.isScriptOpen()) { - info.stopWatcher(); + this.stopWatchingScriptInfo(info, WatcherCloseReason.FileOpened); info.open(fileContent); if (hasMixedContent) { info.registerFileUpdate(); @@ -1408,7 +1445,6 @@ namespace ts.server { return this.filenameToScriptInfo.get(fileName); } - setHostConfiguration(args: protocol.ConfigureRequestArguments) { if (args.file) { const info = this.getScriptInfoForNormalizedPath(toNormalizedPath(args.file)); @@ -1436,6 +1472,37 @@ namespace ts.server { } } + /* @internal */ + closeFileWatcher(watchType: WatchType, project: Project, file: string, watcher: FileWatcher, reason: WatcherCloseReason) { + this.logger.info(`FileWatcher:: Close: ${file} Project: ${project ? project.getProjectName() : ""} WatchType: ${watchType} Reason: ${reason}`); + watcher.close(); + } + + /* @internal */ + addFileWatcher(watchType: WatchType, project: Project, file: string, cb: FileWatcherCallback) { + this.logger.info(`FileWatcher:: Added: ${file} Project: ${project ? project.getProjectName() : ""} WatchType: ${watchType}`); + return this.host.watchFile(file, (fileName, eventKind) => { + this.logger.info(`FileWatcher:: File ${FileWatcherEventKind[eventKind]}: ${file} Project: ${project ? project.getProjectName() : ""} WatchType: ${watchType}`); + cb(fileName, eventKind); + }); + } + + /* @internal */ + closeDirectoryWatcher(watchType: WatchType, project: Project, directory: string, watcher: FileWatcher, recursive: boolean, reason: WatcherCloseReason) { + this.logger.info(`DirectoryWatcher ${recursive ? "recursive" : ""}:: Close: ${directory} Project: ${project.getProjectName()} WatchType: ${watchType} Reason: ${reason}`); + watcher.close(); + } + + /* @internal */ + addDirectoryWatcher(watchType: WatchType, project: Project, directory: string, cb: ServerDirectoryWatcherCallback, recursive: boolean) { + this.logger.info(`DirectoryWatcher ${recursive ? "recursive" : ""}:: Added: ${directory} Project: ${project.getProjectName()} WatchType: ${watchType}`); + return this.host.watchDirectory(directory, fileName => { + const path = toNormalizedPath(getNormalizedAbsolutePath(fileName, directory)); + this.logger.info(`DirectoryWatcher:: EventOn: ${directory} Trigger: ${fileName} Path: ${path} Project: ${project.getProjectName()} WatchType: ${watchType}`); + cb(path); + }, recursive); + } + closeLog() { this.logger.close(); } diff --git a/src/server/project.ts b/src/server/project.ts index 5a397bdf3a4ad..aa492d246dca6 100644 --- a/src/server/project.ts +++ b/src/server/project.ts @@ -117,7 +117,7 @@ namespace ts.server { private rootFilesMap: Map = createMap(); private program: Program; private externalFiles: SortedReadonlyArray; - private missingFilesMap: Map = createMap(); + private missingFilesMap: Map; private cachedUnresolvedImportsPerFile = new UnresolvedImportsMap(); private lastCachedUnresolvedImportsList: SortedReadonlyArray; @@ -327,7 +327,9 @@ namespace ts.server { this.lsHost = undefined; // Clean up file watchers waiting for missing files - this.missingFilesMap.forEach(fileWatcher => fileWatcher.close()); + cleanExistingMap(this.missingFilesMap, (missingFilePath, fileWatcher) => { + this.projectService.closeFileWatcher(WatchType.MissingFilePath, this, missingFilePath, fileWatcher, WatcherCloseReason.ProjectClose); + }); this.missingFilesMap = undefined; // signal language service to release source files acquired from document registry @@ -639,38 +641,38 @@ namespace ts.server { } const missingFilePaths = this.program.getMissingFilePaths(); - const missingFilePathsSet = arrayToSet(missingFilePaths); - - // Files that are no longer missing (e.g. because they are no longer required) - // should no longer be watched. - this.missingFilesMap.forEach((fileWatcher, missingFilePath) => { - if (!missingFilePathsSet.has(missingFilePath)) { - this.missingFilesMap.delete(missingFilePath); - fileWatcher.close(); - } - }); - - // Missing files that are not yet watched should be added to the map. - for (const missingFilePath of missingFilePaths) { - if (!this.missingFilesMap.has(missingFilePath)) { - const fileWatcher = this.projectService.host.watchFile(missingFilePath, (filename: string, eventKind: FileWatcherEventKind) => { - if (eventKind === FileWatcherEventKind.Created && this.missingFilesMap.has(missingFilePath)) { - fileWatcher.close(); - this.missingFilesMap.delete(missingFilePath); - - if (this.projectKind === ProjectKind.Configured) { - const absoluteNormalizedPath = getNormalizedAbsolutePath(filename, getDirectoryPath(missingFilePath)); - (this.lsHost.host as CachedServerHost).addOrDeleteFileOrFolder(toNormalizedPath(absoluteNormalizedPath)); + const newMissingFilePathMap = arrayToSet(missingFilePaths); + // Update the missing file paths watcher + this.missingFilesMap = mutateExistingMapWithNewSet( + this.missingFilesMap, newMissingFilePathMap, + // Watch the missing files + missingFilePath => { + const fileWatcher = this.projectService.addFileWatcher( + WatchType.MissingFilePath, this, missingFilePath, + (filename: string, eventKind: FileWatcherEventKind) => { + if (eventKind === FileWatcherEventKind.Created && this.missingFilesMap.has(missingFilePath)) { + this.missingFilesMap.delete(missingFilePath); + this.projectService.closeFileWatcher(WatchType.MissingFilePath, this, missingFilePath, fileWatcher, WatcherCloseReason.FileCreated); + + if (this.projectKind === ProjectKind.Configured) { + const absoluteNormalizedPath = getNormalizedAbsolutePath(filename, getDirectoryPath(missingFilePath)); + (this.lsHost.host as CachedServerHost).addOrDeleteFileOrFolder(toNormalizedPath(absoluteNormalizedPath)); + } + + // When a missing file is created, we should update the graph. + this.markAsDirty(); + this.projectService.delayUpdateProjectGraphAndInferredProjectsRefresh(this); } - - // When a missing file is created, we should update the graph. - this.markAsDirty(); - this.projectService.delayUpdateProjectGraphAndInferredProjectsRefresh(this); } - }); - this.missingFilesMap.set(missingFilePath, fileWatcher); + ); + return fileWatcher; + }, + // Files that are no longer missing (e.g. because they are no longer required) + // should no longer be watched. + (missingFilePath, fileWatcher) => { + this.projectService.closeFileWatcher(WatchType.MissingFilePath, this, missingFilePath, fileWatcher, WatcherCloseReason.NotNeeded); } - } + ); } const oldExternalFiles = this.externalFiles || emptyArray as SortedReadonlyArray; @@ -694,7 +696,7 @@ namespace ts.server { } isWatchedMissingFile(path: Path) { - return this.missingFilesMap.has(path); + return this.missingFilesMap && this.missingFilesMap.has(path); } getScriptInfoLSHost(fileName: string) { @@ -976,9 +978,10 @@ namespace ts.server { */ export class ConfiguredProject extends Project { private typeAcquisition: TypeAcquisition; - private configFileWatcher: FileWatcher; + /* @internal */ + configFileWatcher: FileWatcher; private directoriesWatchedForWildcards: Map; - private typeRootsWatchers: FileWatcher[]; + private typeRootsWatchers: Map; readonly canonicalConfigFilePath: NormalizedPath; /* @internal */ @@ -1023,7 +1026,7 @@ namespace ts.server { } getConfigFilePath() { - return this.getProjectName(); + return asNormalizedPath(this.getProjectName()); } enablePlugins() { @@ -1131,94 +1134,83 @@ namespace ts.server { })); } - watchConfigFile(callback: (project: ConfiguredProject, eventKind: FileWatcherEventKind) => void) { - this.configFileWatcher = this.projectService.host.watchFile(this.getConfigFilePath(), (_fileName, eventKind) => callback(this, eventKind)); - } - - watchTypeRoots(callback: (project: ConfiguredProject, path: NormalizedPath) => void) { - const roots = this.getEffectiveTypeRoots(); - const watchers: FileWatcher[] = []; - for (const root of roots) { - this.projectService.logger.info(`Add type root watcher for: ${root}`); - watchers.push(this.projectService.host.watchDirectory(root, path => callback(this, toNormalizedPath(getNormalizedAbsolutePath(path, root))), /*recursive*/ false)); - } - this.typeRootsWatchers = watchers; - } - - private addWatcherForDirectory(flag: WatchDirectoryFlags, directory: string, replaceExisting: boolean) { - if (replaceExisting || !this.directoriesWatchedForWildcards.has(directory)) { - const recursive = (flag & WatchDirectoryFlags.Recursive) !== 0; - this.projectService.logger.info(`Add ${recursive ? "recursive " : ""} watcher for: ${directory}`); - this.directoriesWatchedForWildcards.set(directory, { - watcher: this.projectService.host.watchDirectory( - directory, - path => this.projectService.onFileAddOrRemoveInWatchedDirectoryOfProject(this, toNormalizedPath(getNormalizedAbsolutePath(path, directory))), - recursive - ), - recursive - }); - } - } - watchWildcards(wildcardDirectories: Map) { - if (wildcardDirectories) { - if (this.directoriesWatchedForWildcards) { - this.directoriesWatchedForWildcards.forEach(({ watcher, recursive }, directory) => { - const currentFlag = wildcardDirectories.get(directory); - // Remove already watching wild card if it isnt in updated map - if (currentFlag === undefined) { - this.projectService.logger.info(`Removing ${recursive ? "recursive " : ""} watcher for: ${directory}`); - watcher.close(); - this.directoriesWatchedForWildcards.delete(directory); - } - // Or if the recursive doesnt match (add the updated one here) - else { - const currentRecursive = (currentFlag & WatchDirectoryFlags.Recursive) !== 0; - if (currentRecursive !== recursive) { - this.projectService.logger.info(`Removing ${recursive ? "recursive " : ""} watcher for: ${directory}`); - watcher.close(); - this.addWatcherForDirectory(currentFlag, directory, /*replaceExisting*/ true); - } - } - }); - } - else { - this.directoriesWatchedForWildcards = createMap(); - } - wildcardDirectories.forEach((flag, directory) => - this.addWatcherForDirectory(flag, directory, /*replaceExisting*/ false)); - } - else { - this.stopWatchingWildCards(); - } - } - - stopWatchingWildCards() { - if (this.directoriesWatchedForWildcards) { - this.directoriesWatchedForWildcards.forEach(({ watcher, recursive }, directory) => { - this.projectService.logger.info(`Removing ${recursive ? "recursive " : ""} watcher for: ${directory}`); - watcher.close(); - }); - this.directoriesWatchedForWildcards = undefined; - } + this.directoriesWatchedForWildcards = mutateExistingMap( + this.directoriesWatchedForWildcards, wildcardDirectories, + // Watcher is same if the recursive flags match + ({ recursive: existingRecursive }, flag) => { + // If the recursive dont match, it needs update + const recursive = (flag & WatchDirectoryFlags.Recursive) !== 0; + return existingRecursive !== recursive; + }, + // Create new watch + (directory, flag) => { + const recursive = (flag & WatchDirectoryFlags.Recursive) !== 0; + return { + watcher: this.projectService.addDirectoryWatcher( + WatchType.WildCardDirectories, this, directory, + path => this.projectService.onFileAddOrRemoveInWatchedDirectoryOfProject(this, path), + recursive + ), + recursive + }; + }, + // Close existing watch thats not needed any more + (directory, { watcher, recursive }) => this.projectService.closeDirectoryWatcher( + WatchType.WildCardDirectories, this, directory, watcher, recursive, WatcherCloseReason.NotNeeded + ), + // Close existing watch that doesnt match in recursive flag + (directory, { watcher, recursive }) => this.projectService.closeDirectoryWatcher( + WatchType.WildCardDirectories, this, directory, watcher, recursive, WatcherCloseReason.RecursiveChanged + ) + ); + } + + stopWatchingWildCards(reason: WatcherCloseReason) { + cleanExistingMap( + this.directoriesWatchedForWildcards, + (directory, { watcher, recursive }) => + this.projectService.closeDirectoryWatcher(WatchType.WildCardDirectories, this, + directory, watcher, recursive, reason) + ); + this.directoriesWatchedForWildcards = undefined; + } + + watchTypeRoots() { + const newTypeRoots = arrayToSet(this.getEffectiveTypeRoots(), dir => this.projectService.toCanonicalFileName(dir)); + this.typeRootsWatchers = mutateExistingMapWithNewSet( + this.typeRootsWatchers, newTypeRoots, + // Create new watch + root => this.projectService.addDirectoryWatcher(WatchType.TypeRoot, this, root, + path => this.projectService.onTypeRootFileChanged(this, path), /*recursive*/ false + ), + // Close existing watch thats not needed any more + (directory, watcher) => this.projectService.closeDirectoryWatcher( + WatchType.TypeRoot, this, directory, watcher, /*recursive*/ false, WatcherCloseReason.NotNeeded + ) + ); + } + + stopWatchingTypeRoots(reason: WatcherCloseReason) { + cleanExistingMap( + this.typeRootsWatchers, + (directory, watcher) => + this.projectService.closeDirectoryWatcher(WatchType.TypeRoot, this, + directory, watcher, /*recursive*/ false, reason) + ); + this.typeRootsWatchers = undefined; } close() { super.close(); if (this.configFileWatcher) { - this.configFileWatcher.close(); + this.projectService.closeFileWatcher(WatchType.ConfigFilePath, this, this.getConfigFilePath(), this.configFileWatcher, WatcherCloseReason.ProjectClose); this.configFileWatcher = undefined; } - if (this.typeRootsWatchers) { - for (const watcher of this.typeRootsWatchers) { - watcher.close(); - } - this.typeRootsWatchers = undefined; - } - - this.stopWatchingWildCards(); + this.stopWatchingTypeRoots(WatcherCloseReason.ProjectClose); + this.stopWatchingWildCards(WatcherCloseReason.ProjectClose); } addOpenRef() { diff --git a/src/server/scriptInfo.ts b/src/server/scriptInfo.ts index 46008e77340c1..902e9d0d97e45 100644 --- a/src/server/scriptInfo.ts +++ b/src/server/scriptInfo.ts @@ -149,7 +149,8 @@ namespace ts.server { readonly containingProjects: Project[] = []; private formatCodeSettings: FormatCodeSettings; - private fileWatcher: FileWatcher; + /* @internal */ + fileWatcher: FileWatcher; private textStorage: TextStorage; private isOpen: boolean; @@ -291,18 +292,6 @@ namespace ts.server { } } - setWatcher(watcher: FileWatcher): void { - this.stopWatcher(); - this.fileWatcher = watcher; - } - - stopWatcher() { - if (this.fileWatcher) { - this.fileWatcher.close(); - this.fileWatcher = undefined; - } - } - getLatestVersion() { return this.textStorage.getVersion(); } diff --git a/src/server/utilities.ts b/src/server/utilities.ts index 3949e7b58dd9d..ce49fcdb9bf7d 100644 --- a/src/server/utilities.ts +++ b/src/server/utilities.ts @@ -226,6 +226,79 @@ namespace ts.server { } } + export function cleanExistingMap( + existingMap: Map, + onDeleteExistingValue: (key: string, existingValue: T) => void) { + if (existingMap) { + // Remove all + existingMap.forEach((existingValue, key) => { + existingMap.delete(key); + onDeleteExistingValue(key, existingValue); + }); + } + } + + export function mutateExistingMapWithNewSet( + existingMap: Map, newMap: Map, + createNewValue: (key: string) => T, + onDeleteExistingValue: (key: string, existingValue: T) => void + ): Map { + return mutateExistingMap( + existingMap, newMap, + // Same value if the value is set in the map + /*isSameValue*/(_existingValue, _valueInNewMap) => true, + /*createNewValue*/(key, _valueInNewMap) => createNewValue(key), + onDeleteExistingValue, + // Should never be called since we say yes to same values all the time + /*OnDeleteExistingMismatchValue*/(_key, _existingValue) => notImplemented() + ); + } + + export function mutateExistingMap( + existingMap: Map, newMap: Map, + isSameValue: (existingValue: T, valueInNewMap: U) => boolean, + createNewValue: (key: string, valueInNewMap: U) => T, + onDeleteExistingValue: (key: string, existingValue: T) => void, + OnDeleteExistingMismatchValue: (key: string, existingValue: T) => void + ): Map { + // If there are new values update them + if (newMap) { + if (existingMap) { + // Needs update + existingMap.forEach((existingValue, key) => { + const valueInNewMap = newMap.get(key); + // Existing value - remove it + if (valueInNewMap === undefined) { + existingMap.delete(key); + onDeleteExistingValue(key, existingValue); + } + // different value - remove it + else if (!isSameValue(existingValue, valueInNewMap)) { + existingMap.delete(key); + OnDeleteExistingMismatchValue(key, existingValue); + } + }); + } + else { + // Create new + existingMap = createMap(); + } + + // Add new values that are not already present + newMap.forEach((valueInNewMap, key) => { + if (!existingMap.has(key)) { + // New values + existingMap.set(key, createNewValue(key, valueInNewMap)); + } + }); + + return existingMap; + } + + cleanExistingMap(existingMap, onDeleteExistingValue); + return undefined; + } + export class ThrottledOperations { private pendingTimeouts: Map = createMap(); constructor(private readonly host: ServerHost) { From 71d79c62d055a288a9766f86069d82549275023e Mon Sep 17 00:00:00 2001 From: Sheetal Nandi Date: Fri, 14 Jul 2017 22:35:07 -0700 Subject: [PATCH 021/109] Some refactoring to combine files removal from inferred project --- src/server/editorServices.ts | 195 +++++++++++++++-------------------- src/server/lsHost.ts | 20 ++-- src/server/project.ts | 15 ++- 3 files changed, 106 insertions(+), 124 deletions(-) diff --git a/src/server/editorServices.ts b/src/server/editorServices.ts index 0aed0ab99fac5..abd8618b060d8 100644 --- a/src/server/editorServices.ts +++ b/src/server/editorServices.ts @@ -245,20 +245,6 @@ namespace ts.server { } } - /** - * TODO: enforce invariants: - * - script info can be never migrate to state - root file in inferred project, this is only a starting point - * - if script info has more that one containing projects - it is not a root file in inferred project because: - * - references in inferred project supercede the root part - * - root/reference in non-inferred project beats root in inferred project - */ - function isRootFileInInferredProject(info: ScriptInfo): boolean { - if (info.containingProjects.length === 0) { - return false; - } - return info.containingProjects[0].projectKind === ProjectKind.Inferred && info.containingProjects[0].isRoot(info); - } - class DirectoryWatchers { /** * a path to directory watcher map that detects added tsconfig files @@ -386,6 +372,7 @@ namespace ts.server { private pendingProjectUpdates = createMap(); private pendingInferredProjectUpdate: boolean; + readonly currentDirectory: string; readonly toCanonicalFileName: (f: string) => string; public readonly host: ServerHost; @@ -417,6 +404,7 @@ namespace ts.server { Debug.assert(!!this.host.createHash, "'ServerHost.createHash' is required for ProjectService"); + this.currentDirectory = this.host.getCurrentDirectory(); this.toCanonicalFileName = createGetCanonicalFileName(this.host.useCaseSensitiveFileNames); this.directoryWatchers = new DirectoryWatchers(this); this.throttledOperations = new ThrottledOperations(this.host); @@ -431,7 +419,11 @@ namespace ts.server { extraFileExtensions: [] }; - this.documentRegistry = createDocumentRegistry(this.host.useCaseSensitiveFileNames, this.host.getCurrentDirectory()); + this.documentRegistry = createDocumentRegistry(this.host.useCaseSensitiveFileNames, this.currentDirectory); + } + + toPath(fileName: string, basePath = this.currentDirectory) { + return toPath(fileName, basePath, this.toCanonicalFileName); } /* @internal */ @@ -721,7 +713,7 @@ namespace ts.server { this.removeProject(project); // Reload the configured projects for these open files in the project as // they could be held up by another config file somewhere in the parent directory - const openFilesInProject = this.getOrphanFiles(); + const openFilesInProject = filter(this.openFiles, file => file.containingProjects.length === 0); this.reloadConfiguredProjectForFiles(openFilesInProject, project => { project.pendingReload = true; this.delayUpdateProjectGraph(project); }); this.delayInferredProjectsRefresh(); } @@ -762,6 +754,7 @@ namespace ts.server { this.projectToSizeMap.delete((project as ExternalProject).externalProjectName); break; case ProjectKind.Configured: + // Update the map of mapOfKnownTsConfigFiles removeItemFromSet(this.configuredProjects, project); this.projectToSizeMap.delete((project as ConfiguredProject).canonicalConfigFilePath); break; @@ -772,59 +765,38 @@ namespace ts.server { } private assignScriptInfoToInferredProjectIfNecessary(info: ScriptInfo, addToListOfOpenFiles: boolean): void { - const externalProject = this.findContainingExternalProject(info.fileName); - if (externalProject) { - // file is already included in some external project - do nothing - if (addToListOfOpenFiles) { - this.openFiles.push(info); - } - return; - } - - let foundConfiguredProject = false; - for (const p of info.containingProjects) { - // file is the part of configured project - if (p.projectKind === ProjectKind.Configured) { - foundConfiguredProject = true; - if (addToListOfOpenFiles) { - ((p)).addOpenRef(); - } - } - } - if (foundConfiguredProject) { - if (addToListOfOpenFiles) { - this.openFiles.push(info); - } - return; - } - if (info.containingProjects.length === 0) { // create new inferred project p with the newly opened file as root // or add root to existing inferred project if 'useOneInferredProject' is true - const inferredProject = this.createInferredProjectWithRootFileIfNecessary(info); - if (!this.useSingleInferredProject) { - // if useOneInferredProject is not set then try to fixup ownership of open files - // check 'defaultProject !== inferredProject' is necessary to handle cases - // when creation inferred project for some file has added other open files into this project (i.e. as referenced files) - // we definitely don't want to delete the project that was just created - for (const f of this.openFiles) { - if (f.containingProjects.length === 0 || !inferredProject.containsScriptInfo(f)) { - // this is orphaned file that we have not processed yet - skip it - continue; - } - - for (const fContainingProject of f.containingProjects) { - if (fContainingProject.projectKind === ProjectKind.Inferred && - fContainingProject.isRoot(f) && - fContainingProject !== inferredProject) { - - // open file used to be root in inferred project, - // this inferred project is different from the one we've just created for current file - // and new inferred project references this open file. - // We should delete old inferred project and attach open file to the new one - this.removeProject(fContainingProject); - f.attachToProject(inferredProject); - } + this.createInferredProjectWithRootFileIfNecessary(info); + + // if useOneInferredProject is not set then try to fixup ownership of open files + // check 'defaultProject !== inferredProject' is necessary to handle cases + // when creation inferred project for some file has added other open files into this project + // (i.e.as referenced files) + // we definitely don't want to delete the project that was just created + // Also note that we need to create a copy of the array since the list of project will change + for (const inferredProject of this.inferredProjects.slice(0, this.inferredProjects.length - 1)) { + Debug.assert(!this.useSingleInferredProject); + // Remove this file from the root of inferred project if its part of more than 2 projects + // This logic is same as iterating over all open files and calling + // this.removRootOfInferredProjectIfNowPartOfOtherProject(f); + // Since this is also called from refreshInferredProject and closeOpen file + // to update inferred projects of the open file, this iteration might be faster + // instead of scanning all open files + const root = inferredProject.getRootScriptInfos(); + Debug.assert(root.length === 1); + if (root[0].containingProjects.length > 1) { + this.removeProject(inferredProject); + } + } + } + else { + for (const p of info.containingProjects) { + // file is the part of configured project + if (p.projectKind === ProjectKind.Configured) { + if (addToListOfOpenFiles) { + ((p)).addOpenRef(); } } } @@ -835,17 +807,6 @@ namespace ts.server { } } - private getOrphanFiles() { - let orphanFiles: ScriptInfo[]; - // for all open files - for (const f of this.openFiles) { - if (f.containingProjects.length === 0) { - (orphanFiles || (orphanFiles = [])).push(f); - } - } - return orphanFiles; - } - /** * Remove this file from the set of open, non-configured files. * @param info The file that has been closed or newly configured @@ -871,8 +832,15 @@ namespace ts.server { } } else if (p.projectKind === ProjectKind.Inferred && p.isRoot(info)) { - // open file in inferred project - (projectsToRemove || (projectsToRemove = [])).push(p); + // If this was the open root file of inferred project + if ((p as InferredProject).isProjectWithSingleRoot()) { + // - when useSingleInferredProject is not set, we can guarantee that this will be the only root + // - other wise remove the project if it is the only root + (projectsToRemove || (projectsToRemove = [])).push(p); + } + else { + p.removeFile(info); + } } if (!p.languageServiceEnabled) { @@ -888,11 +856,10 @@ namespace ts.server { } // collect orphanted files and try to re-add them as newly opened - const orphanFiles = this.getOrphanFiles(); - // treat orphaned files as newly opened - if (orphanFiles) { - for (const f of orphanFiles) { + // for all open files + for (const f of this.openFiles) { + if (f.containingProjects.length === 0) { this.assignScriptInfoToInferredProjectIfNecessary(f, /*addToListOfOpenFiles*/ false); } } @@ -1003,9 +970,9 @@ namespace ts.server { private findConfiguredProjectByProjectName(configFileName: NormalizedPath) { // make sure that casing of config file name is consistent - configFileName = asNormalizedPath(this.toCanonicalFileName(configFileName)); + const canonicalConfigFilePath = asNormalizedPath(this.toCanonicalFileName(configFileName)); for (const proj of this.configuredProjects) { - if (proj.canonicalConfigFilePath === configFileName) { + if (proj.canonicalConfigFilePath === canonicalConfigFilePath) { return proj; } } @@ -1186,7 +1153,6 @@ namespace ts.server { languageServiceEnabled, projectOptions.compileOnSave === undefined ? false : projectOptions.compileOnSave, cachedServerHost); - this.addFilesToProjectAndUpdateGraph(project, projectOptions.files, fileNamePropertyReader, clientFileName, projectOptions.typeAcquisition, configFileErrors); project.configFileSpecs = configFileSpecs; project.configFileWatcher = this.addFileWatcher(WatchType.ConfigFilePath, project, @@ -1197,6 +1163,8 @@ namespace ts.server { project.watchTypeRoots(); } + this.addFilesToProjectAndUpdateGraph(project, projectOptions.files, fileNamePropertyReader, clientFileName, projectOptions.typeAcquisition, configFileErrors); + this.configuredProjects.push(project); this.sendProjectTelemetry(project.getConfigFilePath(), project, projectOptions); return project; @@ -1242,7 +1210,7 @@ namespace ts.server { let scriptInfo: ScriptInfo | NormalizedPath; let path: Path; if (!project.lsHost.fileExists(newRootFile)) { - path = normalizedPathToPath(normalizedPath, this.host.getCurrentDirectory(), this.toCanonicalFileName); + path = normalizedPathToPath(normalizedPath, this.currentDirectory, this.toCanonicalFileName); const existingValue = projectRootFilesMap.get(path); if (isScriptInfo(existingValue)) { project.removeFile(existingValue); @@ -1257,16 +1225,12 @@ namespace ts.server { path = scriptInfo.path; // If this script info is not already a root add it if (!project.isRoot(scriptInfo)) { - if (scriptInfo.isScriptOpen() && isRootFileInInferredProject(scriptInfo)) { + project.addRoot(scriptInfo); + if (scriptInfo.isScriptOpen()) { // if file is already root in some inferred project // - remove the file from that project and delete the project if necessary - const inferredProject = scriptInfo.containingProjects[0]; - inferredProject.removeFile(scriptInfo); - if (!inferredProject.hasRoots()) { - this.removeProject(inferredProject); - } + this.removRootOfInferredProjectIfNowPartOfOtherProject(scriptInfo); } - project.addRoot(scriptInfo); } } newRootScriptInfoMap.set(path, scriptInfo); @@ -1362,7 +1326,6 @@ namespace ts.server { root.fileName, project, fileName => this.onConfigFileAddedForInferredProject(fileName)); - project.updateGraph(); if (!useExistingProject) { @@ -1403,7 +1366,7 @@ namespace ts.server { } getOrCreateScriptInfoForNormalizedPath(fileName: NormalizedPath, openedByClient: boolean, fileContent?: string, scriptKind?: ScriptKind, hasMixedContent?: boolean) { - const path = toPath(fileName, this.host.getCurrentDirectory(), this.toCanonicalFileName); + const path = normalizedPathToPath(fileName, this.currentDirectory, this.toCanonicalFileName); let info = this.getScriptInfoForPath(path); if (!info) { if (openedByClient || this.host.fileExists(fileName)) { @@ -1438,7 +1401,7 @@ namespace ts.server { } getScriptInfoForNormalizedPath(fileName: NormalizedPath) { - return this.getScriptInfoForPath(normalizedPathToPath(fileName, this.host.getCurrentDirectory(), this.toCanonicalFileName)); + return this.getScriptInfoForPath(normalizedPathToPath(fileName, this.currentDirectory, this.toCanonicalFileName)); } getScriptInfoForPath(fileName: Path) { @@ -1539,6 +1502,26 @@ namespace ts.server { } } + /** + * - script info can be never migrate to state - root file in inferred project, this is only a starting point + * - if script info has more that one containing projects - it is not a root file in inferred project because: + * - references in inferred project supercede the root part + * - root/reference in non-inferred project beats root in inferred project + */ + private removRootOfInferredProjectIfNowPartOfOtherProject(info: ScriptInfo) { + if (info.containingProjects.length > 1 && + info.containingProjects[0].projectKind === ProjectKind.Inferred && + info.containingProjects[0].isRoot(info)) { + const inferredProject = info.containingProjects[0] as InferredProject; + if (inferredProject.isProjectWithSingleRoot()) { + this.removeProject(inferredProject); + } + else { + inferredProject.removeFile(info); + } + } + } + /** * This function is to update the project structure for every projects. * It is called on the premise that all the configured projects are @@ -1548,26 +1531,16 @@ namespace ts.server { this.logger.info("updating project structure from ..."); this.printProjects(); - const orphantedFiles: ScriptInfo[] = []; - // collect all orphanted script infos from open files for (const info of this.openFiles) { + // collect all orphanted script infos from open files if (info.containingProjects.length === 0) { - orphantedFiles.push(info); + this.assignScriptInfoToInferredProjectIfNecessary(info, /*addToListOfOpenFiles*/ false); } + // Or remove the root of inferred project if is referenced in more than one projects else { - if (isRootFileInInferredProject(info) && info.containingProjects.length > 1) { - const inferredProject = info.containingProjects[0]; - Debug.assert(inferredProject.projectKind === ProjectKind.Inferred); - inferredProject.removeFile(info); - if (!inferredProject.hasRoots()) { - this.removeProject(inferredProject); - } - } + this.removRootOfInferredProjectIfNowPartOfOtherProject(info); } } - for (const f of orphantedFiles) { - this.assignScriptInfoToInferredProjectIfNecessary(f, /*addToListOfOpenFiles*/ false); - } for (const p of this.inferredProjects) { p.updateGraph(); diff --git a/src/server/lsHost.ts b/src/server/lsHost.ts index a7e9c5e231cca..1be0f2032064b 100644 --- a/src/server/lsHost.ts +++ b/src/server/lsHost.ts @@ -27,8 +27,12 @@ namespace ts.server { this.currentDirectory = this.host.getCurrentDirectory(); } + private toPath(fileName: string) { + return toPath(fileName, this.currentDirectory, this.getCanonicalFileName); + } + private getFileSystemEntries(rootDir: string) { - const path = toPath(rootDir, this.currentDirectory, this.getCanonicalFileName); + const path = this.toPath(rootDir); const cachedResult = this.cachedReadDirectoryResult.get(path); if (cachedResult) { return cachedResult; @@ -45,7 +49,7 @@ namespace ts.server { private canWorkWithCacheForDir(rootDir: string) { // Some of the hosts might not be able to handle read directory or getDirectories - const path = toPath(rootDir, this.currentDirectory, this.getCanonicalFileName); + const path = this.toPath(rootDir); if (this.cachedReadDirectoryResult.get(path)) { return true; } @@ -62,7 +66,7 @@ namespace ts.server { } writeFile(fileName: string, data: string, writeByteOrderMark?: boolean) { - const path = toPath(fileName, this.currentDirectory, this.getCanonicalFileName); + const path = this.toPath(fileName); const result = this.cachedReadDirectoryResult.get(getDirectoryPath(path)); const baseFileName = getBaseFileName(toNormalizedPath(fileName)); if (result) { @@ -111,14 +115,14 @@ namespace ts.server { } fileExists(fileName: string): boolean { - const path = toPath(fileName, this.currentDirectory, this.getCanonicalFileName); + const path = this.toPath(fileName); const result = this.cachedReadDirectoryResult.get(getDirectoryPath(path)); const baseName = getBaseFileName(toNormalizedPath(fileName)); return (result && this.hasEntry(result.files, baseName)) || this.host.fileExists(fileName); } directoryExists(dirPath: string) { - const path = toPath(dirPath, this.currentDirectory, this.getCanonicalFileName); + const path = this.toPath(dirPath); return this.cachedReadDirectoryResult.has(path) || this.host.directoryExists(dirPath); } @@ -147,7 +151,7 @@ namespace ts.server { } addOrDeleteFileOrFolder(fileOrFolder: NormalizedPath) { - const path = toPath(fileOrFolder, this.currentDirectory, this.getCanonicalFileName); + const path = this.toPath(fileOrFolder); const existingResult = this.cachedReadDirectoryResult.get(path); if (existingResult) { if (!this.host.directoryExists(fileOrFolder)) { @@ -262,7 +266,7 @@ namespace ts.server { getResultFileName: (result: R) => string | undefined, logChanges: boolean): R[] { - const path = toPath(containingFile, this.host.getCurrentDirectory(), this.project.projectService.toCanonicalFileName); + const path = this.project.projectService.toPath(containingFile); const currentResolutionsInFile = cache.get(path); const newResolutions: Map = createMap(); @@ -403,7 +407,7 @@ namespace ts.server { fileExists(file: string): boolean { // As an optimization, don't hit the disks for files we already know don't exist // (because we're watching for their creation). - const path = toPath(file, this.host.getCurrentDirectory(), this.project.projectService.toCanonicalFileName); + const path = this.project.projectService.toPath(file); return !this.project.isWatchedMissingFile(path) && this.host.fileExists(file); } diff --git a/src/server/project.ts b/src/server/project.ts index aa492d246dca6..1b99e76545e25 100644 --- a/src/server/project.ts +++ b/src/server/project.ts @@ -496,8 +496,7 @@ namespace ts.server { // add a root file to project addMissingFileRoot(fileName: NormalizedPath) { - const path = toPath(fileName, this.projectService.host.getCurrentDirectory(), - this.projectService.toCanonicalFileName); + const path = this.projectService.toPath(fileName); this.rootFilesMap.set(path, fileName); this.markAsDirty(); } @@ -842,7 +841,7 @@ namespace ts.server { // Handle triple slash references if (sourceFile.referencedFiles && sourceFile.referencedFiles.length > 0) { for (const referencedFile of sourceFile.referencedFiles) { - const referencedPath = toPath(referencedFile.fileName, currentDirectory, this.projectService.toCanonicalFileName); + const referencedPath = this.projectService.toPath(referencedFile.fileName, currentDirectory); referencedFiles.set(referencedPath, true); } } @@ -855,7 +854,7 @@ namespace ts.server { } const fileName = resolvedTypeReferenceDirective.resolvedFileName; - const typeFilePath = toPath(fileName, currentDirectory, this.projectService.toCanonicalFileName); + const typeFilePath = this.projectService.toPath(fileName, currentDirectory); referencedFiles.set(typeFilePath, true); }); } @@ -943,6 +942,12 @@ namespace ts.server { } } + isProjectWithSingleRoot() { + // - when useSingleInferredProject is not set, we can guarantee that this will be the only root + // - other wise it has single root if it has single root script info + return !this.projectService.useSingleInferredProject || this.getRootScriptInfos().length === 1; + } + getProjectRootPath() { // Single inferred project does not have a project root. if (this.projectService.useSingleInferredProject) { @@ -1143,7 +1148,7 @@ namespace ts.server { const recursive = (flag & WatchDirectoryFlags.Recursive) !== 0; return existingRecursive !== recursive; }, - // Create new watch + // Create new watch and recursive info (directory, flag) => { const recursive = (flag & WatchDirectoryFlags.Recursive) !== 0; return { From f12980dd208b1c375d46053a400baab743811c58 Mon Sep 17 00:00:00 2001 From: Sheetal Nandi Date: Sat, 15 Jul 2017 13:37:22 -0700 Subject: [PATCH 022/109] Remove the duplcate error about no input files found --- src/compiler/diagnosticMessages.json | 4 - .../unittests/tsserverProjectSystem.ts | 9 +- src/server/editorServices.ts | 146 +++++++----------- 3 files changed, 58 insertions(+), 101 deletions(-) diff --git a/src/compiler/diagnosticMessages.json b/src/compiler/diagnosticMessages.json index cf198ec06ec8e..d5ac603bf1b25 100644 --- a/src/compiler/diagnosticMessages.json +++ b/src/compiler/diagnosticMessages.json @@ -3062,10 +3062,6 @@ "category": "Message", "code": 6128 }, - "The config file '{0}' found doesn't contain any source files.": { - "category": "Error", - "code": 6129 - }, "Resolving real path for '{0}', result '{1}'.": { "category": "Message", "code": 6130 diff --git a/src/harness/unittests/tsserverProjectSystem.ts b/src/harness/unittests/tsserverProjectSystem.ts index 273f7dd15340a..2272102924c5d 100644 --- a/src/harness/unittests/tsserverProjectSystem.ts +++ b/src/harness/unittests/tsserverProjectSystem.ts @@ -817,18 +817,19 @@ namespace ts.projectSystem { const host = createServerHost([f1, config], { useCaseSensitiveFileNames: false }); const service = createProjectService(host); + const upperCaseConfigFilePath = combinePaths(getDirectoryPath(config.path).toUpperCase(), getBaseFileName(config.path)); service.openExternalProject({ projectFileName: "/a/b/project.csproj", - rootFiles: toExternalFiles([f1.path, combinePaths(getDirectoryPath(config.path).toUpperCase(), getBaseFileName(config.path))]), + rootFiles: toExternalFiles([f1.path, upperCaseConfigFilePath]), options: {} }); service.checkNumberOfProjects({ configuredProjects: 1 }); - checkProjectActualFiles(service.configuredProjects[0], []); + checkProjectActualFiles(service.configuredProjects[0], [upperCaseConfigFilePath]); service.openClientFile(f1.path); service.checkNumberOfProjects({ configuredProjects: 1, inferredProjects: 1 }); - checkProjectActualFiles(service.configuredProjects[0], []); + checkProjectActualFiles(service.configuredProjects[0], [upperCaseConfigFilePath]); checkProjectActualFiles(service.inferredProjects[0], [f1.path]); }); @@ -3495,7 +3496,7 @@ namespace ts.projectSystem { checkNumberOfInferredProjects(projectService, 1); const configuredProject = projectService.configuredProjects[0]; - assert.isTrue(configuredProject.getFileNames().length === 0); + checkProjectActualFiles(configuredProject, [configFile.path]); const inferredProject = projectService.inferredProjects[0]; assert.isTrue(inferredProject.containsFile(file1.path)); diff --git a/src/server/editorServices.ts b/src/server/editorServices.ts index abd8618b060d8..122f88d067565 100644 --- a/src/server/editorServices.ts +++ b/src/server/editorServices.ts @@ -363,6 +363,7 @@ namespace ts.server { private compileOnSaveForInferredProjects: boolean; private readonly projectToSizeMap: Map = createMap(); private readonly directoryWatchers: DirectoryWatchers; + private readonly mapOfTsConfigPresence: Map; private readonly throttledOperations: ThrottledOperations; private readonly hostConfiguration: HostConfiguration; @@ -407,6 +408,7 @@ namespace ts.server { this.currentDirectory = this.host.getCurrentDirectory(); this.toCanonicalFileName = createGetCanonicalFileName(this.host.useCaseSensitiveFileNames); this.directoryWatchers = new DirectoryWatchers(this); + this.mapOfTsConfigPresence = createMap(); this.throttledOperations = new ThrottledOperations(this.host); this.typingsInstaller.attach(this); @@ -661,7 +663,6 @@ namespace ts.server { /* @internal */ onTypeRootFileChanged(project: ConfiguredProject, fileName: NormalizedPath) { - this.logger.info(`Type root file ${fileName} changed`); project.getCachedServerHost().addOrDeleteFileOrFolder(fileName); project.updateTypes(); this.delayUpdateProjectGraphAndInferredProjectsRefresh(project); @@ -675,34 +676,25 @@ namespace ts.server { /* @internal */ onFileAddOrRemoveInWatchedDirectoryOfProject(project: ConfiguredProject, fileName: NormalizedPath) { project.getCachedServerHost().addOrDeleteFileOrFolder(fileName); + const configFilename = project.getConfigFilePath(); // If a change was made inside "folder/file", node will trigger the callback twice: // one with the fileName being "folder/file", and the other one with "folder". // We don't respond to the second one. if (fileName && !isSupportedSourceFileName(fileName, project.getCompilerOptions(), this.hostConfiguration.extraFileExtensions)) { + this.logger.info(`Project: ${configFilename} Detected file add/remove of non supported extension: ${fileName}`); return; } - const configFilename = project.getConfigFilePath(); - this.logger.info(`Project: ${configFilename} Detected source file add/remove: ${fileName}`); - const configFileSpecs = project.configFileSpecs; const result = getFileNamesFromConfigSpecs(configFileSpecs, getDirectoryPath(configFilename), project.getCompilerOptions(), project.getCachedServerHost(), this.hostConfiguration.extraFileExtensions); const errors = project.getAllProjectErrors(); - if (result.fileNames.length === 0) { - if (!configFileSpecs.filesSpecs) { - if (!some(errors, error => error.code === Diagnostics.No_inputs_were_found_in_config_file_0_Specified_include_paths_were_1_and_exclude_paths_were_2.code)) { - errors.push(getErrorForNoInputFiles(configFileSpecs, configFilename)); - } - if (!some(errors, error => error.code === Diagnostics.The_config_file_0_found_doesn_t_contain_any_source_files.code)) { - errors.push(createCompilerDiagnostic(Diagnostics.The_config_file_0_found_doesn_t_contain_any_source_files, configFilename)); - } - } + const isErrorNoInputFiles = (error: Diagnostic) => error.code === Diagnostics.No_inputs_were_found_in_config_file_0_Specified_include_paths_were_1_and_exclude_paths_were_2.code; + if (result.fileNames.length !== 0) { + filterMutate(errors, error => !isErrorNoInputFiles(error)); } - else { - filterMutate(errors, error => - error.code !== Diagnostics.No_inputs_were_found_in_config_file_0_Specified_include_paths_were_1_and_exclude_paths_were_2.code && - error.code !== Diagnostics.The_config_file_0_found_doesn_t_contain_any_source_files.code); + else if (!configFileSpecs.filesSpecs && !some(errors, isErrorNoInputFiles)) { + errors.push(getErrorForNoInputFiles(configFileSpecs, configFilename)); } this.updateNonInferredProjectFiles(project, result.fileNames, fileNamePropertyReader); this.delayUpdateProjectGraphAndInferredProjectsRefresh(project); @@ -888,6 +880,15 @@ namespace ts.server { }); } + private configFileExists(configFileName: NormalizedPath) { + const canonicalConfigFilePath = normalizedPathToPath(configFileName, this.currentDirectory, this.toCanonicalFileName); + const cachedResult = this.mapOfTsConfigPresence.get(canonicalConfigFilePath); + if (cachedResult) { + // Use the information here to answer the question + } + return this.host.fileExists(configFileName); + } + /** * This function tries to search for a tsconfig.json for the given file. * This is different from the method the compiler uses because @@ -897,34 +898,20 @@ namespace ts.server { * the newly opened file. */ private getConfigFileNameForFile(fileName: NormalizedPath, projectRootPath?: NormalizedPath) { - const searchPath = getDirectoryPath(fileName); + let searchPath = getDirectoryPath(fileName); this.logger.info(`Search path: ${searchPath}`); // check if this file is already included in one of external projects - const configFileName = this.findConfigFile(asNormalizedPath(searchPath), projectRootPath); - if (configFileName) { - this.logger.info(`Config file name: ${configFileName}`); - } - else { - this.logger.info("No config files found."); - } - return configFileName; - } - - // This is different from the method the compiler uses because - // the compiler can assume it will always start searching in the - // current directory (the directory in which tsc was invoked). - // The server must start searching from the directory containing - // the newly opened file. - private findConfigFile(searchPath: NormalizedPath, projectRootPath?: NormalizedPath): NormalizedPath { while (!projectRootPath || searchPath.indexOf(projectRootPath) >= 0) { const tsconfigFileName = asNormalizedPath(combinePaths(searchPath, "tsconfig.json")); - if (this.host.fileExists(tsconfigFileName)) { + if (this.configFileExists(tsconfigFileName)) { + this.logger.info(`Config file name: ${tsconfigFileName}`); return tsconfigFileName; } const jsconfigFileName = asNormalizedPath(combinePaths(searchPath, "jsconfig.json")); - if (this.host.fileExists(jsconfigFileName)) { + if (this.configFileExists(jsconfigFileName)) { + this.logger.info(`Config file name: ${jsconfigFileName}`); return jsconfigFileName; } @@ -934,6 +921,7 @@ namespace ts.server { } searchPath = parentPath; } + this.logger.info("No config files found."); return undefined; } @@ -982,7 +970,7 @@ namespace ts.server { return findProjectByName(projectFileName, this.externalProjects); } - private convertConfigFileContentToProjectOptions(configFilename: string, cachedServerHost: CachedServerHost) { + private convertConfigFileContentToProjectOptions(configFilename: string, cachedServerHost: CachedServerHost) { configFilename = normalizePath(configFilename); const configFileContent = this.host.readFile(configFilename); @@ -1007,38 +995,19 @@ namespace ts.server { Debug.assert(!!parsedCommandLine.fileNames); - let success: boolean; - let projectOptions: ProjectOptions; - - if (parsedCommandLine.fileNames.length === 0) { - errors.push(createCompilerDiagnostic(Diagnostics.The_config_file_0_found_doesn_t_contain_any_source_files, configFilename)); - success = false; - projectOptions = { - files: [], - compilerOptions: {}, - configHasExtendsProperty: false, - configHasFilesProperty: false, - configHasIncludeProperty: false, - configHasExcludeProperty: false, - typeAcquisition: { enable: false } - }; - } - else { - success = true; - projectOptions = { - files: parsedCommandLine.fileNames, - compilerOptions: parsedCommandLine.options, - configHasExtendsProperty: parsedCommandLine.raw["extends"] !== undefined, - configHasFilesProperty: parsedCommandLine.raw["files"] !== undefined, - configHasIncludeProperty: parsedCommandLine.raw["include"] !== undefined, - configHasExcludeProperty: parsedCommandLine.raw["exclude"] !== undefined, - wildcardDirectories: createMapFromTemplate(parsedCommandLine.wildcardDirectories), - typeAcquisition: parsedCommandLine.typeAcquisition, - compileOnSave: parsedCommandLine.compileOnSave - }; - } + const projectOptions: ProjectOptions = { + files: parsedCommandLine.fileNames, + compilerOptions: parsedCommandLine.options, + configHasExtendsProperty: parsedCommandLine.raw["extends"] !== undefined, + configHasFilesProperty: parsedCommandLine.raw["files"] !== undefined, + configHasIncludeProperty: parsedCommandLine.raw["include"] !== undefined, + configHasExcludeProperty: parsedCommandLine.raw["exclude"] !== undefined, + wildcardDirectories: createMapFromTemplate(parsedCommandLine.wildcardDirectories), + typeAcquisition: parsedCommandLine.typeAcquisition, + compileOnSave: parsedCommandLine.compileOnSave + }; - return { success, projectOptions, configFileErrors: errors, configFileSpecs: parsedCommandLine.configFileSpecs }; + return { projectOptions, configFileErrors: errors, configFileSpecs: parsedCommandLine.configFileSpecs }; } private exceededTotalSizeLimitForNonTsFiles(name: string, options: CompilerOptions, fileNames: T[], propertyReader: FilePropertyReader) { @@ -1131,17 +1100,6 @@ namespace ts.server { } } - private reportConfigFileDiagnostics(configFileName: string, diagnostics: Diagnostic[], triggerFile: string) { - if (!this.eventHandler) { - return; - } - - this.eventHandler({ - eventName: ConfigFileDiagEvent, - data: { configFileName, diagnostics: diagnostics || [], triggerFile } - }); - } - private createAndAddConfiguredProject(configFileName: NormalizedPath, projectOptions: ProjectOptions, configFileErrors: Diagnostic[], configFileSpecs: ConfigFileSpecs, cachedServerHost: CachedServerHost, clientFileName?: string) { const languageServiceEnabled = !this.exceededTotalSizeLimitForNonTsFiles(configFileName, projectOptions.compilerOptions, projectOptions.files, fileNamePropertyReader); const project = new ConfiguredProject( @@ -1155,6 +1113,7 @@ namespace ts.server { cachedServerHost); project.configFileSpecs = configFileSpecs; + // TODO: (sheetalkamat) We should also watch the configFiles that are extended project.configFileWatcher = this.addFileWatcher(WatchType.ConfigFilePath, project, configFileName, (_fileName, eventKind) => this.onConfigChangedForConfiguredProject(project, eventKind) ); @@ -1192,11 +1151,8 @@ namespace ts.server { private openConfigFile(configFileName: NormalizedPath, clientFileName?: string) { const cachedServerHost = new CachedServerHost(this.host, this.toCanonicalFileName); - const { success, projectOptions, configFileErrors, configFileSpecs } = this.convertConfigFileContentToProjectOptions(configFileName, cachedServerHost); - if (success) { - this.logger.info(`Opened configuration file ${configFileName}`); - } - + const { projectOptions, configFileErrors, configFileSpecs } = this.convertConfigFileContentToProjectOptions(configFileName, cachedServerHost); + this.logger.info(`Opened configuration file ${configFileName}`); return this.createAndAddConfiguredProject(configFileName, projectOptions, configFileErrors, configFileSpecs, cachedServerHost, clientFileName); } @@ -1284,16 +1240,19 @@ namespace ts.server { const host = project.getCachedServerHost(); host.clearCache(); const configFileName = project.getConfigFilePath(); - const { success, projectOptions, configFileErrors, configFileSpecs } = this.convertConfigFileContentToProjectOptions(configFileName, host); + this.logger.info(`Reloading configured project ${configFileName}`); + const { projectOptions, configFileErrors, configFileSpecs } = this.convertConfigFileContentToProjectOptions(configFileName, host); project.configFileSpecs = configFileSpecs; - if (!success) { - // reset project settings to default - this.updateNonInferredProject(project, [], fileNamePropertyReader, {}, {}, /*compileOnSave*/ false, configFileErrors); - } - else { - this.updateConfiguredProject(project, projectOptions, configFileErrors); + this.updateConfiguredProject(project, projectOptions, configFileErrors); + + if (!this.eventHandler) { + return; } - this.reportConfigFileDiagnostics(configFileName, project.getGlobalProjectErrors(), /*triggerFile*/ configFileName); + + this.eventHandler({ + eventName: ConfigFileDiagEvent, + data: { configFileName, diagnostics: project.getGlobalProjectErrors() || [], triggerFile: configFileName } + }); } /** @@ -1528,7 +1487,7 @@ namespace ts.server { * up to date. */ private refreshInferredProjects() { - this.logger.info("updating project structure from ..."); + this.logger.info("refreshInferredProjects: updating project structure from ..."); this.printProjects(); for (const info of this.openFiles) { @@ -1546,6 +1505,7 @@ namespace ts.server { p.updateGraph(); } + this.logger.info("refreshInferredProjects: updated project structure ..."); this.printProjects(); } From 00011a52af387cabeaddb261ca426529f1cdbe5a Mon Sep 17 00:00:00 2001 From: Sheetal Nandi Date: Sat, 15 Jul 2017 14:24:44 -0700 Subject: [PATCH 023/109] Refactor root files addition/update for non inferred project --- src/server/editorServices.ts | 39 +++++++++--------------------------- 1 file changed, 10 insertions(+), 29 deletions(-) diff --git a/src/server/editorServices.ts b/src/server/editorServices.ts index 122f88d067565..410872e369ec1 100644 --- a/src/server/editorServices.ts +++ b/src/server/editorServices.ts @@ -696,7 +696,7 @@ namespace ts.server { else if (!configFileSpecs.filesSpecs && !some(errors, isErrorNoInputFiles)) { errors.push(getErrorForNoInputFiles(configFileSpecs, configFilename)); } - this.updateNonInferredProjectFiles(project, result.fileNames, fileNamePropertyReader); + this.updateNonInferredProjectFiles(project, result.fileNames, fileNamePropertyReader, /*clientFileName*/ undefined); this.delayUpdateProjectGraphAndInferredProjectsRefresh(project); } @@ -1050,7 +1050,7 @@ namespace ts.server { /*languageServiceEnabled*/ !this.exceededTotalSizeLimitForNonTsFiles(projectFileName, compilerOptions, files, externalFilePropertyReader), options.compileOnSave === undefined ? true : options.compileOnSave); - this.addFilesToProjectAndUpdateGraph(project, files, externalFilePropertyReader, /*clientFileName*/ undefined, typeAcquisition, /*configFileErrors*/ undefined); + this.addFilesToNonInferredProjectAndUpdateGraph(project, files, externalFilePropertyReader, /*clientFileName*/ undefined, typeAcquisition, /*configFileErrors*/ undefined); this.externalProjects.push(project); this.sendProjectTelemetry(project.externalProjectName, project); return project; @@ -1122,30 +1122,17 @@ namespace ts.server { project.watchTypeRoots(); } - this.addFilesToProjectAndUpdateGraph(project, projectOptions.files, fileNamePropertyReader, clientFileName, projectOptions.typeAcquisition, configFileErrors); - + this.addFilesToNonInferredProjectAndUpdateGraph(project, projectOptions.files, fileNamePropertyReader, clientFileName, projectOptions.typeAcquisition, configFileErrors); this.configuredProjects.push(project); this.sendProjectTelemetry(project.getConfigFilePath(), project, projectOptions); return project; } - private addFilesToProjectAndUpdateGraph(project: ConfiguredProject | ExternalProject, files: T[], propertyReader: FilePropertyReader, clientFileName: string, typeAcquisition: TypeAcquisition, configFileErrors: Diagnostic[]): void { - for (const f of files) { - const rootFilename = propertyReader.getFileName(f); - const scriptKind = propertyReader.getScriptKind(f); - const hasMixedContent = propertyReader.hasMixedContent(f, this.hostConfiguration.extraFileExtensions); - const fileName = toNormalizedPath(rootFilename); - if (project.lsHost.fileExists(rootFilename)) { - const info = this.getOrCreateScriptInfoForNormalizedPath(fileName, /*openedByClient*/ clientFileName === rootFilename, /*fileContent*/ undefined, scriptKind, hasMixedContent); - project.addRoot(info); - } - else { - // Create the file root with just the filename so that LS will have correct set of roots - project.addMissingFileRoot(fileName); - } - } + private addFilesToNonInferredProjectAndUpdateGraph(project: ConfiguredProject | ExternalProject, files: T[], propertyReader: FilePropertyReader, clientFileName: string, typeAcquisition: TypeAcquisition, configFileErrors: Diagnostic[]): void { project.setProjectErrors(configFileErrors); + this.updateNonInferredProjectFiles(project, files, propertyReader, clientFileName); project.setTypeAcquisition(typeAcquisition); + // This doesnt need scheduling since its either creation or reload of the project project.updateGraph(); } @@ -1156,7 +1143,7 @@ namespace ts.server { return this.createAndAddConfiguredProject(configFileName, projectOptions, configFileErrors, configFileSpecs, cachedServerHost, clientFileName); } - private updateNonInferredProjectFiles(project: ExternalProject | ConfiguredProject, newUncheckedFiles: T[], propertyReader: FilePropertyReader) { + private updateNonInferredProjectFiles(project: ExternalProject | ConfiguredProject, newUncheckedFiles: T[], propertyReader: FilePropertyReader, clientFileName?: string) { const projectRootFilesMap = project.getRootFilesMap(); const newRootScriptInfoMap: Map = createMap(); @@ -1170,14 +1157,14 @@ namespace ts.server { const existingValue = projectRootFilesMap.get(path); if (isScriptInfo(existingValue)) { project.removeFile(existingValue); - projectRootFilesMap.set(path, normalizedPath); } + projectRootFilesMap.set(path, normalizedPath); scriptInfo = normalizedPath; } else { const scriptKind = propertyReader.getScriptKind(f); const hasMixedContent = propertyReader.hasMixedContent(f, this.hostConfiguration.extraFileExtensions); - scriptInfo = this.getOrCreateScriptInfoForNormalizedPath(normalizedPath, /*openedByClient*/ false, /*fileContent*/ undefined, scriptKind, hasMixedContent); + scriptInfo = this.getOrCreateScriptInfoForNormalizedPath(normalizedPath, /*openedByClient*/ clientFileName === newRootFile, /*fileContent*/ undefined, scriptKind, hasMixedContent); path = scriptInfo.path; // If this script info is not already a root add it if (!project.isRoot(scriptInfo)) { @@ -1211,19 +1198,13 @@ namespace ts.server { } private updateNonInferredProject(project: ExternalProject | ConfiguredProject, newUncheckedFiles: T[], propertyReader: FilePropertyReader, newOptions: CompilerOptions, newTypeAcquisition: TypeAcquisition, compileOnSave: boolean, configFileErrors: Diagnostic[]) { - this.updateNonInferredProjectFiles(project, newUncheckedFiles, propertyReader); project.setCompilerOptions(newOptions); - project.setTypeAcquisition(newTypeAcquisition); - // VS only set the CompileOnSaveEnabled option in the request if the option was changed recently // therefore if it is undefined, it should not be updated. if (compileOnSave !== undefined) { project.compileOnSaveEnabled = compileOnSave; } - project.setProjectErrors(configFileErrors); - - // This doesnt need scheduling since its either creation or reload of the project - project.updateGraph(); + this.addFilesToNonInferredProjectAndUpdateGraph(project, newUncheckedFiles, propertyReader, /*clientFileName*/ undefined, newTypeAcquisition, configFileErrors); } /** From 0572b15adc2b3c19ff2912ff7510a0d700d85f06 Mon Sep 17 00:00:00 2001 From: Sheetal Nandi Date: Sat, 15 Jul 2017 19:11:27 -0700 Subject: [PATCH 024/109] Instead of watching directories, watch tsconfig files of inferred project root --- .../unittests/tsserverProjectSystem.ts | 15 +- src/server/editorServices.ts | 288 ++++++++++++------ src/server/project.ts | 13 +- 3 files changed, 215 insertions(+), 101 deletions(-) diff --git a/src/harness/unittests/tsserverProjectSystem.ts b/src/harness/unittests/tsserverProjectSystem.ts index 2272102924c5d..22bf06bef6515 100644 --- a/src/harness/unittests/tsserverProjectSystem.ts +++ b/src/harness/unittests/tsserverProjectSystem.ts @@ -281,7 +281,7 @@ namespace ts.projectSystem { } export function checkMapKeys(caption: string, map: Map, expectedKeys: string[]) { - assert.equal(map.size, expectedKeys.length, `${caption}: incorrect size of map`); + assert.equal(map.size, expectedKeys.length, `${caption}: incorrect size of map: Actual keys: ${arrayFrom(map.keys())} Expected: ${expectedKeys}`); for (const name of expectedKeys) { assert.isTrue(map.has(name), `${caption} is expected to contain ${name}, actual keys: ${arrayFrom(map.keys())}`); } @@ -800,7 +800,7 @@ namespace ts.projectSystem { const project = projectService.inferredProjects[0]; checkFileNames("inferred project", project.getFileNames(), [appFile.path, libFile.path, moduleFile.path]); - checkWatchedDirectories(host, ["/a/b/c", "/a/b", "/a"]); + checkWatchedFiles(host, ["/a/b/c/tsconfig.json", "/a/b/tsconfig.json", "/a/tsconfig.json", libFile.path, moduleFile.path]); }); it("can handle tsconfig file name with difference casing", () => { @@ -890,15 +890,15 @@ namespace ts.projectSystem { projectService.openClientFile(commonFile2.path); checkNumberOfInferredProjects(projectService, 2); - checkWatchedDirectories(host, ["/a/b", "/a"]); + checkWatchedFiles(host, [configFile.path, "/a/tsconfig.json", libFile.path]); // Add a tsconfig file host.reloadFS(filesWithConfig); - + host.checkTimeoutQueueLengthAndRun(1); checkNumberOfInferredProjects(projectService, 1); checkNumberOfConfiguredProjects(projectService, 1); // watching all files except one that was open - checkWatchedFiles(host, [libFile.path, configFile.path]); + checkWatchedFiles(host, [libFile.path, configFile.path, "/a/tsconfig.json"]); // remove the tsconfig file host.reloadFS(filesWithoutConfig); @@ -908,7 +908,7 @@ namespace ts.projectSystem { checkNumberOfInferredProjects(projectService, 2); checkNumberOfConfiguredProjects(projectService, 0); - checkWatchedDirectories(host, ["/a/b", "/a"]); + checkWatchedFiles(host, ["/a/b/tsconfig.json", "/a/tsconfig.json", libFile.path]); }); it("add new files to a configured project without file list", () => { @@ -1315,7 +1315,7 @@ namespace ts.projectSystem { host.reloadFS([file1, configFile, file2, file3, libFile]); - + host.checkTimeoutQueueLengthAndRun(1); checkNumberOfConfiguredProjects(projectService, 1); checkNumberOfInferredProjects(projectService, 1); checkProjectActualFiles(projectService.inferredProjects[0], [file2.path, file3.path, libFile.path]); @@ -1728,6 +1728,7 @@ namespace ts.projectSystem { checkProjectActualFiles(projectService.inferredProjects[1], [file3.path]); host.reloadFS([file1, file2, file3, configFile]); + host.checkTimeoutQueueLengthAndRun(1); checkNumberOfProjects(projectService, { configuredProjects: 1 }); checkProjectActualFiles(projectService.configuredProjects[0], [file1.path, file2.path, file3.path, configFile.path]); }); diff --git a/src/server/editorServices.ts b/src/server/editorServices.ts index 410872e369ec1..698dbe3ffd876 100644 --- a/src/server/editorServices.ts +++ b/src/server/editorServices.ts @@ -245,57 +245,14 @@ namespace ts.server { } } - class DirectoryWatchers { - /** - * a path to directory watcher map that detects added tsconfig files - */ - private readonly directoryWatchersForTsconfig: Map = createMap(); - /** - * count of how many projects are using the directory watcher. - * If the number becomes 0 for a watcher, then we should close it. - */ - private readonly directoryWatchersRefCount: Map = createMap(); - - constructor(private readonly projectService: ProjectService) { - } - - stopWatchingDirectory(directory: string) { - // if the ref count for this directory watcher drops to 0, it's time to close it - const refCount = this.directoryWatchersRefCount.get(directory) - 1; - this.directoryWatchersRefCount.set(directory, refCount); - if (refCount === 0) { - this.projectService.logger.info(`Close directory watcher for: ${directory}`); - this.directoryWatchersForTsconfig.get(directory).close(); - this.directoryWatchersForTsconfig.delete(directory); - } - } - - startWatchingContainingDirectoriesForFile(fileName: string, project: InferredProject, callback: (fileName: string) => void) { - let currentPath = getDirectoryPath(fileName); - let parentPath = getDirectoryPath(currentPath); - while (currentPath !== parentPath) { - if (!this.directoryWatchersForTsconfig.has(currentPath)) { - this.projectService.logger.info(`Add watcher for: ${currentPath}`); - this.directoryWatchersForTsconfig.set(currentPath, this.projectService.host.watchDirectory(currentPath, callback)); - this.directoryWatchersRefCount.set(currentPath, 1); - } - else { - this.directoryWatchersRefCount.set(currentPath, this.directoryWatchersRefCount.get(currentPath) + 1); - } - project.directoriesWatchedForTsconfig.push(currentPath); - currentPath = parentPath; - parentPath = getDirectoryPath(parentPath); - } - } - } - /* @internal */ export const enum WatchType { ConfigFilePath = "Config file for the program", MissingFilePath = "Missing file from program", WildCardDirectories = "Wild card directory", TypeRoot = "Type root of the project", - ClosedScriptInfo = "Closed Script info" + ClosedScriptInfo = "Closed Script info", + ConfigFileForInferredRoot = "Config file for the root script info of the inferred project" } /* @internal */ @@ -308,12 +265,27 @@ namespace ts.server { OrphanScriptInfoWithChange = "Orphan script info, Detected change in file thats not needed any more", OrphanScriptInfo = "Removing Orphan script info as part of cleanup", FileDeleted = "File was deleted", - FileOpened = "File opened" + FileOpened = "File opened", + ConfigProjectCreated = "Config file project created" + } + + const enum ConfigFileWatcherStatus { + ReloadingFiles = "Reloading configured projects files", + NoAction = "No action on files", + UpdatedCallback = "Updated the callback", + TrackingFileAdded = "Tracking file added", + TrackingFileRemoved = "Tracking file removed" } /* @internal */ export type ServerDirectoryWatcherCallback = (path: NormalizedPath) => void; + type ConfigFileExistence = { + exists: boolean; + trackingOpenFiles?: ScriptInfo[]; + configFileWatcher?: FileWatcher; + }; + export interface ProjectServiceOptions { host: ServerHost; logger: Logger; @@ -362,8 +334,7 @@ namespace ts.server { private compilerOptionsForInferredProjects: CompilerOptions; private compileOnSaveForInferredProjects: boolean; private readonly projectToSizeMap: Map = createMap(); - private readonly directoryWatchers: DirectoryWatchers; - private readonly mapOfTsConfigPresence: Map; + private readonly mapOfConfigFilePresence: Map; private readonly throttledOperations: ThrottledOperations; private readonly hostConfiguration: HostConfiguration; @@ -407,8 +378,7 @@ namespace ts.server { this.currentDirectory = this.host.getCurrentDirectory(); this.toCanonicalFileName = createGetCanonicalFileName(this.host.useCaseSensitiveFileNames); - this.directoryWatchers = new DirectoryWatchers(this); - this.mapOfTsConfigPresence = createMap(); + this.mapOfConfigFilePresence = createMap(); this.throttledOperations = new ThrottledOperations(this.host); this.typingsInstaller.attach(this); @@ -519,10 +489,6 @@ namespace ts.server { this.delayUpdateProjectGraphs(this.inferredProjects); } - stopWatchingDirectory(directory: string) { - this.directoryWatchers.stopWatchingDirectory(directory); - } - findProject(projectName: string): Project { if (projectName === undefined) { return undefined; @@ -701,36 +667,53 @@ namespace ts.server { } private onConfigChangedForConfiguredProject(project: ConfiguredProject, eventKind: FileWatcherEventKind) { + const configFilePresenceInfo = this.mapOfConfigFilePresence.get(project.canonicalConfigFilePath); if (eventKind === FileWatcherEventKind.Deleted) { + // Update the cached status + // No action needed on tracking open files since the existing config file anyways didnt affect the tracking file + configFilePresenceInfo.exists = false; + this.logTrackingFiles(project.getConfigFilePath(), configFilePresenceInfo, ConfigFileWatcherStatus.NoAction); this.removeProject(project); + // Reload the configured projects for these open files in the project as // they could be held up by another config file somewhere in the parent directory - const openFilesInProject = filter(this.openFiles, file => file.containingProjects.length === 0); - this.reloadConfiguredProjectForFiles(openFilesInProject, project => { project.pendingReload = true; this.delayUpdateProjectGraph(project); }); - this.delayInferredProjectsRefresh(); + const orphanFiles = filter(this.openFiles, file => file.containingProjects.length === 0); + this.delayReloadConfiguredProjectForFiles(orphanFiles); } else { project.pendingReload = true; - this.delayUpdateProjectGraphAndInferredProjectsRefresh(project); + this.logTrackingFiles(project.getConfigFilePath(), configFilePresenceInfo, ConfigFileWatcherStatus.ReloadingFiles); + if (configFilePresenceInfo.trackingOpenFiles) { + this.delayUpdateProjectGraph(project); + this.delayReloadConfiguredProjectForFiles(configFilePresenceInfo.trackingOpenFiles); + } + else { + this.delayUpdateProjectGraphAndInferredProjectsRefresh(project); + } } } /** - * This is the callback function when a watched directory has an added tsconfig file. + * This is the callback function for the config file add/remove/change for the root in the inferred project */ - private onConfigFileAddedForInferredProject(fileName: string) { - // TODO: check directory separators - if (getBaseFileName(fileName) !== "tsconfig.json") { - this.logger.info(`${fileName} is not tsconfig.json`); - return; - } + private onConfigFileAddedForInferredProject(configFileName: NormalizedPath, eventKind: FileWatcherEventKind) { + // This callback is called only if we dont have config file project for this config file + const cononicalConfigPath = normalizedPathToPath(configFileName, this.currentDirectory, this.toCanonicalFileName); + const configFilePresenceInfo = this.mapOfConfigFilePresence.get(cononicalConfigPath); - // TODO: (sheetalkamat) - // 1. We should only watch tsconfig/jsconfig file here instead of watching directory - // 2. We should try reloading projects with open files in Inferred project only - // 3. We should use this watcher to answer questions to findConfigFile rather than calling host everytime - this.logger.info(`Detected newly added tsconfig file: ${fileName}`); - this.reloadProjects(); + if (eventKind === FileWatcherEventKind.Deleted) { + // No action needed if the event was for deletion of the file + // - because the existing config file didnt affect the inferred project roots anyways + configFilePresenceInfo.exists = false; + this.logTrackingFiles(configFileName, configFilePresenceInfo, ConfigFileWatcherStatus.NoAction); + } + else { + // Either the config file was created or changed + // Reload the projects + configFilePresenceInfo.exists = true; + this.logTrackingFiles(configFileName, configFilePresenceInfo, ConfigFileWatcherStatus.ReloadingFiles); + this.delayReloadConfiguredProjectForFiles(configFilePresenceInfo.trackingOpenFiles); + } } private removeProject(project: Project) { @@ -882,13 +865,131 @@ namespace ts.server { private configFileExists(configFileName: NormalizedPath) { const canonicalConfigFilePath = normalizedPathToPath(configFileName, this.currentDirectory, this.toCanonicalFileName); - const cachedResult = this.mapOfTsConfigPresence.get(canonicalConfigFilePath); - if (cachedResult) { - // Use the information here to answer the question + const configFilePresenceInfo = this.mapOfConfigFilePresence.get(canonicalConfigFilePath); + if (configFilePresenceInfo) { + return configFilePresenceInfo.exists; } return this.host.fileExists(configFileName); } + private setConfigFilePresenceByNewConfiguredProject(project: ConfiguredProject) { + const configFilePresenceInfo = this.mapOfConfigFilePresence.get(project.canonicalConfigFilePath); + if (configFilePresenceInfo) { + configFilePresenceInfo.exists = true; + // close existing watcher + if (configFilePresenceInfo.configFileWatcher) { + const configFileName = project.getConfigFilePath(); + this.closeFileWatcher( + WatchType.ConfigFileForInferredRoot, /*project*/ undefined, configFileName, + configFilePresenceInfo.configFileWatcher, WatcherCloseReason.ConfigProjectCreated + ); + configFilePresenceInfo.configFileWatcher = undefined; + this.logTrackingFiles(configFileName, configFilePresenceInfo, ConfigFileWatcherStatus.UpdatedCallback); + } + } + else { + // Mark existence of the config file with the project creation + this.mapOfConfigFilePresence.set(project.canonicalConfigFilePath, { exists: true }); + } + } + + /* @internal */ + setConfigFilePresenceByClosedConfigFile(closedProject: ConfiguredProject) { + const configFilePresenceInfo = this.mapOfConfigFilePresence.get(closedProject.canonicalConfigFilePath); + Debug.assert(!!configFilePresenceInfo); + if (configFilePresenceInfo.trackingOpenFiles) { + const configFileName = closedProject.getConfigFilePath(); + configFilePresenceInfo.configFileWatcher = this.addFileWatcher( + WatchType.ConfigFileForInferredRoot, /*project*/ undefined, configFileName, + (_filename, eventKind) => this.onConfigFileAddedForInferredProject(configFileName, eventKind) + ); + this.logTrackingFiles(configFileName, configFilePresenceInfo, ConfigFileWatcherStatus.UpdatedCallback); + } + else { + // There is no one tracking anymore. Remove the status + this.mapOfConfigFilePresence.delete(closedProject.canonicalConfigFilePath); + } + } + + private logTrackingFiles(configFileName: NormalizedPath, configFilePresenceInfo: ConfigFileExistence, status: ConfigFileWatcherStatus) { + const watchType = configFilePresenceInfo.configFileWatcher ? WatchType.ConfigFileForInferredRoot : WatchType.ConfigFilePath; + const files = map(configFilePresenceInfo.trackingOpenFiles, info => info.fileName); + this.logger.info(`FileWatcher:: ${watchType}: File: ${configFileName} Currently Tracking for files: ${files} Status: ${status}`); + } + + private watchConfigFileForInferredRoot(configFileName: NormalizedPath, canonicalConfigFilePath: string, root: ScriptInfo) { + let configFilePresenceInfo = this.mapOfConfigFilePresence.get(canonicalConfigFilePath); + if (configFilePresenceInfo) { + // Existing information - just add to tracking files + (configFilePresenceInfo.trackingOpenFiles || (configFilePresenceInfo.trackingOpenFiles = [])).push(root); + } + else { + // Add new callback + configFilePresenceInfo = { + exists: this.host.fileExists(configFileName), + trackingOpenFiles: [root], + configFileWatcher: this.addFileWatcher( + WatchType.ConfigFileForInferredRoot, /*project*/ undefined, configFileName, + (_fileName, eventKind) => this.onConfigFileAddedForInferredProject(configFileName, eventKind) + ) + }; + this.mapOfConfigFilePresence.set(canonicalConfigFilePath, configFilePresenceInfo); + } + this.logTrackingFiles(configFileName, configFilePresenceInfo, ConfigFileWatcherStatus.TrackingFileAdded); + } + + private closeWatchConfigFileForInferredRoot(configFileName: NormalizedPath, canonicalConfigFilePath: string, root: ScriptInfo, reason: WatcherCloseReason) { + const configFilePresenceInfo = this.mapOfConfigFilePresence.get(canonicalConfigFilePath); + Debug.assert(!!configFilePresenceInfo); + if (configFilePresenceInfo.trackingOpenFiles.length === 1) { + configFilePresenceInfo.trackingOpenFiles = undefined; + this.logTrackingFiles(configFileName, configFilePresenceInfo, ConfigFileWatcherStatus.TrackingFileRemoved); + if (configFilePresenceInfo.configFileWatcher) { + this.closeFileWatcher( + WatchType.ConfigFileForInferredRoot, /*project*/ undefined, + configFileName, configFilePresenceInfo.configFileWatcher, reason + ); + this.mapOfConfigFilePresence.delete(canonicalConfigFilePath); + } + } + else { + removeItemFromSet(configFilePresenceInfo.trackingOpenFiles, root); + this.logTrackingFiles(configFileName, configFilePresenceInfo, ConfigFileWatcherStatus.TrackingFileRemoved); + } + } + + private enumerateWatchingRootOfInferredProject(root: ScriptInfo, + action: (configFileName: NormalizedPath, canonicalConfigFilePath: string, root: ScriptInfo) => void) { + let current = root.fileName; + let currentPath = getDirectoryPath(root.path); + let parentPath = getDirectoryPath(currentPath); + while (currentPath !== parentPath) { + current = asNormalizedPath(getDirectoryPath(current)); + action(asNormalizedPath(combinePaths(current, "tsconfig.json")), combinePaths(currentPath, "tsconfig.json"), root); + //if (root.isJavaScript()) { + // this.watchConfigFileForInferredRoot(asNormalizedPath(combinePaths(current, "jsconfig.json")), combinePaths(currentPath, "jsconfig.json"), root); + //} + currentPath = parentPath; + parentPath = getDirectoryPath(parentPath); + } + } + + /*@internal*/ + startWatchingRootOfInferredProject(root: ScriptInfo) { + this.enumerateWatchingRootOfInferredProject(root, + (configFileName, canonicalConfigFilePath, root) => + this.watchConfigFileForInferredRoot(configFileName, canonicalConfigFilePath, root) + ); + } + + /*@internal*/ + stopWatchingRootOfInferredProject(root: ScriptInfo, reason: WatcherCloseReason) { + this.enumerateWatchingRootOfInferredProject(root, + (configFileName, canonicalConfigFilePath, root) => + this.closeWatchConfigFileForInferredRoot(configFileName, canonicalConfigFilePath, root, reason) + ); + } + /** * This function tries to search for a tsconfig.json for the given file. * This is different from the method the compiler uses because @@ -1124,6 +1225,7 @@ namespace ts.server { this.addFilesToNonInferredProjectAndUpdateGraph(project, projectOptions.files, fileNamePropertyReader, clientFileName, projectOptions.typeAcquisition, configFileErrors); this.configuredProjects.push(project); + this.setConfigFilePresenceByNewConfiguredProject(project); this.sendProjectTelemetry(project.getConfigFilePath(), project, projectOptions); return project; } @@ -1172,7 +1274,7 @@ namespace ts.server { if (scriptInfo.isScriptOpen()) { // if file is already root in some inferred project // - remove the file from that project and delete the project if necessary - this.removRootOfInferredProjectIfNowPartOfOtherProject(scriptInfo); + this.removeRootOfInferredProjectIfNowPartOfOtherProject(scriptInfo); } } } @@ -1261,11 +1363,7 @@ namespace ts.server { : new InferredProject(this, this.documentRegistry, this.compilerOptionsForInferredProjects); project.addRoot(root); - - this.directoryWatchers.startWatchingContainingDirectoriesForFile( - root.fileName, - project, - fileName => this.onConfigFileAddedForInferredProject(fileName)); + this.startWatchingRootOfInferredProject(root); project.updateGraph(); if (!useExistingProject) { @@ -1415,11 +1513,22 @@ namespace ts.server { */ reloadProjects() { this.logger.info("reload projects."); - this.reloadConfiguredProjectForFiles(this.openFiles, project => this.reloadConfiguredProject(project)); + this.reloadConfiguredProjectForFiles(this.openFiles, /*delayReload*/ false); this.refreshInferredProjects(); } - reloadConfiguredProjectForFiles(openFiles: ScriptInfo[], reload: (project: ConfiguredProject) => void) { + delayReloadConfiguredProjectForFiles(openFiles: ScriptInfo[]) { + this.reloadConfiguredProjectForFiles(openFiles, /*delayReload*/ true); + this.delayInferredProjectsRefresh(); + } + + /** + * This function goes through all the openFiles and tries to file the config file for them. + * If the config file is found and it refers to existing project, it reloads it either immediately + * or schedules it for reload depending on delayedReload option + * If the there is no existing project it just opens the configured project for the config file + */ + reloadConfiguredProjectForFiles(openFiles: ScriptInfo[], delayReload: boolean) { const mapUpdatedProjects = createMap(); // try to reload config file for all open files for (const info of openFiles) { @@ -1435,7 +1544,13 @@ namespace ts.server { mapUpdatedProjects.set(configFileName, true); } else if (!mapUpdatedProjects.has(configFileName)) { - reload(project); + if (delayReload) { + project.pendingReload = true; + this.delayUpdateProjectGraph(project); + } + else { + this.reloadConfiguredProject(project); + } mapUpdatedProjects.set(configFileName, true); } } @@ -1448,7 +1563,7 @@ namespace ts.server { * - references in inferred project supercede the root part * - root/reference in non-inferred project beats root in inferred project */ - private removRootOfInferredProjectIfNowPartOfOtherProject(info: ScriptInfo) { + private removeRootOfInferredProjectIfNowPartOfOtherProject(info: ScriptInfo) { if (info.containingProjects.length > 1 && info.containingProjects[0].projectKind === ProjectKind.Inferred && info.containingProjects[0].isRoot(info)) { @@ -1478,7 +1593,7 @@ namespace ts.server { } // Or remove the root of inferred project if is referenced in more than one projects else { - this.removRootOfInferredProjectIfNowPartOfOtherProject(info); + this.removeRootOfInferredProjectIfNowPartOfOtherProject(info); } } @@ -1766,7 +1881,8 @@ namespace ts.server { const rootFiles: protocol.ExternalFile[] = []; for (const file of proj.rootFiles) { const normalized = toNormalizedPath(file.fileName); - if (getBaseFileName(normalized) === "tsconfig.json") { + const baseFileName = getBaseFileName(normalized); + if (baseFileName === "tsconfig.json" || baseFileName === "jsconfig.json") { if (this.host.fileExists(normalized)) { (tsConfigFiles || (tsConfigFiles = [])).push(normalized); } diff --git a/src/server/project.ts b/src/server/project.ts index 1b99e76545e25..2e14c75efc5f5 100644 --- a/src/server/project.ts +++ b/src/server/project.ts @@ -648,7 +648,7 @@ namespace ts.server { missingFilePath => { const fileWatcher = this.projectService.addFileWatcher( WatchType.MissingFilePath, this, missingFilePath, - (filename: string, eventKind: FileWatcherEventKind) => { + (filename, eventKind) => { if (eventKind === FileWatcherEventKind.Created && this.missingFilesMap.has(missingFilePath)) { this.missingFilesMap.delete(missingFilePath); this.projectService.closeFileWatcher(WatchType.MissingFilePath, this, missingFilePath, fileWatcher, WatcherCloseReason.FileCreated); @@ -911,9 +911,6 @@ namespace ts.server { super.setCompilerOptions(newOptions); } - // Used to keep track of what directories are watched for this project - directoriesWatchedForTsconfig: string[] = []; - constructor(projectService: ProjectService, documentRegistry: DocumentRegistry, compilerOptions: CompilerOptions) { super(InferredProject.newName(), ProjectKind.Inferred, @@ -927,6 +924,7 @@ namespace ts.server { } addRoot(info: ScriptInfo) { + this.projectService.startWatchingRootOfInferredProject(info); if (!this._isJsInferredProject && info.isJavaScript()) { this.toggleJsInferredProject(/*isJsInferredProject*/ true); } @@ -934,6 +932,7 @@ namespace ts.server { } removeRoot(info: ScriptInfo) { + this.projectService.stopWatchingRootOfInferredProject(info, WatcherCloseReason.NotNeeded); super.removeRoot(info); if (this._isJsInferredProject && info.isJavaScript()) { if (!some(this.getRootScriptInfos(), info => info.isJavaScript())) { @@ -958,11 +957,8 @@ namespace ts.server { } close() { + forEach(this.getRootScriptInfos(), root => this.projectService.stopWatchingRootOfInferredProject(root, WatcherCloseReason.ProjectClose)); super.close(); - - for (const directory of this.directoriesWatchedForTsconfig) { - this.projectService.stopWatchingDirectory(directory); - } } getTypeAcquisition(): TypeAcquisition { @@ -1212,6 +1208,7 @@ namespace ts.server { if (this.configFileWatcher) { this.projectService.closeFileWatcher(WatchType.ConfigFilePath, this, this.getConfigFilePath(), this.configFileWatcher, WatcherCloseReason.ProjectClose); this.configFileWatcher = undefined; + this.projectService.setConfigFilePresenceByClosedConfigFile(this); } this.stopWatchingTypeRoots(WatcherCloseReason.ProjectClose); From 62663a10ba4c2f902d22d2f8a82f2d2df3078510 Mon Sep 17 00:00:00 2001 From: Sheetal Nandi Date: Sat, 15 Jul 2017 20:03:55 -0700 Subject: [PATCH 025/109] Use map for configured project instead of the array --- src/harness/unittests/projectErrors.ts | 10 +- .../unittests/tsserverProjectSystem.ts | 113 ++++++++++-------- src/harness/unittests/typingsInstaller.ts | 10 +- src/server/editorServices.ts | 16 +-- src/server/session.ts | 2 +- 5 files changed, 78 insertions(+), 73 deletions(-) diff --git a/src/harness/unittests/projectErrors.ts b/src/harness/unittests/projectErrors.ts index 3d093300cbcf5..0143aee770143 100644 --- a/src/harness/unittests/projectErrors.ts +++ b/src/harness/unittests/projectErrors.ts @@ -98,7 +98,7 @@ namespace ts.projectSystem { const projectService = session.getProjectService(); openFilesForSession([file1], session); checkNumberOfProjects(projectService, { configuredProjects: 1 }); - const project = projectService.configuredProjects[0]; + const project = configuredProjectAt(projectService, 0); const compilerOptionsRequest = { type: "request", command: server.CommandNames.CompilerOptionsDiagnosticsFull, @@ -141,7 +141,7 @@ namespace ts.projectSystem { const configuredProject = forEach(projectService.synchronizeProjectList([]), f => f.info.projectName === corruptedConfig.path && f); assert.isTrue(configuredProject !== undefined, "should find configured project"); checkProjectErrors(configuredProject, []); - const projectErrors = projectService.configuredProjects[0].getAllProjectErrors(); + const projectErrors = configuredProjectAt(projectService, 0).getAllProjectErrors(); checkProjectErrorsWorker(projectErrors, [ "'{' expected." ]); @@ -155,7 +155,7 @@ namespace ts.projectSystem { const configuredProject = forEach(projectService.synchronizeProjectList([]), f => f.info.projectName === corruptedConfig.path && f); assert.isTrue(configuredProject !== undefined, "should find configured project"); checkProjectErrors(configuredProject, []); - const projectErrors = projectService.configuredProjects[0].getAllProjectErrors(); + const projectErrors = configuredProjectAt(projectService, 0).getAllProjectErrors(); checkProjectErrorsWorker(projectErrors, []); } }); @@ -186,7 +186,7 @@ namespace ts.projectSystem { const configuredProject = forEach(projectService.synchronizeProjectList([]), f => f.info.projectName === corruptedConfig.path && f); assert.isTrue(configuredProject !== undefined, "should find configured project"); checkProjectErrors(configuredProject, []); - const projectErrors = projectService.configuredProjects[0].getAllProjectErrors(); + const projectErrors = configuredProjectAt(projectService, 0).getAllProjectErrors(); checkProjectErrorsWorker(projectErrors, []); } // break config and trigger watcher @@ -196,7 +196,7 @@ namespace ts.projectSystem { const configuredProject = forEach(projectService.synchronizeProjectList([]), f => f.info.projectName === corruptedConfig.path && f); assert.isTrue(configuredProject !== undefined, "should find configured project"); checkProjectErrors(configuredProject, []); - const projectErrors = projectService.configuredProjects[0].getAllProjectErrors(); + const projectErrors = configuredProjectAt(projectService, 0).getAllProjectErrors(); checkProjectErrorsWorker(projectErrors, [ "'{' expected." ]); diff --git a/src/harness/unittests/tsserverProjectSystem.ts b/src/harness/unittests/tsserverProjectSystem.ts index 22bf06bef6515..e26df82731482 100644 --- a/src/harness/unittests/tsserverProjectSystem.ts +++ b/src/harness/unittests/tsserverProjectSystem.ts @@ -295,7 +295,7 @@ namespace ts.projectSystem { } export function checkNumberOfConfiguredProjects(projectService: server.ProjectService, expected: number) { - assert.equal(projectService.configuredProjects.length, expected, `expected ${expected} configured project(s)`); + assert.equal(projectService.configuredProjects.size, expected, `expected ${expected} configured project(s)`); } export function checkNumberOfExternalProjects(projectService: server.ProjectService, expected: number) { @@ -312,6 +312,15 @@ namespace ts.projectSystem { checkNumberOfInferredProjects(projectService, count.inferredProjects || 0); } + export function configuredProjectAt(projectService: server.ProjectService, index: number) { + const values = projectService.configuredProjects.values(); + while (index > 0) { + values.next(); + index--; + } + return values.next().value; + } + export function checkWatchedFiles(host: TestServerHost, expectedFiles: string[]) { checkMapKeys("watchedFiles", host.watchedFiles, expectedFiles); } @@ -824,12 +833,12 @@ namespace ts.projectSystem { options: {} }); service.checkNumberOfProjects({ configuredProjects: 1 }); - checkProjectActualFiles(service.configuredProjects[0], [upperCaseConfigFilePath]); + checkProjectActualFiles(configuredProjectAt(service, 0), [upperCaseConfigFilePath]); service.openClientFile(f1.path); service.checkNumberOfProjects({ configuredProjects: 1, inferredProjects: 1 }); - checkProjectActualFiles(service.configuredProjects[0], [upperCaseConfigFilePath]); + checkProjectActualFiles(configuredProjectAt(service, 0), [upperCaseConfigFilePath]); checkProjectActualFiles(service.inferredProjects[0], [f1.path]); }); @@ -866,7 +875,7 @@ namespace ts.projectSystem { checkNumberOfInferredProjects(projectService, 0); checkNumberOfConfiguredProjects(projectService, 1); - const project = projectService.configuredProjects[0]; + const project = configuredProjectAt(projectService, 0); checkProjectActualFiles(project, [file1.path, libFile.path, file2.path, configFile.path]); checkProjectRootFiles(project, [file1.path, file2.path]); // watching all files except one that was open @@ -922,7 +931,7 @@ namespace ts.projectSystem { checkWatchedDirectories(host, ["/a/b"], /*recursive*/ true); checkNumberOfConfiguredProjects(projectService, 1); - const project = projectService.configuredProjects[0]; + const project = configuredProjectAt(projectService, 0); checkProjectRootFiles(project, [commonFile1.path]); // add a new ts file @@ -949,7 +958,7 @@ namespace ts.projectSystem { projectService.openClientFile(commonFile2.path); checkNumberOfConfiguredProjects(projectService, 1); - const project = projectService.configuredProjects[0]; + const project = configuredProjectAt(projectService, 0); checkProjectRootFiles(project, [commonFile1.path]); checkNumberOfInferredProjects(projectService, 1); }); @@ -1024,7 +1033,7 @@ namespace ts.projectSystem { projectService.openClientFile(commonFile1.path); checkNumberOfConfiguredProjects(projectService, 1); - const project = projectService.configuredProjects[0]; + const project = configuredProjectAt(projectService, 0); checkProjectRootFiles(project, [commonFile1.path, commonFile2.path]); // delete commonFile2 @@ -1085,7 +1094,7 @@ namespace ts.projectSystem { const projectService = createProjectService(host); projectService.openClientFile(commonFile1.path); - const project = projectService.configuredProjects[0]; + const project = configuredProjectAt(projectService, 0); checkProjectRootFiles(project, [commonFile1.path, commonFile2.path]); configFile.content = `{ "compilerOptions": {}, @@ -1121,7 +1130,7 @@ namespace ts.projectSystem { projectService.openClientFile(commonFile1.path); checkNumberOfConfiguredProjects(projectService, 1); - const project = projectService.configuredProjects[0]; + const project = configuredProjectAt(projectService, 0); checkProjectRootFiles(project, [commonFile1.path, commonFile2.path]); projectService.openClientFile(excludedFile1.path); checkNumberOfInferredProjects(projectService, 1); @@ -1157,7 +1166,7 @@ namespace ts.projectSystem { projectService.openClientFile(classicModuleFile.path); checkNumberOfConfiguredProjects(projectService, 1); - const project = projectService.configuredProjects[0]; + const project = configuredProjectAt(projectService, 0); checkProjectActualFiles(project, [file1.path, nodeModuleFile.path, configFile.path]); checkNumberOfInferredProjects(projectService, 1); @@ -1241,7 +1250,7 @@ namespace ts.projectSystem { const projectService = createProjectService(host); projectService.openClientFile(commonFile1.path); checkNumberOfConfiguredProjects(projectService, 1); - checkProjectRootFiles(projectService.configuredProjects[0], [commonFile1.path, commonFile2.path]); + checkProjectRootFiles(configuredProjectAt(projectService, 0), [commonFile1.path, commonFile2.path]); }); it("should disable features when the files are too large", () => { @@ -1730,7 +1739,7 @@ namespace ts.projectSystem { host.reloadFS([file1, file2, file3, configFile]); host.checkTimeoutQueueLengthAndRun(1); checkNumberOfProjects(projectService, { configuredProjects: 1 }); - checkProjectActualFiles(projectService.configuredProjects[0], [file1.path, file2.path, file3.path, configFile.path]); + checkProjectActualFiles(configuredProjectAt(projectService, 0), [file1.path, file2.path, file3.path, configFile.path]); }); it("correctly migrate files between projects", () => { @@ -1788,14 +1797,14 @@ namespace ts.projectSystem { projectService.openClientFile(file1.path); checkNumberOfProjects(projectService, { configuredProjects: 1 }); - checkProjectActualFiles(projectService.configuredProjects[0], [file1.path, configFile.path]); + checkProjectActualFiles(configuredProjectAt(projectService, 0), [file1.path, configFile.path]); host.reloadFS([file1, file2, configFile]); host.checkTimeoutQueueLengthAndRun(2); checkNumberOfProjects(projectService, { configuredProjects: 1 }); - checkProjectRootFiles(projectService.configuredProjects[0], [file1.path, file2.path]); + checkProjectRootFiles(configuredProjectAt(projectService, 0), [file1.path, file2.path]); }); it("can correctly update configured project when set of root files has changed (new file in list of files)", () => { @@ -1817,7 +1826,7 @@ namespace ts.projectSystem { projectService.openClientFile(file1.path); checkNumberOfProjects(projectService, { configuredProjects: 1 }); - checkProjectActualFiles(projectService.configuredProjects[0], [file1.path, configFile.path]); + checkProjectActualFiles(configuredProjectAt(projectService, 0), [file1.path, configFile.path]); const modifiedConfigFile = { path: configFile.path, @@ -1828,7 +1837,7 @@ namespace ts.projectSystem { checkNumberOfProjects(projectService, { configuredProjects: 1 }); host.checkTimeoutQueueLengthAndRun(2); - checkProjectRootFiles(projectService.configuredProjects[0], [file1.path, file2.path]); + checkProjectRootFiles(configuredProjectAt(projectService, 0), [file1.path, file2.path]); }); it("can update configured project when set of root files was not changed", () => { @@ -1850,7 +1859,7 @@ namespace ts.projectSystem { projectService.openClientFile(file1.path); checkNumberOfProjects(projectService, { configuredProjects: 1 }); - checkProjectActualFiles(projectService.configuredProjects[0], [file1.path, file2.path, configFile.path]); + checkProjectActualFiles(configuredProjectAt(projectService, 0), [file1.path, file2.path, configFile.path]); const modifiedConfigFile = { path: configFile.path, @@ -1860,7 +1869,7 @@ namespace ts.projectSystem { host.reloadFS([file1, file2, modifiedConfigFile]); checkNumberOfProjects(projectService, { configuredProjects: 1 }); - checkProjectRootFiles(projectService.configuredProjects[0], [file1.path, file2.path]); + checkProjectRootFiles(configuredProjectAt(projectService, 0), [file1.path, file2.path]); }); it("can correctly update external project when set of root files has changed", () => { @@ -1930,11 +1939,11 @@ namespace ts.projectSystem { projectService.openClientFile(file1.path); checkNumberOfProjects(projectService, { configuredProjects: 1 }); - checkProjectActualFiles(projectService.configuredProjects[0], [file1.path, file2.path, config.path]); + checkProjectActualFiles(configuredProjectAt(projectService, 0), [file1.path, file2.path, config.path]); projectService.openClientFile(file2.path); checkNumberOfProjects(projectService, { configuredProjects: 1 }); - checkProjectActualFiles(projectService.configuredProjects[0], [file1.path, file2.path, config.path]); + checkProjectActualFiles(configuredProjectAt(projectService, 0), [file1.path, file2.path, config.path]); host.reloadFS([file1, file2]); host.checkTimeoutQueueLengthAndRun(1); @@ -1968,13 +1977,13 @@ namespace ts.projectSystem { }); projectService.openClientFile(f1.path); projectService.checkNumberOfProjects({ configuredProjects: 1 }); - checkProjectActualFiles(projectService.configuredProjects[0], [f1.path, config.path]); + checkProjectActualFiles(configuredProjectAt(projectService, 0), [f1.path, config.path]); projectService.closeClientFile(f1.path); projectService.openClientFile(f2.path); projectService.checkNumberOfProjects({ configuredProjects: 1, inferredProjects: 1 }); - checkProjectActualFiles(projectService.configuredProjects[0], [f1.path, config.path]); + checkProjectActualFiles(configuredProjectAt(projectService, 0), [f1.path, config.path]); checkProjectActualFiles(projectService.inferredProjects[0], [f2.path]); }); @@ -1998,7 +2007,7 @@ namespace ts.projectSystem { // HTML file will not be included in any projects yet checkNumberOfProjects(projectService, { configuredProjects: 1 }); - const configuredProj = projectService.configuredProjects[0]; + const configuredProj = configuredProjectAt(projectService, 0); checkProjectActualFiles(configuredProj, [file1.path, config.path]); // Specify .html extension as mixed content @@ -2008,8 +2017,8 @@ namespace ts.projectSystem { // The configured project should now be updated to include html file checkNumberOfProjects(projectService, { configuredProjects: 1 }); - assert.strictEqual(projectService.configuredProjects[0], configuredProj, "Same configured project should be updated"); - checkProjectActualFiles(projectService.configuredProjects[0], [file1.path, file2.path, config.path]); + assert.strictEqual(configuredProjectAt(projectService, 0), configuredProj, "Same configured project should be updated"); + checkProjectActualFiles(configuredProjectAt(projectService, 0), [file1.path, file2.path, config.path]); // Open HTML file projectService.applyChangesInOpenFiles( @@ -2019,10 +2028,10 @@ namespace ts.projectSystem { // Now HTML file is included in the project checkNumberOfProjects(projectService, { configuredProjects: 1 }); - checkProjectActualFiles(projectService.configuredProjects[0], [file1.path, file2.path, config.path]); + checkProjectActualFiles(configuredProjectAt(projectService, 0), [file1.path, file2.path, config.path]); // Check identifiers defined in HTML content are available in .ts file - const project = projectService.configuredProjects[0]; + const project = configuredProjectAt(projectService, 0); let completions = project.getLanguageService().getCompletionsAtPosition(file1.path, 1); assert(completions && completions.entries[0].name === "hello", `expected entry hello to be in completion list`); @@ -2034,7 +2043,7 @@ namespace ts.projectSystem { // HTML file is still included in project checkNumberOfProjects(projectService, { configuredProjects: 1 }); - checkProjectActualFiles(projectService.configuredProjects[0], [file1.path, file2.path, config.path]); + checkProjectActualFiles(configuredProjectAt(projectService, 0), [file1.path, file2.path, config.path]); // Check identifiers defined in HTML content are not available in .ts file completions = project.getLanguageService().getCompletionsAtPosition(file1.path, 5); @@ -2070,7 +2079,7 @@ namespace ts.projectSystem { checkNumberOfProjects(projectService, { configuredProjects: 1 }); - let diagnostics = projectService.configuredProjects[0].getLanguageService().getCompilerOptionsDiagnostics(); + let diagnostics = configuredProjectAt(projectService, 0).getLanguageService().getCompilerOptionsDiagnostics(); assert.deepEqual(diagnostics, []); // #2. Ensure no errors when allowJs is false @@ -2089,7 +2098,7 @@ namespace ts.projectSystem { checkNumberOfProjects(projectService, { configuredProjects: 1 }); - diagnostics = projectService.configuredProjects[0].getLanguageService().getCompilerOptionsDiagnostics(); + diagnostics = configuredProjectAt(projectService, 0).getLanguageService().getCompilerOptionsDiagnostics(); assert.deepEqual(diagnostics, []); // #3. Ensure no errors when compiler options aren't specified @@ -2108,7 +2117,7 @@ namespace ts.projectSystem { checkNumberOfProjects(projectService, { configuredProjects: 1 }); - diagnostics = projectService.configuredProjects[0].getLanguageService().getCompilerOptionsDiagnostics(); + diagnostics = configuredProjectAt(projectService, 0).getLanguageService().getCompilerOptionsDiagnostics(); assert.deepEqual(diagnostics, []); // #4. Ensure no errors when files are explicitly specified in tsconfig @@ -2127,7 +2136,7 @@ namespace ts.projectSystem { checkNumberOfProjects(projectService, { configuredProjects: 1 }); - diagnostics = projectService.configuredProjects[0].getLanguageService().getCompilerOptionsDiagnostics(); + diagnostics = configuredProjectAt(projectService, 0).getLanguageService().getCompilerOptionsDiagnostics(); assert.deepEqual(diagnostics, []); // #4. Ensure no errors when files are explicitly excluded in tsconfig @@ -2146,7 +2155,7 @@ namespace ts.projectSystem { checkNumberOfProjects(projectService, { configuredProjects: 1 }); - diagnostics = projectService.configuredProjects[0].getLanguageService().getCompilerOptionsDiagnostics(); + diagnostics = configuredProjectAt(projectService, 0).getLanguageService().getCompilerOptionsDiagnostics(); assert.deepEqual(diagnostics, []); }); @@ -2273,7 +2282,7 @@ namespace ts.projectSystem { projectService.openClientFile(file2.path); checkNumberOfProjects(projectService, { configuredProjects: 1 }); - const project1 = projectService.configuredProjects[0]; + const project1 = configuredProjectAt(projectService, 0); assert.equal(project1.openRefCount, 1, "Open ref count in project1 - 1"); assert.equal(project1.getScriptInfo(file2.path).containingProjects.length, 1, "containing projects count"); @@ -2281,7 +2290,7 @@ namespace ts.projectSystem { checkNumberOfProjects(projectService, { configuredProjects: 2 }); assert.equal(project1.openRefCount, 2, "Open ref count in project1 - 2"); - const project2 = projectService.configuredProjects[1]; + const project2 = configuredProjectAt(projectService, 1); assert.equal(project2.openRefCount, 1, "Open ref count in project2 - 2"); assert.equal(project1.getScriptInfo(file1.path).containingProjects.length, 2, `${file1.path} containing projects count`); @@ -2413,7 +2422,7 @@ namespace ts.projectSystem { }); const projectService = session.getProjectService(); checkNumberOfProjects(projectService, { configuredProjects: 1 }); - const project = projectService.configuredProjects[0]; + const project = configuredProjectAt(projectService, 0); assert.isFalse(project.languageServiceEnabled, "Language service enabled"); assert.isTrue(!!lastEvent, "should receive event"); assert.equal(lastEvent.data.project, project, "project name"); @@ -2462,7 +2471,7 @@ namespace ts.projectSystem { const projectService = session.getProjectService(); checkNumberOfProjects(projectService, { configuredProjects: 1 }); - const project = projectService.configuredProjects[0]; + const project = configuredProjectAt(projectService, 0); assert.isFalse(project.languageServiceEnabled, "Language service enabled"); assert.isTrue(!!lastEvent, "should receive event"); assert.equal(lastEvent.data.project, project, "project name"); @@ -2650,7 +2659,7 @@ namespace ts.projectSystem { options: {} }); projectService.checkNumberOfProjects({ configuredProjects: 1 }); - checkProjectActualFiles(projectService.configuredProjects[0], [f1.path, tsconfig.path]); + checkProjectActualFiles(configuredProjectAt(projectService, 0), [f1.path, tsconfig.path]); // rename tsconfig.json back to lib.ts host.reloadFS([f1, f2]); @@ -2707,8 +2716,8 @@ namespace ts.projectSystem { options: {} }); projectService.checkNumberOfProjects({ configuredProjects: 2 }); - checkProjectActualFiles(projectService.configuredProjects[0], [cLib.path, cTsconfig.path]); - checkProjectActualFiles(projectService.configuredProjects[1], [dLib.path, dTsconfig.path]); + checkProjectActualFiles(configuredProjectAt(projectService, 0), [cLib.path, cTsconfig.path]); + checkProjectActualFiles(configuredProjectAt(projectService, 1), [dLib.path, dTsconfig.path]); // remove one config file projectService.openExternalProject({ @@ -2718,7 +2727,7 @@ namespace ts.projectSystem { }); projectService.checkNumberOfProjects({ configuredProjects: 1 }); - checkProjectActualFiles(projectService.configuredProjects[0], [dLib.path, dTsconfig.path]); + checkProjectActualFiles(configuredProjectAt(projectService, 0), [dLib.path, dTsconfig.path]); // remove second config file projectService.openExternalProject({ @@ -2738,8 +2747,8 @@ namespace ts.projectSystem { options: {} }); projectService.checkNumberOfProjects({ configuredProjects: 2 }); - checkProjectActualFiles(projectService.configuredProjects[0], [cLib.path, cTsconfig.path]); - checkProjectActualFiles(projectService.configuredProjects[1], [dLib.path, dTsconfig.path]); + checkProjectActualFiles(configuredProjectAt(projectService, 0), [cLib.path, cTsconfig.path]); + checkProjectActualFiles(configuredProjectAt(projectService, 1), [dLib.path, dTsconfig.path]); // close all projects - no projects should be opened projectService.closeExternalProject(projectName); @@ -2795,13 +2804,13 @@ namespace ts.projectSystem { projectService.openClientFile(app.path); projectService.checkNumberOfProjects({ configuredProjects: 1 }); - checkProjectActualFiles(projectService.configuredProjects[0], [libES5.path, app.path, config1.path]); + checkProjectActualFiles(configuredProjectAt(projectService, 0), [libES5.path, app.path, config1.path]); host.reloadFS([libES5, libES2015Promise, app, config2]); host.checkTimeoutQueueLengthAndRun(2); projectService.checkNumberOfProjects({ configuredProjects: 1 }); - checkProjectActualFiles(projectService.configuredProjects[0], [libES5.path, libES2015Promise.path, app.path, config2.path]); + checkProjectActualFiles(configuredProjectAt(projectService, 0), [libES5.path, libES2015Promise.path, app.path, config2.path]); }); it("should handle non-existing directories in config file", () => { @@ -2856,7 +2865,7 @@ namespace ts.projectSystem { projectService.openClientFile(f1.path); projectService.checkNumberOfProjects({ configuredProjects: 1 }); - checkProjectActualFiles(projectService.configuredProjects[0], [f1.path, barTypings.path, config.path]); + checkProjectActualFiles(configuredProjectAt(projectService, 0), [f1.path, barTypings.path, config.path]); }); }); @@ -2927,7 +2936,7 @@ namespace ts.projectSystem { projectService.openClientFile(f1.path); projectService.checkNumberOfProjects({ configuredProjects: 1 }); - checkProjectActualFiles(projectService.configuredProjects[0], [f1.path, t1.path, tsconfig.path]); + checkProjectActualFiles(configuredProjectAt(projectService, 0), [f1.path, t1.path, tsconfig.path]); // delete t1 host.reloadFS([f1, tsconfig]); @@ -2935,7 +2944,7 @@ namespace ts.projectSystem { host.runQueuedTimeoutCallbacks(); projectService.checkNumberOfProjects({ configuredProjects: 1 }); - checkProjectActualFiles(projectService.configuredProjects[0], [f1.path, tsconfig.path]); + checkProjectActualFiles(configuredProjectAt(projectService, 0), [f1.path, tsconfig.path]); // create t2 host.reloadFS([f1, tsconfig, t2]); @@ -2943,7 +2952,7 @@ namespace ts.projectSystem { host.runQueuedTimeoutCallbacks(); projectService.checkNumberOfProjects({ configuredProjects: 1 }); - checkProjectActualFiles(projectService.configuredProjects[0], [f1.path, t2.path, tsconfig.path]); + checkProjectActualFiles(configuredProjectAt(projectService, 0), [f1.path, t2.path, tsconfig.path]); }); }); @@ -3120,7 +3129,7 @@ namespace ts.projectSystem { const projectService = createProjectService(host); projectService.openClientFile(f1.path); projectService.checkNumberOfProjects({ configuredProjects: 1 }); - checkProjectActualFiles(projectService.configuredProjects[0], [f1.path, node.path, config.path]); + checkProjectActualFiles(configuredProjectAt(projectService, 0), [f1.path, node.path, config.path]); }); }); @@ -3496,7 +3505,7 @@ namespace ts.projectSystem { checkNumberOfConfiguredProjects(projectService, 1); checkNumberOfInferredProjects(projectService, 1); - const configuredProject = projectService.configuredProjects[0]; + const configuredProject = configuredProjectAt(projectService, 0); checkProjectActualFiles(configuredProject, [configFile.path]); const inferredProject = projectService.inferredProjects[0]; @@ -4157,7 +4166,7 @@ namespace ts.projectSystem { const projectService = session.getProjectService(); checkNumberOfProjects(projectService, { configuredProjects: 1 }); - const projectName = projectService.configuredProjects[0].getProjectName(); + const projectName = configuredProjectAt(projectService, 0).getProjectName(); const diags = session.executeCommand({ type: "request", diff --git a/src/harness/unittests/typingsInstaller.ts b/src/harness/unittests/typingsInstaller.ts index 287b0fe483d91..c9617625cc739 100644 --- a/src/harness/unittests/typingsInstaller.ts +++ b/src/harness/unittests/typingsInstaller.ts @@ -92,7 +92,7 @@ namespace ts.projectSystem { const service = createProjectService(host, { typingsInstaller: installer }); service.openClientFile(f1.path); service.checkNumberOfProjects({ configuredProjects: 1 }); - checkProjectActualFiles(service.configuredProjects[0], [f1.path, f2.path, config.path]); + checkProjectActualFiles(configuredProjectAt(service, 0), [f1.path, f2.path, config.path]); installer.installAll(0); }); }); @@ -144,7 +144,7 @@ namespace ts.projectSystem { projectService.openClientFile(file1.path); checkNumberOfProjects(projectService, { configuredProjects: 1 }); - const p = projectService.configuredProjects[0]; + const p = configuredProjectAt(projectService, 0); checkProjectActualFiles(p, [file1.path, tsconfig.path]); installer.installAll(/*expectedCount*/ 1); @@ -706,7 +706,7 @@ namespace ts.projectSystem { projectService.openClientFile(app.path); checkNumberOfProjects(projectService, { configuredProjects: 1 }); - const p = projectService.configuredProjects[0]; + const p = configuredProjectAt(projectService, 0); checkProjectActualFiles(p, [app.path, jsconfig.path]); installer.installAll(/*expectedCount*/ 1); @@ -753,7 +753,7 @@ namespace ts.projectSystem { projectService.openClientFile(app.path); checkNumberOfProjects(projectService, { configuredProjects: 1 }); - const p = projectService.configuredProjects[0]; + const p = configuredProjectAt(projectService, 0); checkProjectActualFiles(p, [app.path, jsconfig.path]); checkWatchedFiles(host, [jsconfig.path, "/bower_components", "/node_modules", libFile.path]); @@ -801,7 +801,7 @@ namespace ts.projectSystem { projectService.openClientFile(app.path); checkNumberOfProjects(projectService, { configuredProjects: 1 }); - const p = projectService.configuredProjects[0]; + const p = configuredProjectAt(projectService, 0); checkProjectActualFiles(p, [app.path, jsconfig.path]); installer.installAll(/*expectedCount*/ 1); diff --git a/src/server/editorServices.ts b/src/server/editorServices.ts index 698dbe3ffd876..c2a79913928ab 100644 --- a/src/server/editorServices.ts +++ b/src/server/editorServices.ts @@ -325,7 +325,7 @@ namespace ts.server { /** * projects specified by a tsconfig.json file */ - readonly configuredProjects: ConfiguredProject[] = []; + readonly configuredProjects = createMap(); /** * list of open files */ @@ -730,7 +730,7 @@ namespace ts.server { break; case ProjectKind.Configured: // Update the map of mapOfKnownTsConfigFiles - removeItemFromSet(this.configuredProjects, project); + this.configuredProjects.delete((project).canonicalConfigFilePath); this.projectToSizeMap.delete((project as ConfiguredProject).canonicalConfigFilePath); break; case ProjectKind.Inferred: @@ -1035,7 +1035,7 @@ namespace ts.server { let counter = 0; counter = printProjects(this.logger, this.externalProjects, counter); - counter = printProjects(this.logger, this.configuredProjects, counter); + counter = printProjects(this.logger, arrayFrom(this.configuredProjects.values()), counter); counter = printProjects(this.logger, this.inferredProjects, counter); this.logger.info("Open files: "); @@ -1060,11 +1060,7 @@ namespace ts.server { private findConfiguredProjectByProjectName(configFileName: NormalizedPath) { // make sure that casing of config file name is consistent const canonicalConfigFilePath = asNormalizedPath(this.toCanonicalFileName(configFileName)); - for (const proj of this.configuredProjects) { - if (proj.canonicalConfigFilePath === canonicalConfigFilePath) { - return proj; - } - } + return this.configuredProjects.get(canonicalConfigFilePath); } private findExternalProjectByProjectName(projectFileName: string) { @@ -1224,7 +1220,7 @@ namespace ts.server { } this.addFilesToNonInferredProjectAndUpdateGraph(project, projectOptions.files, fileNamePropertyReader, clientFileName, projectOptions.typeAcquisition, configFileErrors); - this.configuredProjects.push(project); + this.configuredProjects.set(project.canonicalConfigFilePath, project); this.setConfigFilePresenceByNewConfiguredProject(project); this.sendProjectTelemetry(project.getConfigFilePath(), project, projectOptions); return project; @@ -1678,7 +1674,7 @@ namespace ts.server { synchronizeProjectList(knownProjects: protocol.ProjectVersionInfo[]): ProjectFilesWithTSDiagnostics[] { const files: ProjectFilesWithTSDiagnostics[] = []; this.collectChanges(knownProjects, this.externalProjects, files); - this.collectChanges(knownProjects, this.configuredProjects, files); + this.collectChanges(knownProjects, arrayFrom(this.configuredProjects.values()), files); this.collectChanges(knownProjects, this.inferredProjects, files); return files; } diff --git a/src/server/session.ts b/src/server/session.ts index 7f1f571c62985..02784145d4cf1 100644 --- a/src/server/session.ts +++ b/src/server/session.ts @@ -498,7 +498,7 @@ namespace ts.server { private cleanup() { this.cleanProjects("inferred projects", this.projectService.inferredProjects); - this.cleanProjects("configured projects", this.projectService.configuredProjects); + this.cleanProjects("configured projects", arrayFrom(this.projectService.configuredProjects.values())); this.cleanProjects("external projects", this.projectService.externalProjects); if (this.host.gc) { this.logger.info(`host.gc()`); From dcbd7b10f19037dc907774a8fc0877e316a6cd3c Mon Sep 17 00:00:00 2001 From: Sheetal Nandi Date: Sat, 15 Jul 2017 22:26:10 -0700 Subject: [PATCH 026/109] Combine the logic to find config file as well as the watch. This should speed up the file open scenarios where the file belongs to same configured project as we would use cache to answer those fileExists answers --- .../unittests/tsserverProjectSystem.ts | 16 +- src/harness/unittests/typingsInstaller.ts | 2 +- src/server/editorServices.ts | 196 ++++++++---------- src/server/project.ts | 4 - 4 files changed, 99 insertions(+), 119 deletions(-) diff --git a/src/harness/unittests/tsserverProjectSystem.ts b/src/harness/unittests/tsserverProjectSystem.ts index e26df82731482..e662e7b792c5c 100644 --- a/src/harness/unittests/tsserverProjectSystem.ts +++ b/src/harness/unittests/tsserverProjectSystem.ts @@ -809,7 +809,9 @@ namespace ts.projectSystem { const project = projectService.inferredProjects[0]; checkFileNames("inferred project", project.getFileNames(), [appFile.path, libFile.path, moduleFile.path]); - checkWatchedFiles(host, ["/a/b/c/tsconfig.json", "/a/b/tsconfig.json", "/a/tsconfig.json", libFile.path, moduleFile.path]); + const configFileLocations = ["/a/b/c/", "/a/b/", "/a/", "/"]; + const configFiles = flatMap(configFileLocations, location => [location + "tsconfig.json", location + "jsconfig.json"]); + checkWatchedFiles(host, configFiles.concat(libFile.path, moduleFile.path)); }); it("can handle tsconfig file name with difference casing", () => { @@ -879,7 +881,8 @@ namespace ts.projectSystem { checkProjectActualFiles(project, [file1.path, libFile.path, file2.path, configFile.path]); checkProjectRootFiles(project, [file1.path, file2.path]); // watching all files except one that was open - checkWatchedFiles(host, [configFile.path, file2.path, libFile.path]); + // And also tsconfig files for the open files + checkWatchedFiles(host, [configFile.path, file2.path, libFile.path, "/a/b/c/tsconfig.json", "/a/b/c/jsconfig.json"]); checkWatchedDirectories(host, [getDirectoryPath(configFile.path)], /*recursive*/ true); }); @@ -899,15 +902,16 @@ namespace ts.projectSystem { projectService.openClientFile(commonFile2.path); checkNumberOfInferredProjects(projectService, 2); - checkWatchedFiles(host, [configFile.path, "/a/tsconfig.json", libFile.path]); + const configFileLocations = ["/", "/a/", "/a/b/"]; + const watchedFiles = flatMap(configFileLocations, location => [location + "tsconfig.json", location + "jsconfig.json"]).concat(libFile.path); + checkWatchedFiles(host, watchedFiles); // Add a tsconfig file host.reloadFS(filesWithConfig); host.checkTimeoutQueueLengthAndRun(1); checkNumberOfInferredProjects(projectService, 1); checkNumberOfConfiguredProjects(projectService, 1); - // watching all files except one that was open - checkWatchedFiles(host, [libFile.path, configFile.path, "/a/tsconfig.json"]); + checkWatchedFiles(host, watchedFiles); // remove the tsconfig file host.reloadFS(filesWithoutConfig); @@ -917,7 +921,7 @@ namespace ts.projectSystem { checkNumberOfInferredProjects(projectService, 2); checkNumberOfConfiguredProjects(projectService, 0); - checkWatchedFiles(host, ["/a/b/tsconfig.json", "/a/tsconfig.json", libFile.path]); + checkWatchedFiles(host, watchedFiles); }); it("add new files to a configured project without file list", () => { diff --git a/src/harness/unittests/typingsInstaller.ts b/src/harness/unittests/typingsInstaller.ts index c9617625cc739..7fab4ffdcc4c6 100644 --- a/src/harness/unittests/typingsInstaller.ts +++ b/src/harness/unittests/typingsInstaller.ts @@ -755,7 +755,7 @@ namespace ts.projectSystem { checkNumberOfProjects(projectService, { configuredProjects: 1 }); const p = configuredProjectAt(projectService, 0); checkProjectActualFiles(p, [app.path, jsconfig.path]); - checkWatchedFiles(host, [jsconfig.path, "/bower_components", "/node_modules", libFile.path]); + checkWatchedFiles(host, ["/tsconfig.json", jsconfig.path, "/bower_components", "/node_modules", libFile.path]); installer.installAll(/*expectedCount*/ 1); diff --git a/src/server/editorServices.ts b/src/server/editorServices.ts index c2a79913928ab..c2b712c06a2cd 100644 --- a/src/server/editorServices.ts +++ b/src/server/editorServices.ts @@ -252,7 +252,7 @@ namespace ts.server { WildCardDirectories = "Wild card directory", TypeRoot = "Type root of the project", ClosedScriptInfo = "Closed Script info", - ConfigFileForInferredRoot = "Config file for the root script info of the inferred project" + ConfigFileForOpenFile = "Config file changes for the open script info" } /* @internal */ @@ -266,12 +266,12 @@ namespace ts.server { OrphanScriptInfo = "Removing Orphan script info as part of cleanup", FileDeleted = "File was deleted", FileOpened = "File opened", - ConfigProjectCreated = "Config file project created" + ConfigProjectCreated = "Config file project created", + FileClosed = "File is closed" } const enum ConfigFileWatcherStatus { ReloadingFiles = "Reloading configured projects files", - NoAction = "No action on files", UpdatedCallback = "Updated the callback", TrackingFileAdded = "Tracking file added", TrackingFileRemoved = "Tracking file removed" @@ -282,7 +282,7 @@ namespace ts.server { type ConfigFileExistence = { exists: boolean; - trackingOpenFiles?: ScriptInfo[]; + trackingOpenFileSet?: Map; configFileWatcher?: FileWatcher; }; @@ -672,48 +672,31 @@ namespace ts.server { // Update the cached status // No action needed on tracking open files since the existing config file anyways didnt affect the tracking file configFilePresenceInfo.exists = false; - this.logTrackingFiles(project.getConfigFilePath(), configFilePresenceInfo, ConfigFileWatcherStatus.NoAction); this.removeProject(project); - // Reload the configured projects for these open files in the project as - // they could be held up by another config file somewhere in the parent directory - const orphanFiles = filter(this.openFiles, file => file.containingProjects.length === 0); - this.delayReloadConfiguredProjectForFiles(orphanFiles); + // Reload the configured projects for the open files in the map as they are affectected by this config file + this.logConfigFileWatch(project.getConfigFilePath(), configFilePresenceInfo, ConfigFileWatcherStatus.ReloadingFiles); + this.delayReloadConfiguredProjectForFiles(configFilePresenceInfo.trackingOpenFileSet); } else { + this.logConfigFileWatch(project.getConfigFilePath(), configFilePresenceInfo, ConfigFileWatcherStatus.ReloadingFiles); project.pendingReload = true; - this.logTrackingFiles(project.getConfigFilePath(), configFilePresenceInfo, ConfigFileWatcherStatus.ReloadingFiles); - if (configFilePresenceInfo.trackingOpenFiles) { - this.delayUpdateProjectGraph(project); - this.delayReloadConfiguredProjectForFiles(configFilePresenceInfo.trackingOpenFiles); - } - else { - this.delayUpdateProjectGraphAndInferredProjectsRefresh(project); - } + this.delayUpdateProjectGraph(project); + this.delayReloadConfiguredProjectForFiles(configFilePresenceInfo.trackingOpenFileSet); } } /** - * This is the callback function for the config file add/remove/change for the root in the inferred project + * This is the callback function for the config file add/remove/change at any location that matters to open + * script info but doesnt have configured project open for the config file */ - private onConfigFileAddedForInferredProject(configFileName: NormalizedPath, eventKind: FileWatcherEventKind) { + private onConfigFileChangeForOpenScriptInfo(configFileName: NormalizedPath, eventKind: FileWatcherEventKind) { // This callback is called only if we dont have config file project for this config file const cononicalConfigPath = normalizedPathToPath(configFileName, this.currentDirectory, this.toCanonicalFileName); const configFilePresenceInfo = this.mapOfConfigFilePresence.get(cononicalConfigPath); - - if (eventKind === FileWatcherEventKind.Deleted) { - // No action needed if the event was for deletion of the file - // - because the existing config file didnt affect the inferred project roots anyways - configFilePresenceInfo.exists = false; - this.logTrackingFiles(configFileName, configFilePresenceInfo, ConfigFileWatcherStatus.NoAction); - } - else { - // Either the config file was created or changed - // Reload the projects - configFilePresenceInfo.exists = true; - this.logTrackingFiles(configFileName, configFilePresenceInfo, ConfigFileWatcherStatus.ReloadingFiles); - this.delayReloadConfiguredProjectForFiles(configFilePresenceInfo.trackingOpenFiles); - } + configFilePresenceInfo.exists = (eventKind !== FileWatcherEventKind.Deleted); + this.logConfigFileWatch(configFileName, configFilePresenceInfo, ConfigFileWatcherStatus.ReloadingFiles); + this.delayReloadConfiguredProjectForFiles(configFilePresenceInfo.trackingOpenFileSet); } private removeProject(project: Project) { @@ -729,7 +712,7 @@ namespace ts.server { this.projectToSizeMap.delete((project as ExternalProject).externalProjectName); break; case ProjectKind.Configured: - // Update the map of mapOfKnownTsConfigFiles + this.setConfigFilePresenceByClosedConfigFile(project); this.configuredProjects.delete((project).canonicalConfigFilePath); this.projectToSizeMap.delete((project as ConfiguredProject).canonicalConfigFilePath); break; @@ -791,6 +774,7 @@ namespace ts.server { // because the user may chose to discard the buffer content before saving // to the disk, and the server's version of the file can be out of sync. info.close(); + this.stopWatchingConfigFileForScriptInfo(info); removeItemFromSet(this.openFiles, info); @@ -863,28 +847,24 @@ namespace ts.server { }); } - private configFileExists(configFileName: NormalizedPath) { + private configFileExists(configFileName: NormalizedPath, info: ScriptInfo) { const canonicalConfigFilePath = normalizedPathToPath(configFileName, this.currentDirectory, this.toCanonicalFileName); - const configFilePresenceInfo = this.mapOfConfigFilePresence.get(canonicalConfigFilePath); - if (configFilePresenceInfo) { - return configFilePresenceInfo.exists; - } - return this.host.fileExists(configFileName); + return this.watchConfigFileForScriptInfo(configFileName, canonicalConfigFilePath, info).exists; } private setConfigFilePresenceByNewConfiguredProject(project: ConfiguredProject) { const configFilePresenceInfo = this.mapOfConfigFilePresence.get(project.canonicalConfigFilePath); if (configFilePresenceInfo) { - configFilePresenceInfo.exists = true; + Debug.assert(configFilePresenceInfo.exists); // close existing watcher if (configFilePresenceInfo.configFileWatcher) { const configFileName = project.getConfigFilePath(); this.closeFileWatcher( - WatchType.ConfigFileForInferredRoot, /*project*/ undefined, configFileName, + WatchType.ConfigFileForOpenFile, /*project*/ undefined, configFileName, configFilePresenceInfo.configFileWatcher, WatcherCloseReason.ConfigProjectCreated ); configFilePresenceInfo.configFileWatcher = undefined; - this.logTrackingFiles(configFileName, configFilePresenceInfo, ConfigFileWatcherStatus.UpdatedCallback); + this.logConfigFileWatch(configFileName, configFilePresenceInfo, ConfigFileWatcherStatus.UpdatedCallback); } } else { @@ -893,17 +873,16 @@ namespace ts.server { } } - /* @internal */ - setConfigFilePresenceByClosedConfigFile(closedProject: ConfiguredProject) { + private setConfigFilePresenceByClosedConfigFile(closedProject: ConfiguredProject) { const configFilePresenceInfo = this.mapOfConfigFilePresence.get(closedProject.canonicalConfigFilePath); Debug.assert(!!configFilePresenceInfo); - if (configFilePresenceInfo.trackingOpenFiles) { + if (configFilePresenceInfo.trackingOpenFileSet) { const configFileName = closedProject.getConfigFilePath(); configFilePresenceInfo.configFileWatcher = this.addFileWatcher( - WatchType.ConfigFileForInferredRoot, /*project*/ undefined, configFileName, - (_filename, eventKind) => this.onConfigFileAddedForInferredProject(configFileName, eventKind) + WatchType.ConfigFileForOpenFile, /*project*/ undefined, configFileName, + (_filename, eventKind) => this.onConfigFileChangeForOpenScriptInfo(configFileName, eventKind) ); - this.logTrackingFiles(configFileName, configFilePresenceInfo, ConfigFileWatcherStatus.UpdatedCallback); + this.logConfigFileWatch(configFileName, configFilePresenceInfo, ConfigFileWatcherStatus.UpdatedCallback); } else { // There is no one tracking anymore. Remove the status @@ -911,85 +890,83 @@ namespace ts.server { } } - private logTrackingFiles(configFileName: NormalizedPath, configFilePresenceInfo: ConfigFileExistence, status: ConfigFileWatcherStatus) { - const watchType = configFilePresenceInfo.configFileWatcher ? WatchType.ConfigFileForInferredRoot : WatchType.ConfigFilePath; - const files = map(configFilePresenceInfo.trackingOpenFiles, info => info.fileName); + private logConfigFileWatch(configFileName: NormalizedPath, configFilePresenceInfo: ConfigFileExistence, status: ConfigFileWatcherStatus) { + const watchType = configFilePresenceInfo.configFileWatcher ? WatchType.ConfigFileForOpenFile : WatchType.ConfigFilePath; + const files = configFilePresenceInfo.trackingOpenFileSet ? + arrayFrom(configFilePresenceInfo.trackingOpenFileSet.keys(), key => + this.getScriptInfoForPath(key as Path).fileName) : + []; this.logger.info(`FileWatcher:: ${watchType}: File: ${configFileName} Currently Tracking for files: ${files} Status: ${status}`); } - private watchConfigFileForInferredRoot(configFileName: NormalizedPath, canonicalConfigFilePath: string, root: ScriptInfo) { + private watchConfigFileForScriptInfo(configFileName: NormalizedPath, canonicalConfigFilePath: string, info: ScriptInfo) { let configFilePresenceInfo = this.mapOfConfigFilePresence.get(canonicalConfigFilePath); if (configFilePresenceInfo) { // Existing information - just add to tracking files - (configFilePresenceInfo.trackingOpenFiles || (configFilePresenceInfo.trackingOpenFiles = [])).push(root); + if (!configFilePresenceInfo.trackingOpenFileSet) { + configFilePresenceInfo.trackingOpenFileSet = createMap(); + configFilePresenceInfo.trackingOpenFileSet.set(info.path, true); + this.logConfigFileWatch(configFileName, configFilePresenceInfo, ConfigFileWatcherStatus.TrackingFileAdded); + } + else if (!configFilePresenceInfo.trackingOpenFileSet.has(info.path)) { + configFilePresenceInfo.trackingOpenFileSet.set(info.path, true); + this.logConfigFileWatch(configFileName, configFilePresenceInfo, ConfigFileWatcherStatus.TrackingFileAdded); + } } else { // Add new callback + const trackingOpenFileSet = createMap(); + trackingOpenFileSet.set(info.path, true); + const exists = this.host.fileExists(configFileName); configFilePresenceInfo = { - exists: this.host.fileExists(configFileName), - trackingOpenFiles: [root], + exists, + trackingOpenFileSet, configFileWatcher: this.addFileWatcher( - WatchType.ConfigFileForInferredRoot, /*project*/ undefined, configFileName, - (_fileName, eventKind) => this.onConfigFileAddedForInferredProject(configFileName, eventKind) + WatchType.ConfigFileForOpenFile, /*project*/ undefined, configFileName, + (_fileName, eventKind) => this.onConfigFileChangeForOpenScriptInfo(configFileName, eventKind) ) }; this.mapOfConfigFilePresence.set(canonicalConfigFilePath, configFilePresenceInfo); + this.logConfigFileWatch(configFileName, configFilePresenceInfo, ConfigFileWatcherStatus.TrackingFileAdded); } - this.logTrackingFiles(configFileName, configFilePresenceInfo, ConfigFileWatcherStatus.TrackingFileAdded); + return configFilePresenceInfo; } - private closeWatchConfigFileForInferredRoot(configFileName: NormalizedPath, canonicalConfigFilePath: string, root: ScriptInfo, reason: WatcherCloseReason) { + private closeConfigFileWatchForScriptInfo(configFileName: NormalizedPath, canonicalConfigFilePath: string, info: ScriptInfo) { const configFilePresenceInfo = this.mapOfConfigFilePresence.get(canonicalConfigFilePath); - Debug.assert(!!configFilePresenceInfo); - if (configFilePresenceInfo.trackingOpenFiles.length === 1) { - configFilePresenceInfo.trackingOpenFiles = undefined; - this.logTrackingFiles(configFileName, configFilePresenceInfo, ConfigFileWatcherStatus.TrackingFileRemoved); - if (configFilePresenceInfo.configFileWatcher) { - this.closeFileWatcher( - WatchType.ConfigFileForInferredRoot, /*project*/ undefined, - configFileName, configFilePresenceInfo.configFileWatcher, reason - ); - this.mapOfConfigFilePresence.delete(canonicalConfigFilePath); + if (configFilePresenceInfo) { + if (configFilePresenceInfo.trackingOpenFileSet.size === 1) { + configFilePresenceInfo.trackingOpenFileSet = undefined; + this.logConfigFileWatch(configFileName, configFilePresenceInfo, ConfigFileWatcherStatus.TrackingFileRemoved); + if (configFilePresenceInfo.configFileWatcher) { + this.closeFileWatcher( + WatchType.ConfigFileForOpenFile, /*project*/ undefined, configFileName, + configFilePresenceInfo.configFileWatcher, WatcherCloseReason.FileClosed + ); + this.mapOfConfigFilePresence.delete(canonicalConfigFilePath); + } + } + else { + configFilePresenceInfo.trackingOpenFileSet.delete(info.path); + this.logConfigFileWatch(configFileName, configFilePresenceInfo, ConfigFileWatcherStatus.TrackingFileRemoved); } - } - else { - removeItemFromSet(configFilePresenceInfo.trackingOpenFiles, root); - this.logTrackingFiles(configFileName, configFilePresenceInfo, ConfigFileWatcherStatus.TrackingFileRemoved); } } - private enumerateWatchingRootOfInferredProject(root: ScriptInfo, - action: (configFileName: NormalizedPath, canonicalConfigFilePath: string, root: ScriptInfo) => void) { - let current = root.fileName; - let currentPath = getDirectoryPath(root.path); + private stopWatchingConfigFileForScriptInfo(info: ScriptInfo) { + Debug.assert(!info.isScriptOpen()); + let current = info.fileName; + let currentPath = getDirectoryPath(info.path); let parentPath = getDirectoryPath(currentPath); while (currentPath !== parentPath) { current = asNormalizedPath(getDirectoryPath(current)); - action(asNormalizedPath(combinePaths(current, "tsconfig.json")), combinePaths(currentPath, "tsconfig.json"), root); - //if (root.isJavaScript()) { - // this.watchConfigFileForInferredRoot(asNormalizedPath(combinePaths(current, "jsconfig.json")), combinePaths(currentPath, "jsconfig.json"), root); - //} + this.closeConfigFileWatchForScriptInfo(asNormalizedPath(combinePaths(current, "tsconfig.json")), combinePaths(currentPath, "tsconfig.json"), info); + this.closeConfigFileWatchForScriptInfo(asNormalizedPath(combinePaths(current, "jsconfig.json")), combinePaths(currentPath, "jsconfig.json"), info); currentPath = parentPath; parentPath = getDirectoryPath(parentPath); } } - /*@internal*/ - startWatchingRootOfInferredProject(root: ScriptInfo) { - this.enumerateWatchingRootOfInferredProject(root, - (configFileName, canonicalConfigFilePath, root) => - this.watchConfigFileForInferredRoot(configFileName, canonicalConfigFilePath, root) - ); - } - - /*@internal*/ - stopWatchingRootOfInferredProject(root: ScriptInfo, reason: WatcherCloseReason) { - this.enumerateWatchingRootOfInferredProject(root, - (configFileName, canonicalConfigFilePath, root) => - this.closeWatchConfigFileForInferredRoot(configFileName, canonicalConfigFilePath, root, reason) - ); - } - /** * This function tries to search for a tsconfig.json for the given file. * This is different from the method the compiler uses because @@ -998,20 +975,21 @@ namespace ts.server { * The server must start searching from the directory containing * the newly opened file. */ - private getConfigFileNameForFile(fileName: NormalizedPath, projectRootPath?: NormalizedPath) { - let searchPath = getDirectoryPath(fileName); + private getConfigFileNameForFile(info: ScriptInfo, projectRootPath?: NormalizedPath) { + Debug.assert(info.isScriptOpen()); + let searchPath = getDirectoryPath(info.fileName); this.logger.info(`Search path: ${searchPath}`); // check if this file is already included in one of external projects while (!projectRootPath || searchPath.indexOf(projectRootPath) >= 0) { const tsconfigFileName = asNormalizedPath(combinePaths(searchPath, "tsconfig.json")); - if (this.configFileExists(tsconfigFileName)) { + if (this.configFileExists(tsconfigFileName, info)) { this.logger.info(`Config file name: ${tsconfigFileName}`); return tsconfigFileName; } const jsconfigFileName = asNormalizedPath(combinePaths(searchPath, "jsconfig.json")); - if (this.configFileExists(jsconfigFileName)) { + if (this.configFileExists(jsconfigFileName, info)) { this.logger.info(`Config file name: ${jsconfigFileName}`); return jsconfigFileName; } @@ -1359,7 +1337,6 @@ namespace ts.server { : new InferredProject(this, this.documentRegistry, this.compilerOptionsForInferredProjects); project.addRoot(root); - this.startWatchingRootOfInferredProject(root); project.updateGraph(); if (!useExistingProject) { @@ -1513,8 +1490,11 @@ namespace ts.server { this.refreshInferredProjects(); } - delayReloadConfiguredProjectForFiles(openFiles: ScriptInfo[]) { - this.reloadConfiguredProjectForFiles(openFiles, /*delayReload*/ true); + delayReloadConfiguredProjectForFiles(openFileSet: Map) { + if (openFileSet) { + const openFiles = arrayFrom(openFileSet.keys(), path => this.getScriptInfoForPath(path as Path)); + this.reloadConfiguredProjectForFiles(openFiles, /*delayReload*/ true); + } this.delayInferredProjectsRefresh(); } @@ -1532,7 +1512,7 @@ namespace ts.server { // we first detect if there is already a configured project created for it: if so, // we re- read the tsconfig file content and update the project only if we havent already done so // otherwise we create a new one. - const configFileName = this.getConfigFileNameForFile(info.fileName); + const configFileName = this.getConfigFileNameForFile(info); if (configFileName) { let project = this.findConfiguredProjectByProjectName(configFileName); if (!project) { @@ -1614,9 +1594,10 @@ namespace ts.server { let configFileName: NormalizedPath; let configFileErrors: Diagnostic[]; + const info = this.getOrCreateScriptInfoForNormalizedPath(fileName, /*openedByClient*/ true, fileContent, scriptKind, hasMixedContent); let project: ConfiguredProject | ExternalProject = this.findContainingExternalProject(fileName); if (!project) { - configFileName = this.getConfigFileNameForFile(fileName, projectRootPath); + configFileName = this.getConfigFileNameForFile(info, projectRootPath); if (configFileName) { project = this.findConfiguredProjectByProjectName(configFileName); if (!project) { @@ -1640,7 +1621,6 @@ namespace ts.server { } // at this point if file is the part of some configured/external project then this project should be created - const info = this.getOrCreateScriptInfoForNormalizedPath(fileName, /*openedByClient*/ true, fileContent, scriptKind, hasMixedContent); this.assignScriptInfoToInferredProjectIfNecessary(info, /*addToListOfOpenFiles*/ true); // Delete the orphan files here because there might be orphan script infos (which are not part of project) // when some file/s were closed which resulted in project removal. diff --git a/src/server/project.ts b/src/server/project.ts index 2e14c75efc5f5..774865b335edc 100644 --- a/src/server/project.ts +++ b/src/server/project.ts @@ -924,7 +924,6 @@ namespace ts.server { } addRoot(info: ScriptInfo) { - this.projectService.startWatchingRootOfInferredProject(info); if (!this._isJsInferredProject && info.isJavaScript()) { this.toggleJsInferredProject(/*isJsInferredProject*/ true); } @@ -932,7 +931,6 @@ namespace ts.server { } removeRoot(info: ScriptInfo) { - this.projectService.stopWatchingRootOfInferredProject(info, WatcherCloseReason.NotNeeded); super.removeRoot(info); if (this._isJsInferredProject && info.isJavaScript()) { if (!some(this.getRootScriptInfos(), info => info.isJavaScript())) { @@ -957,7 +955,6 @@ namespace ts.server { } close() { - forEach(this.getRootScriptInfos(), root => this.projectService.stopWatchingRootOfInferredProject(root, WatcherCloseReason.ProjectClose)); super.close(); } @@ -1208,7 +1205,6 @@ namespace ts.server { if (this.configFileWatcher) { this.projectService.closeFileWatcher(WatchType.ConfigFilePath, this, this.getConfigFilePath(), this.configFileWatcher, WatcherCloseReason.ProjectClose); this.configFileWatcher = undefined; - this.projectService.setConfigFilePresenceByClosedConfigFile(this); } this.stopWatchingTypeRoots(WatcherCloseReason.ProjectClose); From 62ef6b1cda057823d935147493a6fc48ac0f8f76 Mon Sep 17 00:00:00 2001 From: Sheetal Nandi Date: Mon, 17 Jul 2017 12:37:36 -0700 Subject: [PATCH 027/109] Added another todo as now we are watching too many files --- src/server/editorServices.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/server/editorServices.ts b/src/server/editorServices.ts index c2b712c06a2cd..8c9731ef6ea27 100644 --- a/src/server/editorServices.ts +++ b/src/server/editorServices.ts @@ -849,6 +849,13 @@ namespace ts.server { private configFileExists(configFileName: NormalizedPath, info: ScriptInfo) { const canonicalConfigFilePath = normalizedPathToPath(configFileName, this.currentDirectory, this.toCanonicalFileName); + // TODO: (sheetalkamat) Need to reduce the number of watches by not watching directories right here. + // Theorotically current approach is correct but in practice there will be very few scenarios where + // the config file gets added somewhere inside the another config file. And technically we could handle that case in configDirectory watcher in some cases + // But given that its a rare scenario it seems like too much overhead. + // So what we want to be watching is: configFile if the project is open + // And the whole chain only for the inferred project roots + // instead create the cache and send it across, and put watches only if info gets added to the inferred project root? return this.watchConfigFileForScriptInfo(configFileName, canonicalConfigFilePath, info).exists; } From 2439e7affb368430d39b0e257b8ba2ff5fc88d60 Mon Sep 17 00:00:00 2001 From: Sheetal Nandi Date: Mon, 17 Jul 2017 15:20:59 -0700 Subject: [PATCH 028/109] Reduce the number of watched config files by watching the chain only in case of inferred root --- src/compiler/core.ts | 11 +- .../unittests/tsserverProjectSystem.ts | 3 +- src/harness/unittests/typingsInstaller.ts | 2 +- src/server/editorServices.ts | 345 ++++++++++++------ src/server/project.ts | 3 + 5 files changed, 253 insertions(+), 111 deletions(-) diff --git a/src/compiler/core.ts b/src/compiler/core.ts index a1880b4647129..b4ea5943eec72 100644 --- a/src/compiler/core.ts +++ b/src/compiler/core.ts @@ -467,13 +467,20 @@ namespace ts { return result; } - export function flatMapIter(iter: Iterator, mapfn: (x: T) => U[] | undefined): U[] { + export function flatMapIter(iter: Iterator, mapfn: (x: T) => U | U[] | undefined): U[] { const result: U[] = []; while (true) { const { value, done } = iter.next(); if (done) break; const res = mapfn(value); - if (res) result.push(...res); + if (res) { + if (isArray(res)) { + result.push(...res); + } + else { + result.push(res); + } + } } return result; } diff --git a/src/harness/unittests/tsserverProjectSystem.ts b/src/harness/unittests/tsserverProjectSystem.ts index e662e7b792c5c..a63a8c3daf803 100644 --- a/src/harness/unittests/tsserverProjectSystem.ts +++ b/src/harness/unittests/tsserverProjectSystem.ts @@ -881,8 +881,7 @@ namespace ts.projectSystem { checkProjectActualFiles(project, [file1.path, libFile.path, file2.path, configFile.path]); checkProjectRootFiles(project, [file1.path, file2.path]); // watching all files except one that was open - // And also tsconfig files for the open files - checkWatchedFiles(host, [configFile.path, file2.path, libFile.path, "/a/b/c/tsconfig.json", "/a/b/c/jsconfig.json"]); + checkWatchedFiles(host, [configFile.path, file2.path, libFile.path]); checkWatchedDirectories(host, [getDirectoryPath(configFile.path)], /*recursive*/ true); }); diff --git a/src/harness/unittests/typingsInstaller.ts b/src/harness/unittests/typingsInstaller.ts index 7fab4ffdcc4c6..c9617625cc739 100644 --- a/src/harness/unittests/typingsInstaller.ts +++ b/src/harness/unittests/typingsInstaller.ts @@ -755,7 +755,7 @@ namespace ts.projectSystem { checkNumberOfProjects(projectService, { configuredProjects: 1 }); const p = configuredProjectAt(projectService, 0); checkProjectActualFiles(p, [app.path, jsconfig.path]); - checkWatchedFiles(host, ["/tsconfig.json", jsconfig.path, "/bower_components", "/node_modules", libFile.path]); + checkWatchedFiles(host, [jsconfig.path, "/bower_components", "/node_modules", libFile.path]); installer.installAll(/*expectedCount*/ 1); diff --git a/src/server/editorServices.ts b/src/server/editorServices.ts index 8c9731ef6ea27..5f660defcf2f2 100644 --- a/src/server/editorServices.ts +++ b/src/server/editorServices.ts @@ -252,7 +252,7 @@ namespace ts.server { WildCardDirectories = "Wild card directory", TypeRoot = "Type root of the project", ClosedScriptInfo = "Closed Script info", - ConfigFileForOpenFile = "Config file changes for the open script info" + ConfigFileForInferredRoot = "Config file for the inferred project root" } /* @internal */ @@ -271,18 +271,32 @@ namespace ts.server { } const enum ConfigFileWatcherStatus { - ReloadingFiles = "Reloading configured projects files", + ReloadingFiles = "Reloading configured projects for files", + ReloadingInferredRootFiles = "Reloading configured projects for only inferred root files", UpdatedCallback = "Updated the callback", TrackingFileAdded = "Tracking file added", - TrackingFileRemoved = "Tracking file removed" + TrackingFileRemoved = "Tracking file removed", + InferredRootAdded = "Inferred Root file added", + InferredRootRemoved = "Inferred Root file removed", } /* @internal */ export type ServerDirectoryWatcherCallback = (path: NormalizedPath) => void; type ConfigFileExistence = { + /** + * Cached value of existence of config file + */ exists: boolean; - trackingOpenFileSet?: Map; + /** + * The value in the open files map is true if the file is inferred project root + * Otherwise its false + */ + trackingOpenFilesMap: Map; + /** + * The file watcher corresponding to this config file for the inferred project root + * The watcher is present only when there is no open configured project for this config file + */ configFileWatcher?: FileWatcher; }; @@ -675,14 +689,16 @@ namespace ts.server { this.removeProject(project); // Reload the configured projects for the open files in the map as they are affectected by this config file - this.logConfigFileWatch(project.getConfigFilePath(), configFilePresenceInfo, ConfigFileWatcherStatus.ReloadingFiles); - this.delayReloadConfiguredProjectForFiles(configFilePresenceInfo.trackingOpenFileSet); + this.logConfigFileWatchUpdate(project.getConfigFilePath(), configFilePresenceInfo, ConfigFileWatcherStatus.ReloadingFiles); + // Since the configured project was deleted, we want to reload projects for all the open files + this.delayReloadConfiguredProjectForFiles(configFilePresenceInfo.trackingOpenFilesMap, /*ignoreIfNotInferredProjectRoot*/ false); } else { - this.logConfigFileWatch(project.getConfigFilePath(), configFilePresenceInfo, ConfigFileWatcherStatus.ReloadingFiles); + this.logConfigFileWatchUpdate(project.getConfigFilePath(), configFilePresenceInfo, ConfigFileWatcherStatus.ReloadingInferredRootFiles); project.pendingReload = true; this.delayUpdateProjectGraph(project); - this.delayReloadConfiguredProjectForFiles(configFilePresenceInfo.trackingOpenFileSet); + // As we scheduled the updated project graph, we would need to only schedule the project reload for the inferred project roots + this.delayReloadConfiguredProjectForFiles(configFilePresenceInfo.trackingOpenFilesMap, /*ignoreIfNotInferredProjectRoot*/ true); } } @@ -695,8 +711,9 @@ namespace ts.server { const cononicalConfigPath = normalizedPathToPath(configFileName, this.currentDirectory, this.toCanonicalFileName); const configFilePresenceInfo = this.mapOfConfigFilePresence.get(cononicalConfigPath); configFilePresenceInfo.exists = (eventKind !== FileWatcherEventKind.Deleted); - this.logConfigFileWatch(configFileName, configFilePresenceInfo, ConfigFileWatcherStatus.ReloadingFiles); - this.delayReloadConfiguredProjectForFiles(configFilePresenceInfo.trackingOpenFileSet); + this.logConfigFileWatchUpdate(configFileName, configFilePresenceInfo, ConfigFileWatcherStatus.ReloadingFiles); + // The tracking opens files would only contaion the inferred root so no need to check + this.delayReloadConfiguredProjectForFiles(configFilePresenceInfo.trackingOpenFilesMap, /*ignoreIfNotInferredProjectRoot*/ false); } private removeProject(project: Project) { @@ -712,9 +729,9 @@ namespace ts.server { this.projectToSizeMap.delete((project as ExternalProject).externalProjectName); break; case ProjectKind.Configured: - this.setConfigFilePresenceByClosedConfigFile(project); this.configuredProjects.delete((project).canonicalConfigFilePath); this.projectToSizeMap.delete((project as ConfiguredProject).canonicalConfigFilePath); + this.setConfigFilePresenceByClosedConfigFile(project); break; case ProjectKind.Inferred: removeItemFromSet(this.inferredProjects, project); @@ -774,7 +791,7 @@ namespace ts.server { // because the user may chose to discard the buffer content before saving // to the disk, and the server's version of the file can be out of sync. info.close(); - this.stopWatchingConfigFileForScriptInfo(info); + this.stopWatchingConfigFilesForClosedScriptInfo(info); removeItemFromSet(this.openFiles, info); @@ -847,16 +864,34 @@ namespace ts.server { }); } - private configFileExists(configFileName: NormalizedPath, info: ScriptInfo) { - const canonicalConfigFilePath = normalizedPathToPath(configFileName, this.currentDirectory, this.toCanonicalFileName); - // TODO: (sheetalkamat) Need to reduce the number of watches by not watching directories right here. - // Theorotically current approach is correct but in practice there will be very few scenarios where - // the config file gets added somewhere inside the another config file. And technically we could handle that case in configDirectory watcher in some cases - // But given that its a rare scenario it seems like too much overhead. - // So what we want to be watching is: configFile if the project is open - // And the whole chain only for the inferred project roots - // instead create the cache and send it across, and put watches only if info gets added to the inferred project root? - return this.watchConfigFileForScriptInfo(configFileName, canonicalConfigFilePath, info).exists; + private configFileExists(configFileName: NormalizedPath, canonicalConfigFilePath: string, info: ScriptInfo) { + let configFilePresenceInfo = this.mapOfConfigFilePresence.get(canonicalConfigFilePath); + if (configFilePresenceInfo) { + // By default the info is belong to the config file. + // Only adding the info as a root to inferred project will make it the root + if (!configFilePresenceInfo.trackingOpenFilesMap.has(info.path)) { + configFilePresenceInfo.trackingOpenFilesMap.set(info.path, false); + this.logConfigFileWatchUpdate(configFileName, configFilePresenceInfo, ConfigFileWatcherStatus.TrackingFileAdded); + } + return configFilePresenceInfo.exists; + } + + // Theorotically we should be adding watch for the directory here itself. + // In practice there will be very few scenarios where the config file gets added + // somewhere inside the another config file directory. + // And technically we could handle that case in configFile's directory watcher in some cases + // But given that its a rare scenario it seems like too much overhead. (we werent watching those directories earlier either) + // So what we are now watching is: configFile if the project is open + // And the whole chain of config files only for the inferred project roots + + // Cache the host value of file exists and add the info tio to the tracked root + const trackingOpenFilesMap = createMap(); + trackingOpenFilesMap.set(info.path, false); + const exists = this.host.fileExists(configFileName); + configFilePresenceInfo = { exists, trackingOpenFilesMap }; + this.mapOfConfigFilePresence.set(canonicalConfigFilePath, configFilePresenceInfo); + this.logConfigFileWatchUpdate(configFileName, configFilePresenceInfo, ConfigFileWatcherStatus.TrackingFileAdded); + return exists; } private setConfigFilePresenceByNewConfiguredProject(project: ConfiguredProject) { @@ -867,29 +902,41 @@ namespace ts.server { if (configFilePresenceInfo.configFileWatcher) { const configFileName = project.getConfigFilePath(); this.closeFileWatcher( - WatchType.ConfigFileForOpenFile, /*project*/ undefined, configFileName, + WatchType.ConfigFileForInferredRoot, /*project*/ undefined, configFileName, configFilePresenceInfo.configFileWatcher, WatcherCloseReason.ConfigProjectCreated ); configFilePresenceInfo.configFileWatcher = undefined; - this.logConfigFileWatch(configFileName, configFilePresenceInfo, ConfigFileWatcherStatus.UpdatedCallback); + this.logConfigFileWatchUpdate(configFileName, configFilePresenceInfo, ConfigFileWatcherStatus.UpdatedCallback); } } else { - // Mark existence of the config file with the project creation - this.mapOfConfigFilePresence.set(project.canonicalConfigFilePath, { exists: true }); + // We could be in this scenario if it is the external project tracked configured file + // Since that route doesnt check if the config file is present or not + this.mapOfConfigFilePresence.set(project.canonicalConfigFilePath, { + exists: true, + trackingOpenFilesMap: createMap() + }); } } + private configFileExistenceTracksInferredRoot(configFilePresenceInfo: ConfigFileExistence) { + return forEachEntry(configFilePresenceInfo.trackingOpenFilesMap, (value, __key) => value); + } + private setConfigFilePresenceByClosedConfigFile(closedProject: ConfiguredProject) { const configFilePresenceInfo = this.mapOfConfigFilePresence.get(closedProject.canonicalConfigFilePath); Debug.assert(!!configFilePresenceInfo); - if (configFilePresenceInfo.trackingOpenFileSet) { + const trackingOpenFilesMap = configFilePresenceInfo.trackingOpenFilesMap; + if (trackingOpenFilesMap.size) { const configFileName = closedProject.getConfigFilePath(); - configFilePresenceInfo.configFileWatcher = this.addFileWatcher( - WatchType.ConfigFileForOpenFile, /*project*/ undefined, configFileName, - (_filename, eventKind) => this.onConfigFileChangeForOpenScriptInfo(configFileName, eventKind) - ); - this.logConfigFileWatch(configFileName, configFilePresenceInfo, ConfigFileWatcherStatus.UpdatedCallback); + if (this.configFileExistenceTracksInferredRoot(configFilePresenceInfo)) { + Debug.assert(!configFilePresenceInfo.configFileWatcher); + configFilePresenceInfo.configFileWatcher = this.addFileWatcher( + WatchType.ConfigFileForInferredRoot, /*project*/ undefined, configFileName, + (_filename, eventKind) => this.onConfigFileChangeForOpenScriptInfo(configFileName, eventKind) + ); + this.logConfigFileWatchUpdate(configFileName, configFilePresenceInfo, ConfigFileWatcherStatus.UpdatedCallback); + } } else { // There is no one tracking anymore. Remove the status @@ -897,81 +944,135 @@ namespace ts.server { } } - private logConfigFileWatch(configFileName: NormalizedPath, configFilePresenceInfo: ConfigFileExistence, status: ConfigFileWatcherStatus) { - const watchType = configFilePresenceInfo.configFileWatcher ? WatchType.ConfigFileForOpenFile : WatchType.ConfigFilePath; - const files = configFilePresenceInfo.trackingOpenFileSet ? - arrayFrom(configFilePresenceInfo.trackingOpenFileSet.keys(), key => - this.getScriptInfoForPath(key as Path).fileName) : - []; - this.logger.info(`FileWatcher:: ${watchType}: File: ${configFileName} Currently Tracking for files: ${files} Status: ${status}`); + private logConfigFileWatchUpdate(configFileName: NormalizedPath, configFilePresenceInfo: ConfigFileExistence, status: ConfigFileWatcherStatus) { + if (this.logger.loggingEnabled()) { + const inferredRoots: string[] = []; + const otherFiles: string[] = []; + configFilePresenceInfo.trackingOpenFilesMap.forEach((value, key: Path) => { + const info = this.getScriptInfoForPath(key); + if (value) { + inferredRoots.push(info.fileName); + } + else { + otherFiles.push(info.fileName); + } + }); + const watchType = status === ConfigFileWatcherStatus.UpdatedCallback || + status === ConfigFileWatcherStatus.ReloadingFiles || + status === ConfigFileWatcherStatus.ReloadingInferredRootFiles ? + (configFilePresenceInfo.configFileWatcher ? WatchType.ConfigFileForInferredRoot : WatchType.ConfigFilePath) : + ""; + this.logger.info(`ConfigFilePresence ${watchType}:: File: ${configFileName} Currently Tracking: InferredRootFiles: ${inferredRoots} OtherFiles: ${otherFiles} Status: ${status}`); + } + } + + private closeConfigFileWatcherIfInferredRoot(configFileName: NormalizedPath, canonicalConfigFilePath: string, + configFilePresenceInfo: ConfigFileExistence, infoIsInferredRoot: boolean, reason: WatcherCloseReason) { + // Close the config file watcher if it was the last inferred root + if (infoIsInferredRoot && + configFilePresenceInfo.configFileWatcher && + !this.configFileExistenceTracksInferredRoot(configFilePresenceInfo)) { + this.closeFileWatcher( + WatchType.ConfigFileForInferredRoot, /*project*/ undefined, configFileName, + configFilePresenceInfo.configFileWatcher, reason + ); + configFilePresenceInfo.configFileWatcher = undefined; + } + + // If this was the last tracking file open for this config file, remove the cached value + if (!configFilePresenceInfo.trackingOpenFilesMap.size && + !this.getConfiguredProjectByCanonicalConfigFilePath(canonicalConfigFilePath)) { + this.mapOfConfigFilePresence.delete(canonicalConfigFilePath); + } } - private watchConfigFileForScriptInfo(configFileName: NormalizedPath, canonicalConfigFilePath: string, info: ScriptInfo) { - let configFilePresenceInfo = this.mapOfConfigFilePresence.get(canonicalConfigFilePath); + private closeConfigFileWatchForClosedScriptInfo(configFileName: NormalizedPath, canonicalConfigFilePath: string, info: ScriptInfo) { + const configFilePresenceInfo = this.mapOfConfigFilePresence.get(canonicalConfigFilePath); if (configFilePresenceInfo) { - // Existing information - just add to tracking files - if (!configFilePresenceInfo.trackingOpenFileSet) { - configFilePresenceInfo.trackingOpenFileSet = createMap(); - configFilePresenceInfo.trackingOpenFileSet.set(info.path, true); - this.logConfigFileWatch(configFileName, configFilePresenceInfo, ConfigFileWatcherStatus.TrackingFileAdded); - } - else if (!configFilePresenceInfo.trackingOpenFileSet.has(info.path)) { - configFilePresenceInfo.trackingOpenFileSet.set(info.path, true); - this.logConfigFileWatch(configFileName, configFilePresenceInfo, ConfigFileWatcherStatus.TrackingFileAdded); - } + const isInferredRoot = configFilePresenceInfo.trackingOpenFilesMap.get(info.path); + + // Delete the info from tracking + configFilePresenceInfo.trackingOpenFilesMap.delete(info.path); + this.logConfigFileWatchUpdate(configFileName, configFilePresenceInfo, ConfigFileWatcherStatus.TrackingFileRemoved); + + // Close the config file watcher if it was the last inferred root + this.closeConfigFileWatcherIfInferredRoot(configFileName, canonicalConfigFilePath, + configFilePresenceInfo, isInferredRoot, WatcherCloseReason.FileClosed + ); } - else { - // Add new callback - const trackingOpenFileSet = createMap(); - trackingOpenFileSet.set(info.path, true); - const exists = this.host.fileExists(configFileName); + } + + /** + * This is called on file close, so that we stop watching the config file for this script info + * @param info + */ + private stopWatchingConfigFilesForClosedScriptInfo(info: ScriptInfo) { + Debug.assert(!info.isScriptOpen()); + this.enumerateConfigFileLocations(info, (configFileName, canonicalConfigFilePath) => + this.closeConfigFileWatchForClosedScriptInfo(configFileName, canonicalConfigFilePath, info) + ); + } + + private watchConfigFileForInferredProjectRoot(configFileName: NormalizedPath, canonicalConfigFilePath: string, info: ScriptInfo) { + let configFilePresenceInfo = this.mapOfConfigFilePresence.get(canonicalConfigFilePath); + if (!configFilePresenceInfo) { + // Create the cache configFilePresenceInfo = { - exists, - trackingOpenFileSet, - configFileWatcher: this.addFileWatcher( - WatchType.ConfigFileForOpenFile, /*project*/ undefined, configFileName, - (_fileName, eventKind) => this.onConfigFileChangeForOpenScriptInfo(configFileName, eventKind) - ) + exists: this.host.fileExists(configFileName), + trackingOpenFilesMap: createMap() }; this.mapOfConfigFilePresence.set(canonicalConfigFilePath, configFilePresenceInfo); - this.logConfigFileWatch(configFileName, configFilePresenceInfo, ConfigFileWatcherStatus.TrackingFileAdded); } - return configFilePresenceInfo; + + // Set this file as inferred root + configFilePresenceInfo.trackingOpenFilesMap.set(info.path, true); + this.logConfigFileWatchUpdate(configFileName, configFilePresenceInfo, ConfigFileWatcherStatus.InferredRootAdded); + + // If there is no configured project for this config file, create the watcher + if (!configFilePresenceInfo.configFileWatcher && + !this.getConfiguredProjectByCanonicalConfigFilePath(canonicalConfigFilePath)) { + configFilePresenceInfo.configFileWatcher = this.addFileWatcher(WatchType.ConfigFileForInferredRoot, /*project*/ undefined, configFileName, + (_fileName, eventKind) => this.onConfigFileChangeForOpenScriptInfo(configFileName, eventKind) + ); + this.logConfigFileWatchUpdate(configFileName, configFilePresenceInfo, ConfigFileWatcherStatus.UpdatedCallback); + } + } + + /** + * This is called by inferred project whenever script info is added as a root + */ + /* @internal */ + startWatchingConfigFilesForInferredProjectRoot(info: ScriptInfo) { + Debug.assert(info.isScriptOpen()); + this.enumerateConfigFileLocations(info, (configFileName, canonicalConfigFilePath) => + this.watchConfigFileForInferredProjectRoot(configFileName, canonicalConfigFilePath, info) + ); } - private closeConfigFileWatchForScriptInfo(configFileName: NormalizedPath, canonicalConfigFilePath: string, info: ScriptInfo) { + private closeWatchConfigFileForInferredProjectRoot(configFileName: NormalizedPath, canonicalConfigFilePath: string, info: ScriptInfo, reason: WatcherCloseReason) { const configFilePresenceInfo = this.mapOfConfigFilePresence.get(canonicalConfigFilePath); if (configFilePresenceInfo) { - if (configFilePresenceInfo.trackingOpenFileSet.size === 1) { - configFilePresenceInfo.trackingOpenFileSet = undefined; - this.logConfigFileWatch(configFileName, configFilePresenceInfo, ConfigFileWatcherStatus.TrackingFileRemoved); - if (configFilePresenceInfo.configFileWatcher) { - this.closeFileWatcher( - WatchType.ConfigFileForOpenFile, /*project*/ undefined, configFileName, - configFilePresenceInfo.configFileWatcher, WatcherCloseReason.FileClosed - ); - this.mapOfConfigFilePresence.delete(canonicalConfigFilePath); - } - } - else { - configFilePresenceInfo.trackingOpenFileSet.delete(info.path); - this.logConfigFileWatch(configFileName, configFilePresenceInfo, ConfigFileWatcherStatus.TrackingFileRemoved); + // Set this as not inferred root + if (configFilePresenceInfo.trackingOpenFilesMap.has(info.path)) { + configFilePresenceInfo.trackingOpenFilesMap.set(info.path, false); + this.logConfigFileWatchUpdate(configFileName, configFilePresenceInfo, ConfigFileWatcherStatus.InferredRootRemoved); } + + // Close the watcher if present + this.closeConfigFileWatcherIfInferredRoot(configFileName, canonicalConfigFilePath, + configFilePresenceInfo, /*infoIsInferredRoot*/ true, reason + ); } } - private stopWatchingConfigFileForScriptInfo(info: ScriptInfo) { - Debug.assert(!info.isScriptOpen()); - let current = info.fileName; - let currentPath = getDirectoryPath(info.path); - let parentPath = getDirectoryPath(currentPath); - while (currentPath !== parentPath) { - current = asNormalizedPath(getDirectoryPath(current)); - this.closeConfigFileWatchForScriptInfo(asNormalizedPath(combinePaths(current, "tsconfig.json")), combinePaths(currentPath, "tsconfig.json"), info); - this.closeConfigFileWatchForScriptInfo(asNormalizedPath(combinePaths(current, "jsconfig.json")), combinePaths(currentPath, "jsconfig.json"), info); - currentPath = parentPath; - parentPath = getDirectoryPath(parentPath); - } + /** + * This is called by inferred project whenever root script info is removed from it + */ + /* @internal */ + stopWatchingConfigFilesForInferredProjectRoot(info: ScriptInfo, reason: WatcherCloseReason) { + this.enumerateConfigFileLocations(info, (configFileName, canonicalConfigFilePath) => + this.closeWatchConfigFileForInferredProjectRoot(configFileName, canonicalConfigFilePath, info, reason) + ); } /** @@ -982,22 +1083,22 @@ namespace ts.server { * The server must start searching from the directory containing * the newly opened file. */ - private getConfigFileNameForFile(info: ScriptInfo, projectRootPath?: NormalizedPath) { - Debug.assert(info.isScriptOpen()); - let searchPath = getDirectoryPath(info.fileName); - this.logger.info(`Search path: ${searchPath}`); + private enumerateConfigFileLocations(info: ScriptInfo, + action: (configFileName: NormalizedPath, canonicalConfigFilePath: string) => boolean | void, + projectRootPath?: NormalizedPath) { + let searchPath = asNormalizedPath(getDirectoryPath(info.fileName)); - // check if this file is already included in one of external projects while (!projectRootPath || searchPath.indexOf(projectRootPath) >= 0) { + const canonicalSearchPath = normalizedPathToPath(searchPath, this.currentDirectory, this.toCanonicalFileName); const tsconfigFileName = asNormalizedPath(combinePaths(searchPath, "tsconfig.json")); - if (this.configFileExists(tsconfigFileName, info)) { - this.logger.info(`Config file name: ${tsconfigFileName}`); + let result = action(tsconfigFileName, combinePaths(canonicalSearchPath, "tsconfig.json")); + if (result) { return tsconfigFileName; } const jsconfigFileName = asNormalizedPath(combinePaths(searchPath, "jsconfig.json")); - if (this.configFileExists(jsconfigFileName, info)) { - this.logger.info(`Config file name: ${jsconfigFileName}`); + result = action(jsconfigFileName, combinePaths(canonicalSearchPath, "jsconfig.json")); + if (result) { return jsconfigFileName; } @@ -1007,10 +1108,35 @@ namespace ts.server { } searchPath = parentPath; } - this.logger.info("No config files found."); + return undefined; } + /** + * This function tries to search for a tsconfig.json for the given file. + * This is different from the method the compiler uses because + * the compiler can assume it will always start searching in the + * current directory (the directory in which tsc was invoked). + * The server must start searching from the directory containing + * the newly opened file. + */ + private getConfigFileNameForFile(info: ScriptInfo, projectRootPath?: NormalizedPath) { + Debug.assert(info.isScriptOpen()); + this.logger.info(`Search path: ${getDirectoryPath(info.fileName)}`); + const configFileName = this.enumerateConfigFileLocations(info, + (configFileName: NormalizedPath, canonicalConfigFilePath: string) => + this.configFileExists(configFileName, canonicalConfigFilePath, info), + projectRootPath + ); + if (configFileName) { + this.logger.info(`For info: ${info.fileName} :: Config file name: ${configFileName}`); + } + else { + this.logger.info(`For info: ${info.fileName} :: No config files found.`); + } + return configFileName; + } + private printProjects() { if (!this.logger.hasLevel(LogLevel.verbose)) { return; @@ -1045,6 +1171,10 @@ namespace ts.server { private findConfiguredProjectByProjectName(configFileName: NormalizedPath) { // make sure that casing of config file name is consistent const canonicalConfigFilePath = asNormalizedPath(this.toCanonicalFileName(configFileName)); + return this.getConfiguredProjectByCanonicalConfigFilePath(canonicalConfigFilePath); + } + + private getConfiguredProjectByCanonicalConfigFilePath(canonicalConfigFilePath: string) { return this.configuredProjects.get(canonicalConfigFilePath); } @@ -1497,11 +1627,14 @@ namespace ts.server { this.refreshInferredProjects(); } - delayReloadConfiguredProjectForFiles(openFileSet: Map) { - if (openFileSet) { - const openFiles = arrayFrom(openFileSet.keys(), path => this.getScriptInfoForPath(path as Path)); - this.reloadConfiguredProjectForFiles(openFiles, /*delayReload*/ true); - } + delayReloadConfiguredProjectForFiles(openFilesMap: Map, ignoreIfNotInferredProjectRoot: boolean) { + // Get open files to reload projects for + const openFiles = flatMapIter(openFilesMap.keys(), path => { + if (!ignoreIfNotInferredProjectRoot || openFilesMap.get(path)) { + return this.getScriptInfoForPath(path as Path); + } + }); + this.reloadConfiguredProjectForFiles(openFiles, /*delayReload*/ true); this.delayInferredProjectsRefresh(); } diff --git a/src/server/project.ts b/src/server/project.ts index 774865b335edc..d45f2e99be4da 100644 --- a/src/server/project.ts +++ b/src/server/project.ts @@ -924,6 +924,7 @@ namespace ts.server { } addRoot(info: ScriptInfo) { + this.projectService.startWatchingConfigFilesForInferredProjectRoot(info); if (!this._isJsInferredProject && info.isJavaScript()) { this.toggleJsInferredProject(/*isJsInferredProject*/ true); } @@ -931,6 +932,7 @@ namespace ts.server { } removeRoot(info: ScriptInfo) { + this.projectService.stopWatchingConfigFilesForInferredProjectRoot(info, WatcherCloseReason.NotNeeded); super.removeRoot(info); if (this._isJsInferredProject && info.isJavaScript()) { if (!some(this.getRootScriptInfos(), info => info.isJavaScript())) { @@ -955,6 +957,7 @@ namespace ts.server { } close() { + forEach(this.getRootScriptInfos(), info => this.projectService.stopWatchingConfigFilesForInferredProjectRoot(info, WatcherCloseReason.ProjectClose)); super.close(); } From 802e283aa7252b6ae4ddcd78dc2c88c754893361 Mon Sep 17 00:00:00 2001 From: Sheetal Nandi Date: Tue, 18 Jul 2017 17:04:29 -0700 Subject: [PATCH 029/109] Refactoring of the builder --- src/compiler/utilities.ts | 8 +- src/server/builder.ts | 464 +++++++++++++++-------------------- src/server/editorServices.ts | 5 + src/server/project.ts | 23 +- src/server/session.ts | 4 +- src/server/utilities.ts | 14 +- 6 files changed, 225 insertions(+), 293 deletions(-) diff --git a/src/compiler/utilities.ts b/src/compiler/utilities.ts index 76d242e8b08d8..088733e21ef6b 100644 --- a/src/compiler/utilities.ts +++ b/src/compiler/utilities.ts @@ -1394,6 +1394,10 @@ namespace ts { return SpecialPropertyAssignmentKind.None; } + export function isModuleWithStringLiteralName(node: Node): node is ModuleDeclaration { + return isModuleDeclaration(node) && node.name.kind === SyntaxKind.StringLiteral; + } + export function getExternalModuleName(node: Node): Expression { if (node.kind === SyntaxKind.ImportDeclaration) { return (node).moduleSpecifier; @@ -1407,8 +1411,8 @@ namespace ts { if (node.kind === SyntaxKind.ExportDeclaration) { return (node).moduleSpecifier; } - if (node.kind === SyntaxKind.ModuleDeclaration && (node).name.kind === SyntaxKind.StringLiteral) { - return (node).name; + if (isModuleWithStringLiteralName(node)) { + return node.name; } } diff --git a/src/server/builder.ts b/src/server/builder.ts index 8a10682b4cc22..285ab9ece6025 100644 --- a/src/server/builder.ts +++ b/src/server/builder.ts @@ -3,152 +3,105 @@ /// namespace ts.server { - export function shouldEmitFile(scriptInfo: ScriptInfo) { return !scriptInfo.hasMixedContent; } - /** - * An abstract file info that maintains a shape signature. - */ - export class BuilderFileInfo { - - private lastCheckedShapeSignature: string; - - constructor(public readonly scriptInfo: ScriptInfo, public readonly project: Project) { - } - - public isExternalModuleOrHasOnlyAmbientExternalModules() { - const sourceFile = this.getSourceFile(); - return isExternalModule(sourceFile) || this.containsOnlyAmbientModules(sourceFile); - } - + export interface Builder { /** - * For script files that contains only ambient external modules, although they are not actually external module files, - * they can only be consumed via importing elements from them. Regular script files cannot consume them. Therefore, - * there are no point to rebuild all script files if these special files have changed. However, if any statement - * in the file is not ambient external module, we treat it as a regular script file. + * This is the callback when file infos in the builder are updated */ - private containsOnlyAmbientModules(sourceFile: SourceFile) { - for (const statement of sourceFile.statements) { - if (statement.kind !== SyntaxKind.ModuleDeclaration || (statement).name.kind !== SyntaxKind.StringLiteral) { - return false; - } - } - return true; - } - - private computeHash(text: string): string { - return this.project.projectService.host.createHash(text); - } - - private getSourceFile(): SourceFile { - return this.project.getSourceFile(this.scriptInfo.path); - } - + onProjectUpdateGraph(): void; + getFilesAffectedBy(scriptInfo: ScriptInfo): string[]; /** - * @return {boolean} indicates if the shape signature has changed since last update. + * @returns {boolean} whether the emit was conducted or not */ - public updateShapeSignature() { - const sourceFile = this.getSourceFile(); - if (!sourceFile) { - return true; - } - - const lastSignature = this.lastCheckedShapeSignature; - if (sourceFile.isDeclarationFile) { - this.lastCheckedShapeSignature = this.computeHash(sourceFile.text); - } - else { - const emitOutput = this.project.getFileEmitOutput(this.scriptInfo, /*emitOnlyDtsFiles*/ true); - if (emitOutput.outputFiles && emitOutput.outputFiles.length > 0) { - this.lastCheckedShapeSignature = this.computeHash(emitOutput.outputFiles[0].text); - } - } - return !lastSignature || this.lastCheckedShapeSignature !== lastSignature; - } - } - - export interface Builder { - readonly project: Project; - getFilesAffectedBy(scriptInfo: ScriptInfo): string[]; - onProjectUpdateGraph(): void; emitFile(scriptInfo: ScriptInfo, writeFile: (path: string, data: string, writeByteOrderMark?: boolean) => void): boolean; clear(): void; } - abstract class AbstractBuilder implements Builder { - + interface EmitHandler { + addScriptInfo(scriptInfo: ScriptInfo): void; + removeScriptInfo(path: Path): void; + updateScriptInfo(scriptInfo: ScriptInfo): void; /** - * stores set of files from the project. - * NOTE: this field is created on demand and should not be accessed directly. - * Use 'getFileInfos' instead. + * Gets the files affected by the script info which has updated shape from the known one */ - private fileInfos_doNotAccessDirectly: Map; - - constructor(public readonly project: Project, private ctor: { new (scriptInfo: ScriptInfo, project: Project): T }) { - } - - private getFileInfos() { - return this.fileInfos_doNotAccessDirectly || (this.fileInfos_doNotAccessDirectly = createMap()); - } - - protected hasFileInfos() { - return !!this.fileInfos_doNotAccessDirectly; - } + getFilesAffectedByUpdatedShape(scriptInfo: ScriptInfo, singleFileResult: string[]): string[]; + } - public clear() { - // drop the existing list - it will be re-created as necessary - this.fileInfos_doNotAccessDirectly = undefined; - } + export function createBuilder(project: Project): Builder { + let isModuleEmit: boolean | undefined; + let projectVersionForDependencyGraph: string; + // Last checked shape signature for the file info + let fileInfos: Map; + let emitHandler: EmitHandler; + return { + onProjectUpdateGraph, + getFilesAffectedBy, + emitFile, + clear + }; + + function createProjectGraph() { + const currentIsModuleEmit = project.getCompilerOptions().module !== ModuleKind.None; + if (isModuleEmit !== currentIsModuleEmit) { + isModuleEmit = currentIsModuleEmit; + emitHandler = isModuleEmit ? getModuleEmitHandler() : getNonModuleEmitHandler(); + fileInfos = undefined; + } - protected getFileInfo(path: Path): T { - return this.getFileInfos().get(path); + fileInfos = mutateExistingMap( + fileInfos, arrayToMap(project.getScriptInfos(), info => info.path), + (_path, info) => { + emitHandler.addScriptInfo(info); + return ""; + }, + (path: Path, _value) => emitHandler.removeScriptInfo(path), + /*isSameValue*/ undefined, + /*OnDeleteExistingMismatchValue*/ undefined, + (_prevValue, scriptInfo) => emitHandler.updateScriptInfo(scriptInfo) + ); + projectVersionForDependencyGraph = project.getProjectVersion(); } - protected getOrCreateFileInfo(path: Path): T { - let fileInfo = this.getFileInfo(path); - if (!fileInfo) { - const scriptInfo = this.project.getScriptInfo(path); - fileInfo = new this.ctor(scriptInfo, this.project); - this.setFileInfo(path, fileInfo); + function ensureFileInfos() { + if (!emitHandler) { + createProjectGraph(); } - return fileInfo; + Debug.assert(projectVersionForDependencyGraph === project.getProjectVersion()); } - protected getFileInfoPaths(): Path[] { - return arrayFrom(this.getFileInfos().keys() as Iterator); + function onProjectUpdateGraph() { + if (emitHandler) { + createProjectGraph(); + } } - protected setFileInfo(path: Path, info: T) { - this.getFileInfos().set(path, info); - } + function getFilesAffectedBy(scriptInfo: ScriptInfo): string[] { + ensureFileInfos(); - protected removeFileInfo(path: Path) { - this.getFileInfos().delete(path); - } + const singleFileResult = scriptInfo.hasMixedContent ? [] : [scriptInfo.fileName]; + const path = scriptInfo.path; + if (!fileInfos || !fileInfos.has(path) || !updateShapeSignature(scriptInfo)) { + return singleFileResult; + } - protected forEachFileInfo(action: (fileInfo: T) => any) { - this.getFileInfos().forEach(action); + return emitHandler.getFilesAffectedByUpdatedShape(scriptInfo, singleFileResult); } - abstract getFilesAffectedBy(scriptInfo: ScriptInfo): string[]; - abstract onProjectUpdateGraph(): void; - protected abstract ensureFileInfoIfInProject(scriptInfo: ScriptInfo): void; - /** * @returns {boolean} whether the emit was conducted or not */ - emitFile(scriptInfo: ScriptInfo, writeFile: (path: string, data: string, writeByteOrderMark?: boolean) => void): boolean { - this.ensureFileInfoIfInProject(scriptInfo); - const fileInfo = this.getFileInfo(scriptInfo.path); - if (!fileInfo) { + function emitFile(scriptInfo: ScriptInfo, writeFile: (path: string, data: string, writeByteOrderMark?: boolean) => void): boolean { + ensureFileInfos(); + if (!fileInfos || !fileInfos.has(scriptInfo.path)) { return false; } - const { emitSkipped, outputFiles } = this.project.getFileEmitOutput(fileInfo.scriptInfo, /*emitOnlyDtsFiles*/ false); + const { emitSkipped, outputFiles } = project.getFileEmitOutput(scriptInfo, /*emitOnlyDtsFiles*/ false); if (!emitSkipped) { - const projectRootPath = this.project.getProjectRootPath(); + const projectRootPath = project.getProjectRootPath(); for (const outputFile of outputFiles) { const outputFileAbsoluteFileName = getNormalizedAbsolutePath(outputFile.name, projectRootPath ? projectRootPath : getDirectoryPath(scriptInfo.fileName)); writeFile(outputFileAbsoluteFileName, outputFile.text, outputFile.writeByteOrderMark); @@ -156,204 +109,175 @@ namespace ts.server { } return !emitSkipped; } - } - class NonModuleBuilder extends AbstractBuilder { + function clear() { + isModuleEmit = undefined; + emitHandler = undefined; + fileInfos = undefined; + projectVersionForDependencyGraph = undefined; + } - constructor(public readonly project: Project) { - super(project, BuilderFileInfo); + function getSourceFile(path: Path) { + return project.getSourceFile(path); } - protected ensureFileInfoIfInProject(scriptInfo: ScriptInfo) { - if (this.project.containsScriptInfo(scriptInfo)) { - this.getOrCreateFileInfo(scriptInfo.path); - } + function getScriptInfo(path: Path) { + return project.projectService.getScriptInfoForPath(path); } - onProjectUpdateGraph() { - if (this.hasFileInfos()) { - this.forEachFileInfo(fileInfo => { - if (!this.project.containsScriptInfo(fileInfo.scriptInfo)) { - // This file was deleted from this project - this.removeFileInfo(fileInfo.scriptInfo.path); - } - }); - } + function isExternalModuleOrHasOnlyAmbientExternalModules(sourceFile: SourceFile) { + return sourceFile && (isExternalModule(sourceFile) || containsOnlyAmbientModules(sourceFile)); } /** - * Note: didn't use path as parameter because the returned file names will be directly - * consumed by the API user, which will use it to interact with file systems. Path - * should only be used internally, because the case sensitivity is not trustable. + * For script files that contains only ambient external modules, although they are not actually external module files, + * they can only be consumed via importing elements from them. Regular script files cannot consume them. Therefore, + * there are no point to rebuild all script files if these special files have changed. However, if any statement + * in the file is not ambient external module, we treat it as a regular script file. */ - getFilesAffectedBy(scriptInfo: ScriptInfo): string[] { - const info = this.getOrCreateFileInfo(scriptInfo.path); - const singleFileResult = scriptInfo.hasMixedContent ? [] : [scriptInfo.fileName]; - if (info.updateShapeSignature()) { - const options = this.project.getCompilerOptions(); - // If `--out` or `--outFile` is specified, any new emit will result in re-emitting the entire project, - // so returning the file itself is good enough. - if (options && (options.out || options.outFile)) { - return singleFileResult; + function containsOnlyAmbientModules(sourceFile: SourceFile) { + for (const statement of sourceFile.statements) { + if (!isModuleWithStringLiteralName(statement)) { + return false; } - return this.project.getAllEmittableFiles(); } - return singleFileResult; - } - } - - class ModuleBuilderFileInfo extends BuilderFileInfo { - references = createSortedArray(); - readonly referencedBy = createSortedArray(); - scriptVersionForReferences: string; - - static compareFileInfos(lf: ModuleBuilderFileInfo, rf: ModuleBuilderFileInfo): Comparison { - return compareStrings(lf.scriptInfo.fileName, rf.scriptInfo.fileName); - } - - addReferencedBy(fileInfo: ModuleBuilderFileInfo): void { - insertSorted(this.referencedBy, fileInfo, ModuleBuilderFileInfo.compareFileInfos); - } - - removeReferencedBy(fileInfo: ModuleBuilderFileInfo): void { - removeSorted(this.referencedBy, fileInfo, ModuleBuilderFileInfo.compareFileInfos); + return true; } - removeFileReferences() { - for (const reference of this.references) { - reference.removeReferencedBy(this); + /** + * @return {boolean} indicates if the shape signature has changed since last update. + */ + function updateShapeSignature(scriptInfo: ScriptInfo) { + const path = scriptInfo.path; + const sourceFile = getSourceFile(path); + if (!sourceFile) { + return true; } - clear(this.references); - } - } - - class ModuleBuilder extends AbstractBuilder { - - constructor(public readonly project: Project) { - super(project, ModuleBuilderFileInfo); - } - - private projectVersionForDependencyGraph: string; - - public clear() { - this.projectVersionForDependencyGraph = undefined; - super.clear(); - } - private getReferencedFileInfos(fileInfo: ModuleBuilderFileInfo): SortedArray { - if (!fileInfo.isExternalModuleOrHasOnlyAmbientExternalModules()) { - return createSortedArray(); + const prevSignature = fileInfos.get(path); + let latestSignature = prevSignature; + if (sourceFile.isDeclarationFile) { + latestSignature = computeHash(sourceFile.text); + fileInfos.set(path, latestSignature); + } + else { + const emitOutput = project.getFileEmitOutput(scriptInfo, /*emitOnlyDtsFiles*/ true); + if (emitOutput.outputFiles && emitOutput.outputFiles.length > 0) { + latestSignature = computeHash(emitOutput.outputFiles[0].text); + fileInfos.set(path, latestSignature); + } } - const referencedFilePaths = this.project.getReferencedFiles(fileInfo.scriptInfo.path); - return toSortedArray(referencedFilePaths.map(f => this.getOrCreateFileInfo(f)), ModuleBuilderFileInfo.compareFileInfos); + return !prevSignature || latestSignature !== prevSignature; } - protected ensureFileInfoIfInProject(_scriptInfo: ScriptInfo) { - this.ensureProjectDependencyGraphUpToDate(); + function computeHash(text: string) { + return project.projectService.host.createHash(text); } - onProjectUpdateGraph() { - // Update the graph only if we have computed graph earlier - if (this.hasFileInfos()) { - this.ensureProjectDependencyGraphUpToDate(); - } - } + function noop() { } - private ensureProjectDependencyGraphUpToDate() { - if (!this.projectVersionForDependencyGraph || this.project.getProjectVersion() !== this.projectVersionForDependencyGraph) { - const currentScriptInfos = this.project.getScriptInfos(); - for (const scriptInfo of currentScriptInfos) { - const fileInfo = this.getOrCreateFileInfo(scriptInfo.path); - this.updateFileReferences(fileInfo); + function getNonModuleEmitHandler(): EmitHandler { + return { + addScriptInfo: noop, + removeScriptInfo: noop, + updateScriptInfo: noop, + getFilesAffectedByUpdatedShape + }; + + function getFilesAffectedByUpdatedShape(_scriptInfo: ScriptInfo, singleFileResult: string[]): string[] { + const options = project.getCompilerOptions(); + // If `--out` or `--outFile` is specified, any new emit will result in re-emitting the entire project, + // so returning the file itself is good enough. + if (options && (options.out || options.outFile)) { + return singleFileResult; } - this.forEachFileInfo(fileInfo => { - if (!this.project.containsScriptInfo(fileInfo.scriptInfo)) { - // This file was deleted from this project - fileInfo.removeFileReferences(); - this.removeFileInfo(fileInfo.scriptInfo.path); - } - }); - this.projectVersionForDependencyGraph = this.project.getProjectVersion(); + return project.getAllEmittableFiles(); } } - private updateFileReferences(fileInfo: ModuleBuilderFileInfo) { - // Only need to update if the content of the file changed. - if (fileInfo.scriptVersionForReferences === fileInfo.scriptInfo.getLatestVersion()) { - return; + function getModuleEmitHandler(): EmitHandler { + const references = createMap>(); + const referencedBy = createMultiMap(); + const scriptVersionForReferences = createMap(); + return { + addScriptInfo, + removeScriptInfo, + updateScriptInfo, + getFilesAffectedByUpdatedShape + }; + + function setReferences(path: Path, latestVersion: string, existingMap: Map) { + existingMap = mutateExistingMapWithNewSet(existingMap, project.getReferencedFiles(path), + // Creating new Reference: Also add referenced by + key => { referencedBy.add(key, path); return true; }, + // Remove existing reference + (key, _existingValue) => { referencedBy.remove(key, path); } + ); + references.set(path, existingMap); + scriptVersionForReferences.set(path, latestVersion); } - const newReferences = this.getReferencedFileInfos(fileInfo); - const oldReferences = fileInfo.references; - enumerateInsertsAndDeletes(newReferences, oldReferences, - /*inserted*/ newReference => newReference.addReferencedBy(fileInfo), - /*deleted*/ oldReference => { - // New reference is greater then current reference. That means - // the current reference doesn't exist anymore after parsing. So delete - // references. - oldReference.removeReferencedBy(fileInfo); - }, - /*compare*/ ModuleBuilderFileInfo.compareFileInfos); - - fileInfo.references = newReferences; - fileInfo.scriptVersionForReferences = fileInfo.scriptInfo.getLatestVersion(); - } - - getFilesAffectedBy(scriptInfo: ScriptInfo): string[] { - this.ensureProjectDependencyGraphUpToDate(); + function addScriptInfo(info: ScriptInfo) { + setReferences(info.path, info.getLatestVersion(), undefined); + } - const singleFileResult = scriptInfo.hasMixedContent ? [] : [scriptInfo.fileName]; - const fileInfo = this.getFileInfo(scriptInfo.path); - if (!fileInfo || !fileInfo.updateShapeSignature()) { - return singleFileResult; + function removeScriptInfo(path: Path) { + references.delete(path); + scriptVersionForReferences.delete(path); } - if (!fileInfo.isExternalModuleOrHasOnlyAmbientExternalModules()) { - return this.project.getAllEmittableFiles(); + function updateScriptInfo(scriptInfo: ScriptInfo) { + const path = scriptInfo.path; + const lastUpdatedVersion = scriptVersionForReferences.get(path); + const latestVersion = scriptInfo.getLatestVersion(); + if (lastUpdatedVersion !== latestVersion) { + setReferences(path, latestVersion, references.get(path)); + } } - const options = this.project.getCompilerOptions(); - if (options && (options.isolatedModules || options.out || options.outFile)) { - return singleFileResult; + function getReferencedByPaths(path: Path) { + return referencedBy.get(path) || []; } - // Now we need to if each file in the referencedBy list has a shape change as well. - // Because if so, its own referencedBy files need to be saved as well to make the - // emitting result consistent with files on disk. - - // Use slice to clone the array to avoid manipulating in place - const queue = fileInfo.referencedBy.slice(0); - const fileNameSet = createMap(); - fileNameSet.set(scriptInfo.fileName, scriptInfo); - while (queue.length > 0) { - const processingFileInfo = queue.pop(); - if (processingFileInfo.updateShapeSignature() && processingFileInfo.referencedBy.length > 0) { - for (const potentialFileInfo of processingFileInfo.referencedBy) { - if (!fileNameSet.has(potentialFileInfo.scriptInfo.fileName)) { - queue.push(potentialFileInfo); + function getFilesAffectedByUpdatedShape(scriptInfo: ScriptInfo, singleFileResult: string[]): string[] { + const path = scriptInfo.path; + const sourceFile = getSourceFile(path); + if (!isExternalModuleOrHasOnlyAmbientExternalModules(sourceFile)) { + return project.getAllEmittableFiles(); + } + + const options = project.getCompilerOptions(); + if (options && (options.isolatedModules || options.out || options.outFile)) { + return singleFileResult; + } + + // Now we need to if each file in the referencedBy list has a shape change as well. + // Because if so, its own referencedBy files need to be saved as well to make the + // emitting result consistent with files on disk. + + const fileNamesMap = createMap(); + const setFileName = (path: Path, scriptInfo: ScriptInfo) => { + fileNamesMap.set(path, scriptInfo && shouldEmitFile(scriptInfo) ? scriptInfo.fileName : undefined); + }; + + // Start with the paths this file was referenced by + setFileName(path, scriptInfo); + const queue = getReferencedByPaths(path).slice(); + while (queue.length > 0) { + const currentPath = queue.pop(); + if (!fileNamesMap.has(currentPath)) { + const currentScriptInfo = getScriptInfo(currentPath); + if (currentScriptInfo && updateShapeSignature(currentScriptInfo)) { + queue.push(...getReferencedByPaths(currentPath)); } + setFileName(currentPath, currentScriptInfo); } } - fileNameSet.set(processingFileInfo.scriptInfo.fileName, processingFileInfo.scriptInfo); - } - const result: string[] = []; - fileNameSet.forEach((scriptInfo, fileName) => { - if (shouldEmitFile(scriptInfo)) { - result.push(fileName); - } - }); - return result; - } - } - export function createBuilder(project: Project): Builder { - const moduleKind = project.getCompilerOptions().module; - switch (moduleKind) { - case ModuleKind.None: - return new NonModuleBuilder(project); - default: - return new ModuleBuilder(project); + // Return array of values that needs emit + return flatMapIter(fileNamesMap.values(), value => value); + } } } } diff --git a/src/server/editorServices.ts b/src/server/editorServices.ts index 003bb87f6a932..ad8e355a500f6 100644 --- a/src/server/editorServices.ts +++ b/src/server/editorServices.ts @@ -519,6 +519,11 @@ namespace ts.server { return scriptInfo && scriptInfo.getDefaultProject(); } + getScriptInfoEnsuringProjectsUptoDate(uncheckedFileName: string) { + this.ensureInferredProjectsUpToDate(); + return this.getScriptInfo(uncheckedFileName); + } + /** * Ensures the project structures are upto date * @param refreshInferredProjects when true updates the inferred projects even if there is no pending work diff --git a/src/server/project.ts b/src/server/project.ts index 62c450f9f4f90..73d7f2d501729 100644 --- a/src/server/project.ts +++ b/src/server/project.ts @@ -811,19 +811,19 @@ namespace ts.server { } } - getReferencedFiles(path: Path): Path[] { + getReferencedFiles(path: Path): Map { + const referencedFiles = createMap(); if (!this.languageServiceEnabled) { - return []; + return referencedFiles; } const sourceFile = this.getSourceFile(path); if (!sourceFile) { - return []; + return referencedFiles; } // We need to use a set here since the code can contain the same import twice, // but that will only be one dependency. // To avoid invernal conversion, the key of the referencedFiles map must be of type Path - const referencedFiles = createMap(); if (sourceFile.imports && sourceFile.imports.length > 0) { const checker: TypeChecker = this.program.getTypeChecker(); for (const importName of sourceFile.imports) { @@ -859,8 +859,7 @@ namespace ts.server { }); } - const allFileNames = arrayFrom(referencedFiles.keys()) as Path[]; - return filter(allFileNames, file => this.lsHost.host.fileExists(file)); + return referencedFiles; } // remove a root file from project @@ -1138,12 +1137,6 @@ namespace ts.server { watchWildcards(wildcardDirectories: Map) { this.directoriesWatchedForWildcards = mutateExistingMap( this.directoriesWatchedForWildcards, wildcardDirectories, - // Watcher is same if the recursive flags match - ({ recursive: existingRecursive }, flag) => { - // If the recursive dont match, it needs update - const recursive = (flag & WatchDirectoryFlags.Recursive) !== 0; - return existingRecursive !== recursive; - }, // Create new watch and recursive info (directory, flag) => { const recursive = (flag & WatchDirectoryFlags.Recursive) !== 0; @@ -1160,6 +1153,12 @@ namespace ts.server { (directory, { watcher, recursive }) => this.projectService.closeDirectoryWatcher( WatchType.WildCardDirectories, this, directory, watcher, recursive, WatcherCloseReason.NotNeeded ), + // Watcher is same if the recursive flags match + ({ recursive: existingRecursive }, flag) => { + // If the recursive dont match, it needs update + const recursive = (flag & WatchDirectoryFlags.Recursive) !== 0; + return existingRecursive !== recursive; + }, // Close existing watch that doesnt match in recursive flag (directory, { watcher, recursive }) => this.projectService.closeDirectoryWatcher( WatchType.WildCardDirectories, this, directory, watcher, recursive, WatcherCloseReason.RecursiveChanged diff --git a/src/server/session.ts b/src/server/session.ts index 0df2dd1e399aa..1e851d09615f1 100644 --- a/src/server/session.ts +++ b/src/server/session.ts @@ -1193,11 +1193,11 @@ namespace ts.server { } private getCompileOnSaveAffectedFileList(args: protocol.FileRequestArgs): protocol.CompileOnSaveAffectedFileListSingleProject[] { - const info = this.projectService.getScriptInfo(args.file); + const info = this.projectService.getScriptInfoEnsuringProjectsUptoDate(args.file); const result: protocol.CompileOnSaveAffectedFileListSingleProject[] = []; if (!info) { - return []; + return result; } // if specified a project, we only return affected file list in this project diff --git a/src/server/utilities.ts b/src/server/utilities.ts index 6c884cebf5930..36b769182e510 100644 --- a/src/server/utilities.ts +++ b/src/server/utilities.ts @@ -312,21 +312,18 @@ namespace ts.server { ): Map { return mutateExistingMap( existingMap, newMap, - // Same value if the value is set in the map - /*isSameValue*/(_existingValue, _valueInNewMap) => true, /*createNewValue*/(key, _valueInNewMap) => createNewValue(key), onDeleteExistingValue, - // Should never be called since we say yes to same values all the time - /*OnDeleteExistingMismatchValue*/(_key, _existingValue) => notImplemented() ); } export function mutateExistingMap( existingMap: Map, newMap: Map, - isSameValue: (existingValue: T, valueInNewMap: U) => boolean, createNewValue: (key: string, valueInNewMap: U) => T, onDeleteExistingValue: (key: string, existingValue: T) => void, - OnDeleteExistingMismatchValue: (key: string, existingValue: T) => void + isSameValue?: (existingValue: T, valueInNewMap: U) => boolean, + OnDeleteExistingMismatchValue?: (key: string, existingValue: T) => void, + onSameExistingValue?: (existingValue: T, valueInNewMap: U) => void ): Map { // If there are new values update them if (newMap) { @@ -340,10 +337,13 @@ namespace ts.server { onDeleteExistingValue(key, existingValue); } // different value - remove it - else if (!isSameValue(existingValue, valueInNewMap)) { + else if (isSameValue && !isSameValue(existingValue, valueInNewMap)) { existingMap.delete(key); OnDeleteExistingMismatchValue(key, existingValue); } + else if (onSameExistingValue) { + onSameExistingValue(existingValue, valueInNewMap); + } }); } else { From 499fabc2c1d827f9c468ee9ef9a48553300d2a31 Mon Sep 17 00:00:00 2001 From: Sheetal Nandi Date: Tue, 18 Jul 2017 17:34:56 -0700 Subject: [PATCH 030/109] Do not update graph in builder if compile on save is not on --- src/server/project.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/server/project.ts b/src/server/project.ts index 73d7f2d501729..483dfe841a700 100644 --- a/src/server/project.ts +++ b/src/server/project.ts @@ -594,7 +594,7 @@ namespace ts.server { // update builder only if language service is enabled // otherwise tell it to drop its internal state - if (this.languageServiceEnabled) { + if (this.languageServiceEnabled && this.compileOnSaveEnabled) { this.builder.onProjectUpdateGraph(); } else { From 273569f6fe85412470f80e745cd6ba985b275d99 Mon Sep 17 00:00:00 2001 From: Sheetal Nandi Date: Wed, 19 Jul 2017 13:07:55 -0700 Subject: [PATCH 031/109] Make the host cache store the fileName instead of undefined for the missing host files --- src/compiler/checker.ts | 2 +- src/compiler/commandLineParser.ts | 20 ++++----- src/compiler/core.ts | 15 +++++-- src/compiler/factory.ts | 8 ++-- src/compiler/moduleNameResolver.ts | 6 +-- src/compiler/program.ts | 2 +- src/compiler/sys.ts | 2 +- src/compiler/utilities.ts | 2 +- src/harness/fourslash.ts | 10 ++--- src/harness/harness.ts | 2 +- src/harness/projectsRunner.ts | 2 +- .../unittests/cachingInServerLSHost.ts | 3 +- .../unittests/configurationExtension.ts | 2 +- src/harness/unittests/telemetry.ts | 2 +- .../unittests/tsserverProjectSystem.ts | 10 ++--- src/server/client.ts | 2 +- src/server/editorServices.ts | 10 ++--- src/services/services.ts | 43 +++++++++++-------- src/services/shims.ts | 2 +- src/services/transpile.ts | 2 +- 20 files changed, 81 insertions(+), 66 deletions(-) diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts index a8af11e780305..a578b7208ae50 100644 --- a/src/compiler/checker.ts +++ b/src/compiler/checker.ts @@ -1161,7 +1161,7 @@ namespace ts { } function diagnosticName(nameArg: __String | Identifier) { - return typeof nameArg === "string" ? unescapeLeadingUnderscores(nameArg as __String) : declarationNameToString(nameArg as Identifier); + return isString(nameArg) ? unescapeLeadingUnderscores(nameArg as __String) : declarationNameToString(nameArg as Identifier); } function isTypeParameterSymbolDeclaredInContainer(symbol: Symbol, container: Node) { diff --git a/src/compiler/commandLineParser.ts b/src/compiler/commandLineParser.ts index efb574ac97107..5a12f3d0ce40e 100644 --- a/src/compiler/commandLineParser.ts +++ b/src/compiler/commandLineParser.ts @@ -880,7 +880,7 @@ namespace ts { */ export function readConfigFile(fileName: string, readFile: (path: string) => string | undefined): { config?: any; error?: Diagnostic } { const textOrDiagnostic = tryReadFile(fileName, readFile); - return typeof textOrDiagnostic === "string" ? parseConfigFileTextToJson(fileName, textOrDiagnostic) : { config: {}, error: textOrDiagnostic }; + return isString(textOrDiagnostic) ? parseConfigFileTextToJson(fileName, textOrDiagnostic) : { config: {}, error: textOrDiagnostic }; } /** @@ -902,7 +902,7 @@ namespace ts { */ export function readJsonConfigFile(fileName: string, readFile: (path: string) => string | undefined): JsonSourceFile { const textOrDiagnostic = tryReadFile(fileName, readFile); - return typeof textOrDiagnostic === "string" ? parseJsonText(fileName, textOrDiagnostic) : { parseDiagnostics: [textOrDiagnostic] }; + return isString(textOrDiagnostic) ? parseJsonText(fileName, textOrDiagnostic) : { parseDiagnostics: [textOrDiagnostic] }; } function tryReadFile(fileName: string, readFile: (path: string) => string | undefined): string | Diagnostic { @@ -1106,9 +1106,9 @@ namespace ts { if (!isDoubleQuotedString(valueExpression)) { errors.push(createDiagnosticForNodeInSourceFile(sourceFile, valueExpression, Diagnostics.String_literal_with_double_quotes_expected)); } - reportInvalidOptionValue(option && (typeof option.type === "string" && option.type !== "string")); + reportInvalidOptionValue(option && (isString(option.type) && option.type !== "string")); const text = (valueExpression).text; - if (option && typeof option.type !== "string") { + if (option && !isString(option.type)) { const customOption = option; // Validate custom option type if (!customOption.type.has(text)) { @@ -1179,7 +1179,7 @@ namespace ts { function getCompilerOptionValueTypeString(option: CommandLineOption) { return option.type === "list" ? "Array" : - typeof option.type === "string" ? option.type : "string"; + isString(option.type) ? option.type : "string"; } function isCompilerOptionsValue(option: CommandLineOption, value: any): value is CompilerOptionsValue { @@ -1187,7 +1187,7 @@ namespace ts { if (option.type === "list") { return isArray(value); } - const expectedType = typeof option.type === "string" ? option.type : "string"; + const expectedType = isString(option.type) ? option.type : "string"; return typeof value === expectedType; } } @@ -1571,7 +1571,7 @@ namespace ts { let extendedConfigPath: Path; if (json.extends) { - if (typeof json.extends !== "string") { + if (!isString(json.extends)) { errors.push(createCompilerDiagnostic(Diagnostics.Compiler_option_0_requires_a_value_of_type_1, "extends", "string")); } else { @@ -1796,7 +1796,7 @@ namespace ts { if (optType === "list" && isArray(value)) { return convertJsonOptionOfListType(opt, value, basePath, errors); } - else if (typeof optType !== "string") { + else if (!isString(optType)) { return convertJsonOptionOfCustomType(opt, value, errors); } return normalizeNonListOptionValue(opt, basePath, value); @@ -1809,12 +1809,12 @@ namespace ts { function normalizeOptionValue(option: CommandLineOption, basePath: string, value: any): CompilerOptionsValue { if (option.type === "list") { const listOption = option; - if (listOption.element.isFilePath || typeof listOption.element.type !== "string") { + if (listOption.element.isFilePath || !isString(listOption.element.type)) { return filter(map(value, v => normalizeOptionValue(listOption.element, basePath, v)), v => !!v); } return value; } - else if (typeof option.type !== "string") { + else if (!isString(option.type)) { return option.type.get(value); } return normalizeNonListOptionValue(option, basePath, value); diff --git a/src/compiler/core.ts b/src/compiler/core.ts index 54b772cc29c4a..2bec904437e1f 100644 --- a/src/compiler/core.ts +++ b/src/compiler/core.ts @@ -1201,6 +1201,13 @@ namespace ts { return Array.isArray ? Array.isArray(value) : value instanceof Array; } + /** + * Tests whether a value is string + */ + export function isString(text: any): text is string { + return typeof text === "string"; + } + export function tryCast(value: TIn | undefined, test: (value: TIn) => value is TOut): TOut | undefined { return value !== undefined && test(value) ? value : undefined; } @@ -1454,16 +1461,16 @@ namespace ts { function compareMessageText(text1: string | DiagnosticMessageChain, text2: string | DiagnosticMessageChain): Comparison { while (text1 && text2) { // We still have both chains. - const string1 = typeof text1 === "string" ? text1 : text1.messageText; - const string2 = typeof text2 === "string" ? text2 : text2.messageText; + const string1 = isString(text1) ? text1 : text1.messageText; + const string2 = isString(text2) ? text2 : text2.messageText; const res = compareValues(string1, string2); if (res) { return res; } - text1 = typeof text1 === "string" ? undefined : text1.next; - text2 = typeof text2 === "string" ? undefined : text2.next; + text1 = isString(text1) ? undefined : text1.next; + text2 = isString(text2) ? undefined : text2.next; } if (!text1 && !text2) { diff --git a/src/compiler/factory.ts b/src/compiler/factory.ts index 34727e4c41422..ac889ba9c045d 100644 --- a/src/compiler/factory.ts +++ b/src/compiler/factory.ts @@ -81,7 +81,7 @@ namespace ts { if (typeof value === "boolean") { return value ? createTrue() : createFalse(); } - if (typeof value === "string") { + if (isString(value)) { return createStringLiteral(value); } return createLiteralFromNode(value); @@ -2130,7 +2130,7 @@ namespace ts { export function createCatchClause(variableDeclaration: string | VariableDeclaration, block: Block) { const node = createSynthesizedNode(SyntaxKind.CatchClause); - node.variableDeclaration = typeof variableDeclaration === "string" ? createVariableDeclaration(variableDeclaration) : variableDeclaration; + node.variableDeclaration = isString(variableDeclaration) ? createVariableDeclaration(variableDeclaration) : variableDeclaration; node.block = block; return node; } @@ -2438,11 +2438,11 @@ namespace ts { function asName(name: string | EntityName): EntityName; function asName(name: string | Identifier | ThisTypeNode): Identifier | ThisTypeNode; function asName(name: string | Identifier | BindingName | PropertyName | QualifiedName | ThisTypeNode) { - return typeof name === "string" ? createIdentifier(name) : name; + return isString(name) ? createIdentifier(name) : name; } function asExpression(value: string | number | Expression) { - return typeof value === "string" || typeof value === "number" ? createLiteral(value) : value; + return isString(value) || typeof value === "number" ? createLiteral(value) : value; } function asNodeArray(array: ReadonlyArray | undefined): NodeArray | undefined { diff --git a/src/compiler/moduleNameResolver.ts b/src/compiler/moduleNameResolver.ts index 513c741ba0960..027d7ca202535 100644 --- a/src/compiler/moduleNameResolver.ts +++ b/src/compiler/moduleNameResolver.ts @@ -68,7 +68,7 @@ namespace ts { } const fileName = jsonContent[fieldName]; - if (typeof fileName !== "string") { + if (!isString(fileName)) { if (state.traceEnabled) { trace(state.host, Diagnostics.Expected_type_of_0_field_in_package_json_to_be_string_got_1, fieldName, typeof fileName); } @@ -630,8 +630,8 @@ namespace ts { } if (matchedPattern) { - const matchedStar = typeof matchedPattern === "string" ? undefined : matchedText(matchedPattern, moduleName); - const matchedPatternText = typeof matchedPattern === "string" ? matchedPattern : patternText(matchedPattern); + const matchedStar = isString(matchedPattern) ? undefined : matchedText(matchedPattern, moduleName); + const matchedPatternText = isString(matchedPattern) ? matchedPattern : patternText(matchedPattern); if (state.traceEnabled) { trace(state.host, Diagnostics.Module_name_0_matched_pattern_1, moduleName, matchedPatternText); } diff --git a/src/compiler/program.ts b/src/compiler/program.ts index 64e8eb0c80392..5999329a14cae 100644 --- a/src/compiler/program.ts +++ b/src/compiler/program.ts @@ -337,7 +337,7 @@ namespace ts { } export function flattenDiagnosticMessageText(messageText: string | DiagnosticMessageChain, newLine: string): string { - if (typeof messageText === "string") { + if (isString(messageText)) { return messageText; } else { diff --git a/src/compiler/sys.ts b/src/compiler/sys.ts index 89cfb074bffed..8533ec95f3d66 100644 --- a/src/compiler/sys.ts +++ b/src/compiler/sys.ts @@ -184,7 +184,7 @@ namespace ts { function fileEventHandler(eventName: string, relativeFileName: string, baseDirPath: string) { // When files are deleted from disk, the triggered "rename" event would have a relativefileName of "undefined" - const fileName = typeof relativeFileName !== "string" + const fileName = !isString(relativeFileName) ? undefined : ts.getNormalizedAbsolutePath(relativeFileName, baseDirPath); // Some applications save a working file via rename operations diff --git a/src/compiler/utilities.ts b/src/compiler/utilities.ts index 088733e21ef6b..facb0bdc2ee6c 100644 --- a/src/compiler/utilities.ts +++ b/src/compiler/utilities.ts @@ -355,7 +355,7 @@ namespace ts { } export function getTextOfConstantValue(value: string | number) { - return typeof value === "string" ? '"' + escapeNonAsciiString(value) + '"' : "" + value; + return isString(value) ? '"' + escapeNonAsciiString(value) + '"' : "" + value; } // Add an extra underscore to identifiers that start with two underscores to avoid issues with magic names like '__proto__' diff --git a/src/harness/fourslash.ts b/src/harness/fourslash.ts index c6ec0f257b606..cd501b4395906 100644 --- a/src/harness/fourslash.ts +++ b/src/harness/fourslash.ts @@ -391,7 +391,7 @@ namespace FourSlash { // Entry points from fourslash.ts public goToMarker(name: string | Marker = "") { - const marker = typeof name === "string" ? this.getMarkerByName(name) : name; + const marker = ts.isString(name) ? this.getMarkerByName(name) : name; if (this.activeFile.fileName !== marker.fileName) { this.openFile(marker.fileName); } @@ -400,7 +400,7 @@ namespace FourSlash { if (marker.position === -1 || marker.position > content.length) { throw new Error(`Marker "${name}" has been invalidated by unrecoverable edits to the file.`); } - const mName = typeof name === "string" ? name : this.markerName(marker); + const mName = ts.isString(name) ? name : this.markerName(marker); this.lastKnownMarker = mName; this.goToPosition(marker.position); } @@ -1028,7 +1028,7 @@ namespace FourSlash { public verifyNoReferences(markerNameOrRange?: string | Range) { if (markerNameOrRange) { - if (typeof markerNameOrRange === "string") { + if (ts.isString(markerNameOrRange)) { this.goToMarker(markerNameOrRange); } else { @@ -1524,7 +1524,7 @@ namespace FourSlash { resultString += "Diagnostics:" + Harness.IO.newLine(); const diagnostics = ts.getPreEmitDiagnostics(this.languageService.getProgram()); for (const diagnostic of diagnostics) { - if (typeof diagnostic.messageText !== "string") { + if (!ts.isString(diagnostic.messageText)) { let chainedMessage = diagnostic.messageText; let indentation = " "; while (chainedMessage) { @@ -2858,7 +2858,7 @@ namespace FourSlash { result = this.testData.files[index]; } } - else if (typeof indexOrName === "string") { + else if (ts.isString(indexOrName)) { let name = indexOrName; // names are stored in the compiler with this relative path, this allows people to use goTo.file on just the fileName diff --git a/src/harness/harness.ts b/src/harness/harness.ts index 55a8f3ebb4d71..8f4dae7011602 100644 --- a/src/harness/harness.ts +++ b/src/harness/harness.ts @@ -225,7 +225,7 @@ namespace Utils { return JSON.stringify(file, (_, v) => isNodeOrArray(v) ? serializeNode(v) : v, " "); function getKindName(k: number | string): string { - if (typeof k === "string") { + if (ts.isString(k)) { return k; } diff --git a/src/harness/projectsRunner.ts b/src/harness/projectsRunner.ts index 7344c432b97ff..fd8d06427cfcb 100644 --- a/src/harness/projectsRunner.ts +++ b/src/harness/projectsRunner.ts @@ -254,7 +254,7 @@ class ProjectRunner extends RunnerBase { if (option) { const optType = option.type; let value = testCase[name]; - if (typeof optType !== "string") { + if (!ts.isString(optType)) { const key = value.toLowerCase(); const optTypeValue = optType.get(key); if (optTypeValue) { diff --git a/src/harness/unittests/cachingInServerLSHost.ts b/src/harness/unittests/cachingInServerLSHost.ts index eb2907e89dea6..5265a12355e52 100644 --- a/src/harness/unittests/cachingInServerLSHost.ts +++ b/src/harness/unittests/cachingInServerLSHost.ts @@ -199,7 +199,8 @@ namespace ts { let diags = project.getLanguageService().getSemanticDiagnostics(root.name); assert.isTrue(fileExistsCalledForBar, "'fileExists' should be called"); assert.isTrue(diags.length === 1, "one diagnostic expected"); - assert.isTrue(typeof diags[0].messageText === "string" && ((diags[0].messageText).indexOf("Cannot find module") === 0), "should be 'cannot find module' message"); + const messageText = diags[0].messageText; + assert.isTrue(isString(messageText) && messageText.indexOf("Cannot find module") === 0, "should be 'cannot find module' message"); fileMap.set(imported.name, imported); fileExistsCalledForBar = false; diff --git a/src/harness/unittests/configurationExtension.ts b/src/harness/unittests/configurationExtension.ts index 2d50d2cb2af97..f3a5e930457e2 100644 --- a/src/harness/unittests/configurationExtension.ts +++ b/src/harness/unittests/configurationExtension.ts @@ -87,7 +87,7 @@ namespace ts { "/dev/tests/scenarios/first.json": "", "/dev/tests/baselines/first/output.ts": "" }); - const testContents = mapEntries(testContentsJson, (k, v) => [k, typeof v === "string" ? v : JSON.stringify(v)]); + const testContents = mapEntries(testContentsJson, (k, v) => [k, isString(v) ? v : JSON.stringify(v)]); const caseInsensitiveBasePath = "c:/dev/"; const caseInsensitiveHost = new Utils.MockParseConfigHost(caseInsensitiveBasePath, /*useCaseSensitiveFileNames*/ false, mapEntries(testContents, (key, content) => [`c:${key}`, content])); diff --git a/src/harness/unittests/telemetry.ts b/src/harness/unittests/telemetry.ts index 7f3428c839324..a856250307475 100644 --- a/src/harness/unittests/telemetry.ts +++ b/src/harness/unittests/telemetry.ts @@ -282,7 +282,7 @@ namespace ts.projectSystem { } function makeFile(path: string, content: {} = ""): projectSystem.FileOrFolder { - return { path, content: typeof content === "string" ? "" : JSON.stringify(content) }; + return { path, content: isString(content) ? "" : JSON.stringify(content) }; } function fileStats(nonZeroStats: Partial): server.FileStats { diff --git a/src/harness/unittests/tsserverProjectSystem.ts b/src/harness/unittests/tsserverProjectSystem.ts index fa6ffc74e6557..ca86138803cb0 100644 --- a/src/harness/unittests/tsserverProjectSystem.ts +++ b/src/harness/unittests/tsserverProjectSystem.ts @@ -101,7 +101,7 @@ namespace ts.projectSystem { } addPostExecAction(stdout: string | string[], cb: TI.RequestCompletedAction) { - const out = typeof stdout === "string" ? stdout : createNpmPackageJsonString(stdout); + const out = isString(stdout) ? stdout : createNpmPackageJsonString(stdout); const action: PostExecAction = { success: !!out, callback: cb @@ -258,7 +258,7 @@ namespace ts.projectSystem { } export function isFile(s: FSEntry): s is File { - return s && typeof (s).content === "string"; + return s && isString((s).content); } function invokeDirectoryWatcher(callbacks: DirectoryWatcherCallback[], getRelativeFilePath: () => string) { @@ -413,7 +413,7 @@ namespace ts.projectSystem { const currentEntry = this.fs.get(path); if (currentEntry) { if (isFile(currentEntry)) { - if (typeof fileOrFolder.content === "string") { + if (isString(fileOrFolder.content)) { // Update file if (currentEntry.content !== fileOrFolder.content) { currentEntry.content = fileOrFolder.content; @@ -426,7 +426,7 @@ namespace ts.projectSystem { } else { // Folder - if (typeof fileOrFolder.content === "string") { + if (isString(fileOrFolder.content)) { // TODO: Changing from folder => file } else { @@ -453,7 +453,7 @@ namespace ts.projectSystem { } ensureFileOrFolder(fileOrFolder: FileOrFolder) { - if (typeof fileOrFolder.content === "string") { + if (isString(fileOrFolder.content)) { const file = this.toFile(fileOrFolder); Debug.assert(!this.fs.get(file.path)); const baseFolder = this.ensureFolder(getDirectoryPath(file.fullPath)); diff --git a/src/server/client.ts b/src/server/client.ts index a7e0615dce866..4d23ca058058a 100644 --- a/src/server/client.ts +++ b/src/server/client.ts @@ -342,7 +342,7 @@ namespace ts.server { convertDiagnostic(entry: protocol.DiagnosticWithLinePosition, _fileName: string): Diagnostic { let category: DiagnosticCategory; for (const id in DiagnosticCategory) { - if (typeof id === "string" && entry.category === id.toLowerCase()) { + if (isString(id) && entry.category === id.toLowerCase()) { category = (DiagnosticCategory)[id]; } } diff --git a/src/server/editorServices.ts b/src/server/editorServices.ts index ad8e355a500f6..0b5cb8c119a82 100644 --- a/src/server/editorServices.ts +++ b/src/server/editorServices.ts @@ -159,7 +159,7 @@ namespace ts.server { }; export function convertFormatOptions(protocolOptions: protocol.FormatCodeSettings): FormatCodeSettings { - if (typeof protocolOptions.indentStyle === "string") { + if (isString(protocolOptions.indentStyle)) { protocolOptions.indentStyle = indentStyle.get(protocolOptions.indentStyle.toLowerCase()); Debug.assert(protocolOptions.indentStyle !== undefined); } @@ -169,7 +169,7 @@ namespace ts.server { export function convertCompilerOptions(protocolOptions: protocol.ExternalProjectCompilerOptions): CompilerOptions & protocol.CompileOnSaveMixin { compilerOptionConverters.forEach((mappedValues, id) => { const propertyValue = protocolOptions[id]; - if (typeof propertyValue === "string") { + if (isString(propertyValue)) { protocolOptions[id] = mappedValues.get(propertyValue.toLowerCase()); } }); @@ -177,9 +177,7 @@ namespace ts.server { } export function tryConvertScriptKindName(scriptKindName: protocol.ScriptKindName | ScriptKind): ScriptKind { - return typeof scriptKindName === "string" - ? convertScriptKindName(scriptKindName) - : scriptKindName; + return isString(scriptKindName) ? convertScriptKindName(scriptKindName) : scriptKindName; } export function convertScriptKindName(scriptKindName: protocol.ScriptKindName) { @@ -1947,7 +1945,7 @@ namespace ts.server { // RegExp group numbers are 1-based, but the first element in groups // is actually the original string, so it all works out in the end. if (typeof groupNumberOrString === "number") { - if (typeof groups[groupNumberOrString] !== "string") { + if (!isString(groups[groupNumberOrString])) { // Specification was wrong - exclude nothing! this.logger.info(`Incorrect RegExp specification in safelist rule ${name} - not enough groups`); // * can't appear in a filename; escape it because it's feeding into a RegExp diff --git a/src/services/services.ts b/src/services/services.ts index 2e031f3cad2f9..fb87c0065d55b 100644 --- a/src/services/services.ts +++ b/src/services/services.ts @@ -812,18 +812,21 @@ 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: Map; + private fileNameToEntry: Map; private _compilationSettings: CompilerOptions; private currentDirectory: string; constructor(private host: LanguageServiceHost, getCanonicalFileName: (fileName: string) => string) { // script id => script index this.currentDirectory = host.getCurrentDirectory(); - this.fileNameToEntry = createMap(); + this.fileNameToEntry = createMap(); // Initialize the list with the root file names const rootFileNames = host.getScriptFileNames(); @@ -840,7 +843,7 @@ namespace ts { } private createEntry(fileName: string, path: Path) { - let entry: HostFileInformation; + let entry: CachedHostFileInformation; const scriptSnapshot = this.host.getScriptSnapshot(fileName); if (scriptSnapshot) { entry = { @@ -850,36 +853,41 @@ namespace ts { scriptKind: getScriptKind(fileName, this.host) }; } + else { + entry = fileName; + } this.fileNameToEntry.set(path, entry); return entry; } - public getEntryByPath(path: Path): HostFileInformation { + public getEntryByPath(path: Path): CachedHostFileInformation | undefined { return this.fileNameToEntry.get(path); } - public containsEntryByPath(path: Path): boolean { - return this.fileNameToEntry.has(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 { - return this.containsEntryByPath(path) - ? this.getEntryByPath(path) - : this.createEntry(fileName, path); + const info = this.getEntryByPath(path) || this.createEntry(fileName, path); + return isString(info) ? undefined : info; } public getRootFileNames(): string[] { - return this.host.getScriptFileNames(); + return arrayFrom(this.fileNameToEntry.values(), entry => { + return isString(entry) ? entry : entry.hostFileName; + }); } public getVersion(path: Path): string { - const file = this.getEntryByPath(path); + const file = this.getHostFileInformation(path); return file && file.version; } public getScriptSnapshot(path: Path): IScriptSnapshot { - const file = this.getEntryByPath(path); + const file = this.getHostFileInformation(path); return file && file.scriptSnapshot; } } @@ -1145,16 +1153,17 @@ namespace ts { fileExists: (fileName): boolean => { // stub missing host functionality const path = toPath(fileName, currentDirectory, getCanonicalFileName); - return hostCache.containsEntryByPath(path) ? - !!hostCache.getEntryByPath(path) : + const entry = hostCache.getEntryByPath(path); + return entry ? + !isString(entry) : (host.fileExists && host.fileExists(fileName)); }, readFile(fileName) { // stub missing host functionality const path = toPath(fileName, currentDirectory, getCanonicalFileName); - if (hostCache.containsEntryByPath(path)) { - const entry = hostCache.getEntryByPath(path); - return entry && entry.scriptSnapshot.getText(0, entry.scriptSnapshot.getLength()); + const entry = hostCache.getEntryByPath(path); + if (entry) { + return isString(entry) ? undefined : entry.scriptSnapshot.getText(0, entry.scriptSnapshot.getLength()); } return host.readFile && host.readFile(fileName); }, diff --git a/src/services/shims.ts b/src/services/shims.ts index b7c85d2ce82d4..574ec70d36589 100644 --- a/src/services/shims.ts +++ b/src/services/shims.ts @@ -522,7 +522,7 @@ namespace ts { if (logPerformance) { const end = timestamp(); logger.log(`${actionDescription} completed in ${end - start} msec`); - if (typeof result === "string") { + if (isString(result)) { let str = result; if (str.length > 128) { str = str.substring(0, 128) + "..."; diff --git a/src/services/transpile.ts b/src/services/transpile.ts index 561c188c6cdf4..fc381ba8e5035 100644 --- a/src/services/transpile.ts +++ b/src/services/transpile.ts @@ -139,7 +139,7 @@ namespace ts { const value = options[opt.name]; // Value should be a key of opt.type - if (typeof value === "string") { + if (isString(value)) { // If value is not a string, this will fail options[opt.name] = parseCustomTypeOption(opt, value, diagnostics); } From 94a589b3bb72d4e886daa0829521c1e3605138be Mon Sep 17 00:00:00 2001 From: Sheetal Nandi Date: Fri, 21 Jul 2017 14:58:06 -0700 Subject: [PATCH 032/109] Program cannot be reused if the missing file is now present Also dont update filesByName array just to update missing file paths --- src/compiler/program.ts | 27 ++++++++----------- .../unittests/reuseProgramStructure.ts | 2 +- 2 files changed, 12 insertions(+), 17 deletions(-) diff --git a/src/compiler/program.ts b/src/compiler/program.ts index 5999329a14cae..8bb7427fa2feb 100644 --- a/src/compiler/program.ts +++ b/src/compiler/program.ts @@ -473,6 +473,7 @@ namespace ts { } const filesByName = createMap(); + let missingFilePaths: Path[]; // stores 'filename -> file association' ignoring case // used to track cases when two file names differ only in casing const filesByNameIgnoreCase = host.useCaseSensitiveFileNames() ? createMap() : undefined; @@ -511,9 +512,9 @@ namespace ts { }); } } - } - const missingFilePaths = arrayFrom(filesByName.keys(), p => p).filter(p => !filesByName.get(p)); + missingFilePaths = arrayFrom(filesByName.keys(), p => p).filter(p => !filesByName.get(p)); + } // unconditionally set moduleResolutionCache to undefined to avoid unnecessary leaks moduleResolutionCache = undefined; @@ -773,6 +774,13 @@ namespace ts { const modifiedSourceFiles: { oldFile: SourceFile, newFile: SourceFile }[] = []; oldProgram.structureIsReused = StructureIsReused.Completely; + // If the missing file paths are now present, it can change the progam structure, + // and hence cant reuse the structure. + // This is same as how we dont reuse the structure if one of the file from old program is now missing + if (oldProgram.getMissingFilePaths().some(missingFilePath => host.fileExists(missingFilePath))) { + return oldProgram.structureIsReused = StructureIsReused.Not; + } + for (const oldSourceFile of oldProgram.getSourceFiles()) { const newSourceFile = host.getSourceFileByPath ? host.getSourceFileByPath(oldSourceFile.fileName, oldSourceFile.path, options.target) @@ -869,20 +877,7 @@ namespace ts { return oldProgram.structureIsReused; } - // If a file has ceased to be missing, then we need to discard some of the old - // structure in order to pick it up. - // Caution: if the file has created and then deleted between since it was discovered to - // be missing, then the corresponding file watcher will have been closed and no new one - // will be created until we encounter a change that prevents complete structure reuse. - // During this interval, creation of the file will go unnoticed. We expect this to be - // both rare and low-impact. - if (oldProgram.getMissingFilePaths().some(missingFilePath => host.fileExists(missingFilePath))) { - return oldProgram.structureIsReused = StructureIsReused.SafeModules; - } - - for (const p of oldProgram.getMissingFilePaths()) { - filesByName.set(p, undefined); - } + missingFilePaths = oldProgram.getMissingFilePaths(); // update fileName -> file mapping for (let i = 0; i < newSourceFiles.length; i++) { diff --git a/src/harness/unittests/reuseProgramStructure.ts b/src/harness/unittests/reuseProgramStructure.ts index de5f5a756d866..a434ed8025dda 100644 --- a/src/harness/unittests/reuseProgramStructure.ts +++ b/src/harness/unittests/reuseProgramStructure.ts @@ -338,7 +338,7 @@ namespace ts { const program_2 = updateProgram(program_1, ["a.ts"], options, noop, newTexts); assert.deepEqual(emptyArray, program_2.getMissingFilePaths()); - assert.equal(StructureIsReused.SafeModules, program_1.structureIsReused); + assert.equal(StructureIsReused.Not, program_1.structureIsReused); }); it("resolution cache follows imports", () => { From ef5935b52ca1b4e14e03710b052d84b1b24d27de Mon Sep 17 00:00:00 2001 From: Sheetal Nandi Date: Mon, 24 Jul 2017 16:57:49 -0700 Subject: [PATCH 033/109] Initial refactoring so that watch from tsc follows the tsserver projects --- src/compiler/commandLineParser.ts | 2 +- src/compiler/core.ts | 164 +++++++ src/compiler/program.ts | 126 +++++- src/compiler/tsc.ts | 685 ++++++++++++++++++++++-------- src/compiler/types.ts | 6 +- src/compiler/utilities.ts | 93 ++++ src/server/editorServices.ts | 2 +- src/server/lsHost.ts | 111 +---- src/server/project.ts | 94 ++-- src/server/utilities.ts | 73 ---- src/services/services.ts | 92 +--- src/services/utilities.ts | 20 - 12 files changed, 960 insertions(+), 508 deletions(-) diff --git a/src/compiler/commandLineParser.ts b/src/compiler/commandLineParser.ts index 5a12f3d0ce40e..e334889284449 100644 --- a/src/compiler/commandLineParser.ts +++ b/src/compiler/commandLineParser.ts @@ -1979,7 +1979,7 @@ namespace ts { * @param host The host used to resolve files and directories. * @param errors An array for diagnostic reporting. */ - export function getFileNamesFromConfigSpecs(spec: ConfigFileSpecs, basePath: string, options: CompilerOptions, host: ParseConfigHost, extraFileExtensions: ReadonlyArray): ExpandResult { + export function getFileNamesFromConfigSpecs(spec: ConfigFileSpecs, basePath: string, options: CompilerOptions, host: ParseConfigHost, extraFileExtensions: ReadonlyArray = []): ExpandResult { basePath = normalizePath(basePath); const keyMapper = host.useCaseSensitiveFileNames ? caseSensitiveKeyMapper : caseInsensitiveKeyMapper; diff --git a/src/compiler/core.ts b/src/compiler/core.ts index 2bec904437e1f..cbccdcbed02c6 100644 --- a/src/compiler/core.ts +++ b/src/compiler/core.ts @@ -2559,4 +2559,168 @@ namespace ts { export function isCheckJsEnabledForFile(sourceFile: SourceFile, compilerOptions: CompilerOptions) { return sourceFile.checkJsDirective ? sourceFile.checkJsDirective.enabled : compilerOptions.checkJs; } + + export interface HostForCaching { + useCaseSensitiveFileNames: boolean; + writeFile(path: string, data: string, writeByteOrderMark?: boolean): void; + fileExists(path: string): boolean; + directoryExists(path: string): boolean; + createDirectory(path: string): void; + getCurrentDirectory(): string; + getDirectories(path: string): string[]; + readDirectory(path: string, extensions?: ReadonlyArray, exclude?: ReadonlyArray, include?: ReadonlyArray, depth?: number): string[]; + } + + export interface CachedHost { + writeFile(path: string, data: string, writeByteOrderMark?: boolean): void; + fileExists(path: string): boolean; + directoryExists(path: string): boolean; + createDirectory(path: string): void; + getCurrentDirectory(): string; + getDirectories(path: string): string[]; + readDirectory(path: string, extensions?: ReadonlyArray, exclude?: ReadonlyArray, include?: ReadonlyArray, depth?: number): string[]; + addOrDeleteFileOrFolder(fileOrFolder: string): void; + clearCache(): void; + } + + export function createCachedHost(host: HostForCaching): CachedHost { + const cachedReadDirectoryResult = createMap(); + const getCurrentDirectory = memoize(() => host.getCurrentDirectory()); + const getCanonicalFileName = createGetCanonicalFileName(host.useCaseSensitiveFileNames); + return { + writeFile, + fileExists, + directoryExists, + createDirectory, + getCurrentDirectory, + getDirectories, + readDirectory, + addOrDeleteFileOrFolder, + clearCache + }; + + function toPath(fileName: string) { + return ts.toPath(fileName, getCurrentDirectory(), getCanonicalFileName); + } + + function getFileSystemEntries(rootDir: string) { + const path = toPath(rootDir); + const cachedResult = cachedReadDirectoryResult.get(path); + if (cachedResult) { + return cachedResult; + } + + const resultFromHost: FileSystemEntries = { + files: host.readDirectory(rootDir, /*extensions*/ undefined, /*exclude*/ undefined, /*include*/["*.*"]) || [], + directories: host.getDirectories(rootDir) || [] + }; + + cachedReadDirectoryResult.set(path, resultFromHost); + return resultFromHost; + } + + function canWorkWithCacheForDir(rootDir: string) { + // Some of the hosts might not be able to handle read directory or getDirectories + const path = toPath(rootDir); + if (cachedReadDirectoryResult.get(path)) { + return true; + } + try { + return getFileSystemEntries(rootDir); + } + catch (_e) { + return false; + } + } + + function fileNameEqual(name1: string, name2: string) { + return getCanonicalFileName(name1) === getCanonicalFileName(name2); + } + + function hasEntry(entries: ReadonlyArray, name: string) { + return some(entries, file => fileNameEqual(file, name)); + } + + function updateFileSystemEntry(entries: ReadonlyArray, baseName: string, isValid: boolean) { + if (hasEntry(entries, baseName)) { + if (!isValid) { + return filter(entries, entry => !fileNameEqual(entry, baseName)); + } + } + else if (isValid) { + return entries.concat(baseName); + } + return entries; + } + + function writeFile(fileName: string, data: string, writeByteOrderMark?: boolean): void { + const path = toPath(fileName); + const result = cachedReadDirectoryResult.get(getDirectoryPath(path)); + const baseFileName = getBaseFileName(normalizePath(fileName)); + if (result) { + result.files = updateFileSystemEntry(result.files, baseFileName, /*isValid*/ true); + } + return host.writeFile(fileName, data, writeByteOrderMark); + } + + function fileExists(fileName: string): boolean { + const path = toPath(fileName); + const result = cachedReadDirectoryResult.get(getDirectoryPath(path)); + const baseName = getBaseFileName(normalizePath(fileName)); + return (result && hasEntry(result.files, baseName)) || host.fileExists(fileName); + } + + function directoryExists(dirPath: string): boolean { + const path = toPath(dirPath); + return cachedReadDirectoryResult.has(path) || host.directoryExists(dirPath); + } + + function createDirectory(dirPath: string) { + const path = toPath(dirPath); + const result = cachedReadDirectoryResult.get(getDirectoryPath(path)); + const baseFileName = getBaseFileName(path); + if (result) { + result.directories = updateFileSystemEntry(result.directories, baseFileName, /*isValid*/ true); + } + host.createDirectory(dirPath); + } + + function getDirectories(rootDir: string): string[] { + if (canWorkWithCacheForDir(rootDir)) { + return getFileSystemEntries(rootDir).directories.slice(); + } + return host.getDirectories(rootDir); + } + function readDirectory(rootDir: string, extensions?: ReadonlyArray, excludes?: ReadonlyArray, includes?: ReadonlyArray, depth?: number): string[] { + if (canWorkWithCacheForDir(rootDir)) { + return matchFiles(rootDir, extensions, excludes, includes, host.useCaseSensitiveFileNames, getCurrentDirectory(), depth, path => getFileSystemEntries(path)); + } + return host.readDirectory(rootDir, extensions, excludes, includes, depth); + } + + function addOrDeleteFileOrFolder(fileOrFolder: string) { + const path = toPath(fileOrFolder); + const existingResult = cachedReadDirectoryResult.get(path); + if (existingResult) { + if (!host.directoryExists(fileOrFolder)) { + cachedReadDirectoryResult.delete(path); + } + } + else { + // Was this earlier file + const parentResult = cachedReadDirectoryResult.get(getDirectoryPath(path)); + if (parentResult) { + const baseName = getBaseFileName(fileOrFolder); + if (parentResult) { + parentResult.files = updateFileSystemEntry(parentResult.files, baseName, host.fileExists(path)); + parentResult.directories = updateFileSystemEntry(parentResult.directories, baseName, host.directoryExists(path)); + } + } + } + } + + function clearCache() { + cachedReadDirectoryResult.clear(); + } + } } diff --git a/src/compiler/program.ts b/src/compiler/program.ts index 8bb7427fa2feb..0c319b18053f5 100644 --- a/src/compiler/program.ts +++ b/src/compiler/program.ts @@ -386,6 +386,114 @@ namespace ts { allDiagnostics?: Diagnostic[]; } + export function isProgramUptoDate(program: Program, rootFileNames: string[], newOptions: CompilerOptions, getSourceVersion: (path: Path) => string): boolean { + // If we haven't create a program yet, then it is not up-to-date + if (!program) { + return false; + } + + // If number of files in the program do not match, it is not up-to-date + if (program.getRootFileNames().length !== rootFileNames.length) { + return false; + } + + const fileNames = concatenate(rootFileNames, map(program.getSourceFiles(), sourceFile => sourceFile.fileName)); + // If any file is not up-to-date, then the whole program is not up-to-date + for (const fileName of fileNames) { + if (!sourceFileUpToDate(program.getSourceFile(fileName))) { + return false; + } + } + + const currentOptions = program.getCompilerOptions(); + // If the compilation settings do no match, then the program is not up-to-date + if (!compareDataObjects(currentOptions, newOptions)) { + return false; + } + + // If everything matches but the text of config file is changed, + // error locations can change for program options, so update the program + if (currentOptions.configFile && newOptions.configFile) { + return currentOptions.configFile.text === newOptions.configFile.text; + } + + return true; + + function sourceFileUpToDate(sourceFile: SourceFile): boolean { + if (!sourceFile) { + return false; + } + return sourceFile.version === getSourceVersion(sourceFile.path); + } + } + + function shouldProgramCreateNewSourceFiles(program: Program, newOptions: CompilerOptions) { + // If any of these options change, we cant reuse old source file even if version match + const oldOptions = program && program.getCompilerOptions(); + return oldOptions && + (oldOptions.target !== newOptions.target || + oldOptions.module !== newOptions.module || + oldOptions.moduleResolution !== newOptions.moduleResolution || + oldOptions.noResolve !== newOptions.noResolve || + oldOptions.jsx !== newOptions.jsx || + oldOptions.allowJs !== newOptions.allowJs || + oldOptions.disableSizeLimit !== newOptions.disableSizeLimit || + oldOptions.baseUrl !== newOptions.baseUrl || + !equalOwnProperties(oldOptions.paths, newOptions.paths)); + } + + /** + * Updates the existing missing file watches with the new set of missing files after new program is created + * @param program + * @param existingMap + * @param createMissingFileWatch + * @param closeExistingFileWatcher + */ + export function updateMissingFilePathsWatch(program: Program, existingMap: Map, + createMissingFileWatch: (missingFilePath: Path) => FileWatcher, + closeExistingFileWatcher: (missingFilePath: Path, fileWatcher: FileWatcher) => void) { + + const missingFilePaths = program.getMissingFilePaths(); + const newMissingFilePathMap = arrayToSet(missingFilePaths); + // Update the missing file paths watcher + return mutateExistingMapWithNewSet( + existingMap, newMissingFilePathMap, + // Watch the missing files + createMissingFileWatch, + // Files that are no longer missing (e.g. because they are no longer required) + // should no longer be watched. + closeExistingFileWatcher + ); + } + + export type WildCardDirectoryWatchers = { watcher: FileWatcher, recursive: boolean }; + + export function updateWatchingWildcardDirectories(existingWatchedForWildcards: Map, wildcardDirectories: Map, + watchDirectory: (directory: string, recursive: boolean) => FileWatcher, + closeDirectoryWatcher: (directory: string, watcher: FileWatcher, recursive: boolean, recursiveChanged: boolean) => void) { + return mutateExistingMap( + existingWatchedForWildcards, wildcardDirectories, + // Create new watch and recursive info + (directory, flag) => { + const recursive = (flag & WatchDirectoryFlags.Recursive) !== 0; + return { + watcher: watchDirectory(directory, recursive), + recursive + }; + }, + // Close existing watch thats not needed any more + (directory, { watcher, recursive }) => closeDirectoryWatcher(directory, watcher, recursive, /*recursiveChanged*/ false), + // Watcher is same if the recursive flags match + ({ recursive: existingRecursive }, flag) => { + // If the recursive dont match, it needs update + const recursive = (flag & WatchDirectoryFlags.Recursive) !== 0; + return existingRecursive !== recursive; + }, + // Close existing watch that doesnt match in recursive flag + (directory, { watcher, recursive }) => closeDirectoryWatcher(directory, watcher, recursive, /*recursiveChanged*/ true) + ); + } + /** * Create a new 'Program' instance. A Program is an immutable collection of 'SourceFile's and a 'CompilerOptions' * that represent a compilation unit. @@ -478,6 +586,7 @@ namespace ts { // used to track cases when two file names differ only in casing const filesByNameIgnoreCase = host.useCaseSensitiveFileNames() ? createMap() : undefined; + const shouldCreateNewSourceFile = shouldProgramCreateNewSourceFiles(oldProgram, options); const structuralIsReused = tryReuseStructureFromOldProgram(); if (structuralIsReused !== StructureIsReused.Completely) { forEach(rootNames, name => processRootFile(name, /*isDefaultLib*/ false)); @@ -519,6 +628,17 @@ namespace ts { // unconditionally set moduleResolutionCache to undefined to avoid unnecessary leaks moduleResolutionCache = undefined; + // Release any files we have acquired in the old program but are + // not part of the new program. + if (oldProgram && host.onReleaseOldSourceFile) { + const oldSourceFiles = oldProgram.getSourceFiles(); + for (const oldSourceFile of oldSourceFiles) { + if (!getSourceFile(oldSourceFile.path) || shouldCreateNewSourceFile) { + host.onReleaseOldSourceFile(oldSourceFile, oldProgram.getCompilerOptions()); + } + } + } + // unconditionally set oldProgram to undefined to prevent it from being captured in closure oldProgram = undefined; @@ -783,8 +903,8 @@ namespace ts { for (const oldSourceFile of oldProgram.getSourceFiles()) { const newSourceFile = host.getSourceFileByPath - ? host.getSourceFileByPath(oldSourceFile.fileName, oldSourceFile.path, options.target) - : host.getSourceFile(oldSourceFile.fileName, options.target); + ? host.getSourceFileByPath(oldSourceFile.fileName, oldSourceFile.path, options.target, /*onError*/ undefined, shouldCreateNewSourceFile) + : host.getSourceFile(oldSourceFile.fileName, options.target, /*onError*/ undefined, shouldCreateNewSourceFile); if (!newSourceFile) { return oldProgram.structureIsReused = StructureIsReused.Not; @@ -1593,7 +1713,7 @@ namespace ts { else { fileProcessingDiagnostics.add(createCompilerDiagnostic(Diagnostics.Cannot_read_file_0_Colon_1, fileName, hostErrorMessage)); } - }); + }, shouldCreateNewSourceFile); filesByName.set(path, file); if (file) { diff --git a/src/compiler/tsc.ts b/src/compiler/tsc.ts index db25afe45b62a..cf793919a4c43 100644 --- a/src/compiler/tsc.ts +++ b/src/compiler/tsc.ts @@ -98,22 +98,9 @@ namespace ts { export function executeCommandLine(args: string[]): void { const commandLine = parseCommandLine(args); - let configFileName: string; // Configuration file name (if any) - let cachedConfigFileText: string; // Cached configuration file text, used for reparsing (if any) - let configFileWatcher: FileWatcher; // Configuration file watcher - let directoryWatcher: FileWatcher; // Directory watcher to monitor source file addition/removal - let cachedProgram: Program; // Program cached from last compilation - let rootFileNames: string[]; // Root fileNames for compilation - let compilerOptions: CompilerOptions; // Compiler options for compilation - let compilerHost: CompilerHost; // Compiler host - let hostGetSourceFile: typeof compilerHost.getSourceFile; // getSourceFile method from default host - let timerHandleForRecompilation: any; // Handle for 0.25s wait timer to trigger recompilation - let timerHandleForDirectoryChanges: any; // Handle for 0.25s wait timer to trigger directory change handler - - // This map stores and reuses results of fileExists check that happen inside 'createProgram' - // This allows to save time in module resolution heavy scenarios when existence of the same file might be checked multiple times. - let cachedExistingFiles: Map; - let hostFileExists: typeof compilerHost.fileExists; + + // Configuration file name (if any) + let configFileName: string; if (commandLine.options.locale) { if (!isJSONSupported()) { @@ -126,7 +113,7 @@ namespace ts { // If there are any errors due to command line parsing and/or // setting up localization, report them and quit. if (commandLine.errors.length > 0) { - reportDiagnostics(commandLine.errors, compilerHost); + reportDiagnostics(commandLine.errors, /*host*/ undefined); return sys.exit(ExitStatus.DiagnosticsPresent_OutputsSkipped); } @@ -183,232 +170,570 @@ namespace ts { return sys.exit(ExitStatus.Success); } - if (isWatchSet(commandLine.options)) { - if (!sys.watchFile) { + if (configFileName) { + const configParseResult = parseConfigFile(configFileName, commandLine, sys); + const { fileNames, options } = configParseResult; + if (isWatchSet(configParseResult.options)) { + reportWatchModeWithoutSysSupport(); + createWatchMode(commandLine, configFileName, fileNames, options, configParseResult.configFileSpecs, configParseResult.wildcardDirectories); + } + else { + performCompilation(fileNames, options); + } + } + else if (isWatchSet(commandLine.options)) { + reportWatchModeWithoutSysSupport(); + createWatchMode(commandLine); + } + else { + performCompilation(commandLine.fileNames, commandLine.options); + } + + function reportWatchModeWithoutSysSupport() { + if (!sys.watchFile || !sys.watchDirectory) { reportDiagnostic(createCompilerDiagnostic(Diagnostics.The_current_host_does_not_support_the_0_option, "--watch"), /* host */ undefined); - return sys.exit(ExitStatus.DiagnosticsPresent_OutputsSkipped); + sys.exit(ExitStatus.DiagnosticsPresent_OutputsSkipped); } - if (configFileName) { - configFileWatcher = sys.watchFile(configFileName, configFileChanged); + } + + function performCompilation(rootFileNames: string[], compilerOptions: CompilerOptions) { + if (compilerOptions.pretty) { + reportDiagnosticWorker = reportDiagnosticWithColorAndContext; } - if (sys.watchDirectory && configFileName) { - const directory = ts.getDirectoryPath(configFileName); - directoryWatcher = sys.watchDirectory( - // When the configFileName is just "tsconfig.json", the watched directory should be - // the current directory; if there is a given "project" parameter, then the configFileName - // is an absolute file name. - directory === "" ? "." : directory, - watchedDirectoryChanged, /*recursive*/ true); + + const compilerHost = createCompilerHost(compilerOptions); + const compileResult = compile(rootFileNames, compilerOptions, compilerHost); + return sys.exit(compileResult.exitStatus); + } + } + + interface HostFileInfo { + version: number; + sourceFile: SourceFile; + fileWatcher: FileWatcher; + } + + function createWatchMode(commandLine: ParsedCommandLine, configFileName?: string, configFileRootFiles?: string[], configFileOptions?: CompilerOptions, configFileSpecs?: ConfigFileSpecs, configFileWildCardDirectories?: MapLike) { + let program: Program; + let needsReload: boolean; + let missingFilesMap: Map; + let configFileWatcher: FileWatcher; + let watchedWildCardDirectories: Map; + let timerToUpdateProgram: any; + + let compilerOptions: CompilerOptions; + let rootFileNames: string[]; + + const sourceFilesCache = createMap(); + + let host: System; + if (configFileName) { + rootFileNames = configFileRootFiles; + compilerOptions = configFileOptions; + host = createCachedSystem(sys); + configFileWatcher = sys.watchFile(configFileName, onConfigFileChanged); + } + else { + rootFileNames = commandLine.fileNames; + compilerOptions = commandLine.options; + host = sys; + } + const currentDirectory = host.getCurrentDirectory(); + const getCanonicalFileName = createGetCanonicalFileName(host.useCaseSensitiveFileNames); + + if (compilerOptions.pretty) { + reportDiagnosticWorker = reportDiagnosticWithColorAndContext; + } + + synchronizeProgram(); + + // Update the wild card directory watch + watchConfigFileWildCardDirectories(); + + function synchronizeProgram() { + writeLog(`Synchronizing program`); + + if (isProgramUptoDate(program, rootFileNames, compilerOptions, getSourceVersion)) { + return; } + + // Create the compiler host + const compilerHost = createWatchedCompilerHost(compilerOptions); + program = compile(rootFileNames, compilerOptions, compilerHost, program).program; + + // Update watches + missingFilesMap = updateMissingFilePathsWatch(program, missingFilesMap, watchMissingFilePath, closeMissingFilePathWatcher); + + reportWatchDiagnostic(createCompilerDiagnostic(Diagnostics.Compilation_complete_Watching_for_file_changes)); } - performCompilation(); + function createWatchedCompilerHost(options: CompilerOptions): CompilerHost { + const existingDirectories = createMap(); + function directoryExists(directoryPath: string): boolean { + if (existingDirectories.has(directoryPath)) { + return true; + } + if (host.directoryExists(directoryPath)) { + existingDirectories.set(directoryPath, true); + return true; + } + return false; + } + + function ensureDirectoriesExist(directoryPath: string) { + if (directoryPath.length > getRootLength(directoryPath) && !directoryExists(directoryPath)) { + const parentDirectory = getDirectoryPath(directoryPath); + ensureDirectoriesExist(parentDirectory); + host.createDirectory(directoryPath); + } + } + + type OutputFingerprint = { + hash: string; + byteOrderMark: boolean; + mtime: Date; + }; + let outputFingerprints: Map; + + function writeFileIfUpdated(fileName: string, data: string, writeByteOrderMark: boolean): void { + if (!outputFingerprints) { + outputFingerprints = createMap(); + } + + const hash = host.createHash(data); + const mtimeBefore = host.getModifiedTime(fileName); + + if (mtimeBefore) { + const fingerprint = outputFingerprints.get(fileName); + // If output has not been changed, and the file has no external modification + if (fingerprint && + fingerprint.byteOrderMark === writeByteOrderMark && + fingerprint.hash === hash && + fingerprint.mtime.getTime() === mtimeBefore.getTime()) { + return; + } + } + + host.writeFile(fileName, data, writeByteOrderMark); + + const mtimeAfter = host.getModifiedTime(fileName); + + outputFingerprints.set(fileName, { + hash, + byteOrderMark: writeByteOrderMark, + mtime: mtimeAfter + }); + } - function parseConfigFile(): ParsedCommandLine { - if (!cachedConfigFileText) { + function writeFile(fileName: string, data: string, writeByteOrderMark: boolean, onError?: (message: string) => void) { try { - cachedConfigFileText = sys.readFile(configFileName); + performance.mark("beforeIOWrite"); + ensureDirectoriesExist(getDirectoryPath(normalizePath(fileName))); + + //if (isWatchSet(options) && sys.createHash && sys.getModifiedTime) { + writeFileIfUpdated(fileName, data, writeByteOrderMark); + //} + //else { + //host.writeFile(fileName, data, writeByteOrderMark); + //} + + performance.mark("afterIOWrite"); + performance.measure("I/O Write", "beforeIOWrite", "afterIOWrite"); } catch (e) { - const error = createCompilerDiagnostic(Diagnostics.Cannot_read_file_0_Colon_1, configFileName, e.message); - reportWatchDiagnostic(error); - sys.exit(ExitStatus.DiagnosticsPresent_OutputsSkipped); - return; + if (onError) { + onError(e.message); + } } } - if (!cachedConfigFileText) { - const error = createCompilerDiagnostic(Diagnostics.File_0_not_found, configFileName); - reportDiagnostics([error], /* compilerHost */ undefined); - sys.exit(ExitStatus.DiagnosticsPresent_OutputsSkipped); - return; + + const newLine = getNewLineCharacter(options); + const realpath = host.realpath && ((path: string) => host.realpath(path)); + + return { + getSourceFile: getVersionedSourceFile, + getSourceFileByPath: getVersionedSourceFileByPath, + getDefaultLibLocation, + getDefaultLibFileName: options => combinePaths(getDefaultLibLocation(), getDefaultLibFileName(options)), + writeFile, + getCurrentDirectory: memoize(() => host.getCurrentDirectory()), + useCaseSensitiveFileNames: () => host.useCaseSensitiveFileNames, + getCanonicalFileName, + getNewLine: () => newLine, + fileExists, + readFile: fileName => host.readFile(fileName), + trace: (s: string) => host.write(s + newLine), + directoryExists: directoryName => host.directoryExists(directoryName), + getEnvironmentVariable: name => host.getEnvironmentVariable ? host.getEnvironmentVariable(name) : "", + getDirectories: (path: string) => host.getDirectories(path), + realpath, + onReleaseOldSourceFile + }; + + // TODO: cache module resolution + //if (host.resolveModuleNames) { + // compilerHost.resolveModuleNames = (moduleNames, containingFile) => host.resolveModuleNames(moduleNames, containingFile); + //} + //if (host.resolveTypeReferenceDirectives) { + // compilerHost.resolveTypeReferenceDirectives = (typeReferenceDirectiveNames, containingFile) => { + // return host.resolveTypeReferenceDirectives(typeReferenceDirectiveNames, containingFile); + // }; + //} + } + + function fileExists(fileName: string) { + const path = toPath(fileName, currentDirectory, getCanonicalFileName); + const hostSourceFileInfo = sourceFilesCache.get(path); + if (hostSourceFileInfo !== undefined) { + return !isString(hostSourceFileInfo); + } + + return host.fileExists(fileName); + } + + function getDefaultLibLocation(): string { + return getDirectoryPath(normalizePath(host.getExecutingFilePath())); + } + + function getVersionedSourceFile(fileName: string, languageVersion: ScriptTarget, onError?: (message: string) => void, shouldCreateNewSourceFile?: boolean): SourceFile { + return getVersionedSourceFileByPath(fileName, toPath(fileName, currentDirectory, getCanonicalFileName), languageVersion, onError, shouldCreateNewSourceFile); + } + + function getVersionedSourceFileByPath(fileName: string, path: Path, languageVersion: ScriptTarget, onError?: (message: string) => void, shouldCreateNewSourceFile?: boolean): SourceFile { + const hostSourceFile = sourceFilesCache.get(path); + // No source file on the host + if (isString(hostSourceFile)) { + return undefined; + } + + // Create new source file if requested or the versions dont match + if (!hostSourceFile) { + const sourceFile = getSourceFile(fileName, languageVersion, onError); + if (sourceFile) { + sourceFile.version = "0"; + const fileWatcher = watchSourceFileForChanges(sourceFile.path); + sourceFilesCache.set(path, { sourceFile, version: 0, fileWatcher }); + } + else { + sourceFilesCache.set(path, "0"); + } + return sourceFile; } + else if (shouldCreateNewSourceFile || hostSourceFile.version.toString() !== hostSourceFile.sourceFile.version) { + if (shouldCreateNewSourceFile) { + hostSourceFile.version++; + } + const newSourceFile = getSourceFile(fileName, languageVersion, onError); + if (newSourceFile) { + newSourceFile.version = hostSourceFile.version.toString(); + hostSourceFile.sourceFile = newSourceFile; + } + else { + // File doesnt exist any more + hostSourceFile.fileWatcher.close(); + sourceFilesCache.set(path, hostSourceFile.version.toString()); + } - const result = parseJsonText(configFileName, cachedConfigFileText); - reportDiagnostics(result.parseDiagnostics, /* compilerHost */ undefined); + return newSourceFile; + } - const cwd = sys.getCurrentDirectory(); - const configParseResult = parseJsonSourceFileConfigFileContent(result, sys, getNormalizedAbsolutePath(getDirectoryPath(configFileName), cwd), commandLine.options, getNormalizedAbsolutePath(configFileName, cwd)); - reportDiagnostics(configParseResult.errors, /* compilerHost */ undefined); + return hostSourceFile.sourceFile; - if (isWatchSet(configParseResult.options)) { - if (!sys.watchFile) { - reportDiagnostic(createCompilerDiagnostic(Diagnostics.The_current_host_does_not_support_the_0_option, "--watch"), /* host */ undefined); - sys.exit(ExitStatus.DiagnosticsPresent_OutputsSkipped); + function getSourceFile(fileName: string, languageVersion: ScriptTarget, onError?: (message: string) => void): SourceFile { + let text: string; + try { + performance.mark("beforeIORead"); + text = host.readFile(fileName, compilerOptions.charset); + performance.mark("afterIORead"); + performance.measure("I/O Read", "beforeIORead", "afterIORead"); } + catch (e) { + if (onError) { + onError(e.message); + } + text = ""; + } + + return text !== undefined ? createSourceFile(fileName, text, languageVersion) : undefined; + } + } - if (!directoryWatcher && sys.watchDirectory && configFileName) { - const directory = ts.getDirectoryPath(configFileName); - directoryWatcher = sys.watchDirectory( - // When the configFileName is just "tsconfig.json", the watched directory should be - // the current directory; if there is a given "project" parameter, then the configFileName - // is an absolute file name. - directory === "" ? "." : directory, - watchedDirectoryChanged, /*recursive*/ true); + function removeSourceFile(path: Path) { + const hostSourceFile = sourceFilesCache.get(path); + if (hostSourceFile !== undefined) { + if (!isString(hostSourceFile)) { + hostSourceFile.fileWatcher.close(); } + sourceFilesCache.delete(path); } - return configParseResult; } - // Invoked to perform initial compilation or re-compilation in watch mode - function performCompilation() { + function getSourceVersion(path: Path): string { + const hostSourceFile = sourceFilesCache.get(path); + return !hostSourceFile || isString(hostSourceFile) ? undefined : hostSourceFile.version.toString(); + } - if (!cachedProgram) { - if (configFileName) { - const configParseResult = parseConfigFile(); - rootFileNames = configParseResult.fileNames; - compilerOptions = configParseResult.options; - } - else { - rootFileNames = commandLine.fileNames; - compilerOptions = commandLine.options; - } - compilerHost = createCompilerHost(compilerOptions); - hostGetSourceFile = compilerHost.getSourceFile; - compilerHost.getSourceFile = getSourceFile; + function onReleaseOldSourceFile(oldSourceFile: SourceFile, _oldOptions: CompilerOptions) { + const hostSourceFileInfo = sourceFilesCache.get(oldSourceFile.path); + // If this is the source file thats in the cache and new program doesnt need it, + // remove the cached entry. + // Note we arent deleting entry if file became missing in new program or + // there was version update and new source file was created. + if (hostSourceFileInfo && !isString(hostSourceFileInfo) && hostSourceFileInfo.sourceFile === oldSourceFile) { + sourceFilesCache.delete(oldSourceFile.path); + } + } - hostFileExists = compilerHost.fileExists; - compilerHost.fileExists = cachedFileExists; + // 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. + function scheduleProgramUpdate() { + if (!sys.setTimeout || !sys.clearTimeout) { + return; } - if (compilerOptions.pretty) { - reportDiagnosticWorker = reportDiagnosticWithColorAndContext; + if (timerToUpdateProgram) { + sys.clearTimeout(timerToUpdateProgram); } + timerToUpdateProgram = sys.setTimeout(updateProgram, 250); + } - // reset the cache of existing files - cachedExistingFiles = createMap(); + function scheduleProgramReload() { + Debug.assert(!!configFileName); + needsReload = true; + scheduleProgramUpdate(); + } - const compileResult = compile(rootFileNames, compilerOptions, compilerHost); + function updateProgram() { + timerToUpdateProgram = undefined; + reportWatchDiagnostic(createCompilerDiagnostic(Diagnostics.File_change_detected_Starting_incremental_compilation)); - if (!isWatchSet(compilerOptions)) { - return sys.exit(compileResult.exitStatus); + if (needsReload) { + reloadConfigFile(); } + else { + synchronizeProgram(); + } + } - setCachedProgram(compileResult.program); - reportWatchDiagnostic(createCompilerDiagnostic(Diagnostics.Compilation_complete_Watching_for_file_changes)); + function reloadConfigFile() { + writeLog(`Reloading config file: ${configFileName}`); + reportWatchDiagnostic(createCompilerDiagnostic(Diagnostics.File_change_detected_Starting_incremental_compilation)); - const missingPaths = compileResult.program.getMissingFilePaths(); - missingPaths.forEach(path => { - const fileWatcher = sys.watchFile(path, (_fileName, eventKind) => { - if (eventKind === FileWatcherEventKind.Created) { - fileWatcher.close(); - startTimerForRecompilation(); - } - }); - }); + needsReload = false; + + const cachedHost = host as CachedSystem; + cachedHost.clearCache(); + const configParseResult = parseConfigFile(configFileName, commandLine, cachedHost); + rootFileNames = configParseResult.fileNames; + compilerOptions = configParseResult.options; + configFileSpecs = configParseResult.configFileSpecs; + configFileWildCardDirectories = configParseResult.wildcardDirectories; + + synchronizeProgram(); + + // Update the wild card directory watch + watchConfigFileWildCardDirectories(); } - function cachedFileExists(fileName: string): boolean { - let fileExists = cachedExistingFiles.get(fileName); - if (fileExists === undefined) { - cachedExistingFiles.set(fileName, fileExists = hostFileExists(fileName)); - } - return fileExists; + function watchSourceFileForChanges(path: Path) { + return host.watchFile(path, (fileName, eventKind) => onSourceFileChange(fileName, path, eventKind)); } - function getSourceFile(fileName: string, languageVersion: ScriptTarget, onError?: (message: string) => void) { - // Return existing SourceFile object if one is available - if (cachedProgram) { - const sourceFile = cachedProgram.getSourceFile(fileName); - // A modified source file has no watcher and should not be reused - if (sourceFile && sourceFile.fileWatcher) { - return sourceFile; + function onSourceFileChange(fileName: string, path: Path, eventKind: FileWatcherEventKind) { + writeLog(`Source file path : ${path} changed: ${FileWatcherEventKind[eventKind]}, fileName: ${fileName}`); + const hostSourceFile = sourceFilesCache.get(path); + if (hostSourceFile) { + // Update the cache + if (eventKind === FileWatcherEventKind.Deleted) { + if (!isString(hostSourceFile)) { + hostSourceFile.fileWatcher.close(); + sourceFilesCache.set(path, (hostSourceFile.version++).toString()); + } } - } - // Use default host function - const sourceFile = hostGetSourceFile(fileName, languageVersion, onError); - if (sourceFile && isWatchSet(compilerOptions) && sys.watchFile) { - // Attach a file watcher - sourceFile.fileWatcher = sys.watchFile(sourceFile.fileName, (_fileName, eventKind) => sourceFileChanged(sourceFile, eventKind)); - } - return sourceFile; - } - - // Change cached program to the given program - function setCachedProgram(program: Program) { - if (cachedProgram) { - const newSourceFiles = program ? program.getSourceFiles() : undefined; - forEach(cachedProgram.getSourceFiles(), sourceFile => { - if (!(newSourceFiles && contains(newSourceFiles, sourceFile))) { - if (sourceFile.fileWatcher) { - sourceFile.fileWatcher.close(); - sourceFile.fileWatcher = undefined; - } + else { + // Deleted file created + if (isString(hostSourceFile)) { + sourceFilesCache.delete(path); } - }); + else { + // file changed - just update the version + hostSourceFile.version++; + } + } } - cachedProgram = program; + // Update the program + scheduleProgramUpdate(); } - // If a source file changes, mark it as unwatched and start the recompilation timer - function sourceFileChanged(sourceFile: SourceFile, eventKind: FileWatcherEventKind) { - sourceFile.fileWatcher.close(); - sourceFile.fileWatcher = undefined; - if (eventKind === FileWatcherEventKind.Deleted) { - unorderedRemoveItem(rootFileNames, sourceFile.fileName); - } - startTimerForRecompilation(); + function watchMissingFilePath(missingFilePath: Path) { + return host.watchFile(missingFilePath, (fileName, eventKind) => onMissingFileChange(fileName, missingFilePath, eventKind)); } - // If the configuration file changes, forget cached program and start the recompilation timer - function configFileChanged() { - setCachedProgram(undefined); - cachedConfigFileText = undefined; - startTimerForRecompilation(); + function closeMissingFilePathWatcher(_missingFilePath: Path, fileWatcher: FileWatcher) { + fileWatcher.close(); } - function watchedDirectoryChanged(fileName: string) { - if (fileName && !ts.isSupportedSourceFileName(fileName, compilerOptions)) { - return; + function onMissingFileChange(filename: string, missingFilePath: Path, eventKind: FileWatcherEventKind) { + writeLog(`Missing file path : ${missingFilePath} changed: ${FileWatcherEventKind[eventKind]}, fileName: ${filename}`); + if (eventKind === FileWatcherEventKind.Created && missingFilesMap.has(missingFilePath)) { + closeMissingFilePathWatcher(missingFilePath, missingFilesMap.get(missingFilePath)); + missingFilesMap.delete(missingFilePath); + + if (configFileName) { + const absoluteNormalizedPath = getNormalizedAbsolutePath(filename, getDirectoryPath(missingFilePath)); + (host as CachedSystem).addOrDeleteFileOrFolder(normalizePath(absoluteNormalizedPath)); + } + + // Delete the entry in the source files cache so that new source file is created + removeSourceFile(missingFilePath); + + // When a missing file is created, we should update the graph. + scheduleProgramUpdate(); } + } - startTimerForHandlingDirectoryChanges(); + function watchConfigFileWildCardDirectories() { + const wildcards = createMapFromTemplate(configFileWildCardDirectories); + watchedWildCardDirectories = updateWatchingWildcardDirectories( + watchedWildCardDirectories, wildcards, + watchWildCardDirectory, stopWatchingWildCardDirectory + ); } - function startTimerForHandlingDirectoryChanges() { - if (!sys.setTimeout || !sys.clearTimeout) { + function watchWildCardDirectory(directory: string, recursive: boolean) { + return host.watchDirectory(directory, fileName => + onFileAddOrRemoveInWatchedDirectory(getNormalizedAbsolutePath(fileName, directory)), + recursive); + } + + function stopWatchingWildCardDirectory(_directory: string, fileWatcher: FileWatcher, _recursive: boolean, _recursiveChanged: boolean) { + fileWatcher.close(); + } + + function onFileAddOrRemoveInWatchedDirectory(fileName: string) { + Debug.assert(!!configFileName); + (host as CachedSystem).addOrDeleteFileOrFolder(fileName); + + // Since the file existance changed, update the sourceFiles cache + removeSourceFile(toPath(fileName, currentDirectory, getCanonicalFileName)); + + // If a change was made inside "folder/file", node will trigger the callback twice: + // one with the fileName being "folder/file", and the other one with "folder". + // We don't respond to the second one. + if (fileName && !isSupportedSourceFileName(fileName, compilerOptions)) { + writeLog(`Project: ${configFileName} Detected file add/remove of non supported extension: ${fileName}`); return; } - if (timerHandleForDirectoryChanges) { - sys.clearTimeout(timerHandleForDirectoryChanges); - } - timerHandleForDirectoryChanges = sys.setTimeout(directoryChangeHandler, 250); - } + writeLog(`Project: ${configFileName} Detected file add/remove of supported extension: ${fileName}`); - function directoryChangeHandler() { - const parsedCommandLine = parseConfigFile(); - const newFileNames = ts.map(parsedCommandLine.fileNames, compilerHost.getCanonicalFileName); - const canonicalRootFileNames = ts.map(rootFileNames, compilerHost.getCanonicalFileName); + // Reload is pending, do the reload + if (!needsReload) { + const result = getFileNamesFromConfigSpecs(configFileSpecs, getDirectoryPath(configFileName), compilerOptions, host, /*extraFileExtensions*/ []); + if (!configFileSpecs.filesSpecs) { + reportDiagnostics([getErrorForNoInputFiles(configFileSpecs, configFileName)], /*host*/ undefined); + } + rootFileNames = result.fileNames; - // We check if the project file list has changed. If so, we just throw away the old program and start fresh. - if (!arrayIsEqualTo(newFileNames && newFileNames.sort(), canonicalRootFileNames && canonicalRootFileNames.sort())) { - setCachedProgram(undefined); - startTimerForRecompilation(); + // Schedule Update the program + 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. - function startTimerForRecompilation() { - if (!sys.setTimeout || !sys.clearTimeout) { - return; - } + function onConfigFileChanged(fileName: string, eventKind: FileWatcherEventKind) { + writeLog(`Config file : ${configFileName} changed: ${FileWatcherEventKind[eventKind]}, fileName: ${fileName}`); + scheduleProgramReload(); + } - if (timerHandleForRecompilation) { - sys.clearTimeout(timerHandleForRecompilation); + function writeLog(s: string) { + const hasDiagnostics = compilerOptions.diagnostics || compilerOptions.extendedDiagnostics; + if (hasDiagnostics) { + host.write(s); } - timerHandleForRecompilation = sys.setTimeout(recompile, 250); } + } - function recompile() { - timerHandleForRecompilation = undefined; - reportWatchDiagnostic(createCompilerDiagnostic(Diagnostics.File_change_detected_Starting_incremental_compilation)); - performCompilation(); + interface CachedSystem extends System { + addOrDeleteFileOrFolder(fileOrFolder: string): void; + clearCache(): void; + } + + function createCachedSystem(host: System): CachedSystem { + const getFileSize = host.getFileSize ? (path: string) => host.getFileSize(path) : undefined; + const watchFile = host.watchFile ? (path: string, callback: FileWatcherCallback, pollingInterval?: number) => host.watchFile(path, callback, pollingInterval) : undefined; + const watchDirectory = host.watchDirectory ? (path: string, callback: DirectoryWatcherCallback, recursive?: boolean) => host.watchDirectory(path, callback, recursive) : undefined; + const getModifiedTime = host.getModifiedTime ? (path: string) => host.getModifiedTime(path) : undefined; + const createHash = host.createHash ? (data: string) => host.createHash(data) : undefined; + const getMemoryUsage = host.getMemoryUsage ? () => host.getMemoryUsage() : undefined; + const realpath = host.realpath ? (path: string) => host.realpath(path) : undefined; + const tryEnableSourceMapsForHost = host.tryEnableSourceMapsForHost ? () => host.tryEnableSourceMapsForHost() : undefined; + const setTimeout = host.setTimeout ? (callback: (...args: any[]) => void, ms: number, ...args: any[]) => host.setTimeout(callback, ms, ...args) : undefined; + const clearTimeout = host.clearTimeout ? (timeoutId: any) => host.clearTimeout(timeoutId) : undefined; + + const cachedHost = createCachedHost(host); + return { + args: host.args, + newLine: host.newLine, + useCaseSensitiveFileNames: host.useCaseSensitiveFileNames, + write: s => host.write(s), + readFile: (path, encoding?) => host.readFile(path, encoding), + getFileSize, + writeFile: (fileName, data, writeByteOrderMark?) => cachedHost.writeFile(fileName, data, writeByteOrderMark), + watchFile, + watchDirectory, + resolvePath: path => host.resolvePath(path), + fileExists: fileName => cachedHost.fileExists(fileName), + directoryExists: dir => cachedHost.directoryExists(dir), + createDirectory: dir => cachedHost.createDirectory(dir), + getExecutingFilePath: () => host.getExecutingFilePath(), + getCurrentDirectory: () => cachedHost.getCurrentDirectory(), + getDirectories: dir => cachedHost.getDirectories(dir), + readDirectory: (path, extensions, excludes, includes, depth) => cachedHost.readDirectory(path, extensions, excludes, includes, depth), + getModifiedTime, + createHash, + getMemoryUsage, + exit: exitCode => host.exit(exitCode), + realpath, + getEnvironmentVariable: name => host.getEnvironmentVariable(name), + tryEnableSourceMapsForHost, + debugMode: host.debugMode, + setTimeout, + clearTimeout, + addOrDeleteFileOrFolder: fileOrFolder => cachedHost.addOrDeleteFileOrFolder(fileOrFolder), + clearCache: () => cachedHost.clearCache() + }; + } + + function parseConfigFile(configFileName: string, commandLine: ParsedCommandLine, host: System): ParsedCommandLine { + let configFileText: string; + try { + configFileText = host.readFile(configFileName); + } + catch (e) { + const error = createCompilerDiagnostic(Diagnostics.Cannot_read_file_0_Colon_1, configFileName, e.message); + reportWatchDiagnostic(error); + host.exit(ExitStatus.DiagnosticsPresent_OutputsSkipped); + return; + } + if (!configFileText) { + const error = createCompilerDiagnostic(Diagnostics.File_0_not_found, configFileName); + reportDiagnostics([error], /* compilerHost */ undefined); + host.exit(ExitStatus.DiagnosticsPresent_OutputsSkipped); + return; } + + const result = parseJsonText(configFileName, configFileText); + reportDiagnostics(result.parseDiagnostics, /* compilerHost */ undefined); + + const cwd = host.getCurrentDirectory(); + const configParseResult = parseJsonSourceFileConfigFileContent(result, host, getNormalizedAbsolutePath(getDirectoryPath(configFileName), cwd), commandLine.options, getNormalizedAbsolutePath(configFileName, cwd)); + reportDiagnostics(configParseResult.errors, /* compilerHost */ undefined); + + return configParseResult; } - function compile(fileNames: string[], compilerOptions: CompilerOptions, compilerHost: CompilerHost) { + function compile(fileNames: string[], compilerOptions: CompilerOptions, compilerHost: CompilerHost, oldProgram?: Program) { const hasDiagnostics = compilerOptions.diagnostics || compilerOptions.extendedDiagnostics; let statistics: Statistic[]; if (hasDiagnostics) { @@ -416,7 +741,7 @@ namespace ts { statistics = []; } - const program = createProgram(fileNames, compilerOptions, compilerHost); + const program = createProgram(fileNames, compilerOptions, compilerHost, oldProgram); const exitStatus = compileProgram(); if (compilerOptions.listFiles) { @@ -481,6 +806,8 @@ namespace ts { } } + // TODO: in watch mode to emit only affected files + // Otherwise, emit and report any errors we ran into. const emitOutput = program.emit(); diagnostics = diagnostics.concat(emitOutput.diagnostics); diff --git a/src/compiler/types.ts b/src/compiler/types.ts index 8929b19bfe331..45f5a16a80c7d 100644 --- a/src/compiler/types.ts +++ b/src/compiler/types.ts @@ -2326,6 +2326,7 @@ namespace ts { /* @internal */ patternAmbientModules?: PatternAmbientModule[]; /* @internal */ ambientModuleNames: ReadonlyArray; /* @internal */ checkJsDirective: CheckJsDirective | undefined; + /* @internal */ version: string; } export interface Bundle extends Node { @@ -3952,8 +3953,8 @@ namespace ts { } export interface CompilerHost extends ModuleResolutionHost { - getSourceFile(fileName: string, languageVersion: ScriptTarget, onError?: (message: string) => void): SourceFile; - getSourceFileByPath?(fileName: string, path: Path, languageVersion: ScriptTarget, onError?: (message: string) => void): SourceFile; + getSourceFile(fileName: string, languageVersion: ScriptTarget, onError?: (message: string) => void, shouldCreateNewSourceFile?: boolean): SourceFile; + getSourceFileByPath?(fileName: string, path: Path, languageVersion: ScriptTarget, onError?: (message: string) => void, shouldCreateNewSourceFile?: boolean): SourceFile; getCancellationToken?(): CancellationToken; getDefaultLibFileName(options: CompilerOptions): string; getDefaultLibLocation?(): string; @@ -3977,6 +3978,7 @@ namespace ts { */ resolveTypeReferenceDirectives?(typeReferenceDirectiveNames: string[], containingFile: string): ResolvedTypeReferenceDirective[]; getEnvironmentVariable?(name: string): string; + onReleaseOldSourceFile?(oldSourceFile: SourceFile, oldOptions: CompilerOptions): void; } /* @internal */ diff --git a/src/compiler/utilities.ts b/src/compiler/utilities.ts index facb0bdc2ee6c..08816382ecf64 100644 --- a/src/compiler/utilities.ts +++ b/src/compiler/utilities.ts @@ -3611,6 +3611,99 @@ namespace ts { export function getCombinedLocalAndExportSymbolFlags(symbol: Symbol): SymbolFlags { return symbol.exportSymbol ? symbol.exportSymbol.flags | symbol.flags : symbol.flags; } + + export function compareDataObjects(dst: any, src: any): boolean { + if (!dst || !src || Object.keys(dst).length !== Object.keys(src).length) { + return false; + } + + for (const e in dst) { + if (typeof dst[e] === "object") { + if (!compareDataObjects(dst[e], src[e])) { + return false; + } + } + else if (typeof dst[e] !== "function") { + if (dst[e] !== src[e]) { + return false; + } + } + } + return true; + } + + export function cleanExistingMap( + existingMap: Map, + onDeleteExistingValue: (key: string, existingValue: T) => void) { + if (existingMap) { + // Remove all + existingMap.forEach((existingValue, key) => { + existingMap.delete(key); + onDeleteExistingValue(key, existingValue); + }); + } + } + + export function mutateExistingMapWithNewSet( + existingMap: Map, newMap: Map, + createNewValue: (key: string) => T, + onDeleteExistingValue: (key: string, existingValue: T) => void + ): Map { + return mutateExistingMap( + existingMap, newMap, + /*createNewValue*/(key, _valueInNewMap) => createNewValue(key), + onDeleteExistingValue, + ); + } + + export function mutateExistingMap( + existingMap: Map, newMap: Map, + createNewValue: (key: string, valueInNewMap: U) => T, + onDeleteExistingValue: (key: string, existingValue: T) => void, + isSameValue?: (existingValue: T, valueInNewMap: U) => boolean, + OnDeleteExistingMismatchValue?: (key: string, existingValue: T) => void, + onSameExistingValue?: (existingValue: T, valueInNewMap: U) => void + ): Map { + // If there are new values update them + if (newMap) { + if (existingMap) { + // Needs update + existingMap.forEach((existingValue, key) => { + const valueInNewMap = newMap.get(key); + // Existing value - remove it + if (valueInNewMap === undefined) { + existingMap.delete(key); + onDeleteExistingValue(key, existingValue); + } + // different value - remove it + else if (isSameValue && !isSameValue(existingValue, valueInNewMap)) { + existingMap.delete(key); + OnDeleteExistingMismatchValue(key, existingValue); + } + else if (onSameExistingValue) { + onSameExistingValue(existingValue, valueInNewMap); + } + }); + } + else { + // Create new + existingMap = createMap(); + } + + // Add new values that are not already present + newMap.forEach((valueInNewMap, key) => { + if (!existingMap.has(key)) { + // New values + existingMap.set(key, createNewValue(key, valueInNewMap)); + } + }); + + return existingMap; + } + + cleanExistingMap(existingMap, onDeleteExistingValue); + return undefined; + } } namespace ts { diff --git a/src/server/editorServices.ts b/src/server/editorServices.ts index 0b5cb8c119a82..d617bdb2b9e05 100644 --- a/src/server/editorServices.ts +++ b/src/server/editorServices.ts @@ -1350,7 +1350,7 @@ namespace ts.server { } private openConfigFile(configFileName: NormalizedPath, clientFileName?: string) { - const cachedServerHost = new CachedServerHost(this.host, this.toCanonicalFileName); + const cachedServerHost = new CachedServerHost(this.host); const { projectOptions, configFileErrors, configFileSpecs } = this.convertConfigFileContentToProjectOptions(configFileName, cachedServerHost); this.logger.info(`Opened configuration file ${configFileName}`); return this.createAndAddConfiguredProject(configFileName, projectOptions, configFileErrors, configFileSpecs, cachedServerHost, clientFileName); diff --git a/src/server/lsHost.ts b/src/server/lsHost.ts index fb1cd80b66063..692547e4915d9 100644 --- a/src/server/lsHost.ts +++ b/src/server/lsHost.ts @@ -8,13 +8,12 @@ namespace ts.server { newLine: string; useCaseSensitiveFileNames: boolean; + private readonly cachedHost: CachedHost; + readonly trace: (s: string) => void; readonly realpath?: (path: string) => string; - private cachedReadDirectoryResult = createMap(); - private readonly currentDirectory: string; - - constructor(private readonly host: ServerHost, private getCanonicalFileName: (fileName: string) => string) { + constructor(private readonly host: ServerHost) { this.args = host.args; this.newLine = host.newLine; this.useCaseSensitiveFileNames = host.useCaseSensitiveFileNames; @@ -24,41 +23,7 @@ namespace ts.server { if (this.host.realpath) { this.realpath = path => this.host.realpath(path); } - this.currentDirectory = this.host.getCurrentDirectory(); - } - - private toPath(fileName: string) { - return toPath(fileName, this.currentDirectory, this.getCanonicalFileName); - } - - private getFileSystemEntries(rootDir: string) { - const path = this.toPath(rootDir); - const cachedResult = this.cachedReadDirectoryResult.get(path); - if (cachedResult) { - return cachedResult; - } - - const resultFromHost: FileSystemEntries = { - files: this.host.readDirectory(rootDir, /*extensions*/ undefined, /*exclude*/ undefined, /*include*/["*.*"]) || [], - directories: this.host.getDirectories(rootDir) || [] - }; - - this.cachedReadDirectoryResult.set(path, resultFromHost); - return resultFromHost; - } - - private canWorkWithCacheForDir(rootDir: string) { - // Some of the hosts might not be able to handle read directory or getDirectories - const path = this.toPath(rootDir); - if (this.cachedReadDirectoryResult.get(path)) { - return true; - } - try { - return this.getFileSystemEntries(rootDir); - } - catch (_e) { - return false; - } + this.cachedHost = createCachedHost(host); } write(s: string) { @@ -66,13 +31,7 @@ namespace ts.server { } writeFile(fileName: string, data: string, writeByteOrderMark?: boolean) { - const path = this.toPath(fileName); - const result = this.cachedReadDirectoryResult.get(getDirectoryPath(path)); - const baseFileName = getBaseFileName(toNormalizedPath(fileName)); - if (result) { - result.files = this.updateFileSystemEntry(result.files, baseFileName, /*isValid*/ true); - } - return this.host.writeFile(fileName, data, writeByteOrderMark); + this.cachedHost.writeFile(fileName, data, writeByteOrderMark); } resolvePath(path: string) { @@ -88,7 +47,7 @@ namespace ts.server { } getCurrentDirectory() { - return this.currentDirectory; + return this.cachedHost.getCurrentDirectory(); } exit(exitCode?: number) { @@ -101,78 +60,32 @@ namespace ts.server { } getDirectories(rootDir: string) { - if (this.canWorkWithCacheForDir(rootDir)) { - return this.getFileSystemEntries(rootDir).directories.slice(); - } - return this.host.getDirectories(rootDir); + return this.cachedHost.getDirectories(rootDir); } readDirectory(rootDir: string, extensions: string[], excludes: string[], includes: string[], depth: number): string[] { - if (this.canWorkWithCacheForDir(rootDir)) { - return matchFiles(rootDir, extensions, excludes, includes, this.useCaseSensitiveFileNames, this.currentDirectory, depth, path => this.getFileSystemEntries(path)); - } - return this.host.readDirectory(rootDir, extensions, excludes, includes, depth); + return this.cachedHost.readDirectory(rootDir, extensions, excludes, includes, depth); } fileExists(fileName: string): boolean { - const path = this.toPath(fileName); - const result = this.cachedReadDirectoryResult.get(getDirectoryPath(path)); - const baseName = getBaseFileName(toNormalizedPath(fileName)); - return (result && this.hasEntry(result.files, baseName)) || this.host.fileExists(fileName); + return this.cachedHost.fileExists(fileName); } directoryExists(dirPath: string) { - const path = this.toPath(dirPath); - return this.cachedReadDirectoryResult.has(path) || this.host.directoryExists(dirPath); + return this.cachedHost.directoryExists(dirPath); } readFile(path: string, encoding?: string): string { return this.host.readFile(path, encoding); } - private fileNameEqual(name1: string, name2: string) { - return this.getCanonicalFileName(name1) === this.getCanonicalFileName(name2); - } - - private hasEntry(entries: ReadonlyArray, name: string) { - return some(entries, file => this.fileNameEqual(file, name)); - } - - private updateFileSystemEntry(entries: ReadonlyArray, baseName: string, isValid: boolean) { - if (this.hasEntry(entries, baseName)) { - if (!isValid) { - return filter(entries, entry => !this.fileNameEqual(entry, baseName)); - } - } - else if (isValid) { - return entries.concat(baseName); - } - return entries; - } addOrDeleteFileOrFolder(fileOrFolder: NormalizedPath) { - const path = this.toPath(fileOrFolder); - const existingResult = this.cachedReadDirectoryResult.get(path); - if (existingResult) { - if (!this.host.directoryExists(fileOrFolder)) { - this.cachedReadDirectoryResult.delete(path); - } - } - else { - // Was this earlier file - const parentResult = this.cachedReadDirectoryResult.get(getDirectoryPath(path)); - if (parentResult) { - const baseName = getBaseFileName(fileOrFolder); - if (parentResult) { - parentResult.files = this.updateFileSystemEntry(parentResult.files, baseName, this.host.fileExists(path)); - parentResult.directories = this.updateFileSystemEntry(parentResult.directories, baseName, this.host.directoryExists(path)); - } - } - } + return this.cachedHost.addOrDeleteFileOrFolder(fileOrFolder); } clearCache() { - this.cachedReadDirectoryResult = createMap(); + return this.cachedHost.clearCache(); } setTimeout(callback: (...args: any[]) => void, ms: number, ...args: any[]) { diff --git a/src/server/project.ts b/src/server/project.ts index 483dfe841a700..d7c062178c442 100644 --- a/src/server/project.ts +++ b/src/server/project.ts @@ -639,38 +639,13 @@ namespace ts.server { } } - const missingFilePaths = this.program.getMissingFilePaths(); - const newMissingFilePathMap = arrayToSet(missingFilePaths); // Update the missing file paths watcher - this.missingFilesMap = mutateExistingMapWithNewSet( - this.missingFilesMap, newMissingFilePathMap, + this.missingFilesMap = updateMissingFilePathsWatch(this.program, this.missingFilesMap, // Watch the missing files - missingFilePath => { - const fileWatcher = this.projectService.addFileWatcher( - WatchType.MissingFilePath, this, missingFilePath, - (filename, eventKind) => { - if (eventKind === FileWatcherEventKind.Created && this.missingFilesMap.has(missingFilePath)) { - this.missingFilesMap.delete(missingFilePath); - this.projectService.closeFileWatcher(WatchType.MissingFilePath, this, missingFilePath, fileWatcher, WatcherCloseReason.FileCreated); - - if (this.projectKind === ProjectKind.Configured) { - const absoluteNormalizedPath = getNormalizedAbsolutePath(filename, getDirectoryPath(missingFilePath)); - (this.lsHost.host as CachedServerHost).addOrDeleteFileOrFolder(toNormalizedPath(absoluteNormalizedPath)); - } - - // When a missing file is created, we should update the graph. - this.markAsDirty(); - this.projectService.delayUpdateProjectGraphAndInferredProjectsRefresh(this); - } - } - ); - return fileWatcher; - }, + missingFilePath => this.addMissingFileWatcher(missingFilePath), // Files that are no longer missing (e.g. because they are no longer required) // should no longer be watched. - (missingFilePath, fileWatcher) => { - this.projectService.closeFileWatcher(WatchType.MissingFilePath, this, missingFilePath, fileWatcher, WatcherCloseReason.NotNeeded); - } + (missingFilePath, fileWatcher) => this.closeMissingFileWatcher(missingFilePath, fileWatcher, WatcherCloseReason.NotNeeded) ); } @@ -694,6 +669,32 @@ namespace ts.server { return hasChanges; } + private addMissingFileWatcher(missingFilePath: Path) { + const fileWatcher = this.projectService.addFileWatcher( + WatchType.MissingFilePath, this, missingFilePath, + (filename, eventKind) => { + if (eventKind === FileWatcherEventKind.Created && this.missingFilesMap.has(missingFilePath)) { + this.missingFilesMap.delete(missingFilePath); + this.closeMissingFileWatcher(missingFilePath, fileWatcher, WatcherCloseReason.FileCreated); + + if (this.projectKind === ProjectKind.Configured) { + const absoluteNormalizedPath = getNormalizedAbsolutePath(filename, getDirectoryPath(missingFilePath)); + (this.lsHost.host as CachedServerHost).addOrDeleteFileOrFolder(toNormalizedPath(absoluteNormalizedPath)); + } + + // When a missing file is created, we should update the graph. + this.markAsDirty(); + this.projectService.delayUpdateProjectGraphAndInferredProjectsRefresh(this); + } + } + ); + return fileWatcher; + } + + private closeMissingFileWatcher(missingFilePath: Path, fileWatcher: FileWatcher, reason: WatcherCloseReason) { + this.projectService.closeFileWatcher(WatchType.MissingFilePath, this, missingFilePath, fileWatcher, reason); + } + isWatchedMissingFile(path: Path) { return this.missingFilesMap && this.missingFilesMap.has(path); } @@ -1135,33 +1136,18 @@ namespace ts.server { } watchWildcards(wildcardDirectories: Map) { - this.directoriesWatchedForWildcards = mutateExistingMap( - this.directoriesWatchedForWildcards, wildcardDirectories, - // Create new watch and recursive info - (directory, flag) => { - const recursive = (flag & WatchDirectoryFlags.Recursive) !== 0; - return { - watcher: this.projectService.addDirectoryWatcher( - WatchType.WildCardDirectories, this, directory, - path => this.projectService.onFileAddOrRemoveInWatchedDirectoryOfProject(this, path), - recursive - ), - recursive - }; - }, - // Close existing watch thats not needed any more - (directory, { watcher, recursive }) => this.projectService.closeDirectoryWatcher( - WatchType.WildCardDirectories, this, directory, watcher, recursive, WatcherCloseReason.NotNeeded + this.directoriesWatchedForWildcards = updateWatchingWildcardDirectories(this.directoriesWatchedForWildcards, + wildcardDirectories, + // Create new directory watcher + (directory, recursive) => this.projectService.addDirectoryWatcher( + WatchType.WildCardDirectories, this, directory, + path => this.projectService.onFileAddOrRemoveInWatchedDirectoryOfProject(this, path), + recursive ), - // Watcher is same if the recursive flags match - ({ recursive: existingRecursive }, flag) => { - // If the recursive dont match, it needs update - const recursive = (flag & WatchDirectoryFlags.Recursive) !== 0; - return existingRecursive !== recursive; - }, - // Close existing watch that doesnt match in recursive flag - (directory, { watcher, recursive }) => this.projectService.closeDirectoryWatcher( - WatchType.WildCardDirectories, this, directory, watcher, recursive, WatcherCloseReason.RecursiveChanged + // Close directory watcher + (directory, watcher, recursive, recursiveChanged) => this.projectService.closeDirectoryWatcher( + WatchType.WildCardDirectories, this, directory, watcher, recursive, + recursiveChanged ? WatcherCloseReason.RecursiveChanged : WatcherCloseReason.NotNeeded ) ); } diff --git a/src/server/utilities.ts b/src/server/utilities.ts index 36b769182e510..85bba441e51c7 100644 --- a/src/server/utilities.ts +++ b/src/server/utilities.ts @@ -292,77 +292,4 @@ namespace ts.server { deleted(oldItems[oldIndex++]); } } - - export function cleanExistingMap( - existingMap: Map, - onDeleteExistingValue: (key: string, existingValue: T) => void) { - if (existingMap) { - // Remove all - existingMap.forEach((existingValue, key) => { - existingMap.delete(key); - onDeleteExistingValue(key, existingValue); - }); - } - } - - export function mutateExistingMapWithNewSet( - existingMap: Map, newMap: Map, - createNewValue: (key: string) => T, - onDeleteExistingValue: (key: string, existingValue: T) => void - ): Map { - return mutateExistingMap( - existingMap, newMap, - /*createNewValue*/(key, _valueInNewMap) => createNewValue(key), - onDeleteExistingValue, - ); - } - - export function mutateExistingMap( - existingMap: Map, newMap: Map, - createNewValue: (key: string, valueInNewMap: U) => T, - onDeleteExistingValue: (key: string, existingValue: T) => void, - isSameValue?: (existingValue: T, valueInNewMap: U) => boolean, - OnDeleteExistingMismatchValue?: (key: string, existingValue: T) => void, - onSameExistingValue?: (existingValue: T, valueInNewMap: U) => void - ): Map { - // If there are new values update them - if (newMap) { - if (existingMap) { - // Needs update - existingMap.forEach((existingValue, key) => { - const valueInNewMap = newMap.get(key); - // Existing value - remove it - if (valueInNewMap === undefined) { - existingMap.delete(key); - onDeleteExistingValue(key, existingValue); - } - // different value - remove it - else if (isSameValue && !isSameValue(existingValue, valueInNewMap)) { - existingMap.delete(key); - OnDeleteExistingMismatchValue(key, existingValue); - } - else if (onSameExistingValue) { - onSameExistingValue(existingValue, valueInNewMap); - } - }); - } - else { - // Create new - existingMap = createMap(); - } - - // Add new values that are not already present - newMap.forEach((valueInNewMap, key) => { - if (!existingMap.has(key)) { - // New values - existingMap.set(key, createNewValue(key, valueInNewMap)); - } - }); - - return existingMap; - } - - cleanExistingMap(existingMap, onDeleteExistingValue); - return undefined; - } } diff --git a/src/services/services.ts b/src/services/services.ts index fb87c0065d55b..e22d6041159c1 100644 --- a/src/services/services.ts +++ b/src/services/services.ts @@ -1114,9 +1114,9 @@ namespace ts { // Get a fresh cache of the host information let hostCache = new HostCache(host, getCanonicalFileName); - + const rootFileNames = hostCache.getRootFileNames(); // If the program is already up-to-date, we can reuse it - if (programUpToDate()) { + if (isProgramUptoDate(program, rootFileNames, hostCache.compilationSettings(), (path) => hostCache.getVersion(path))) { return; } @@ -1126,18 +1126,7 @@ namespace ts { // the program points to old source files that have been invalidated because of // incremental parsing. - const oldSettings = program && program.getCompilerOptions(); const newSettings = hostCache.compilationSettings(); - const shouldCreateNewSourceFiles = oldSettings && - (oldSettings.target !== newSettings.target || - oldSettings.module !== newSettings.module || - oldSettings.moduleResolution !== newSettings.moduleResolution || - oldSettings.noResolve !== newSettings.noResolve || - oldSettings.jsx !== newSettings.jsx || - oldSettings.allowJs !== newSettings.allowJs || - oldSettings.disableSizeLimit !== oldSettings.disableSizeLimit || - oldSettings.baseUrl !== newSettings.baseUrl || - !equalOwnProperties(oldSettings.paths, newSettings.paths)); // Now create a new compiler const compilerHost: CompilerHost = { @@ -1172,7 +1161,8 @@ namespace ts { }, getDirectories: path => { return host.getDirectories ? host.getDirectories(path) : []; - } + }, + onReleaseOldSourceFile }; if (host.trace) { compilerHost.trace = message => host.trace(message); @@ -1188,36 +1178,29 @@ namespace ts { } const documentRegistryBucketKey = documentRegistry.getKeyForCompilationSettings(newSettings); - const newProgram = createProgram(hostCache.getRootFileNames(), newSettings, compilerHost, program); - - // Release any files we have acquired in the old program but are - // not part of the new program. - if (program) { - const oldSourceFiles = program.getSourceFiles(); - const oldSettingsKey = documentRegistry.getKeyForCompilationSettings(oldSettings); - for (const oldSourceFile of oldSourceFiles) { - if (!newProgram.getSourceFile(oldSourceFile.fileName) || shouldCreateNewSourceFiles) { - documentRegistry.releaseDocumentWithKey(oldSourceFile.path, oldSettingsKey); - } - } - } + program = createProgram(rootFileNames, newSettings, compilerHost, program); // 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; - program = newProgram; - // Make sure all the nodes in the program are both bound, and have their parent // pointers set property. program.getTypeChecker(); return; - function getOrCreateSourceFile(fileName: string): SourceFile { - return getOrCreateSourceFileByPath(fileName, toPath(fileName, currentDirectory, getCanonicalFileName)); + // Release any files we have acquired in the old program but are + // not part of the new program. + function onReleaseOldSourceFile(oldSourceFile: SourceFile, oldOptions: CompilerOptions) { + const oldSettingsKey = documentRegistry.getKeyForCompilationSettings(oldOptions); + documentRegistry.releaseDocumentWithKey(oldSourceFile.path, oldSettingsKey); + } + + function getOrCreateSourceFile(fileName: string, languageVersion: ScriptTarget, onError?: (message: string) => void, shouldCreateNewSourceFile?: boolean): SourceFile { + return getOrCreateSourceFileByPath(fileName, toPath(fileName, currentDirectory, getCanonicalFileName), languageVersion, onError, shouldCreateNewSourceFile); } - function getOrCreateSourceFileByPath(fileName: string, path: Path): SourceFile { + function getOrCreateSourceFileByPath(fileName: string, path: Path, _languageVersion: ScriptTarget, _onError?: (message: string) => void, shouldCreateNewSourceFile?: boolean): SourceFile { Debug.assert(hostCache !== undefined); // 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 @@ -1230,7 +1213,7 @@ namespace ts { // 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. - if (!shouldCreateNewSourceFiles) { + if (!shouldCreateNewSourceFile) { // Check if the old program had this file already const oldSourceFile = program && program.getSourceFileByPath(path); if (oldSourceFile) { @@ -1270,49 +1253,6 @@ namespace ts { // Could not find this file in the old program, create a new SourceFile for it. return documentRegistry.acquireDocumentWithKey(fileName, path, newSettings, documentRegistryBucketKey, hostFileInformation.scriptSnapshot, hostFileInformation.version, hostFileInformation.scriptKind); } - - function sourceFileUpToDate(sourceFile: SourceFile): boolean { - if (!sourceFile) { - return false; - } - const path = sourceFile.path || toPath(sourceFile.fileName, currentDirectory, getCanonicalFileName); - return sourceFile.version === hostCache.getVersion(path); - } - - function programUpToDate(): boolean { - // If we haven't create a program yet, then it is not up-to-date - if (!program) { - return false; - } - - // If number of files in the program do not match, it is not up-to-date - const rootFileNames = hostCache.getRootFileNames(); - if (program.getSourceFiles().length !== rootFileNames.length) { - return false; - } - - // If any file is not up-to-date, then the whole program is not up-to-date - for (const fileName of rootFileNames) { - if (!sourceFileUpToDate(program.getSourceFile(fileName))) { - return false; - } - } - - const currentOptions = program.getCompilerOptions(); - const newOptions = hostCache.compilationSettings(); - // If the compilation settings do no match, then the program is not up-to-date - if (!compareDataObjects(currentOptions, newOptions)) { - return false; - } - - // If everything matches but the text of config file is changed, - // error locations can change for program options, so update the program - if (currentOptions.configFile && newOptions.configFile) { - return currentOptions.configFile.text === newOptions.configFile.text; - } - - return true; - } } function getProgram(): Program { diff --git a/src/services/utilities.ts b/src/services/utilities.ts index a5b035154beae..05d2ebf477cef 100644 --- a/src/services/utilities.ts +++ b/src/services/utilities.ts @@ -994,26 +994,6 @@ namespace ts { return result; } - export function compareDataObjects(dst: any, src: any): boolean { - if (!dst || !src || Object.keys(dst).length !== Object.keys(src).length) { - return false; - } - - for (const e in dst) { - if (typeof dst[e] === "object") { - if (!compareDataObjects(dst[e], src[e])) { - return false; - } - } - else if (typeof dst[e] !== "function") { - if (dst[e] !== src[e]) { - return false; - } - } - } - return true; - } - export function isArrayLiteralOrObjectLiteralDestructuringPattern(node: Node) { if (node.kind === SyntaxKind.ArrayLiteralExpression || node.kind === SyntaxKind.ObjectLiteralExpression) { From e06847503c547340b879fadfeef5e7f8cb682b22 Mon Sep 17 00:00:00 2001 From: Sheetal Nandi Date: Mon, 24 Jul 2017 16:57:49 -0700 Subject: [PATCH 034/109] Refactor so that builder handles only source files and program --- src/server/builder.ts | 217 ++++++++++++++++++++++++------------------ src/server/project.ts | 63 +++++++----- src/server/session.ts | 2 +- 3 files changed, 164 insertions(+), 118 deletions(-) diff --git a/src/server/builder.ts b/src/server/builder.ts index 285ab9ece6025..001bd708b2e10 100644 --- a/src/server/builder.ts +++ b/src/server/builder.ts @@ -3,48 +3,45 @@ /// namespace ts.server { - export function shouldEmitFile(scriptInfo: ScriptInfo) { - return !scriptInfo.hasMixedContent; - } - export interface Builder { /** * This is the callback when file infos in the builder are updated */ - onProjectUpdateGraph(): void; - getFilesAffectedBy(scriptInfo: ScriptInfo): string[]; - /** - * @returns {boolean} whether the emit was conducted or not - */ - emitFile(scriptInfo: ScriptInfo, writeFile: (path: string, data: string, writeByteOrderMark?: boolean) => void): boolean; + onProgramUpdateGraph(program: Program): void; + getFilesAffectedBy(program: Program, path: Path): string[]; + emitFile(program: Program, path: Path): EmitOutput; clear(): void; } interface EmitHandler { - addScriptInfo(scriptInfo: ScriptInfo): void; + addScriptInfo(program: Program, sourceFile: SourceFile): void; removeScriptInfo(path: Path): void; - updateScriptInfo(scriptInfo: ScriptInfo): void; + updateScriptInfo(program: Program, sourceFile: SourceFile): void; /** * Gets the files affected by the script info which has updated shape from the known one */ - getFilesAffectedByUpdatedShape(scriptInfo: ScriptInfo, singleFileResult: string[]): string[]; + getFilesAffectedByUpdatedShape(program: Program, sourceFile: SourceFile, singleFileResult: string[]): string[]; } - export function createBuilder(project: Project): Builder { + export function createBuilder( + getCanonicalFileName: (fileName: string) => string, + getEmitOutput: (program: Program, sourceFile: SourceFile, emitOnlyDtsFiles?: boolean) => EmitOutput, + computeHash: (data: string) => string, + shouldEmitFile: (sourceFile: SourceFile) => boolean + ): Builder { let isModuleEmit: boolean | undefined; - let projectVersionForDependencyGraph: string; // Last checked shape signature for the file info let fileInfos: Map; let emitHandler: EmitHandler; return { - onProjectUpdateGraph, + onProgramUpdateGraph, getFilesAffectedBy, emitFile, clear }; - function createProjectGraph() { - const currentIsModuleEmit = project.getCompilerOptions().module !== ModuleKind.None; + function createProgramGraph(program: Program) { + const currentIsModuleEmit = program.getCompilerOptions().module !== ModuleKind.None; if (isModuleEmit !== currentIsModuleEmit) { isModuleEmit = currentIsModuleEmit; emitHandler = isModuleEmit ? getModuleEmitHandler() : getNonModuleEmitHandler(); @@ -52,77 +49,55 @@ namespace ts.server { } fileInfos = mutateExistingMap( - fileInfos, arrayToMap(project.getScriptInfos(), info => info.path), - (_path, info) => { - emitHandler.addScriptInfo(info); + fileInfos, arrayToMap(program.getSourceFiles(), sourceFile => sourceFile.path), + (_path, sourceFile) => { + emitHandler.addScriptInfo(program, sourceFile); return ""; }, (path: Path, _value) => emitHandler.removeScriptInfo(path), /*isSameValue*/ undefined, /*OnDeleteExistingMismatchValue*/ undefined, - (_prevValue, scriptInfo) => emitHandler.updateScriptInfo(scriptInfo) + (_prevValue, sourceFile) => emitHandler.updateScriptInfo(program, sourceFile) ); - projectVersionForDependencyGraph = project.getProjectVersion(); } - function ensureFileInfos() { + function ensureProgramGraph(program: Program) { if (!emitHandler) { - createProjectGraph(); + createProgramGraph(program); } - Debug.assert(projectVersionForDependencyGraph === project.getProjectVersion()); } - function onProjectUpdateGraph() { + function onProgramUpdateGraph(program: Program) { if (emitHandler) { - createProjectGraph(); + createProgramGraph(program); } } - function getFilesAffectedBy(scriptInfo: ScriptInfo): string[] { - ensureFileInfos(); + function getFilesAffectedBy(program: Program, path: Path): string[] { + ensureProgramGraph(program); - const singleFileResult = scriptInfo.hasMixedContent ? [] : [scriptInfo.fileName]; - const path = scriptInfo.path; - if (!fileInfos || !fileInfos.has(path) || !updateShapeSignature(scriptInfo)) { + const sourceFile = program.getSourceFile(path); + const singleFileResult = sourceFile && shouldEmitFile(sourceFile) ? [sourceFile.fileName] : []; + if (!fileInfos || !fileInfos.has(path) || !updateShapeSignature(program, sourceFile)) { return singleFileResult; } - return emitHandler.getFilesAffectedByUpdatedShape(scriptInfo, singleFileResult); + return emitHandler.getFilesAffectedByUpdatedShape(program, sourceFile, singleFileResult); } - /** - * @returns {boolean} whether the emit was conducted or not - */ - function emitFile(scriptInfo: ScriptInfo, writeFile: (path: string, data: string, writeByteOrderMark?: boolean) => void): boolean { - ensureFileInfos(); - if (!fileInfos || !fileInfos.has(scriptInfo.path)) { - return false; + function emitFile(program: Program, path: Path): EmitOutput { + ensureProgramGraph(program); + if (!fileInfos || !fileInfos.has(path)) { + return { outputFiles: [], emitSkipped: true }; } - const { emitSkipped, outputFiles } = project.getFileEmitOutput(scriptInfo, /*emitOnlyDtsFiles*/ false); - if (!emitSkipped) { - const projectRootPath = project.getProjectRootPath(); - for (const outputFile of outputFiles) { - const outputFileAbsoluteFileName = getNormalizedAbsolutePath(outputFile.name, projectRootPath ? projectRootPath : getDirectoryPath(scriptInfo.fileName)); - writeFile(outputFileAbsoluteFileName, outputFile.text, outputFile.writeByteOrderMark); - } - } - return !emitSkipped; + return getEmitOutput(program, program.getSourceFileByPath(path), /*emitOnlyDtsFiles*/ false); } function clear() { isModuleEmit = undefined; emitHandler = undefined; fileInfos = undefined; - projectVersionForDependencyGraph = undefined; - } - - function getSourceFile(path: Path) { - return project.getSourceFile(path); - } - - function getScriptInfo(path: Path) { - return project.projectService.getScriptInfoForPath(path); } function isExternalModuleOrHasOnlyAmbientExternalModules(sourceFile: SourceFile) { @@ -147,12 +122,8 @@ namespace ts.server { /** * @return {boolean} indicates if the shape signature has changed since last update. */ - function updateShapeSignature(scriptInfo: ScriptInfo) { - const path = scriptInfo.path; - const sourceFile = getSourceFile(path); - if (!sourceFile) { - return true; - } + function updateShapeSignature(program: Program, sourceFile: SourceFile) { + const path = sourceFile.path; const prevSignature = fileInfos.get(path); let latestSignature = prevSignature; @@ -161,7 +132,7 @@ namespace ts.server { fileInfos.set(path, latestSignature); } else { - const emitOutput = project.getFileEmitOutput(scriptInfo, /*emitOnlyDtsFiles*/ true); + const emitOutput = getEmitOutput(program, sourceFile, /*emitOnlyDtsFiles*/ true); if (emitOutput.outputFiles && emitOutput.outputFiles.length > 0) { latestSignature = computeHash(emitOutput.outputFiles[0].text); fileInfos.set(path, latestSignature); @@ -171,8 +142,67 @@ namespace ts.server { return !prevSignature || latestSignature !== prevSignature; } - function computeHash(text: string) { - return project.projectService.host.createHash(text); + /** + * Gets the referenced files for a file from the program + * @param program + * @param path + */ + function getReferencedFiles(program: Program, sourceFile: SourceFile): Map { + const referencedFiles = createMap(); + // We need to use a set here since the code can contain the same import twice, + // but that will only be one dependency. + // To avoid invernal conversion, the key of the referencedFiles map must be of type Path + if (sourceFile.imports && sourceFile.imports.length > 0) { + const checker: TypeChecker = program.getTypeChecker(); + for (const importName of sourceFile.imports) { + const symbol = checker.getSymbolAtLocation(importName); + if (symbol && symbol.declarations && symbol.declarations[0]) { + const declarationSourceFile = symbol.declarations[0].getSourceFile(); + if (declarationSourceFile) { + referencedFiles.set(declarationSourceFile.path, true); + } + } + } + } + + const sourceFileDirectory = getDirectoryPath(sourceFile.path); + // Handle triple slash references + if (sourceFile.referencedFiles && sourceFile.referencedFiles.length > 0) { + for (const referencedFile of sourceFile.referencedFiles) { + const referencedPath = toPath(referencedFile.fileName, sourceFileDirectory, getCanonicalFileName); + referencedFiles.set(referencedPath, true); + } + } + + // Handle type reference directives + if (sourceFile.resolvedTypeReferenceDirectiveNames) { + sourceFile.resolvedTypeReferenceDirectiveNames.forEach((resolvedTypeReferenceDirective) => { + if (!resolvedTypeReferenceDirective) { + return; + } + + const fileName = resolvedTypeReferenceDirective.resolvedFileName; + const typeFilePath = toPath(fileName, sourceFileDirectory, getCanonicalFileName); + referencedFiles.set(typeFilePath, true); + }); + } + + return referencedFiles; + } + + /** + * Gets all the emittable files from the program + */ + function getAllEmittableFiles(program: Program) { + const defaultLibraryFileName = getDefaultLibFileName(program.getCompilerOptions()); + const sourceFiles = program.getSourceFiles(); + const result: string[] = []; + for (const sourceFile of sourceFiles) { + if (getBaseFileName(sourceFile.fileName) !== defaultLibraryFileName && shouldEmitFile(sourceFile)) { + result.push(sourceFile.fileName); + } + } + return result; } function noop() { } @@ -185,14 +215,14 @@ namespace ts.server { getFilesAffectedByUpdatedShape }; - function getFilesAffectedByUpdatedShape(_scriptInfo: ScriptInfo, singleFileResult: string[]): string[] { - const options = project.getCompilerOptions(); + function getFilesAffectedByUpdatedShape(program: Program, _sourceFile: SourceFile, singleFileResult: string[]): string[] { + const options = program.getCompilerOptions(); // If `--out` or `--outFile` is specified, any new emit will result in re-emitting the entire project, // so returning the file itself is good enough. if (options && (options.out || options.outFile)) { return singleFileResult; } - return project.getAllEmittableFiles(); + return getAllEmittableFiles(program); } } @@ -207,19 +237,22 @@ namespace ts.server { getFilesAffectedByUpdatedShape }; - function setReferences(path: Path, latestVersion: string, existingMap: Map) { - existingMap = mutateExistingMapWithNewSet(existingMap, project.getReferencedFiles(path), + function setReferences(program: Program, sourceFile: SourceFile, existingMap: Map) { + const path = sourceFile.path; + existingMap = mutateExistingMapWithNewSet( + existingMap, + getReferencedFiles(program, sourceFile), // Creating new Reference: Also add referenced by key => { referencedBy.add(key, path); return true; }, // Remove existing reference (key, _existingValue) => { referencedBy.remove(key, path); } ); references.set(path, existingMap); - scriptVersionForReferences.set(path, latestVersion); + scriptVersionForReferences.set(path, sourceFile.version); } - function addScriptInfo(info: ScriptInfo) { - setReferences(info.path, info.getLatestVersion(), undefined); + function addScriptInfo(program: Program, sourceFile: SourceFile) { + setReferences(program, sourceFile, undefined); } function removeScriptInfo(path: Path) { @@ -227,12 +260,11 @@ namespace ts.server { scriptVersionForReferences.delete(path); } - function updateScriptInfo(scriptInfo: ScriptInfo) { - const path = scriptInfo.path; + function updateScriptInfo(program: Program, sourceFile: SourceFile) { + const path = sourceFile.path; const lastUpdatedVersion = scriptVersionForReferences.get(path); - const latestVersion = scriptInfo.getLatestVersion(); - if (lastUpdatedVersion !== latestVersion) { - setReferences(path, latestVersion, references.get(path)); + if (lastUpdatedVersion !== sourceFile.version) { + setReferences(program, sourceFile, references.get(path)); } } @@ -240,14 +272,12 @@ namespace ts.server { return referencedBy.get(path) || []; } - function getFilesAffectedByUpdatedShape(scriptInfo: ScriptInfo, singleFileResult: string[]): string[] { - const path = scriptInfo.path; - const sourceFile = getSourceFile(path); + function getFilesAffectedByUpdatedShape(program: Program, sourceFile: SourceFile, singleFileResult: string[]): string[] { if (!isExternalModuleOrHasOnlyAmbientExternalModules(sourceFile)) { - return project.getAllEmittableFiles(); + return getAllEmittableFiles(program); } - const options = project.getCompilerOptions(); + const options = program.getCompilerOptions(); if (options && (options.isolatedModules || options.out || options.outFile)) { return singleFileResult; } @@ -256,22 +286,23 @@ namespace ts.server { // Because if so, its own referencedBy files need to be saved as well to make the // emitting result consistent with files on disk. - const fileNamesMap = createMap(); - const setFileName = (path: Path, scriptInfo: ScriptInfo) => { - fileNamesMap.set(path, scriptInfo && shouldEmitFile(scriptInfo) ? scriptInfo.fileName : undefined); + const fileNamesMap = createMap(); + const setFileName = (path: Path, sourceFile: SourceFile) => { + fileNamesMap.set(path, sourceFile && shouldEmitFile(sourceFile) ? sourceFile.fileName : undefined); }; // Start with the paths this file was referenced by - setFileName(path, scriptInfo); + const path = sourceFile.path; + setFileName(path, sourceFile); const queue = getReferencedByPaths(path).slice(); while (queue.length > 0) { const currentPath = queue.pop(); if (!fileNamesMap.has(currentPath)) { - const currentScriptInfo = getScriptInfo(currentPath); - if (currentScriptInfo && updateShapeSignature(currentScriptInfo)) { + const currentSourceFile = program.getSourceFileByPath(currentPath); + if (currentSourceFile && updateShapeSignature(program, currentSourceFile)) { queue.push(...getReferencedByPaths(currentPath)); } - setFileName(currentPath, currentScriptInfo); + setFileName(currentPath, currentSourceFile); } } diff --git a/src/server/project.ts b/src/server/project.ts index d7c062178c442..851a26a5d4a9c 100644 --- a/src/server/project.ts +++ b/src/server/project.ts @@ -219,7 +219,6 @@ namespace ts.server { this.disableLanguageService(); } - this.builder = createBuilder(this); this.markAsDirty(); } @@ -247,12 +246,41 @@ namespace ts.server { return this.languageService; } + private ensureBuilder() { + if (!this.builder) { + this.builder = createBuilder( + this.projectService.toCanonicalFileName, + (_program, sourceFile, emitOnlyDts) => this.getFileEmitOutput(sourceFile, emitOnlyDts), + data => this.projectService.host.createHash(data), + sourceFile => !this.projectService.getScriptInfoForPath(sourceFile.path).hasMixedContent + ); + } + } + getCompileOnSaveAffectedFileList(scriptInfo: ScriptInfo): string[] { if (!this.languageServiceEnabled) { return []; } this.updateGraph(); - return this.builder.getFilesAffectedBy(scriptInfo); + this.ensureBuilder(); + return this.builder.getFilesAffectedBy(this.program, scriptInfo.path); + } + + /** + * Returns true if emit was conducted + */ + emitFile(scriptInfo: ScriptInfo, writeFile: (path: string, data: string, writeByteOrderMark?: boolean) => void): boolean { + this.ensureBuilder(); + const { emitSkipped, outputFiles } = this.builder.emitFile(this.program, scriptInfo.path); + if (!emitSkipped) { + const projectRootPath = this.getProjectRootPath(); + for (const outputFile of outputFiles) { + const outputFileAbsoluteFileName = getNormalizedAbsolutePath(outputFile.name, projectRootPath ? projectRootPath : getDirectoryPath(scriptInfo.fileName)); + writeFile(outputFileAbsoluteFileName, outputFile.text, outputFile.writeByteOrderMark); + } + } + + return !emitSkipped; } getProjectVersion() { @@ -390,11 +418,11 @@ namespace ts.server { }); } - getFileEmitOutput(info: ScriptInfo, emitOnlyDtsFiles: boolean) { + private getFileEmitOutput(sourceFile: SourceFile, emitOnlyDtsFiles: boolean) { if (!this.languageServiceEnabled) { return undefined; } - return this.getLanguageService().getEmitOutput(info.fileName, emitOnlyDtsFiles); + return this.getLanguageService().getEmitOutput(sourceFile.fileName, emitOnlyDtsFiles); } getFileNames(excludeFilesFromExternalLibraries?: boolean, excludeConfigFiles?: boolean) { @@ -453,21 +481,6 @@ namespace ts.server { return false; } - getAllEmittableFiles() { - if (!this.languageServiceEnabled) { - return []; - } - const defaultLibraryFileName = getDefaultLibFileName(this.compilerOptions); - const infos = this.getScriptInfos(); - const result: string[] = []; - for (const info of infos) { - if (getBaseFileName(info.fileName) !== defaultLibraryFileName && shouldEmitFile(info)) { - result.push(info.fileName); - } - } - return result; - } - containsScriptInfo(info: ScriptInfo): boolean { return this.isRoot(info) || (this.program && this.program.getSourceFileByPath(info.path) !== undefined); } @@ -594,11 +607,13 @@ namespace ts.server { // update builder only if language service is enabled // otherwise tell it to drop its internal state - if (this.languageServiceEnabled && this.compileOnSaveEnabled) { - this.builder.onProjectUpdateGraph(); - } - else { - this.builder.clear(); + if (this.builder) { + if (this.languageServiceEnabled && this.compileOnSaveEnabled) { + this.builder.onProgramUpdateGraph(this.program); + } + else { + this.builder.clear(); + } } if (hasChanges) { diff --git a/src/server/session.ts b/src/server/session.ts index 1e851d09615f1..31cb8c1e15d97 100644 --- a/src/server/session.ts +++ b/src/server/session.ts @@ -1223,7 +1223,7 @@ namespace ts.server { return false; } const scriptInfo = project.getScriptInfo(file); - return project.builder.emitFile(scriptInfo, (path, data, writeByteOrderMark) => this.host.writeFile(path, data, writeByteOrderMark)); + return project.emitFile(scriptInfo, (path, data, writeByteOrderMark) => this.host.writeFile(path, data, writeByteOrderMark)); } private getSignatureHelpItems(args: protocol.SignatureHelpRequestArgs, simplifiedResult: boolean): protocol.SignatureHelpItems | SignatureHelpItems { From 6237b221da8dc57c2b17d7a962b94133715aab06 Mon Sep 17 00:00:00 2001 From: Sheetal Nandi Date: Mon, 24 Jul 2017 16:57:49 -0700 Subject: [PATCH 035/109] Move the builder to compiler directory --- src/{server => compiler}/builder.ts | 33 ++++++++++++++++++++++++----- src/compiler/program.ts | 1 + src/compiler/tsconfig.json | 1 + src/server/project.ts | 2 +- src/services/services.ts | 13 +----------- src/services/types.ts | 11 ---------- 6 files changed, 32 insertions(+), 29 deletions(-) rename src/{server => compiler}/builder.ts (90%) diff --git a/src/server/builder.ts b/src/compiler/builder.ts similarity index 90% rename from src/server/builder.ts rename to src/compiler/builder.ts index 001bd708b2e10..b830e517e5c16 100644 --- a/src/server/builder.ts +++ b/src/compiler/builder.ts @@ -1,8 +1,17 @@ -/// -/// -/// +/// + +namespace ts { + export interface EmitOutput { + outputFiles: OutputFile[]; + emitSkipped: boolean; + } + + export interface OutputFile { + name: string; + writeByteOrderMark: boolean; + text: string; + } -namespace ts.server { export interface Builder { /** * This is the callback when file infos in the builder are updated @@ -13,6 +22,20 @@ namespace ts.server { clear(): void; } + export function getFileEmitOutput(program: Program, sourceFile: SourceFile, emitOnlyDtsFiles?: boolean, + cancellationToken?: CancellationToken, customTransformers?: CustomTransformers): EmitOutput { + const outputFiles: OutputFile[] = []; + const emitOutput = program.emit(sourceFile, writeFile, cancellationToken, emitOnlyDtsFiles, customTransformers); + return { + outputFiles, + emitSkipped: emitOutput.emitSkipped + }; + + function writeFile(fileName: string, text: string, writeByteOrderMark: boolean) { + outputFiles.push({ name: fileName, writeByteOrderMark, text }); + } + } + interface EmitHandler { addScriptInfo(program: Program, sourceFile: SourceFile): void; removeScriptInfo(path: Path): void; @@ -157,7 +180,7 @@ namespace ts.server { for (const importName of sourceFile.imports) { const symbol = checker.getSymbolAtLocation(importName); if (symbol && symbol.declarations && symbol.declarations[0]) { - const declarationSourceFile = symbol.declarations[0].getSourceFile(); + const declarationSourceFile = getSourceFileOfNode(symbol.declarations[0]); if (declarationSourceFile) { referencedFiles.set(declarationSourceFile.path, true); } diff --git a/src/compiler/program.ts b/src/compiler/program.ts index 0c319b18053f5..e2bfa647eb7d6 100644 --- a/src/compiler/program.ts +++ b/src/compiler/program.ts @@ -1,6 +1,7 @@ /// /// /// +/// namespace ts { const ignoreDiagnosticCommentRegEx = /(^\s*$)|(^\s*\/\/\/?\s*(@ts-ignore)?)/; diff --git a/src/compiler/tsconfig.json b/src/compiler/tsconfig.json index 3709d65b7fd23..6c50bef7216cb 100644 --- a/src/compiler/tsconfig.json +++ b/src/compiler/tsconfig.json @@ -36,6 +36,7 @@ "declarationEmitter.ts", "emitter.ts", "program.ts", + "builder.ts", "commandLineParser.ts", "tsc.ts", "diagnosticInformationMap.generated.ts" diff --git a/src/server/project.ts b/src/server/project.ts index 851a26a5d4a9c..d764e1701d8d9 100644 --- a/src/server/project.ts +++ b/src/server/project.ts @@ -3,7 +3,7 @@ /// /// /// -/// +/// namespace ts.server { diff --git a/src/services/services.ts b/src/services/services.ts index e22d6041159c1..091364b5c50ee 100644 --- a/src/services/services.ts +++ b/src/services/services.ts @@ -1471,19 +1471,8 @@ namespace ts { synchronizeHostData(); const sourceFile = getValidSourceFile(fileName); - const outputFiles: OutputFile[] = []; - - function writeFile(fileName: string, text: string, writeByteOrderMark: boolean) { - outputFiles.push({ name: fileName, writeByteOrderMark, text }); - } - const customTransformers = host.getCustomTransformers && host.getCustomTransformers(); - const emitOutput = program.emit(sourceFile, writeFile, cancellationToken, emitOnlyDtsFiles, customTransformers); - - return { - outputFiles, - emitSkipped: emitOutput.emitSkipped - }; + return getFileEmitOutput(program, sourceFile, emitOnlyDtsFiles, cancellationToken, customTransformers); } // Signature help diff --git a/src/services/types.ts b/src/services/types.ts index 6ff7402f3ad0c..34d79b311ca74 100644 --- a/src/services/types.ts +++ b/src/services/types.ts @@ -693,23 +693,12 @@ namespace ts { autoCollapse: boolean; } - export interface EmitOutput { - outputFiles: OutputFile[]; - emitSkipped: boolean; - } - export const enum OutputFileType { JavaScript, SourceMap, Declaration } - export interface OutputFile { - name: string; - writeByteOrderMark: boolean; - text: string; - } - export const enum EndOfLineState { None, InMultiLineCommentTrivia, From 9b18f7b61c68d34bb6f4110cf84b7e86f862bfff Mon Sep 17 00:00:00 2001 From: Sheetal Nandi Date: Mon, 24 Jul 2017 16:57:49 -0700 Subject: [PATCH 036/109] Use builder to emit the files from the tsc.js --- src/compiler/builder.ts | 69 ++++++++++---- src/compiler/tsc.ts | 200 +++++++++++++++++++++------------------- src/server/project.ts | 2 +- 3 files changed, 156 insertions(+), 115 deletions(-) diff --git a/src/compiler/builder.ts b/src/compiler/builder.ts index b830e517e5c16..5661270e8f723 100644 --- a/src/compiler/builder.ts +++ b/src/compiler/builder.ts @@ -6,36 +6,28 @@ namespace ts { emitSkipped: boolean; } + export interface EmitOutputDetailed extends EmitOutput { + diagnostics: Diagnostic[]; + sourceMaps: SourceMapData[]; + emittedSourceFiles: SourceFile[]; + } + export interface OutputFile { name: string; writeByteOrderMark: boolean; text: string; } - export interface Builder { + export interface Builder { /** * This is the callback when file infos in the builder are updated */ onProgramUpdateGraph(program: Program): void; getFilesAffectedBy(program: Program, path: Path): string[]; - emitFile(program: Program, path: Path): EmitOutput; + emitFile(program: Program, path: Path): T | EmitOutput; clear(): void; } - export function getFileEmitOutput(program: Program, sourceFile: SourceFile, emitOnlyDtsFiles?: boolean, - cancellationToken?: CancellationToken, customTransformers?: CustomTransformers): EmitOutput { - const outputFiles: OutputFile[] = []; - const emitOutput = program.emit(sourceFile, writeFile, cancellationToken, emitOnlyDtsFiles, customTransformers); - return { - outputFiles, - emitSkipped: emitOutput.emitSkipped - }; - - function writeFile(fileName: string, text: string, writeByteOrderMark: boolean) { - outputFiles.push({ name: fileName, writeByteOrderMark, text }); - } - } - interface EmitHandler { addScriptInfo(program: Program, sourceFile: SourceFile): void; removeScriptInfo(path: Path): void; @@ -46,12 +38,49 @@ namespace ts { getFilesAffectedByUpdatedShape(program: Program, sourceFile: SourceFile, singleFileResult: string[]): string[]; } - export function createBuilder( + export function getDetailedEmitOutput(program: Program, sourceFile: SourceFile, emitOnlyDtsFiles: boolean, + cancellationToken ?: CancellationToken, customTransformers ?: CustomTransformers): EmitOutputDetailed { + return getEmitOutput(/*detailed*/ true, program, sourceFile, emitOnlyDtsFiles, + cancellationToken, customTransformers) as EmitOutputDetailed; + } + + export function getFileEmitOutput(program: Program, sourceFile: SourceFile, emitOnlyDtsFiles: boolean, + cancellationToken?: CancellationToken, customTransformers?: CustomTransformers): EmitOutput { + return getEmitOutput(/*detailed*/ false, program, sourceFile, emitOnlyDtsFiles, + cancellationToken, customTransformers); + } + + function getEmitOutput(isDetailed: boolean, program: Program, sourceFile: SourceFile, emitOnlyDtsFiles: boolean, + cancellationToken?: CancellationToken, customTransformers?: CustomTransformers): EmitOutput | EmitOutputDetailed { + const outputFiles: OutputFile[] = []; + let emittedSourceFiles: SourceFile[]; + const emitResult = program.emit(sourceFile, writeFile, cancellationToken, emitOnlyDtsFiles, customTransformers); + if (!isDetailed) { + return { outputFiles, emitSkipped: emitResult.emitSkipped }; + } + + return { + outputFiles, + emitSkipped: emitResult.emitSkipped, + diagnostics: emitResult.diagnostics, + sourceMaps: emitResult.sourceMaps, + emittedSourceFiles + }; + + function writeFile(fileName: string, text: string, writeByteOrderMark: boolean, _onError: (message: string) => void, sourceFiles: SourceFile[]) { + outputFiles.push({ name: fileName, writeByteOrderMark, text }); + if (isDetailed) { + emittedSourceFiles = concatenate(emittedSourceFiles, sourceFiles); + } + } + } + + export function createBuilder( getCanonicalFileName: (fileName: string) => string, - getEmitOutput: (program: Program, sourceFile: SourceFile, emitOnlyDtsFiles?: boolean) => EmitOutput, + getEmitOutput: (program: Program, sourceFile: SourceFile, emitOnlyDtsFiles?: boolean) => T, computeHash: (data: string) => string, shouldEmitFile: (sourceFile: SourceFile) => boolean - ): Builder { + ): Builder { let isModuleEmit: boolean | undefined; // Last checked shape signature for the file info let fileInfos: Map; @@ -108,7 +137,7 @@ namespace ts { return emitHandler.getFilesAffectedByUpdatedShape(program, sourceFile, singleFileResult); } - function emitFile(program: Program, path: Path): EmitOutput { + function emitFile(program: Program, path: Path): T | EmitOutput { ensureProgramGraph(program); if (!fileInfos || !fileInfos.has(path)) { return { outputFiles: [], emitSkipped: true }; diff --git a/src/compiler/tsc.ts b/src/compiler/tsc.ts index cf793919a4c43..e15a411d30fe7 100644 --- a/src/compiler/tsc.ts +++ b/src/compiler/tsc.ts @@ -2,8 +2,9 @@ /// namespace ts { - export interface SourceFile { - fileWatcher?: FileWatcher; + export interface CompilerHost { + /** If this is the emit based on the graph builder, use it to emit */ + emitWithBuilder?(program: Program): EmitResult; } interface Statistic { @@ -215,16 +216,17 @@ namespace ts { function createWatchMode(commandLine: ParsedCommandLine, configFileName?: string, configFileRootFiles?: string[], configFileOptions?: CompilerOptions, configFileSpecs?: ConfigFileSpecs, configFileWildCardDirectories?: MapLike) { let program: Program; - let needsReload: boolean; - let missingFilesMap: Map; - let configFileWatcher: FileWatcher; - let watchedWildCardDirectories: Map; - let timerToUpdateProgram: any; + let needsReload: boolean; // true if the config file changed and needs to reload it from the disk + let missingFilesMap: Map; // Map of file watchers for the missing files + let configFileWatcher: FileWatcher; // watcher for the config file + 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 compilerOptions: CompilerOptions; let rootFileNames: string[]; - const sourceFilesCache = createMap(); + const sourceFilesCache = createMap(); // Cache that stores the source file and version info + let changedFilePaths: Path[] = []; let host: System; if (configFileName) { @@ -241,6 +243,9 @@ namespace ts { const currentDirectory = host.getCurrentDirectory(); const getCanonicalFileName = createGetCanonicalFileName(host.useCaseSensitiveFileNames); + // There is no extra check needed since we can just rely on the program to decide emit + const builder = createBuilder(getCanonicalFileName, getDetailedEmitOutput, computeHash, _sourceFile => true); + if (compilerOptions.pretty) { reportDiagnosticWorker = reportDiagnosticWithColorAndContext; } @@ -268,85 +273,6 @@ namespace ts { } function createWatchedCompilerHost(options: CompilerOptions): CompilerHost { - const existingDirectories = createMap(); - function directoryExists(directoryPath: string): boolean { - if (existingDirectories.has(directoryPath)) { - return true; - } - if (host.directoryExists(directoryPath)) { - existingDirectories.set(directoryPath, true); - return true; - } - return false; - } - - function ensureDirectoriesExist(directoryPath: string) { - if (directoryPath.length > getRootLength(directoryPath) && !directoryExists(directoryPath)) { - const parentDirectory = getDirectoryPath(directoryPath); - ensureDirectoriesExist(parentDirectory); - host.createDirectory(directoryPath); - } - } - - type OutputFingerprint = { - hash: string; - byteOrderMark: boolean; - mtime: Date; - }; - let outputFingerprints: Map; - - function writeFileIfUpdated(fileName: string, data: string, writeByteOrderMark: boolean): void { - if (!outputFingerprints) { - outputFingerprints = createMap(); - } - - const hash = host.createHash(data); - const mtimeBefore = host.getModifiedTime(fileName); - - if (mtimeBefore) { - const fingerprint = outputFingerprints.get(fileName); - // If output has not been changed, and the file has no external modification - if (fingerprint && - fingerprint.byteOrderMark === writeByteOrderMark && - fingerprint.hash === hash && - fingerprint.mtime.getTime() === mtimeBefore.getTime()) { - return; - } - } - - host.writeFile(fileName, data, writeByteOrderMark); - - const mtimeAfter = host.getModifiedTime(fileName); - - outputFingerprints.set(fileName, { - hash, - byteOrderMark: writeByteOrderMark, - mtime: mtimeAfter - }); - } - - function writeFile(fileName: string, data: string, writeByteOrderMark: boolean, onError?: (message: string) => void) { - try { - performance.mark("beforeIOWrite"); - ensureDirectoriesExist(getDirectoryPath(normalizePath(fileName))); - - //if (isWatchSet(options) && sys.createHash && sys.getModifiedTime) { - writeFileIfUpdated(fileName, data, writeByteOrderMark); - //} - //else { - //host.writeFile(fileName, data, writeByteOrderMark); - //} - - performance.mark("afterIOWrite"); - performance.measure("I/O Write", "beforeIOWrite", "afterIOWrite"); - } - catch (e) { - if (onError) { - onError(e.message); - } - } - } - const newLine = getNewLineCharacter(options); const realpath = host.realpath && ((path: string) => host.realpath(path)); @@ -355,7 +281,7 @@ namespace ts { getSourceFileByPath: getVersionedSourceFileByPath, getDefaultLibLocation, getDefaultLibFileName: options => combinePaths(getDefaultLibLocation(), getDefaultLibFileName(options)), - writeFile, + writeFile: (_fileName, _data, _writeByteOrderMark, _onError?, _sourceFiles?) => { }, getCurrentDirectory: memoize(() => host.getCurrentDirectory()), useCaseSensitiveFileNames: () => host.useCaseSensitiveFileNames, getCanonicalFileName, @@ -367,7 +293,8 @@ namespace ts { getEnvironmentVariable: name => host.getEnvironmentVariable ? host.getEnvironmentVariable(name) : "", getDirectories: (path: string) => host.getDirectories(path), realpath, - onReleaseOldSourceFile + onReleaseOldSourceFile, + emitWithBuilder }; // TODO: cache module resolution @@ -379,6 +306,81 @@ namespace ts { // return host.resolveTypeReferenceDirectives(typeReferenceDirectiveNames, containingFile); // }; //} + + function ensureDirectoriesExist(directoryPath: string) { + if (directoryPath.length > getRootLength(directoryPath) && !host.directoryExists(directoryPath)) { + const parentDirectory = getDirectoryPath(directoryPath); + ensureDirectoriesExist(parentDirectory); + host.createDirectory(directoryPath); + } + } + + function writeFile(fileName: string, data: string, writeByteOrderMark: boolean) { + try { + performance.mark("beforeIOWrite"); + ensureDirectoriesExist(getDirectoryPath(normalizePath(fileName))); + + host.writeFile(fileName, data, writeByteOrderMark); + + performance.mark("afterIOWrite"); + performance.measure("I/O Write", "beforeIOWrite", "afterIOWrite"); + } + catch (e) { + return createCompilerDiagnostic(Diagnostics.Could_not_write_file_0_Colon_1, fileName, e); + } + } + + function emitWithBuilder(program: Program): EmitResult { + builder.onProgramUpdateGraph(program); + const filesPendingToEmit = changedFilePaths; + changedFilePaths = []; + + const seenFiles = createMap(); + + let emitSkipped: boolean; + let diagnostics: Diagnostic[]; + const emittedFiles: string[] = program.getCompilerOptions().listEmittedFiles ? [] : undefined; + let sourceMaps: SourceMapData[]; + while (filesPendingToEmit.length) { + const filePath = filesPendingToEmit.pop(); + const affectedFiles = builder.getFilesAffectedBy(program, filePath); + for (const file of affectedFiles) { + if (!seenFiles.has(file)) { + seenFiles.set(file, true); + const sourceFile = program.getSourceFile(file); + if (sourceFile) { + writeFiles(builder.emitFile(program, sourceFile.path)); + } + } + } + } + + return { emitSkipped, diagnostics, emittedFiles, sourceMaps }; + + function writeFiles(emitOutput: EmitOutputDetailed) { + if (emitOutput.emitSkipped) { + emitSkipped = true; + } + + diagnostics = concatenate(diagnostics, emitOutput.diagnostics); + sourceMaps = concatenate(sourceMaps, emitOutput.sourceMaps); + // If it emitted more than one source files, just mark all those source files as seen + if (emitOutput.emittedSourceFiles && emitOutput.emittedSourceFiles.length > 1) { + for (const file of emitOutput.emittedSourceFiles) { + seenFiles.set(file.fileName, true); + } + } + for (const outputFile of emitOutput.outputFiles) { + const error = writeFile(outputFile.name, outputFile.text, outputFile.writeByteOrderMark); + if (error) { + (diagnostics || (diagnostics = [])).push(error); + } + if (emittedFiles) { + emittedFiles.push(outputFile.name); + } + } + } + } } function fileExists(fileName: string) { @@ -408,6 +410,7 @@ namespace ts { // Create new source file if requested or the versions dont match if (!hostSourceFile) { + changedFilePaths.push(path); const sourceFile = getSourceFile(fileName, languageVersion, onError); if (sourceFile) { sourceFile.version = "0"; @@ -420,6 +423,7 @@ namespace ts { return sourceFile; } else if (shouldCreateNewSourceFile || hostSourceFile.version.toString() !== hostSourceFile.sourceFile.version) { + changedFilePaths.push(path); if (shouldCreateNewSourceFile) { hostSourceFile.version++; } @@ -652,6 +656,10 @@ namespace ts { host.write(s); } } + + function computeHash(data: string) { + return sys.createHash ? sys.createHash(data) : data; + } } interface CachedSystem extends System { @@ -806,16 +814,20 @@ namespace ts { } } - // TODO: in watch mode to emit only affected files - - // Otherwise, emit and report any errors we ran into. - const emitOutput = program.emit(); + // Emit and report any errors we ran into. + let emitOutput: EmitResult; + if (compilerHost.emitWithBuilder) { + emitOutput = compilerHost.emitWithBuilder(program); + } + else { + // Emit whole program + emitOutput = program.emit(); + } diagnostics = diagnostics.concat(emitOutput.diagnostics); reportDiagnostics(sortAndDeduplicateDiagnostics(diagnostics), compilerHost); reportEmittedFiles(emitOutput.emittedFiles); - if (emitOutput.emitSkipped && diagnostics.length > 0) { // If the emitter didn't emit anything, then pass that value along. return ExitStatus.DiagnosticsPresent_OutputsSkipped; diff --git a/src/server/project.ts b/src/server/project.ts index d764e1701d8d9..942ff9f71a41f 100644 --- a/src/server/project.ts +++ b/src/server/project.ts @@ -130,7 +130,7 @@ namespace ts.server { /*@internal*/ lsHost: LSHost; - builder: Builder; + builder: Builder; /** * Set of files names that were updated since the last call to getChangesSinceVersion. */ From 85b9254a60af7c56c8167a56583ba7df3b3cb4ae Mon Sep 17 00:00:00 2001 From: Sheetal Nandi Date: Wed, 26 Jul 2017 10:59:02 -0700 Subject: [PATCH 037/109] Refactor out the tsc logic into another file so we can use that to test it out --- src/compiler/tsc.ts | 1016 +---------------------------------- src/compiler/tscLib.ts | 1018 ++++++++++++++++++++++++++++++++++++ src/compiler/tsconfig.json | 1 + 3 files changed, 1020 insertions(+), 1015 deletions(-) create mode 100644 src/compiler/tscLib.ts diff --git a/src/compiler/tsc.ts b/src/compiler/tsc.ts index e15a411d30fe7..ec93b61bbdf53 100644 --- a/src/compiler/tsc.ts +++ b/src/compiler/tsc.ts @@ -1,1017 +1,3 @@ -/// -/// - -namespace ts { - export interface CompilerHost { - /** If this is the emit based on the graph builder, use it to emit */ - emitWithBuilder?(program: Program): EmitResult; - } - - interface Statistic { - name: string; - value: string; - } - - const defaultFormatDiagnosticsHost: FormatDiagnosticsHost = { - getCurrentDirectory: () => sys.getCurrentDirectory(), - getNewLine: () => sys.newLine, - getCanonicalFileName: createGetCanonicalFileName(sys.useCaseSensitiveFileNames) - }; - - let reportDiagnosticWorker = reportDiagnosticSimply; - - function reportDiagnostic(diagnostic: Diagnostic, host: FormatDiagnosticsHost) { - reportDiagnosticWorker(diagnostic, host || defaultFormatDiagnosticsHost); - } - - function reportDiagnostics(diagnostics: Diagnostic[], host: FormatDiagnosticsHost): void { - for (const diagnostic of diagnostics) { - reportDiagnostic(diagnostic, host); - } - } - - function reportEmittedFiles(files: string[]): void { - if (!files || files.length === 0) { - return; - } - - const currentDir = sys.getCurrentDirectory(); - - for (const file of files) { - const filepath = getNormalizedAbsolutePath(file, currentDir); - - sys.write(`TSFILE: ${filepath}${sys.newLine}`); - } - } - - function countLines(program: Program): number { - let count = 0; - forEach(program.getSourceFiles(), file => { - count += getLineStarts(file).length; - }); - return count; - } - - function getDiagnosticText(_message: DiagnosticMessage, ..._args: any[]): string { - const diagnostic = createCompilerDiagnostic.apply(undefined, arguments); - return diagnostic.messageText; - } - - function reportDiagnosticSimply(diagnostic: Diagnostic, host: FormatDiagnosticsHost): void { - sys.write(ts.formatDiagnostics([diagnostic], host)); - } - - function reportDiagnosticWithColorAndContext(diagnostic: Diagnostic, host: FormatDiagnosticsHost): void { - sys.write(ts.formatDiagnosticsWithColorAndContext([diagnostic], host) + sys.newLine); - } - - function reportWatchDiagnostic(diagnostic: Diagnostic) { - let output = new Date().toLocaleTimeString() + " - "; - - if (diagnostic.file) { - const loc = getLineAndCharacterOfPosition(diagnostic.file, diagnostic.start); - output += `${ diagnostic.file.fileName }(${ loc.line + 1 },${ loc.character + 1 }): `; - } - - output += `${ flattenDiagnosticMessageText(diagnostic.messageText, sys.newLine) }${ sys.newLine + sys.newLine + sys.newLine }`; - - sys.write(output); - } - - function padLeft(s: string, length: number) { - while (s.length < length) { - s = " " + s; - } - return s; - } - - function padRight(s: string, length: number) { - while (s.length < length) { - s = s + " "; - } - - return s; - } - - function isJSONSupported() { - return typeof JSON === "object" && typeof JSON.parse === "function"; - } - - export function executeCommandLine(args: string[]): void { - const commandLine = parseCommandLine(args); - - // Configuration file name (if any) - let configFileName: string; - - if (commandLine.options.locale) { - if (!isJSONSupported()) { - reportDiagnostic(createCompilerDiagnostic(Diagnostics.The_current_host_does_not_support_the_0_option, "--locale"), /* host */ undefined); - return sys.exit(ExitStatus.DiagnosticsPresent_OutputsSkipped); - } - validateLocaleAndSetLanguage(commandLine.options.locale, sys, commandLine.errors); - } - - // If there are any errors due to command line parsing and/or - // setting up localization, report them and quit. - if (commandLine.errors.length > 0) { - reportDiagnostics(commandLine.errors, /*host*/ undefined); - return sys.exit(ExitStatus.DiagnosticsPresent_OutputsSkipped); - } - - if (commandLine.options.init) { - writeConfigFile(commandLine.options, commandLine.fileNames); - return sys.exit(ExitStatus.Success); - } - - if (commandLine.options.version) { - printVersion(); - return sys.exit(ExitStatus.Success); - } - - if (commandLine.options.help || commandLine.options.all) { - printVersion(); - printHelp(commandLine.options.all); - return sys.exit(ExitStatus.Success); - } - - if (commandLine.options.project) { - if (!isJSONSupported()) { - reportDiagnostic(createCompilerDiagnostic(Diagnostics.The_current_host_does_not_support_the_0_option, "--project"), /* host */ undefined); - return sys.exit(ExitStatus.DiagnosticsPresent_OutputsSkipped); - } - if (commandLine.fileNames.length !== 0) { - reportDiagnostic(createCompilerDiagnostic(Diagnostics.Option_project_cannot_be_mixed_with_source_files_on_a_command_line), /* host */ undefined); - return sys.exit(ExitStatus.DiagnosticsPresent_OutputsSkipped); - } - - const fileOrDirectory = normalizePath(commandLine.options.project); - if (!fileOrDirectory /* current directory "." */ || sys.directoryExists(fileOrDirectory)) { - configFileName = combinePaths(fileOrDirectory, "tsconfig.json"); - if (!sys.fileExists(configFileName)) { - reportDiagnostic(createCompilerDiagnostic(Diagnostics.Cannot_find_a_tsconfig_json_file_at_the_specified_directory_Colon_0, commandLine.options.project), /* host */ undefined); - return sys.exit(ExitStatus.DiagnosticsPresent_OutputsSkipped); - } - } - else { - configFileName = fileOrDirectory; - if (!sys.fileExists(configFileName)) { - reportDiagnostic(createCompilerDiagnostic(Diagnostics.The_specified_path_does_not_exist_Colon_0, commandLine.options.project), /* host */ undefined); - return sys.exit(ExitStatus.DiagnosticsPresent_OutputsSkipped); - } - } - } - else if (commandLine.fileNames.length === 0 && isJSONSupported()) { - const searchPath = normalizePath(sys.getCurrentDirectory()); - configFileName = findConfigFile(searchPath, sys.fileExists); - } - - if (commandLine.fileNames.length === 0 && !configFileName) { - printVersion(); - printHelp(commandLine.options.all); - return sys.exit(ExitStatus.Success); - } - - if (configFileName) { - const configParseResult = parseConfigFile(configFileName, commandLine, sys); - const { fileNames, options } = configParseResult; - if (isWatchSet(configParseResult.options)) { - reportWatchModeWithoutSysSupport(); - createWatchMode(commandLine, configFileName, fileNames, options, configParseResult.configFileSpecs, configParseResult.wildcardDirectories); - } - else { - performCompilation(fileNames, options); - } - } - else if (isWatchSet(commandLine.options)) { - reportWatchModeWithoutSysSupport(); - createWatchMode(commandLine); - } - else { - performCompilation(commandLine.fileNames, commandLine.options); - } - - function reportWatchModeWithoutSysSupport() { - if (!sys.watchFile || !sys.watchDirectory) { - reportDiagnostic(createCompilerDiagnostic(Diagnostics.The_current_host_does_not_support_the_0_option, "--watch"), /* host */ undefined); - sys.exit(ExitStatus.DiagnosticsPresent_OutputsSkipped); - } - } - - function performCompilation(rootFileNames: string[], compilerOptions: CompilerOptions) { - if (compilerOptions.pretty) { - reportDiagnosticWorker = reportDiagnosticWithColorAndContext; - } - - const compilerHost = createCompilerHost(compilerOptions); - const compileResult = compile(rootFileNames, compilerOptions, compilerHost); - return sys.exit(compileResult.exitStatus); - } - } - - interface HostFileInfo { - version: number; - sourceFile: SourceFile; - fileWatcher: FileWatcher; - } - - function createWatchMode(commandLine: ParsedCommandLine, configFileName?: string, configFileRootFiles?: string[], configFileOptions?: CompilerOptions, configFileSpecs?: ConfigFileSpecs, configFileWildCardDirectories?: MapLike) { - let program: Program; - let needsReload: boolean; // true if the config file changed and needs to reload it from the disk - let missingFilesMap: Map; // Map of file watchers for the missing files - let configFileWatcher: FileWatcher; // watcher for the config file - 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 compilerOptions: CompilerOptions; - let rootFileNames: string[]; - - const sourceFilesCache = createMap(); // Cache that stores the source file and version info - let changedFilePaths: Path[] = []; - - let host: System; - if (configFileName) { - rootFileNames = configFileRootFiles; - compilerOptions = configFileOptions; - host = createCachedSystem(sys); - configFileWatcher = sys.watchFile(configFileName, onConfigFileChanged); - } - else { - rootFileNames = commandLine.fileNames; - compilerOptions = commandLine.options; - host = sys; - } - const currentDirectory = host.getCurrentDirectory(); - const getCanonicalFileName = createGetCanonicalFileName(host.useCaseSensitiveFileNames); - - // There is no extra check needed since we can just rely on the program to decide emit - const builder = createBuilder(getCanonicalFileName, getDetailedEmitOutput, computeHash, _sourceFile => true); - - if (compilerOptions.pretty) { - reportDiagnosticWorker = reportDiagnosticWithColorAndContext; - } - - synchronizeProgram(); - - // Update the wild card directory watch - watchConfigFileWildCardDirectories(); - - function synchronizeProgram() { - writeLog(`Synchronizing program`); - - if (isProgramUptoDate(program, rootFileNames, compilerOptions, getSourceVersion)) { - return; - } - - // Create the compiler host - const compilerHost = createWatchedCompilerHost(compilerOptions); - program = compile(rootFileNames, compilerOptions, compilerHost, program).program; - - // Update watches - missingFilesMap = updateMissingFilePathsWatch(program, missingFilesMap, watchMissingFilePath, closeMissingFilePathWatcher); - - reportWatchDiagnostic(createCompilerDiagnostic(Diagnostics.Compilation_complete_Watching_for_file_changes)); - } - - function createWatchedCompilerHost(options: CompilerOptions): CompilerHost { - const newLine = getNewLineCharacter(options); - const realpath = host.realpath && ((path: string) => host.realpath(path)); - - return { - getSourceFile: getVersionedSourceFile, - getSourceFileByPath: getVersionedSourceFileByPath, - getDefaultLibLocation, - getDefaultLibFileName: options => combinePaths(getDefaultLibLocation(), getDefaultLibFileName(options)), - writeFile: (_fileName, _data, _writeByteOrderMark, _onError?, _sourceFiles?) => { }, - getCurrentDirectory: memoize(() => host.getCurrentDirectory()), - useCaseSensitiveFileNames: () => host.useCaseSensitiveFileNames, - getCanonicalFileName, - getNewLine: () => newLine, - fileExists, - readFile: fileName => host.readFile(fileName), - trace: (s: string) => host.write(s + newLine), - directoryExists: directoryName => host.directoryExists(directoryName), - getEnvironmentVariable: name => host.getEnvironmentVariable ? host.getEnvironmentVariable(name) : "", - getDirectories: (path: string) => host.getDirectories(path), - realpath, - onReleaseOldSourceFile, - emitWithBuilder - }; - - // TODO: cache module resolution - //if (host.resolveModuleNames) { - // compilerHost.resolveModuleNames = (moduleNames, containingFile) => host.resolveModuleNames(moduleNames, containingFile); - //} - //if (host.resolveTypeReferenceDirectives) { - // compilerHost.resolveTypeReferenceDirectives = (typeReferenceDirectiveNames, containingFile) => { - // return host.resolveTypeReferenceDirectives(typeReferenceDirectiveNames, containingFile); - // }; - //} - - function ensureDirectoriesExist(directoryPath: string) { - if (directoryPath.length > getRootLength(directoryPath) && !host.directoryExists(directoryPath)) { - const parentDirectory = getDirectoryPath(directoryPath); - ensureDirectoriesExist(parentDirectory); - host.createDirectory(directoryPath); - } - } - - function writeFile(fileName: string, data: string, writeByteOrderMark: boolean) { - try { - performance.mark("beforeIOWrite"); - ensureDirectoriesExist(getDirectoryPath(normalizePath(fileName))); - - host.writeFile(fileName, data, writeByteOrderMark); - - performance.mark("afterIOWrite"); - performance.measure("I/O Write", "beforeIOWrite", "afterIOWrite"); - } - catch (e) { - return createCompilerDiagnostic(Diagnostics.Could_not_write_file_0_Colon_1, fileName, e); - } - } - - function emitWithBuilder(program: Program): EmitResult { - builder.onProgramUpdateGraph(program); - const filesPendingToEmit = changedFilePaths; - changedFilePaths = []; - - const seenFiles = createMap(); - - let emitSkipped: boolean; - let diagnostics: Diagnostic[]; - const emittedFiles: string[] = program.getCompilerOptions().listEmittedFiles ? [] : undefined; - let sourceMaps: SourceMapData[]; - while (filesPendingToEmit.length) { - const filePath = filesPendingToEmit.pop(); - const affectedFiles = builder.getFilesAffectedBy(program, filePath); - for (const file of affectedFiles) { - if (!seenFiles.has(file)) { - seenFiles.set(file, true); - const sourceFile = program.getSourceFile(file); - if (sourceFile) { - writeFiles(builder.emitFile(program, sourceFile.path)); - } - } - } - } - - return { emitSkipped, diagnostics, emittedFiles, sourceMaps }; - - function writeFiles(emitOutput: EmitOutputDetailed) { - if (emitOutput.emitSkipped) { - emitSkipped = true; - } - - diagnostics = concatenate(diagnostics, emitOutput.diagnostics); - sourceMaps = concatenate(sourceMaps, emitOutput.sourceMaps); - // If it emitted more than one source files, just mark all those source files as seen - if (emitOutput.emittedSourceFiles && emitOutput.emittedSourceFiles.length > 1) { - for (const file of emitOutput.emittedSourceFiles) { - seenFiles.set(file.fileName, true); - } - } - for (const outputFile of emitOutput.outputFiles) { - const error = writeFile(outputFile.name, outputFile.text, outputFile.writeByteOrderMark); - if (error) { - (diagnostics || (diagnostics = [])).push(error); - } - if (emittedFiles) { - emittedFiles.push(outputFile.name); - } - } - } - } - } - - function fileExists(fileName: string) { - const path = toPath(fileName, currentDirectory, getCanonicalFileName); - const hostSourceFileInfo = sourceFilesCache.get(path); - if (hostSourceFileInfo !== undefined) { - return !isString(hostSourceFileInfo); - } - - return host.fileExists(fileName); - } - - function getDefaultLibLocation(): string { - return getDirectoryPath(normalizePath(host.getExecutingFilePath())); - } - - function getVersionedSourceFile(fileName: string, languageVersion: ScriptTarget, onError?: (message: string) => void, shouldCreateNewSourceFile?: boolean): SourceFile { - return getVersionedSourceFileByPath(fileName, toPath(fileName, currentDirectory, getCanonicalFileName), languageVersion, onError, shouldCreateNewSourceFile); - } - - function getVersionedSourceFileByPath(fileName: string, path: Path, languageVersion: ScriptTarget, onError?: (message: string) => void, shouldCreateNewSourceFile?: boolean): SourceFile { - const hostSourceFile = sourceFilesCache.get(path); - // No source file on the host - if (isString(hostSourceFile)) { - return undefined; - } - - // Create new source file if requested or the versions dont match - if (!hostSourceFile) { - changedFilePaths.push(path); - const sourceFile = getSourceFile(fileName, languageVersion, onError); - if (sourceFile) { - sourceFile.version = "0"; - const fileWatcher = watchSourceFileForChanges(sourceFile.path); - sourceFilesCache.set(path, { sourceFile, version: 0, fileWatcher }); - } - else { - sourceFilesCache.set(path, "0"); - } - return sourceFile; - } - else if (shouldCreateNewSourceFile || hostSourceFile.version.toString() !== hostSourceFile.sourceFile.version) { - changedFilePaths.push(path); - if (shouldCreateNewSourceFile) { - hostSourceFile.version++; - } - const newSourceFile = getSourceFile(fileName, languageVersion, onError); - if (newSourceFile) { - newSourceFile.version = hostSourceFile.version.toString(); - hostSourceFile.sourceFile = newSourceFile; - } - else { - // File doesnt exist any more - hostSourceFile.fileWatcher.close(); - sourceFilesCache.set(path, hostSourceFile.version.toString()); - } - - return newSourceFile; - } - - return hostSourceFile.sourceFile; - - function getSourceFile(fileName: string, languageVersion: ScriptTarget, onError?: (message: string) => void): SourceFile { - let text: string; - try { - performance.mark("beforeIORead"); - text = host.readFile(fileName, compilerOptions.charset); - performance.mark("afterIORead"); - performance.measure("I/O Read", "beforeIORead", "afterIORead"); - } - catch (e) { - if (onError) { - onError(e.message); - } - text = ""; - } - - return text !== undefined ? createSourceFile(fileName, text, languageVersion) : undefined; - } - } - - function removeSourceFile(path: Path) { - const hostSourceFile = sourceFilesCache.get(path); - if (hostSourceFile !== undefined) { - if (!isString(hostSourceFile)) { - hostSourceFile.fileWatcher.close(); - } - sourceFilesCache.delete(path); - } - } - - function getSourceVersion(path: Path): string { - const hostSourceFile = sourceFilesCache.get(path); - return !hostSourceFile || isString(hostSourceFile) ? undefined : hostSourceFile.version.toString(); - } - - function onReleaseOldSourceFile(oldSourceFile: SourceFile, _oldOptions: CompilerOptions) { - const hostSourceFileInfo = sourceFilesCache.get(oldSourceFile.path); - // If this is the source file thats in the cache and new program doesnt need it, - // remove the cached entry. - // Note we arent deleting entry if file became missing in new program or - // there was version update and new source file was created. - if (hostSourceFileInfo && !isString(hostSourceFileInfo) && hostSourceFileInfo.sourceFile === oldSourceFile) { - sourceFilesCache.delete(oldSourceFile.path); - } - } - - // 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. - function scheduleProgramUpdate() { - if (!sys.setTimeout || !sys.clearTimeout) { - return; - } - - if (timerToUpdateProgram) { - sys.clearTimeout(timerToUpdateProgram); - } - timerToUpdateProgram = sys.setTimeout(updateProgram, 250); - } - - function scheduleProgramReload() { - Debug.assert(!!configFileName); - needsReload = true; - scheduleProgramUpdate(); - } - - function updateProgram() { - timerToUpdateProgram = undefined; - reportWatchDiagnostic(createCompilerDiagnostic(Diagnostics.File_change_detected_Starting_incremental_compilation)); - - if (needsReload) { - reloadConfigFile(); - } - else { - synchronizeProgram(); - } - } - - function reloadConfigFile() { - writeLog(`Reloading config file: ${configFileName}`); - reportWatchDiagnostic(createCompilerDiagnostic(Diagnostics.File_change_detected_Starting_incremental_compilation)); - - needsReload = false; - - const cachedHost = host as CachedSystem; - cachedHost.clearCache(); - const configParseResult = parseConfigFile(configFileName, commandLine, cachedHost); - rootFileNames = configParseResult.fileNames; - compilerOptions = configParseResult.options; - configFileSpecs = configParseResult.configFileSpecs; - configFileWildCardDirectories = configParseResult.wildcardDirectories; - - synchronizeProgram(); - - // Update the wild card directory watch - watchConfigFileWildCardDirectories(); - } - - function watchSourceFileForChanges(path: Path) { - return host.watchFile(path, (fileName, eventKind) => onSourceFileChange(fileName, path, eventKind)); - } - - function onSourceFileChange(fileName: string, path: Path, eventKind: FileWatcherEventKind) { - writeLog(`Source file path : ${path} changed: ${FileWatcherEventKind[eventKind]}, fileName: ${fileName}`); - const hostSourceFile = sourceFilesCache.get(path); - if (hostSourceFile) { - // Update the cache - if (eventKind === FileWatcherEventKind.Deleted) { - if (!isString(hostSourceFile)) { - hostSourceFile.fileWatcher.close(); - sourceFilesCache.set(path, (hostSourceFile.version++).toString()); - } - } - else { - // Deleted file created - if (isString(hostSourceFile)) { - sourceFilesCache.delete(path); - } - else { - // file changed - just update the version - hostSourceFile.version++; - } - } - } - // Update the program - scheduleProgramUpdate(); - } - - function watchMissingFilePath(missingFilePath: Path) { - return host.watchFile(missingFilePath, (fileName, eventKind) => onMissingFileChange(fileName, missingFilePath, eventKind)); - } - - function closeMissingFilePathWatcher(_missingFilePath: Path, fileWatcher: FileWatcher) { - fileWatcher.close(); - } - - function onMissingFileChange(filename: string, missingFilePath: Path, eventKind: FileWatcherEventKind) { - writeLog(`Missing file path : ${missingFilePath} changed: ${FileWatcherEventKind[eventKind]}, fileName: ${filename}`); - if (eventKind === FileWatcherEventKind.Created && missingFilesMap.has(missingFilePath)) { - closeMissingFilePathWatcher(missingFilePath, missingFilesMap.get(missingFilePath)); - missingFilesMap.delete(missingFilePath); - - if (configFileName) { - const absoluteNormalizedPath = getNormalizedAbsolutePath(filename, getDirectoryPath(missingFilePath)); - (host as CachedSystem).addOrDeleteFileOrFolder(normalizePath(absoluteNormalizedPath)); - } - - // Delete the entry in the source files cache so that new source file is created - removeSourceFile(missingFilePath); - - // When a missing file is created, we should update the graph. - scheduleProgramUpdate(); - } - } - - function watchConfigFileWildCardDirectories() { - const wildcards = createMapFromTemplate(configFileWildCardDirectories); - watchedWildCardDirectories = updateWatchingWildcardDirectories( - watchedWildCardDirectories, wildcards, - watchWildCardDirectory, stopWatchingWildCardDirectory - ); - } - - function watchWildCardDirectory(directory: string, recursive: boolean) { - return host.watchDirectory(directory, fileName => - onFileAddOrRemoveInWatchedDirectory(getNormalizedAbsolutePath(fileName, directory)), - recursive); - } - - function stopWatchingWildCardDirectory(_directory: string, fileWatcher: FileWatcher, _recursive: boolean, _recursiveChanged: boolean) { - fileWatcher.close(); - } - - function onFileAddOrRemoveInWatchedDirectory(fileName: string) { - Debug.assert(!!configFileName); - (host as CachedSystem).addOrDeleteFileOrFolder(fileName); - - // Since the file existance changed, update the sourceFiles cache - removeSourceFile(toPath(fileName, currentDirectory, getCanonicalFileName)); - - // If a change was made inside "folder/file", node will trigger the callback twice: - // one with the fileName being "folder/file", and the other one with "folder". - // We don't respond to the second one. - if (fileName && !isSupportedSourceFileName(fileName, compilerOptions)) { - writeLog(`Project: ${configFileName} Detected file add/remove of non supported extension: ${fileName}`); - return; - } - - writeLog(`Project: ${configFileName} Detected file add/remove of supported extension: ${fileName}`); - - // Reload is pending, do the reload - if (!needsReload) { - const result = getFileNamesFromConfigSpecs(configFileSpecs, getDirectoryPath(configFileName), compilerOptions, host, /*extraFileExtensions*/ []); - if (!configFileSpecs.filesSpecs) { - reportDiagnostics([getErrorForNoInputFiles(configFileSpecs, configFileName)], /*host*/ undefined); - } - rootFileNames = result.fileNames; - - // Schedule Update the program - scheduleProgramUpdate(); - } - } - - function onConfigFileChanged(fileName: string, eventKind: FileWatcherEventKind) { - writeLog(`Config file : ${configFileName} changed: ${FileWatcherEventKind[eventKind]}, fileName: ${fileName}`); - scheduleProgramReload(); - } - - function writeLog(s: string) { - const hasDiagnostics = compilerOptions.diagnostics || compilerOptions.extendedDiagnostics; - if (hasDiagnostics) { - host.write(s); - } - } - - function computeHash(data: string) { - return sys.createHash ? sys.createHash(data) : data; - } - } - - interface CachedSystem extends System { - addOrDeleteFileOrFolder(fileOrFolder: string): void; - clearCache(): void; - } - - function createCachedSystem(host: System): CachedSystem { - const getFileSize = host.getFileSize ? (path: string) => host.getFileSize(path) : undefined; - const watchFile = host.watchFile ? (path: string, callback: FileWatcherCallback, pollingInterval?: number) => host.watchFile(path, callback, pollingInterval) : undefined; - const watchDirectory = host.watchDirectory ? (path: string, callback: DirectoryWatcherCallback, recursive?: boolean) => host.watchDirectory(path, callback, recursive) : undefined; - const getModifiedTime = host.getModifiedTime ? (path: string) => host.getModifiedTime(path) : undefined; - const createHash = host.createHash ? (data: string) => host.createHash(data) : undefined; - const getMemoryUsage = host.getMemoryUsage ? () => host.getMemoryUsage() : undefined; - const realpath = host.realpath ? (path: string) => host.realpath(path) : undefined; - const tryEnableSourceMapsForHost = host.tryEnableSourceMapsForHost ? () => host.tryEnableSourceMapsForHost() : undefined; - const setTimeout = host.setTimeout ? (callback: (...args: any[]) => void, ms: number, ...args: any[]) => host.setTimeout(callback, ms, ...args) : undefined; - const clearTimeout = host.clearTimeout ? (timeoutId: any) => host.clearTimeout(timeoutId) : undefined; - - const cachedHost = createCachedHost(host); - return { - args: host.args, - newLine: host.newLine, - useCaseSensitiveFileNames: host.useCaseSensitiveFileNames, - write: s => host.write(s), - readFile: (path, encoding?) => host.readFile(path, encoding), - getFileSize, - writeFile: (fileName, data, writeByteOrderMark?) => cachedHost.writeFile(fileName, data, writeByteOrderMark), - watchFile, - watchDirectory, - resolvePath: path => host.resolvePath(path), - fileExists: fileName => cachedHost.fileExists(fileName), - directoryExists: dir => cachedHost.directoryExists(dir), - createDirectory: dir => cachedHost.createDirectory(dir), - getExecutingFilePath: () => host.getExecutingFilePath(), - getCurrentDirectory: () => cachedHost.getCurrentDirectory(), - getDirectories: dir => cachedHost.getDirectories(dir), - readDirectory: (path, extensions, excludes, includes, depth) => cachedHost.readDirectory(path, extensions, excludes, includes, depth), - getModifiedTime, - createHash, - getMemoryUsage, - exit: exitCode => host.exit(exitCode), - realpath, - getEnvironmentVariable: name => host.getEnvironmentVariable(name), - tryEnableSourceMapsForHost, - debugMode: host.debugMode, - setTimeout, - clearTimeout, - addOrDeleteFileOrFolder: fileOrFolder => cachedHost.addOrDeleteFileOrFolder(fileOrFolder), - clearCache: () => cachedHost.clearCache() - }; - } - - function parseConfigFile(configFileName: string, commandLine: ParsedCommandLine, host: System): ParsedCommandLine { - let configFileText: string; - try { - configFileText = host.readFile(configFileName); - } - catch (e) { - const error = createCompilerDiagnostic(Diagnostics.Cannot_read_file_0_Colon_1, configFileName, e.message); - reportWatchDiagnostic(error); - host.exit(ExitStatus.DiagnosticsPresent_OutputsSkipped); - return; - } - if (!configFileText) { - const error = createCompilerDiagnostic(Diagnostics.File_0_not_found, configFileName); - reportDiagnostics([error], /* compilerHost */ undefined); - host.exit(ExitStatus.DiagnosticsPresent_OutputsSkipped); - return; - } - - const result = parseJsonText(configFileName, configFileText); - reportDiagnostics(result.parseDiagnostics, /* compilerHost */ undefined); - - const cwd = host.getCurrentDirectory(); - const configParseResult = parseJsonSourceFileConfigFileContent(result, host, getNormalizedAbsolutePath(getDirectoryPath(configFileName), cwd), commandLine.options, getNormalizedAbsolutePath(configFileName, cwd)); - reportDiagnostics(configParseResult.errors, /* compilerHost */ undefined); - - return configParseResult; - } - - function compile(fileNames: string[], compilerOptions: CompilerOptions, compilerHost: CompilerHost, oldProgram?: Program) { - const hasDiagnostics = compilerOptions.diagnostics || compilerOptions.extendedDiagnostics; - let statistics: Statistic[]; - if (hasDiagnostics) { - performance.enable(); - statistics = []; - } - - const program = createProgram(fileNames, compilerOptions, compilerHost, oldProgram); - const exitStatus = compileProgram(); - - if (compilerOptions.listFiles) { - forEach(program.getSourceFiles(), file => { - sys.write(file.fileName + sys.newLine); - }); - } - - if (hasDiagnostics) { - const memoryUsed = sys.getMemoryUsage ? sys.getMemoryUsage() : -1; - reportCountStatistic("Files", program.getSourceFiles().length); - reportCountStatistic("Lines", countLines(program)); - reportCountStatistic("Nodes", program.getNodeCount()); - reportCountStatistic("Identifiers", program.getIdentifierCount()); - reportCountStatistic("Symbols", program.getSymbolCount()); - reportCountStatistic("Types", program.getTypeCount()); - - if (memoryUsed >= 0) { - reportStatisticalValue("Memory used", Math.round(memoryUsed / 1000) + "K"); - } - - const programTime = performance.getDuration("Program"); - const bindTime = performance.getDuration("Bind"); - const checkTime = performance.getDuration("Check"); - const emitTime = performance.getDuration("Emit"); - if (compilerOptions.extendedDiagnostics) { - performance.forEachMeasure((name, duration) => reportTimeStatistic(`${name} time`, duration)); - } - else { - // Individual component times. - // Note: To match the behavior of previous versions of the compiler, the reported parse time includes - // I/O read time and processing time for triple-slash references and module imports, and the reported - // emit time includes I/O write time. We preserve this behavior so we can accurately compare times. - reportTimeStatistic("I/O read", performance.getDuration("I/O Read")); - reportTimeStatistic("I/O write", performance.getDuration("I/O Write")); - reportTimeStatistic("Parse time", programTime); - reportTimeStatistic("Bind time", bindTime); - reportTimeStatistic("Check time", checkTime); - reportTimeStatistic("Emit time", emitTime); - } - reportTimeStatistic("Total time", programTime + bindTime + checkTime + emitTime); - reportStatistics(); - - performance.disable(); - } - - return { program, exitStatus }; - - function compileProgram(): ExitStatus { - let diagnostics: Diagnostic[]; - - // First get and report any syntactic errors. - diagnostics = program.getSyntacticDiagnostics(); - - // If we didn't have any syntactic errors, then also try getting the global and - // semantic errors. - if (diagnostics.length === 0) { - diagnostics = program.getOptionsDiagnostics().concat(program.getGlobalDiagnostics()); - - if (diagnostics.length === 0) { - diagnostics = program.getSemanticDiagnostics(); - } - } - - // Emit and report any errors we ran into. - let emitOutput: EmitResult; - if (compilerHost.emitWithBuilder) { - emitOutput = compilerHost.emitWithBuilder(program); - } - else { - // Emit whole program - emitOutput = program.emit(); - } - diagnostics = diagnostics.concat(emitOutput.diagnostics); - - reportDiagnostics(sortAndDeduplicateDiagnostics(diagnostics), compilerHost); - - reportEmittedFiles(emitOutput.emittedFiles); - if (emitOutput.emitSkipped && diagnostics.length > 0) { - // If the emitter didn't emit anything, then pass that value along. - return ExitStatus.DiagnosticsPresent_OutputsSkipped; - } - else if (diagnostics.length > 0) { - // The emitter emitted something, inform the caller if that happened in the presence - // of diagnostics or not. - return ExitStatus.DiagnosticsPresent_OutputsGenerated; - } - return ExitStatus.Success; - } - - function reportStatistics() { - let nameSize = 0; - let valueSize = 0; - for (const { name, value } of statistics) { - if (name.length > nameSize) { - nameSize = name.length; - } - - if (value.length > valueSize) { - valueSize = value.length; - } - } - - for (const { name, value } of statistics) { - sys.write(padRight(name + ":", nameSize + 2) + padLeft(value.toString(), valueSize) + sys.newLine); - } - } - - function reportStatisticalValue(name: string, value: string) { - statistics.push({ name, value }); - } - - function reportCountStatistic(name: string, count: number) { - reportStatisticalValue(name, "" + count); - } - - function reportTimeStatistic(name: string, time: number) { - reportStatisticalValue(name, (time / 1000).toFixed(2) + "s"); - } - } - - function printVersion() { - sys.write(getDiagnosticText(Diagnostics.Version_0, ts.version) + sys.newLine); - } - - function printHelp(showAllOptions: boolean) { - const output: string[] = []; - - // We want to align our "syntax" and "examples" commands to a certain margin. - const syntaxLength = getDiagnosticText(Diagnostics.Syntax_Colon_0, "").length; - const examplesLength = getDiagnosticText(Diagnostics.Examples_Colon_0, "").length; - let marginLength = Math.max(syntaxLength, examplesLength); - - // Build up the syntactic skeleton. - let syntax = makePadding(marginLength - syntaxLength); - syntax += "tsc [" + getDiagnosticText(Diagnostics.options) + "] [" + getDiagnosticText(Diagnostics.file) + " ...]"; - - output.push(getDiagnosticText(Diagnostics.Syntax_Colon_0, syntax)); - output.push(sys.newLine + sys.newLine); - - // Build up the list of examples. - const padding = makePadding(marginLength); - output.push(getDiagnosticText(Diagnostics.Examples_Colon_0, makePadding(marginLength - examplesLength) + "tsc hello.ts") + sys.newLine); - output.push(padding + "tsc --outFile file.js file.ts" + sys.newLine); - output.push(padding + "tsc @args.txt" + sys.newLine); - output.push(sys.newLine); - - output.push(getDiagnosticText(Diagnostics.Options_Colon) + sys.newLine); - - // Sort our options by their names, (e.g. "--noImplicitAny" comes before "--watch") - const optsList = showAllOptions ? - optionDeclarations.slice().sort((a, b) => compareValues(a.name.toLowerCase(), b.name.toLowerCase())) : - filter(optionDeclarations.slice(), v => v.showInSimplifiedHelpView); - - // We want our descriptions to align at the same column in our output, - // so we keep track of the longest option usage string. - marginLength = 0; - const usageColumn: string[] = []; // Things like "-d, --declaration" go in here. - const descriptionColumn: string[] = []; - - const optionsDescriptionMap = createMap(); // Map between option.description and list of option.type if it is a kind - - for (let i = 0; i < optsList.length; i++) { - const option = optsList[i]; - - // If an option lacks a description, - // it is not officially supported. - if (!option.description) { - continue; - } - - let usageText = " "; - if (option.shortName) { - usageText += "-" + option.shortName; - usageText += getParamType(option); - usageText += ", "; - } - - usageText += "--" + option.name; - usageText += getParamType(option); - - usageColumn.push(usageText); - let description: string; - - if (option.name === "lib") { - description = getDiagnosticText(option.description); - const element = (option).element; - const typeMap = >element.type; - optionsDescriptionMap.set(description, arrayFrom(typeMap.keys()).map(key => `'${key}'`)); - } - else { - description = getDiagnosticText(option.description); - } - - descriptionColumn.push(description); - - // Set the new margin for the description column if necessary. - marginLength = Math.max(usageText.length, marginLength); - } - - // Special case that can't fit in the loop. - const usageText = " @<" + getDiagnosticText(Diagnostics.file) + ">"; - usageColumn.push(usageText); - descriptionColumn.push(getDiagnosticText(Diagnostics.Insert_command_line_options_and_files_from_a_file)); - marginLength = Math.max(usageText.length, marginLength); - - // Print out each row, aligning all the descriptions on the same column. - for (let i = 0; i < usageColumn.length; i++) { - const usage = usageColumn[i]; - const description = descriptionColumn[i]; - const kindsList = optionsDescriptionMap.get(description); - output.push(usage + makePadding(marginLength - usage.length + 2) + description + sys.newLine); - - if (kindsList) { - output.push(makePadding(marginLength + 4)); - for (const kind of kindsList) { - output.push(kind + " "); - } - output.push(sys.newLine); - } - } - - for (const line of output) { - sys.write(line); - } - return; - - function getParamType(option: CommandLineOption) { - if (option.paramType !== undefined) { - return " " + getDiagnosticText(option.paramType); - } - return ""; - } - - function makePadding(paddingLength: number): string { - return Array(paddingLength + 1).join(" "); - } - } - - function writeConfigFile(options: CompilerOptions, fileNames: string[]) { - const currentDirectory = sys.getCurrentDirectory(); - const file = normalizePath(combinePaths(currentDirectory, "tsconfig.json")); - if (sys.fileExists(file)) { - reportDiagnostic(createCompilerDiagnostic(Diagnostics.A_tsconfig_json_file_is_already_defined_at_Colon_0, file), /* host */ undefined); - } - else { - sys.writeFile(file, generateTSConfig(options, fileNames, sys.newLine)); - reportDiagnostic(createCompilerDiagnostic(Diagnostics.Successfully_created_a_tsconfig_json_file), /* host */ undefined); - } - - return; - } -} - -ts.setStackTraceLimit(); - -if (ts.Debug.isDebugging) { - ts.Debug.enableDebugInfo(); -} - -if (ts.sys.tryEnableSourceMapsForHost && /^development$/i.test(ts.sys.getEnvironmentVariable("NODE_ENV"))) { - ts.sys.tryEnableSourceMapsForHost(); -} +/// ts.executeCommandLine(ts.sys.args); diff --git a/src/compiler/tscLib.ts b/src/compiler/tscLib.ts new file mode 100644 index 0000000000000..3dba8961674bd --- /dev/null +++ b/src/compiler/tscLib.ts @@ -0,0 +1,1018 @@ +/// +/// + +namespace ts { + export interface CompilerHost { + /** If this is the emit based on the graph builder, use it to emit */ + emitWithBuilder?(program: Program): EmitResult; + } + + interface Statistic { + name: string; + value: string; + } + + const defaultFormatDiagnosticsHost: FormatDiagnosticsHost = { + getCurrentDirectory: () => sys.getCurrentDirectory(), + getNewLine: () => sys.newLine, + getCanonicalFileName: createGetCanonicalFileName(sys.useCaseSensitiveFileNames) + }; + + let reportDiagnosticWorker = reportDiagnosticSimply; + + function reportDiagnostic(diagnostic: Diagnostic, host: FormatDiagnosticsHost) { + reportDiagnosticWorker(diagnostic, host || defaultFormatDiagnosticsHost); + } + + function reportDiagnostics(diagnostics: Diagnostic[], host: FormatDiagnosticsHost): void { + for (const diagnostic of diagnostics) { + reportDiagnostic(diagnostic, host); + } + } + + function reportEmittedFiles(files: string[]): void { + if (!files || files.length === 0) { + return; + } + + const currentDir = sys.getCurrentDirectory(); + + for (const file of files) { + const filepath = getNormalizedAbsolutePath(file, currentDir); + + sys.write(`TSFILE: ${filepath}${sys.newLine}`); + } + } + + function countLines(program: Program): number { + let count = 0; + forEach(program.getSourceFiles(), file => { + count += getLineStarts(file).length; + }); + return count; + } + + function getDiagnosticText(_message: DiagnosticMessage, ..._args: any[]): string { + const diagnostic = createCompilerDiagnostic.apply(undefined, arguments); + return diagnostic.messageText; + } + + function reportDiagnosticSimply(diagnostic: Diagnostic, host: FormatDiagnosticsHost): void { + sys.write(ts.formatDiagnostics([diagnostic], host)); + } + + function reportDiagnosticWithColorAndContext(diagnostic: Diagnostic, host: FormatDiagnosticsHost): void { + sys.write(ts.formatDiagnosticsWithColorAndContext([diagnostic], host) + sys.newLine); + } + + function reportWatchDiagnostic(diagnostic: Diagnostic) { + let output = new Date().toLocaleTimeString() + " - "; + + if (diagnostic.file) { + const loc = getLineAndCharacterOfPosition(diagnostic.file, diagnostic.start); + output += `${ diagnostic.file.fileName }(${ loc.line + 1 },${ loc.character + 1 }): `; + } + + output += `${ flattenDiagnosticMessageText(diagnostic.messageText, sys.newLine) }${ sys.newLine + sys.newLine + sys.newLine }`; + + sys.write(output); + } + + function padLeft(s: string, length: number) { + while (s.length < length) { + s = " " + s; + } + return s; + } + + function padRight(s: string, length: number) { + while (s.length < length) { + s = s + " "; + } + + return s; + } + + function isJSONSupported() { + return typeof JSON === "object" && typeof JSON.parse === "function"; + } + + export function executeCommandLine(args: string[]): void { + const commandLine = parseCommandLine(args); + + // Configuration file name (if any) + let configFileName: string; + if (commandLine.options.locale) { + if (!isJSONSupported()) { + reportDiagnostic(createCompilerDiagnostic(Diagnostics.The_current_host_does_not_support_the_0_option, "--locale"), /* host */ undefined); + return sys.exit(ExitStatus.DiagnosticsPresent_OutputsSkipped); + } + validateLocaleAndSetLanguage(commandLine.options.locale, sys, commandLine.errors); + } + + // If there are any errors due to command line parsing and/or + // setting up localization, report them and quit. + if (commandLine.errors.length > 0) { + reportDiagnostics(commandLine.errors, /*host*/ undefined); + return sys.exit(ExitStatus.DiagnosticsPresent_OutputsSkipped); + } + + if (commandLine.options.init) { + writeConfigFile(commandLine.options, commandLine.fileNames); + return sys.exit(ExitStatus.Success); + } + + if (commandLine.options.version) { + printVersion(); + return sys.exit(ExitStatus.Success); + } + + if (commandLine.options.help || commandLine.options.all) { + printVersion(); + printHelp(commandLine.options.all); + return sys.exit(ExitStatus.Success); + } + + if (commandLine.options.project) { + if (!isJSONSupported()) { + reportDiagnostic(createCompilerDiagnostic(Diagnostics.The_current_host_does_not_support_the_0_option, "--project"), /* host */ undefined); + return sys.exit(ExitStatus.DiagnosticsPresent_OutputsSkipped); + } + if (commandLine.fileNames.length !== 0) { + reportDiagnostic(createCompilerDiagnostic(Diagnostics.Option_project_cannot_be_mixed_with_source_files_on_a_command_line), /* host */ undefined); + return sys.exit(ExitStatus.DiagnosticsPresent_OutputsSkipped); + } + + const fileOrDirectory = normalizePath(commandLine.options.project); + if (!fileOrDirectory /* current directory "." */ || sys.directoryExists(fileOrDirectory)) { + configFileName = combinePaths(fileOrDirectory, "tsconfig.json"); + if (!sys.fileExists(configFileName)) { + reportDiagnostic(createCompilerDiagnostic(Diagnostics.Cannot_find_a_tsconfig_json_file_at_the_specified_directory_Colon_0, commandLine.options.project), /* host */ undefined); + return sys.exit(ExitStatus.DiagnosticsPresent_OutputsSkipped); + } + } + else { + configFileName = fileOrDirectory; + if (!sys.fileExists(configFileName)) { + reportDiagnostic(createCompilerDiagnostic(Diagnostics.The_specified_path_does_not_exist_Colon_0, commandLine.options.project), /* host */ undefined); + return sys.exit(ExitStatus.DiagnosticsPresent_OutputsSkipped); + } + } + } + else if (commandLine.fileNames.length === 0 && isJSONSupported()) { + const searchPath = normalizePath(sys.getCurrentDirectory()); + configFileName = findConfigFile(searchPath, sys.fileExists); + } + + if (commandLine.fileNames.length === 0 && !configFileName) { + printVersion(); + printHelp(commandLine.options.all); + return sys.exit(ExitStatus.Success); + } + + const commandLineOptions = commandLine.options; + if (configFileName) { + const configParseResult = parseConfigFile(configFileName, commandLineOptions, sys); + if (isWatchSet(configParseResult.options)) { + reportWatchModeWithoutSysSupport(); + createWatchModeWithConfigFile(configParseResult, commandLineOptions); + } + else { + performCompilation(configParseResult.fileNames, configParseResult.options); + } + } + else { + if (isWatchSet(commandLine.options)) { + reportWatchModeWithoutSysSupport(); + createWatchModeWithoutConfigFile(commandLine.fileNames, commandLineOptions); + } + else { + performCompilation(commandLine.fileNames, commandLineOptions); + } + } + + function reportWatchModeWithoutSysSupport() { + if (!sys.watchFile || !sys.watchDirectory) { + reportDiagnostic(createCompilerDiagnostic(Diagnostics.The_current_host_does_not_support_the_0_option, "--watch"), /* host */ undefined); + sys.exit(ExitStatus.DiagnosticsPresent_OutputsSkipped); + } + } + + function performCompilation(rootFileNames: string[], compilerOptions: CompilerOptions) { + if (compilerOptions.pretty) { + reportDiagnosticWorker = reportDiagnosticWithColorAndContext; + } + + const compilerHost = createCompilerHost(compilerOptions); + const compileResult = compile(rootFileNames, compilerOptions, compilerHost); + return sys.exit(compileResult.exitStatus); + } + } + + interface HostFileInfo { + version: number; + sourceFile: SourceFile; + fileWatcher: FileWatcher; + } + + /* @internal */ + export function createWatchModeWithConfigFile(configParseResult: ParsedCommandLine, optionsToExtend?: CompilerOptions) { + return createWatchMode(configParseResult.fileNames, configParseResult.options, configParseResult.options.configFilePath, configParseResult.configFileSpecs, configParseResult.wildcardDirectories, optionsToExtend); + } + + /* @internal */ + export function createWatchModeWithoutConfigFile(rootFileNames: string[], compilerOptions: CompilerOptions) { + return createWatchMode(rootFileNames, compilerOptions); + } + + function createWatchMode(rootFileNames: string[], compilerOptions: CompilerOptions, configFileName?: string, configFileSpecs?: ConfigFileSpecs, configFileWildCardDirectories?: MapLike, optionsToExtendForConfigFile?: CompilerOptions) { + let program: Program; + let needsReload: boolean; // true if the config file changed and needs to reload it from the disk + let missingFilesMap: Map; // Map of file watchers for the missing files + let configFileWatcher: FileWatcher; // watcher for the config file + let watchedWildCardDirectories: Map; // map of watchers for the wild card directories in the config file + let timerToUpdateProgram: any; // timer callback to recompile the program + + const sourceFilesCache = createMap(); // Cache that stores the source file and version info + let changedFilePaths: Path[] = []; + + let host: System; + if (configFileName) { + host = createCachedSystem(sys); + configFileWatcher = sys.watchFile(configFileName, onConfigFileChanged); + } + else { + host = sys; + } + const currentDirectory = host.getCurrentDirectory(); + const getCanonicalFileName = createGetCanonicalFileName(host.useCaseSensitiveFileNames); + + // There is no extra check needed since we can just rely on the program to decide emit + const builder = createBuilder(getCanonicalFileName, getDetailedEmitOutput, computeHash, _sourceFile => true); + + if (compilerOptions.pretty) { + reportDiagnosticWorker = reportDiagnosticWithColorAndContext; + } + + synchronizeProgram(); + + // Update the wild card directory watch + watchConfigFileWildCardDirectories(); + + function synchronizeProgram() { + writeLog(`Synchronizing program`); + + if (isProgramUptoDate(program, rootFileNames, compilerOptions, getSourceVersion)) { + return; + } + + // Create the compiler host + const compilerHost = createWatchedCompilerHost(compilerOptions); + program = compile(rootFileNames, compilerOptions, compilerHost, program).program; + + // Update watches + missingFilesMap = updateMissingFilePathsWatch(program, missingFilesMap, watchMissingFilePath, closeMissingFilePathWatcher); + + reportWatchDiagnostic(createCompilerDiagnostic(Diagnostics.Compilation_complete_Watching_for_file_changes)); + } + + function createWatchedCompilerHost(options: CompilerOptions): CompilerHost { + const newLine = getNewLineCharacter(options); + const realpath = host.realpath && ((path: string) => host.realpath(path)); + + return { + getSourceFile: getVersionedSourceFile, + getSourceFileByPath: getVersionedSourceFileByPath, + getDefaultLibLocation, + getDefaultLibFileName: options => combinePaths(getDefaultLibLocation(), getDefaultLibFileName(options)), + writeFile: (_fileName, _data, _writeByteOrderMark, _onError?, _sourceFiles?) => { }, + getCurrentDirectory: memoize(() => host.getCurrentDirectory()), + useCaseSensitiveFileNames: () => host.useCaseSensitiveFileNames, + getCanonicalFileName, + getNewLine: () => newLine, + fileExists, + readFile: fileName => host.readFile(fileName), + trace: (s: string) => host.write(s + newLine), + directoryExists: directoryName => host.directoryExists(directoryName), + getEnvironmentVariable: name => host.getEnvironmentVariable ? host.getEnvironmentVariable(name) : "", + getDirectories: (path: string) => host.getDirectories(path), + realpath, + onReleaseOldSourceFile, + emitWithBuilder + }; + + // TODO: cache module resolution + //if (host.resolveModuleNames) { + // compilerHost.resolveModuleNames = (moduleNames, containingFile) => host.resolveModuleNames(moduleNames, containingFile); + //} + //if (host.resolveTypeReferenceDirectives) { + // compilerHost.resolveTypeReferenceDirectives = (typeReferenceDirectiveNames, containingFile) => { + // return host.resolveTypeReferenceDirectives(typeReferenceDirectiveNames, containingFile); + // }; + //} + + function ensureDirectoriesExist(directoryPath: string) { + if (directoryPath.length > getRootLength(directoryPath) && !host.directoryExists(directoryPath)) { + const parentDirectory = getDirectoryPath(directoryPath); + ensureDirectoriesExist(parentDirectory); + host.createDirectory(directoryPath); + } + } + + function writeFile(fileName: string, data: string, writeByteOrderMark: boolean) { + try { + performance.mark("beforeIOWrite"); + ensureDirectoriesExist(getDirectoryPath(normalizePath(fileName))); + + host.writeFile(fileName, data, writeByteOrderMark); + + performance.mark("afterIOWrite"); + performance.measure("I/O Write", "beforeIOWrite", "afterIOWrite"); + } + catch (e) { + return createCompilerDiagnostic(Diagnostics.Could_not_write_file_0_Colon_1, fileName, e); + } + } + + function emitWithBuilder(program: Program): EmitResult { + builder.onProgramUpdateGraph(program); + const filesPendingToEmit = changedFilePaths; + changedFilePaths = []; + + const seenFiles = createMap(); + + let emitSkipped: boolean; + let diagnostics: Diagnostic[]; + const emittedFiles: string[] = program.getCompilerOptions().listEmittedFiles ? [] : undefined; + let sourceMaps: SourceMapData[]; + while (filesPendingToEmit.length) { + const filePath = filesPendingToEmit.pop(); + const affectedFiles = builder.getFilesAffectedBy(program, filePath); + for (const file of affectedFiles) { + if (!seenFiles.has(file)) { + seenFiles.set(file, true); + const sourceFile = program.getSourceFile(file); + if (sourceFile) { + writeFiles(builder.emitFile(program, sourceFile.path)); + } + } + } + } + + return { emitSkipped, diagnostics, emittedFiles, sourceMaps }; + + function writeFiles(emitOutput: EmitOutputDetailed) { + if (emitOutput.emitSkipped) { + emitSkipped = true; + } + + diagnostics = concatenate(diagnostics, emitOutput.diagnostics); + sourceMaps = concatenate(sourceMaps, emitOutput.sourceMaps); + // If it emitted more than one source files, just mark all those source files as seen + if (emitOutput.emittedSourceFiles && emitOutput.emittedSourceFiles.length > 1) { + for (const file of emitOutput.emittedSourceFiles) { + seenFiles.set(file.fileName, true); + } + } + for (const outputFile of emitOutput.outputFiles) { + const error = writeFile(outputFile.name, outputFile.text, outputFile.writeByteOrderMark); + if (error) { + (diagnostics || (diagnostics = [])).push(error); + } + if (emittedFiles) { + emittedFiles.push(outputFile.name); + } + } + } + } + } + + function fileExists(fileName: string) { + const path = toPath(fileName, currentDirectory, getCanonicalFileName); + const hostSourceFileInfo = sourceFilesCache.get(path); + if (hostSourceFileInfo !== undefined) { + return !isString(hostSourceFileInfo); + } + + return host.fileExists(fileName); + } + + function getDefaultLibLocation(): string { + return getDirectoryPath(normalizePath(host.getExecutingFilePath())); + } + + function getVersionedSourceFile(fileName: string, languageVersion: ScriptTarget, onError?: (message: string) => void, shouldCreateNewSourceFile?: boolean): SourceFile { + return getVersionedSourceFileByPath(fileName, toPath(fileName, currentDirectory, getCanonicalFileName), languageVersion, onError, shouldCreateNewSourceFile); + } + + function getVersionedSourceFileByPath(fileName: string, path: Path, languageVersion: ScriptTarget, onError?: (message: string) => void, shouldCreateNewSourceFile?: boolean): SourceFile { + const hostSourceFile = sourceFilesCache.get(path); + // No source file on the host + if (isString(hostSourceFile)) { + return undefined; + } + + // Create new source file if requested or the versions dont match + if (!hostSourceFile) { + changedFilePaths.push(path); + const sourceFile = getSourceFile(fileName, languageVersion, onError); + if (sourceFile) { + sourceFile.version = "0"; + const fileWatcher = watchSourceFileForChanges(sourceFile.path); + sourceFilesCache.set(path, { sourceFile, version: 0, fileWatcher }); + } + else { + sourceFilesCache.set(path, "0"); + } + return sourceFile; + } + else if (shouldCreateNewSourceFile || hostSourceFile.version.toString() !== hostSourceFile.sourceFile.version) { + changedFilePaths.push(path); + if (shouldCreateNewSourceFile) { + hostSourceFile.version++; + } + const newSourceFile = getSourceFile(fileName, languageVersion, onError); + if (newSourceFile) { + newSourceFile.version = hostSourceFile.version.toString(); + hostSourceFile.sourceFile = newSourceFile; + } + else { + // File doesnt exist any more + hostSourceFile.fileWatcher.close(); + sourceFilesCache.set(path, hostSourceFile.version.toString()); + } + + return newSourceFile; + } + + return hostSourceFile.sourceFile; + + function getSourceFile(fileName: string, languageVersion: ScriptTarget, onError?: (message: string) => void): SourceFile { + let text: string; + try { + performance.mark("beforeIORead"); + text = host.readFile(fileName, compilerOptions.charset); + performance.mark("afterIORead"); + performance.measure("I/O Read", "beforeIORead", "afterIORead"); + } + catch (e) { + if (onError) { + onError(e.message); + } + text = ""; + } + + return text !== undefined ? createSourceFile(fileName, text, languageVersion) : undefined; + } + } + + function removeSourceFile(path: Path) { + const hostSourceFile = sourceFilesCache.get(path); + if (hostSourceFile !== undefined) { + if (!isString(hostSourceFile)) { + hostSourceFile.fileWatcher.close(); + } + sourceFilesCache.delete(path); + } + } + + function getSourceVersion(path: Path): string { + const hostSourceFile = sourceFilesCache.get(path); + return !hostSourceFile || isString(hostSourceFile) ? undefined : hostSourceFile.version.toString(); + } + + function onReleaseOldSourceFile(oldSourceFile: SourceFile, _oldOptions: CompilerOptions) { + const hostSourceFileInfo = sourceFilesCache.get(oldSourceFile.path); + // If this is the source file thats in the cache and new program doesnt need it, + // remove the cached entry. + // Note we arent deleting entry if file became missing in new program or + // there was version update and new source file was created. + if (hostSourceFileInfo && !isString(hostSourceFileInfo) && hostSourceFileInfo.sourceFile === oldSourceFile) { + sourceFilesCache.delete(oldSourceFile.path); + } + } + + // 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. + function scheduleProgramUpdate() { + if (!sys.setTimeout || !sys.clearTimeout) { + return; + } + + if (timerToUpdateProgram) { + sys.clearTimeout(timerToUpdateProgram); + } + timerToUpdateProgram = sys.setTimeout(updateProgram, 250); + } + + function scheduleProgramReload() { + Debug.assert(!!configFileName); + needsReload = true; + scheduleProgramUpdate(); + } + + function updateProgram() { + timerToUpdateProgram = undefined; + reportWatchDiagnostic(createCompilerDiagnostic(Diagnostics.File_change_detected_Starting_incremental_compilation)); + + if (needsReload) { + reloadConfigFile(); + } + else { + synchronizeProgram(); + } + } + + function reloadConfigFile() { + writeLog(`Reloading config file: ${configFileName}`); + reportWatchDiagnostic(createCompilerDiagnostic(Diagnostics.File_change_detected_Starting_incremental_compilation)); + + needsReload = false; + + const cachedHost = host as CachedSystem; + cachedHost.clearCache(); + const configParseResult = parseConfigFile(configFileName, optionsToExtendForConfigFile, cachedHost); + rootFileNames = configParseResult.fileNames; + compilerOptions = configParseResult.options; + configFileSpecs = configParseResult.configFileSpecs; + configFileWildCardDirectories = configParseResult.wildcardDirectories; + + synchronizeProgram(); + + // Update the wild card directory watch + watchConfigFileWildCardDirectories(); + } + + function watchSourceFileForChanges(path: Path) { + return host.watchFile(path, (fileName, eventKind) => onSourceFileChange(fileName, path, eventKind)); + } + + function onSourceFileChange(fileName: string, path: Path, eventKind: FileWatcherEventKind) { + writeLog(`Source file path : ${path} changed: ${FileWatcherEventKind[eventKind]}, fileName: ${fileName}`); + const hostSourceFile = sourceFilesCache.get(path); + if (hostSourceFile) { + // Update the cache + if (eventKind === FileWatcherEventKind.Deleted) { + if (!isString(hostSourceFile)) { + hostSourceFile.fileWatcher.close(); + sourceFilesCache.set(path, (hostSourceFile.version++).toString()); + } + } + else { + // Deleted file created + if (isString(hostSourceFile)) { + sourceFilesCache.delete(path); + } + else { + // file changed - just update the version + hostSourceFile.version++; + } + } + } + // Update the program + scheduleProgramUpdate(); + } + + function watchMissingFilePath(missingFilePath: Path) { + return host.watchFile(missingFilePath, (fileName, eventKind) => onMissingFileChange(fileName, missingFilePath, eventKind)); + } + + function closeMissingFilePathWatcher(_missingFilePath: Path, fileWatcher: FileWatcher) { + fileWatcher.close(); + } + + function onMissingFileChange(filename: string, missingFilePath: Path, eventKind: FileWatcherEventKind) { + writeLog(`Missing file path : ${missingFilePath} changed: ${FileWatcherEventKind[eventKind]}, fileName: ${filename}`); + if (eventKind === FileWatcherEventKind.Created && missingFilesMap.has(missingFilePath)) { + closeMissingFilePathWatcher(missingFilePath, missingFilesMap.get(missingFilePath)); + missingFilesMap.delete(missingFilePath); + + if (configFileName) { + const absoluteNormalizedPath = getNormalizedAbsolutePath(filename, getDirectoryPath(missingFilePath)); + (host as CachedSystem).addOrDeleteFileOrFolder(normalizePath(absoluteNormalizedPath)); + } + + // Delete the entry in the source files cache so that new source file is created + removeSourceFile(missingFilePath); + + // When a missing file is created, we should update the graph. + scheduleProgramUpdate(); + } + } + + function watchConfigFileWildCardDirectories() { + const wildcards = createMapFromTemplate(configFileWildCardDirectories); + watchedWildCardDirectories = updateWatchingWildcardDirectories( + watchedWildCardDirectories, wildcards, + watchWildCardDirectory, stopWatchingWildCardDirectory + ); + } + + function watchWildCardDirectory(directory: string, recursive: boolean) { + return host.watchDirectory(directory, fileName => + onFileAddOrRemoveInWatchedDirectory(getNormalizedAbsolutePath(fileName, directory)), + recursive); + } + + function stopWatchingWildCardDirectory(_directory: string, fileWatcher: FileWatcher, _recursive: boolean, _recursiveChanged: boolean) { + fileWatcher.close(); + } + + function onFileAddOrRemoveInWatchedDirectory(fileName: string) { + Debug.assert(!!configFileName); + (host as CachedSystem).addOrDeleteFileOrFolder(fileName); + + // Since the file existance changed, update the sourceFiles cache + removeSourceFile(toPath(fileName, currentDirectory, getCanonicalFileName)); + + // If a change was made inside "folder/file", node will trigger the callback twice: + // one with the fileName being "folder/file", and the other one with "folder". + // We don't respond to the second one. + if (fileName && !isSupportedSourceFileName(fileName, compilerOptions)) { + writeLog(`Project: ${configFileName} Detected file add/remove of non supported extension: ${fileName}`); + return; + } + + writeLog(`Project: ${configFileName} Detected file add/remove of supported extension: ${fileName}`); + + // Reload is pending, do the reload + if (!needsReload) { + const result = getFileNamesFromConfigSpecs(configFileSpecs, getDirectoryPath(configFileName), compilerOptions, host, /*extraFileExtensions*/ []); + if (!configFileSpecs.filesSpecs) { + reportDiagnostics([getErrorForNoInputFiles(configFileSpecs, configFileName)], /*host*/ undefined); + } + rootFileNames = result.fileNames; + + // Schedule Update the program + scheduleProgramUpdate(); + } + } + + function onConfigFileChanged(fileName: string, eventKind: FileWatcherEventKind) { + writeLog(`Config file : ${configFileName} changed: ${FileWatcherEventKind[eventKind]}, fileName: ${fileName}`); + scheduleProgramReload(); + } + + function writeLog(s: string) { + const hasDiagnostics = compilerOptions.diagnostics || compilerOptions.extendedDiagnostics; + if (hasDiagnostics) { + host.write(s); + } + } + + function computeHash(data: string) { + return sys.createHash ? sys.createHash(data) : data; + } + } + + interface CachedSystem extends System { + addOrDeleteFileOrFolder(fileOrFolder: string): void; + clearCache(): void; + } + + function createCachedSystem(host: System): CachedSystem { + const getFileSize = host.getFileSize ? (path: string) => host.getFileSize(path) : undefined; + const watchFile = host.watchFile ? (path: string, callback: FileWatcherCallback, pollingInterval?: number) => host.watchFile(path, callback, pollingInterval) : undefined; + const watchDirectory = host.watchDirectory ? (path: string, callback: DirectoryWatcherCallback, recursive?: boolean) => host.watchDirectory(path, callback, recursive) : undefined; + const getModifiedTime = host.getModifiedTime ? (path: string) => host.getModifiedTime(path) : undefined; + const createHash = host.createHash ? (data: string) => host.createHash(data) : undefined; + const getMemoryUsage = host.getMemoryUsage ? () => host.getMemoryUsage() : undefined; + const realpath = host.realpath ? (path: string) => host.realpath(path) : undefined; + const tryEnableSourceMapsForHost = host.tryEnableSourceMapsForHost ? () => host.tryEnableSourceMapsForHost() : undefined; + const setTimeout = host.setTimeout ? (callback: (...args: any[]) => void, ms: number, ...args: any[]) => host.setTimeout(callback, ms, ...args) : undefined; + const clearTimeout = host.clearTimeout ? (timeoutId: any) => host.clearTimeout(timeoutId) : undefined; + + const cachedHost = createCachedHost(host); + return { + args: host.args, + newLine: host.newLine, + useCaseSensitiveFileNames: host.useCaseSensitiveFileNames, + write: s => host.write(s), + readFile: (path, encoding?) => host.readFile(path, encoding), + getFileSize, + writeFile: (fileName, data, writeByteOrderMark?) => cachedHost.writeFile(fileName, data, writeByteOrderMark), + watchFile, + watchDirectory, + resolvePath: path => host.resolvePath(path), + fileExists: fileName => cachedHost.fileExists(fileName), + directoryExists: dir => cachedHost.directoryExists(dir), + createDirectory: dir => cachedHost.createDirectory(dir), + getExecutingFilePath: () => host.getExecutingFilePath(), + getCurrentDirectory: () => cachedHost.getCurrentDirectory(), + getDirectories: dir => cachedHost.getDirectories(dir), + readDirectory: (path, extensions, excludes, includes, depth) => cachedHost.readDirectory(path, extensions, excludes, includes, depth), + getModifiedTime, + createHash, + getMemoryUsage, + exit: exitCode => host.exit(exitCode), + realpath, + getEnvironmentVariable: name => host.getEnvironmentVariable(name), + tryEnableSourceMapsForHost, + debugMode: host.debugMode, + setTimeout, + clearTimeout, + addOrDeleteFileOrFolder: fileOrFolder => cachedHost.addOrDeleteFileOrFolder(fileOrFolder), + clearCache: () => cachedHost.clearCache() + }; + } + + /* @internal */ + export function parseConfigFile(configFileName: string, optionsToExtend: CompilerOptions, host: System): ParsedCommandLine { + let configFileText: string; + try { + configFileText = host.readFile(configFileName); + } + catch (e) { + const error = createCompilerDiagnostic(Diagnostics.Cannot_read_file_0_Colon_1, configFileName, e.message); + reportWatchDiagnostic(error); + host.exit(ExitStatus.DiagnosticsPresent_OutputsSkipped); + return; + } + if (!configFileText) { + const error = createCompilerDiagnostic(Diagnostics.File_0_not_found, configFileName); + reportDiagnostics([error], /* compilerHost */ undefined); + host.exit(ExitStatus.DiagnosticsPresent_OutputsSkipped); + return; + } + + const result = parseJsonText(configFileName, configFileText); + reportDiagnostics(result.parseDiagnostics, /* compilerHost */ undefined); + + const cwd = host.getCurrentDirectory(); + const configParseResult = parseJsonSourceFileConfigFileContent(result, host, getNormalizedAbsolutePath(getDirectoryPath(configFileName), cwd), optionsToExtend, getNormalizedAbsolutePath(configFileName, cwd)); + reportDiagnostics(configParseResult.errors, /* compilerHost */ undefined); + + return configParseResult; + } + + function compile(fileNames: string[], compilerOptions: CompilerOptions, compilerHost: CompilerHost, oldProgram?: Program) { + const hasDiagnostics = compilerOptions.diagnostics || compilerOptions.extendedDiagnostics; + let statistics: Statistic[]; + if (hasDiagnostics) { + performance.enable(); + statistics = []; + } + + const program = createProgram(fileNames, compilerOptions, compilerHost, oldProgram); + const exitStatus = compileProgram(); + + if (compilerOptions.listFiles) { + forEach(program.getSourceFiles(), file => { + sys.write(file.fileName + sys.newLine); + }); + } + + if (hasDiagnostics) { + const memoryUsed = sys.getMemoryUsage ? sys.getMemoryUsage() : -1; + reportCountStatistic("Files", program.getSourceFiles().length); + reportCountStatistic("Lines", countLines(program)); + reportCountStatistic("Nodes", program.getNodeCount()); + reportCountStatistic("Identifiers", program.getIdentifierCount()); + reportCountStatistic("Symbols", program.getSymbolCount()); + reportCountStatistic("Types", program.getTypeCount()); + + if (memoryUsed >= 0) { + reportStatisticalValue("Memory used", Math.round(memoryUsed / 1000) + "K"); + } + + const programTime = performance.getDuration("Program"); + const bindTime = performance.getDuration("Bind"); + const checkTime = performance.getDuration("Check"); + const emitTime = performance.getDuration("Emit"); + if (compilerOptions.extendedDiagnostics) { + performance.forEachMeasure((name, duration) => reportTimeStatistic(`${name} time`, duration)); + } + else { + // Individual component times. + // Note: To match the behavior of previous versions of the compiler, the reported parse time includes + // I/O read time and processing time for triple-slash references and module imports, and the reported + // emit time includes I/O write time. We preserve this behavior so we can accurately compare times. + reportTimeStatistic("I/O read", performance.getDuration("I/O Read")); + reportTimeStatistic("I/O write", performance.getDuration("I/O Write")); + reportTimeStatistic("Parse time", programTime); + reportTimeStatistic("Bind time", bindTime); + reportTimeStatistic("Check time", checkTime); + reportTimeStatistic("Emit time", emitTime); + } + reportTimeStatistic("Total time", programTime + bindTime + checkTime + emitTime); + reportStatistics(); + + performance.disable(); + } + + return { program, exitStatus }; + + function compileProgram(): ExitStatus { + let diagnostics: Diagnostic[]; + + // First get and report any syntactic errors. + diagnostics = program.getSyntacticDiagnostics(); + + // If we didn't have any syntactic errors, then also try getting the global and + // semantic errors. + if (diagnostics.length === 0) { + diagnostics = program.getOptionsDiagnostics().concat(program.getGlobalDiagnostics()); + + if (diagnostics.length === 0) { + diagnostics = program.getSemanticDiagnostics(); + } + } + + // Emit and report any errors we ran into. + let emitOutput: EmitResult; + if (compilerHost.emitWithBuilder) { + emitOutput = compilerHost.emitWithBuilder(program); + } + else { + // Emit whole program + emitOutput = program.emit(); + } + diagnostics = diagnostics.concat(emitOutput.diagnostics); + + reportDiagnostics(sortAndDeduplicateDiagnostics(diagnostics), compilerHost); + + reportEmittedFiles(emitOutput.emittedFiles); + if (emitOutput.emitSkipped && diagnostics.length > 0) { + // If the emitter didn't emit anything, then pass that value along. + return ExitStatus.DiagnosticsPresent_OutputsSkipped; + } + else if (diagnostics.length > 0) { + // The emitter emitted something, inform the caller if that happened in the presence + // of diagnostics or not. + return ExitStatus.DiagnosticsPresent_OutputsGenerated; + } + return ExitStatus.Success; + } + + function reportStatistics() { + let nameSize = 0; + let valueSize = 0; + for (const { name, value } of statistics) { + if (name.length > nameSize) { + nameSize = name.length; + } + + if (value.length > valueSize) { + valueSize = value.length; + } + } + + for (const { name, value } of statistics) { + sys.write(padRight(name + ":", nameSize + 2) + padLeft(value.toString(), valueSize) + sys.newLine); + } + } + + function reportStatisticalValue(name: string, value: string) { + statistics.push({ name, value }); + } + + function reportCountStatistic(name: string, count: number) { + reportStatisticalValue(name, "" + count); + } + + function reportTimeStatistic(name: string, time: number) { + reportStatisticalValue(name, (time / 1000).toFixed(2) + "s"); + } + } + + function printVersion() { + sys.write(getDiagnosticText(Diagnostics.Version_0, ts.version) + sys.newLine); + } + + function printHelp(showAllOptions: boolean) { + const output: string[] = []; + + // We want to align our "syntax" and "examples" commands to a certain margin. + const syntaxLength = getDiagnosticText(Diagnostics.Syntax_Colon_0, "").length; + const examplesLength = getDiagnosticText(Diagnostics.Examples_Colon_0, "").length; + let marginLength = Math.max(syntaxLength, examplesLength); + + // Build up the syntactic skeleton. + let syntax = makePadding(marginLength - syntaxLength); + syntax += "tsc [" + getDiagnosticText(Diagnostics.options) + "] [" + getDiagnosticText(Diagnostics.file) + " ...]"; + + output.push(getDiagnosticText(Diagnostics.Syntax_Colon_0, syntax)); + output.push(sys.newLine + sys.newLine); + + // Build up the list of examples. + const padding = makePadding(marginLength); + output.push(getDiagnosticText(Diagnostics.Examples_Colon_0, makePadding(marginLength - examplesLength) + "tsc hello.ts") + sys.newLine); + output.push(padding + "tsc --outFile file.js file.ts" + sys.newLine); + output.push(padding + "tsc @args.txt" + sys.newLine); + output.push(sys.newLine); + + output.push(getDiagnosticText(Diagnostics.Options_Colon) + sys.newLine); + + // Sort our options by their names, (e.g. "--noImplicitAny" comes before "--watch") + const optsList = showAllOptions ? + optionDeclarations.slice().sort((a, b) => compareValues(a.name.toLowerCase(), b.name.toLowerCase())) : + filter(optionDeclarations.slice(), v => v.showInSimplifiedHelpView); + + // We want our descriptions to align at the same column in our output, + // so we keep track of the longest option usage string. + marginLength = 0; + const usageColumn: string[] = []; // Things like "-d, --declaration" go in here. + const descriptionColumn: string[] = []; + + const optionsDescriptionMap = createMap(); // Map between option.description and list of option.type if it is a kind + + for (let i = 0; i < optsList.length; i++) { + const option = optsList[i]; + + // If an option lacks a description, + // it is not officially supported. + if (!option.description) { + continue; + } + + let usageText = " "; + if (option.shortName) { + usageText += "-" + option.shortName; + usageText += getParamType(option); + usageText += ", "; + } + + usageText += "--" + option.name; + usageText += getParamType(option); + + usageColumn.push(usageText); + let description: string; + + if (option.name === "lib") { + description = getDiagnosticText(option.description); + const element = (option).element; + const typeMap = >element.type; + optionsDescriptionMap.set(description, arrayFrom(typeMap.keys()).map(key => `'${key}'`)); + } + else { + description = getDiagnosticText(option.description); + } + + descriptionColumn.push(description); + + // Set the new margin for the description column if necessary. + marginLength = Math.max(usageText.length, marginLength); + } + + // Special case that can't fit in the loop. + const usageText = " @<" + getDiagnosticText(Diagnostics.file) + ">"; + usageColumn.push(usageText); + descriptionColumn.push(getDiagnosticText(Diagnostics.Insert_command_line_options_and_files_from_a_file)); + marginLength = Math.max(usageText.length, marginLength); + + // Print out each row, aligning all the descriptions on the same column. + for (let i = 0; i < usageColumn.length; i++) { + const usage = usageColumn[i]; + const description = descriptionColumn[i]; + const kindsList = optionsDescriptionMap.get(description); + output.push(usage + makePadding(marginLength - usage.length + 2) + description + sys.newLine); + + if (kindsList) { + output.push(makePadding(marginLength + 4)); + for (const kind of kindsList) { + output.push(kind + " "); + } + output.push(sys.newLine); + } + } + + for (const line of output) { + sys.write(line); + } + return; + + function getParamType(option: CommandLineOption) { + if (option.paramType !== undefined) { + return " " + getDiagnosticText(option.paramType); + } + return ""; + } + + function makePadding(paddingLength: number): string { + return Array(paddingLength + 1).join(" "); + } + } + + function writeConfigFile(options: CompilerOptions, fileNames: string[]) { + const currentDirectory = sys.getCurrentDirectory(); + const file = normalizePath(combinePaths(currentDirectory, "tsconfig.json")); + if (sys.fileExists(file)) { + reportDiagnostic(createCompilerDiagnostic(Diagnostics.A_tsconfig_json_file_is_already_defined_at_Colon_0, file), /* host */ undefined); + } + else { + sys.writeFile(file, generateTSConfig(options, fileNames, sys.newLine)); + reportDiagnostic(createCompilerDiagnostic(Diagnostics.Successfully_created_a_tsconfig_json_file), /* host */ undefined); + } + + return; + } +} + +if (ts.Debug.isDebugging) { + ts.Debug.enableDebugInfo(); +} + +if (ts.sys.tryEnableSourceMapsForHost && /^development$/i.test(ts.sys.getEnvironmentVariable("NODE_ENV"))) { + ts.sys.tryEnableSourceMapsForHost(); +} diff --git a/src/compiler/tsconfig.json b/src/compiler/tsconfig.json index 6c50bef7216cb..360819f2b3659 100644 --- a/src/compiler/tsconfig.json +++ b/src/compiler/tsconfig.json @@ -38,6 +38,7 @@ "program.ts", "builder.ts", "commandLineParser.ts", + "tscLib.ts", "tsc.ts", "diagnosticInformationMap.generated.ts" ] From 69e5abd5b774cf34d9204c1e56faaa5b2f228ed3 Mon Sep 17 00:00:00 2001 From: Sheetal Nandi Date: Wed, 26 Jul 2017 11:57:10 -0700 Subject: [PATCH 038/109] Refactor watched system from tsserver tests so that tscLib watch can leverage it --- src/harness/harness.ts | 4 + src/harness/harnessLanguageService.ts | 4 - src/harness/tsconfig.json | 3 +- .../unittests/cachingInServerLSHost.ts | 2 +- src/harness/unittests/session.ts | 2 +- src/harness/unittests/telemetry.ts | 8 +- .../unittests/tsserverProjectSystem.ts | 502 +----------------- src/harness/virtualFileSystemWithWatch.ts | 492 +++++++++++++++++ 8 files changed, 517 insertions(+), 500 deletions(-) create mode 100644 src/harness/virtualFileSystemWithWatch.ts diff --git a/src/harness/harness.ts b/src/harness/harness.ts index 8f4dae7011602..c0181199cbebc 100644 --- a/src/harness/harness.ts +++ b/src/harness/harness.ts @@ -753,6 +753,10 @@ namespace Harness { } } + export function mockHash(s: string): string { + return `hash-${s}`; + } + const environment = Utils.getExecutionEnvironment(); switch (environment) { case Utils.ExecutionEnvironment.Node: diff --git a/src/harness/harnessLanguageService.ts b/src/harness/harnessLanguageService.ts index c604b2246566a..22878f5a89e7d 100644 --- a/src/harness/harnessLanguageService.ts +++ b/src/harness/harnessLanguageService.ts @@ -853,8 +853,4 @@ namespace Harness.LanguageService { getClassifier(): ts.Classifier { throw new Error("getClassifier is not available using the server interface."); } getPreProcessedFileInfo(): ts.PreProcessedFileInfo { throw new Error("getPreProcessedFileInfo is not available using the server interface."); } } - - export function mockHash(s: string): string { - return `hash-${s}`; - } } diff --git a/src/harness/tsconfig.json b/src/harness/tsconfig.json index 66ca2fc3f4837..dfdd0a2fee853 100644 --- a/src/harness/tsconfig.json +++ b/src/harness/tsconfig.json @@ -1,4 +1,4 @@ -{ +{ "extends": "../tsconfig-base", "compilerOptions": { "removeComments": false, @@ -44,6 +44,7 @@ "../compiler/declarationEmitter.ts", "../compiler/emitter.ts", "../compiler/program.ts", + "../compiler/builder.ts", "../compiler/commandLineParser.ts", "../compiler/diagnosticInformationMap.generated.ts", "../services/breakpoints.ts", diff --git a/src/harness/unittests/cachingInServerLSHost.ts b/src/harness/unittests/cachingInServerLSHost.ts index 5265a12355e52..12efa5717a35d 100644 --- a/src/harness/unittests/cachingInServerLSHost.ts +++ b/src/harness/unittests/cachingInServerLSHost.ts @@ -47,7 +47,7 @@ namespace ts { clearTimeout, setImmediate: typeof setImmediate !== "undefined" ? setImmediate : action => setTimeout(action, 0), clearImmediate: typeof clearImmediate !== "undefined" ? clearImmediate : clearTimeout, - createHash: Harness.LanguageService.mockHash, + createHash: Harness.mockHash, }; } diff --git a/src/harness/unittests/session.ts b/src/harness/unittests/session.ts index 862ebee4b03ab..31de635609025 100644 --- a/src/harness/unittests/session.ts +++ b/src/harness/unittests/session.ts @@ -25,7 +25,7 @@ namespace ts.server { clearTimeout: noop, setImmediate: () => 0, clearImmediate: noop, - createHash: Harness.LanguageService.mockHash, + createHash: Harness.mockHash, }; const mockLogger: Logger = { diff --git a/src/harness/unittests/telemetry.ts b/src/harness/unittests/telemetry.ts index a856250307475..e7a64c59903a5 100644 --- a/src/harness/unittests/telemetry.ts +++ b/src/harness/unittests/telemetry.ts @@ -53,7 +53,7 @@ namespace ts.projectSystem { // TODO: Apparently compilerOptions is mutated, so have to repeat it here! et.assertProjectInfoTelemetryEvent({ - projectId: Harness.LanguageService.mockHash("/hunter2/foo.csproj"), + projectId: Harness.mockHash("/hunter2/foo.csproj"), compilerOptions: { strict: true }, compileOnSave: true, // These properties can't be present for an external project, so they are undefined instead of false. @@ -195,7 +195,7 @@ namespace ts.projectSystem { const et = new EventTracker([jsconfig, file]); et.service.openClientFile(file.path); et.assertProjectInfoTelemetryEvent({ - projectId: Harness.LanguageService.mockHash("/jsconfig.json"), + projectId: Harness.mockHash("/jsconfig.json"), fileStats: fileStats({ js: 1 }), compilerOptions: autoJsCompilerOptions, typeAcquisition: { @@ -215,7 +215,7 @@ namespace ts.projectSystem { et.service.openClientFile(file.path); et.getEvent(server.ProjectLanguageServiceStateEvent, /*mayBeMore*/ true); et.assertProjectInfoTelemetryEvent({ - projectId: Harness.LanguageService.mockHash("/jsconfig.json"), + projectId: Harness.mockHash("/jsconfig.json"), fileStats: fileStats({ js: 1 }), compilerOptions: autoJsCompilerOptions, configFileName: "jsconfig.json", @@ -251,7 +251,7 @@ namespace ts.projectSystem { assertProjectInfoTelemetryEvent(partial: Partial): void { assert.deepEqual(this.getEvent(ts.server.ProjectInfoTelemetryEvent), { - projectId: Harness.LanguageService.mockHash("/tsconfig.json"), + projectId: Harness.mockHash("/tsconfig.json"), fileStats: fileStats({ ts: 1 }), compilerOptions: {}, extends: false, diff --git a/src/harness/unittests/tsserverProjectSystem.ts b/src/harness/unittests/tsserverProjectSystem.ts index ca86138803cb0..b7c71f281916a 100644 --- a/src/harness/unittests/tsserverProjectSystem.ts +++ b/src/harness/unittests/tsserverProjectSystem.ts @@ -1,4 +1,5 @@ /// +/// /// namespace ts.projectSystem { @@ -6,17 +7,18 @@ namespace ts.projectSystem { import protocol = server.protocol; import CommandNames = server.CommandNames; - const safeList = { - path: "/safeList.json", - content: JSON.stringify({ - commander: "commander", - express: "express", - jquery: "jquery", - lodash: "lodash", - moment: "moment", - chroma: "chroma-js" - }) - }; + export import TestServerHost = ts.TestFSWithWatch.TestServerHost; + export type TestServerHostCreationParameters = ts.TestFSWithWatch.TestServerHostCreationParameters; + export type File = ts.TestFSWithWatch.File; + export type FileOrFolder = ts.TestFSWithWatch.FileOrFolder; + export type Folder = ts.TestFSWithWatch.Folder; + export type FSEntry = ts.TestFSWithWatch.FSEntry; + export import createServerHost = ts.TestFSWithWatch.createServerHost; + export import checkFileNames = ts.TestFSWithWatch.checkFileNames; + export import libFile = ts.TestFSWithWatch.libFile; + export import checkWatchedFiles = ts.TestFSWithWatch.checkWatchedFiles; + export import checkWatchedDirectories = ts.TestFSWithWatch.checkWatchedDirectories; + import safeList = ts.TestFSWithWatch.safeList; const customSafeList = { path: "/typeMapList.json", @@ -45,12 +47,6 @@ namespace ts.projectSystem { getLogFileName: (): string => undefined }; - export const { content: libFileContent } = Harness.getDefaultLibraryFile(Harness.IO); - export const libFile: FileOrFolder = { - path: "/a/lib/lib.d.ts", - content: libFileContent - }; - export class TestTypingsInstaller extends TI.TypingsInstaller implements server.ITypingsInstaller { protected projectService: server.ProjectService; constructor( @@ -118,10 +114,6 @@ namespace ts.projectSystem { return JSON.stringify({ dependencies }); } - export function getExecutingFilePathFromLibFile(): string { - return combinePaths(getDirectoryPath(libFile.path), "tsc.js"); - } - export function toExternalFile(fileName: string): protocol.ExternalFile { return { fileName }; } @@ -143,26 +135,6 @@ namespace ts.projectSystem { } } - export interface TestServerHostCreationParameters { - useCaseSensitiveFileNames?: boolean; - executingFilePath?: string; - currentDirectory?: string; - newLine?: string; - } - - export function createServerHost(fileOrFolderList: FileOrFolder[], params?: TestServerHostCreationParameters): TestServerHost { - if (!params) { - params = {}; - } - const host = new TestServerHost( - params.useCaseSensitiveFileNames !== undefined ? params.useCaseSensitiveFileNames : false, - params.executingFilePath || getExecutingFilePathFromLibFile(), - params.currentDirectory || "/", - fileOrFolderList, - params.newLine); - return host; - } - class TestSession extends server.Session { private seq = 0; @@ -213,7 +185,6 @@ namespace ts.projectSystem { eventHandler?: server.ProjectServiceEventHandler; } - export class TestProjectService extends server.ProjectService { constructor(host: server.ServerHost, logger: server.Logger, cancellationToken: HostCancellationToken, useSingleInferredProject: boolean, typingsInstaller: server.ITypingsInstaller, eventHandler: server.ProjectServiceEventHandler) { @@ -233,67 +204,6 @@ namespace ts.projectSystem { return new TestProjectService(host, logger, cancellationToken, useSingleInferredProject, parameters.typingsInstaller, parameters.eventHandler); } - export interface FileOrFolder { - path: string; - content?: string; - fileSize?: number; - } - - export interface FSEntry { - path: Path; - fullPath: string; - } - - export interface File extends FSEntry { - content: string; - fileSize?: number; - } - - export interface Folder extends FSEntry { - entries: FSEntry[]; - } - - export function isFolder(s: FSEntry): s is Folder { - return s && isArray((s).entries); - } - - export function isFile(s: FSEntry): s is File { - return s && isString((s).content); - } - - function invokeDirectoryWatcher(callbacks: DirectoryWatcherCallback[], getRelativeFilePath: () => string) { - if (callbacks) { - const cbs = callbacks.slice(); - for (const cb of cbs) { - const fileName = getRelativeFilePath(); - cb(fileName); - } - } - } - - function invokeFileWatcher(callbacks: FileWatcherCallback[], fileName: string, eventId: FileWatcherEventKind) { - if (callbacks) { - const cbs = callbacks.slice(); - for (const cb of cbs) { - cb(fileName, eventId); - } - } - } - - export function checkMapKeys(caption: string, map: Map, expectedKeys: string[]) { - assert.equal(map.size, expectedKeys.length, `${caption}: incorrect size of map: Actual keys: ${arrayFrom(map.keys())} Expected: ${expectedKeys}`); - for (const name of expectedKeys) { - assert.isTrue(map.has(name), `${caption} is expected to contain ${name}, actual keys: ${arrayFrom(map.keys())}`); - } - } - - export function checkFileNames(caption: string, actualFileNames: string[], expectedFileNames: string[]) { - assert.equal(actualFileNames.length, expectedFileNames.length, `${caption}: incorrect actual number of files, expected ${JSON.stringify(expectedFileNames)}, got ${actualFileNames}`); - for (const f of expectedFileNames) { - assert.isTrue(contains(actualFileNames, f), `${caption}: expected to find ${f} in ${JSON.stringify(actualFileNames)}`); - } - } - export function checkNumberOfConfiguredProjects(projectService: server.ProjectService, expected: number) { assert.equal(projectService.configuredProjects.size, expected, `expected ${expected} configured project(s)`); } @@ -321,14 +231,6 @@ namespace ts.projectSystem { return values.next().value; } - export function checkWatchedFiles(host: TestServerHost, expectedFiles: string[]) { - checkMapKeys("watchedFiles", host.watchedFiles, expectedFiles); - } - - export function checkWatchedDirectories(host: TestServerHost, expectedDirectories: string[], recursive = false) { - checkMapKeys("watchedDirectories", recursive ? host.watchedDirectoriesRecursive : host.watchedDirectories, expectedDirectories); - } - export function checkProjectActualFiles(project: server.Project, expectedFiles: string[]) { checkFileNames(`${server.ProjectKind[project.projectKind]} project, actual files`, project.getFileNames(), expectedFiles); } @@ -337,384 +239,6 @@ namespace ts.projectSystem { checkFileNames(`${server.ProjectKind[project.projectKind]} project, rootFileNames`, project.getRootFiles(), expectedFiles); } - export class Callbacks { - private map: TimeOutCallback[] = []; - private nextId = 1; - - register(cb: (...args: any[]) => void, args: any[]) { - const timeoutId = this.nextId; - this.nextId++; - this.map[timeoutId] = cb.bind(/*this*/ undefined, ...args); - return timeoutId; - } - - unregister(id: any) { - if (typeof id === "number") { - delete this.map[id]; - } - } - - count() { - let n = 0; - for (const _ in this.map) { - n++; - } - return n; - } - - invoke() { - // Note: invoking a callback may result in new callbacks been queued, - // so do not clear the entire callback list regardless. Only remove the - // ones we have invoked. - for (const key in this.map) { - this.map[key](); - delete this.map[key]; - } - } - } - - export type TimeOutCallback = () => any; - - export class TestServerHost implements server.ServerHost { - args: string[] = []; - - private readonly output: string[] = []; - - private fs: Map = createMap(); - private getCanonicalFileName: (s: string) => string; - private toPath: (f: string) => Path; - private timeoutCallbacks = new Callbacks(); - private immediateCallbacks = new Callbacks(); - - readonly watchedDirectories = createMultiMap(); - readonly watchedDirectoriesRecursive = createMultiMap(); - readonly watchedFiles = createMultiMap(); - - constructor(public useCaseSensitiveFileNames: boolean, private executingFilePath: string, private currentDirectory: string, fileOrFolderList: FileOrFolder[], public readonly newLine = "\n") { - this.getCanonicalFileName = createGetCanonicalFileName(useCaseSensitiveFileNames); - this.toPath = s => toPath(s, currentDirectory, this.getCanonicalFileName); - - this.reloadFS(fileOrFolderList); - } - - private toFullPath(s: string) { - const fullPath = getNormalizedAbsolutePath(s, this.currentDirectory); - return this.toPath(fullPath); - } - - reloadFS(fileOrFolderList: FileOrFolder[]) { - const mapNewLeaves = createMap(); - const isNewFs = this.fs.size === 0; - // always inject safelist file in the list of files - for (const fileOrFolder of fileOrFolderList.concat(safeList)) { - const path = this.toFullPath(fileOrFolder.path); - mapNewLeaves.set(path, true); - // If its a change - const currentEntry = this.fs.get(path); - if (currentEntry) { - if (isFile(currentEntry)) { - if (isString(fileOrFolder.content)) { - // Update file - if (currentEntry.content !== fileOrFolder.content) { - currentEntry.content = fileOrFolder.content; - this.invokeFileWatcher(currentEntry.fullPath, FileWatcherEventKind.Changed); - } - } - else { - // TODO: Changing from file => folder - } - } - else { - // Folder - if (isString(fileOrFolder.content)) { - // TODO: Changing from folder => file - } - else { - // Folder update: Nothing to do. - } - } - } - else { - this.ensureFileOrFolder(fileOrFolder); - } - } - - if (!isNewFs) { - this.fs.forEach((fileOrFolder, path) => { - // If this entry is not from the new file or folder - if (!mapNewLeaves.get(path)) { - // Leaf entries that arent in new list => remove these - if (isFile(fileOrFolder) || isFolder(fileOrFolder) && fileOrFolder.entries.length === 0) { - this.removeFileOrFolder(fileOrFolder, folder => !mapNewLeaves.get(folder.path)); - } - } - }); - } - } - - ensureFileOrFolder(fileOrFolder: FileOrFolder) { - if (isString(fileOrFolder.content)) { - const file = this.toFile(fileOrFolder); - Debug.assert(!this.fs.get(file.path)); - const baseFolder = this.ensureFolder(getDirectoryPath(file.fullPath)); - this.addFileOrFolderInFolder(baseFolder, file); - } - else { - const fullPath = getNormalizedAbsolutePath(fileOrFolder.path, this.currentDirectory); - this.ensureFolder(fullPath); - } - } - - private ensureFolder(fullPath: string): Folder { - const path = this.toPath(fullPath); - let folder = this.fs.get(path) as Folder; - if (!folder) { - folder = this.toFolder(fullPath); - const baseFullPath = getDirectoryPath(fullPath); - if (fullPath !== baseFullPath) { - // Add folder in the base folder - const baseFolder = this.ensureFolder(baseFullPath); - this.addFileOrFolderInFolder(baseFolder, folder); - } - else { - // root folder - Debug.assert(this.fs.size === 0); - this.fs.set(path, folder); - } - } - Debug.assert(isFolder(folder)); - return folder; - } - - private addFileOrFolderInFolder(folder: Folder, fileOrFolder: File | Folder) { - folder.entries.push(fileOrFolder); - this.fs.set(fileOrFolder.path, fileOrFolder); - - if (isFile(fileOrFolder)) { - this.invokeFileWatcher(fileOrFolder.fullPath, FileWatcherEventKind.Created); - } - this.invokeDirectoryWatcher(folder.fullPath, fileOrFolder.fullPath); - } - - private removeFileOrFolder(fileOrFolder: File | Folder, isRemovableLeafFolder: (folder: Folder) => boolean) { - const basePath = getDirectoryPath(fileOrFolder.path); - const baseFolder = this.fs.get(basePath) as Folder; - if (basePath !== fileOrFolder.path) { - Debug.assert(!!baseFolder); - filterMutate(baseFolder.entries, entry => entry !== fileOrFolder); - } - this.fs.delete(fileOrFolder.path); - - if (isFile(fileOrFolder)) { - this.invokeFileWatcher(fileOrFolder.fullPath, FileWatcherEventKind.Deleted); - } - else { - Debug.assert(fileOrFolder.entries.length === 0); - invokeDirectoryWatcher(this.watchedDirectories.get(fileOrFolder.path), () => this.getRelativePathToDirectory(fileOrFolder.fullPath, fileOrFolder.fullPath)); - invokeDirectoryWatcher(this.watchedDirectoriesRecursive.get(fileOrFolder.path), () => this.getRelativePathToDirectory(fileOrFolder.fullPath, fileOrFolder.fullPath)); - } - - if (basePath !== fileOrFolder.path) { - if (baseFolder.entries.length === 0 && isRemovableLeafFolder(baseFolder)) { - this.removeFileOrFolder(baseFolder, isRemovableLeafFolder); - } - else { - this.invokeRecursiveDirectoryWatcher(baseFolder.fullPath, fileOrFolder.fullPath); - } - } - } - - private invokeFileWatcher(fileFullPath: string, eventId: FileWatcherEventKind) { - const callbacks = this.watchedFiles.get(this.toPath(fileFullPath)); - invokeFileWatcher(callbacks, getBaseFileName(fileFullPath), eventId); - } - - private getRelativePathToDirectory(directoryFullPath: string, fileFullPath: string) { - return getRelativePathToDirectoryOrUrl(directoryFullPath, fileFullPath, this.currentDirectory, this.getCanonicalFileName, /*isAbsolutePathAnUrl*/ false); - } - - private invokeDirectoryWatcher(folderFullPath: string, fileName: string) { - invokeDirectoryWatcher(this.watchedDirectories.get(this.toPath(folderFullPath)), () => this.getRelativePathToDirectory(folderFullPath, fileName)); - this.invokeRecursiveDirectoryWatcher(folderFullPath, fileName); - } - - private invokeRecursiveDirectoryWatcher(fullPath: string, fileName: string) { - invokeDirectoryWatcher(this.watchedDirectoriesRecursive.get(this.toPath(fullPath)), () => this.getRelativePathToDirectory(fullPath, fileName)); - const basePath = getDirectoryPath(fullPath); - if (this.getCanonicalFileName(fullPath) !== this.getCanonicalFileName(basePath)) { - this.invokeRecursiveDirectoryWatcher(basePath, fileName); - } - } - - private toFile(fileOrFolder: FileOrFolder): File { - const fullPath = getNormalizedAbsolutePath(fileOrFolder.path, this.currentDirectory); - return { - path: this.toPath(fullPath), - content: fileOrFolder.content, - fullPath, - fileSize: fileOrFolder.fileSize - }; - } - - private toFolder(path: string): Folder { - const fullPath = getNormalizedAbsolutePath(path, this.currentDirectory); - return { - path: this.toPath(fullPath), - entries: [], - fullPath - }; - } - - fileExists(s: string) { - const path = this.toFullPath(s); - return isFile(this.fs.get(path)); - } - - getFileSize(s: string) { - const path = this.toFullPath(s); - const entry = this.fs.get(path); - if (isFile(entry)) { - return entry.fileSize ? entry.fileSize : entry.content.length; - } - return undefined; - } - - directoryExists(s: string) { - const path = this.toFullPath(s); - return isFolder(this.fs.get(path)); - } - - getDirectories(s: string) { - const path = this.toFullPath(s); - const folder = this.fs.get(path); - if (isFolder(folder)) { - return mapDefined(folder.entries, entry => isFolder(entry) ? getBaseFileName(entry.fullPath) : undefined); - } - Debug.fail(folder ? "getDirectories called on file" : "getDirectories called on missing folder"); - return []; - } - - readDirectory(path: string, extensions?: ReadonlyArray, exclude?: ReadonlyArray, include?: ReadonlyArray, depth?: number): string[] { - return ts.matchFiles(this.toFullPath(path), extensions, exclude, include, this.useCaseSensitiveFileNames, this.getCurrentDirectory(), depth, (dir) => { - const directories: string[] = []; - const files: string[] = []; - const dirEntry = this.fs.get(this.toPath(dir)); - if (isFolder(dirEntry)) { - dirEntry.entries.forEach((entry) => { - if (isFolder(entry)) { - directories.push(getBaseFileName(entry.fullPath)); - } - else if (isFile(entry)) { - files.push(getBaseFileName(entry.fullPath)); - } - else { - Debug.fail("Unknown entry"); - } - }); - } - return { directories, files }; - }); - } - - watchDirectory(directoryName: string, callback: DirectoryWatcherCallback, recursive: boolean): DirectoryWatcher { - const path = this.toFullPath(directoryName); - const map = recursive ? this.watchedDirectoriesRecursive : this.watchedDirectories; - map.add(path, callback); - return { - referenceCount: 0, - directoryName, - close: () => map.remove(path, callback) - }; - } - - createHash(s: string): string { - return Harness.LanguageService.mockHash(s); - } - - watchFile(fileName: string, callback: FileWatcherCallback) { - const path = this.toFullPath(fileName); - this.watchedFiles.add(path, callback); - return { close: () => this.watchedFiles.remove(path, callback) }; - } - - // TOOD: record and invoke callbacks to simulate timer events - setTimeout(callback: TimeOutCallback, _time: number, ...args: any[]) { - return this.timeoutCallbacks.register(callback, args); - } - - clearTimeout(timeoutId: any): void { - this.timeoutCallbacks.unregister(timeoutId); - } - - checkTimeoutQueueLengthAndRun(expected: number) { - this.checkTimeoutQueueLength(expected); - this.runQueuedTimeoutCallbacks(); - } - - checkTimeoutQueueLength(expected: number) { - const callbacksCount = this.timeoutCallbacks.count(); - assert.equal(callbacksCount, expected, `expected ${expected} timeout callbacks queued but found ${callbacksCount}.`); - } - - runQueuedTimeoutCallbacks() { - this.timeoutCallbacks.invoke(); - } - - runQueuedImmediateCallbacks() { - this.immediateCallbacks.invoke(); - } - - setImmediate(callback: TimeOutCallback, _time: number, ...args: any[]) { - return this.immediateCallbacks.register(callback, args); - } - - clearImmediate(timeoutId: any): void { - this.immediateCallbacks.unregister(timeoutId); - } - - createDirectory(directoryName: string): void { - const folder = this.toFolder(directoryName); - - // base folder has to be present - const base = getDirectoryPath(folder.fullPath); - const baseFolder = this.fs.get(base) as Folder; - Debug.assert(isFolder(baseFolder)); - - Debug.assert(!this.fs.get(folder.path), isFile(this.fs.get(folder.path)) ? `Found the file ${folder.path}` : `Found the folder ${folder.path}`); - this.addFileOrFolderInFolder(baseFolder, folder); - } - - writeFile(path: string, content: string): void { - const file = this.toFile({ path, content }); - - // base folder has to be present - const base = getDirectoryPath(file.fullPath); - const folder = this.fs.get(base) as Folder; - Debug.assert(isFolder(folder)); - - this.addFileOrFolderInFolder(folder, file); - } - - write(message: string) { - this.output.push(message); - } - - getOutput(): ReadonlyArray { - return this.output; - } - - clearOutput() { - clear(this.output); - } - - readonly readFile = (s: string) => (this.fs.get(this.toFullPath(s))).content; - readonly resolvePath = (s: string) => s; - readonly getExecutingFilePath = () => this.executingFilePath; - readonly getCurrentDirectory = () => this.currentDirectory; - readonly exit = notImplemented; - readonly getEnvironmentVariable = notImplemented; - } - /** * Test server cancellation token used to mock host token cancellation requests. * The cancelAfterRequest constructor param specifies how many isCancellationRequested() calls diff --git a/src/harness/virtualFileSystemWithWatch.ts b/src/harness/virtualFileSystemWithWatch.ts new file mode 100644 index 0000000000000..f37d685851c3c --- /dev/null +++ b/src/harness/virtualFileSystemWithWatch.ts @@ -0,0 +1,492 @@ +/// + +namespace ts.TestFSWithWatch { + export const { content: libFileContent } = Harness.getDefaultLibraryFile(Harness.IO); + export const libFile: FileOrFolder = { + path: "/a/lib/lib.d.ts", + content: libFileContent + }; + + export const safeList = { + path: "/safeList.json", + content: JSON.stringify({ + commander: "commander", + express: "express", + jquery: "jquery", + lodash: "lodash", + moment: "moment", + chroma: "chroma-js" + }) + }; + + export function getExecutingFilePathFromLibFile(): string { + return combinePaths(getDirectoryPath(libFile.path), "tsc.js"); + } + + export interface TestServerHostCreationParameters { + useCaseSensitiveFileNames?: boolean; + executingFilePath?: string; + currentDirectory?: string; + newLine?: string; + } + + export function createServerHost(fileOrFolderList: FileOrFolder[], params?: TestServerHostCreationParameters): TestServerHost { + if (!params) { + params = {}; + } + const host = new TestServerHost(/*withSafelist*/ true, + params.useCaseSensitiveFileNames !== undefined ? params.useCaseSensitiveFileNames : false, + params.executingFilePath || getExecutingFilePathFromLibFile(), + params.currentDirectory || "/", + fileOrFolderList, + params.newLine); + return host; + } + + export interface FileOrFolder { + path: string; + content?: string; + fileSize?: number; + } + + export interface FSEntry { + path: Path; + fullPath: string; + } + + export interface File extends FSEntry { + content: string; + fileSize?: number; + } + + export interface Folder extends FSEntry { + entries: FSEntry[]; + } + + export function isFolder(s: FSEntry): s is Folder { + return s && isArray((s).entries); + } + + export function isFile(s: FSEntry): s is File { + return s && isString((s).content); + } + + function invokeDirectoryWatcher(callbacks: DirectoryWatcherCallback[], getRelativeFilePath: () => string) { + if (callbacks) { + const cbs = callbacks.slice(); + for (const cb of cbs) { + const fileName = getRelativeFilePath(); + cb(fileName); + } + } + } + + function invokeFileWatcher(callbacks: FileWatcherCallback[], fileName: string, eventId: FileWatcherEventKind) { + if (callbacks) { + const cbs = callbacks.slice(); + for (const cb of cbs) { + cb(fileName, eventId); + } + } + } + + export function checkMapKeys(caption: string, map: Map, expectedKeys: string[]) { + assert.equal(map.size, expectedKeys.length, `${caption}: incorrect size of map: Actual keys: ${arrayFrom(map.keys())} Expected: ${expectedKeys}`); + for (const name of expectedKeys) { + assert.isTrue(map.has(name), `${caption} is expected to contain ${name}, actual keys: ${arrayFrom(map.keys())}`); + } + } + + export function checkFileNames(caption: string, actualFileNames: string[], expectedFileNames: string[]) { + assert.equal(actualFileNames.length, expectedFileNames.length, `${caption}: incorrect actual number of files, expected ${JSON.stringify(expectedFileNames)}, got ${actualFileNames}`); + for (const f of expectedFileNames) { + assert.isTrue(contains(actualFileNames, f), `${caption}: expected to find ${f} in ${JSON.stringify(actualFileNames)}`); + } + } + + export function checkWatchedFiles(host: TestServerHost, expectedFiles: string[]) { + checkMapKeys("watchedFiles", host.watchedFiles, expectedFiles); + } + + export function checkWatchedDirectories(host: TestServerHost, expectedDirectories: string[], recursive = false) { + checkMapKeys("watchedDirectories", recursive ? host.watchedDirectoriesRecursive : host.watchedDirectories, expectedDirectories); + } + + export class Callbacks { + private map: TimeOutCallback[] = []; + private nextId = 1; + + register(cb: (...args: any[]) => void, args: any[]) { + const timeoutId = this.nextId; + this.nextId++; + this.map[timeoutId] = cb.bind(/*this*/ undefined, ...args); + return timeoutId; + } + + unregister(id: any) { + if (typeof id === "number") { + delete this.map[id]; + } + } + + count() { + let n = 0; + for (const _ in this.map) { + n++; + } + return n; + } + + invoke() { + // Note: invoking a callback may result in new callbacks been queued, + // so do not clear the entire callback list regardless. Only remove the + // ones we have invoked. + for (const key in this.map) { + this.map[key](); + delete this.map[key]; + } + } + } + + export type TimeOutCallback = () => any; + + export class TestServerHost implements server.ServerHost { + args: string[] = []; + + private readonly output: string[] = []; + + private fs: Map = createMap(); + private getCanonicalFileName: (s: string) => string; + private toPath: (f: string) => Path; + private timeoutCallbacks = new Callbacks(); + private immediateCallbacks = new Callbacks(); + + readonly watchedDirectories = createMultiMap(); + readonly watchedDirectoriesRecursive = createMultiMap(); + readonly watchedFiles = createMultiMap(); + + constructor(public withSafeList: boolean, public useCaseSensitiveFileNames: boolean, private executingFilePath: string, private currentDirectory: string, fileOrFolderList: FileOrFolder[], public readonly newLine = "\n") { + this.getCanonicalFileName = createGetCanonicalFileName(useCaseSensitiveFileNames); + this.toPath = s => toPath(s, currentDirectory, this.getCanonicalFileName); + + this.reloadFS(fileOrFolderList); + } + + private toFullPath(s: string) { + const fullPath = getNormalizedAbsolutePath(s, this.currentDirectory); + return this.toPath(fullPath); + } + + reloadFS(fileOrFolderList: FileOrFolder[]) { + const mapNewLeaves = createMap(); + const isNewFs = this.fs.size === 0; + // always inject safelist file in the list of files + for (const fileOrFolder of fileOrFolderList.concat(this.withSafeList ? safeList : [])) { + const path = this.toFullPath(fileOrFolder.path); + mapNewLeaves.set(path, true); + // If its a change + const currentEntry = this.fs.get(path); + if (currentEntry) { + if (isFile(currentEntry)) { + if (isString(fileOrFolder.content)) { + // Update file + if (currentEntry.content !== fileOrFolder.content) { + currentEntry.content = fileOrFolder.content; + this.invokeFileWatcher(currentEntry.fullPath, FileWatcherEventKind.Changed); + } + } + else { + // TODO: Changing from file => folder + } + } + else { + // Folder + if (isString(fileOrFolder.content)) { + // TODO: Changing from folder => file + } + else { + // Folder update: Nothing to do. + } + } + } + else { + this.ensureFileOrFolder(fileOrFolder); + } + } + + if (!isNewFs) { + this.fs.forEach((fileOrFolder, path) => { + // If this entry is not from the new file or folder + if (!mapNewLeaves.get(path)) { + // Leaf entries that arent in new list => remove these + if (isFile(fileOrFolder) || isFolder(fileOrFolder) && fileOrFolder.entries.length === 0) { + this.removeFileOrFolder(fileOrFolder, folder => !mapNewLeaves.get(folder.path)); + } + } + }); + } + } + + ensureFileOrFolder(fileOrFolder: FileOrFolder) { + if (isString(fileOrFolder.content)) { + const file = this.toFile(fileOrFolder); + Debug.assert(!this.fs.get(file.path)); + const baseFolder = this.ensureFolder(getDirectoryPath(file.fullPath)); + this.addFileOrFolderInFolder(baseFolder, file); + } + else { + const fullPath = getNormalizedAbsolutePath(fileOrFolder.path, this.currentDirectory); + this.ensureFolder(fullPath); + } + } + + private ensureFolder(fullPath: string): Folder { + const path = this.toPath(fullPath); + let folder = this.fs.get(path) as Folder; + if (!folder) { + folder = this.toFolder(fullPath); + const baseFullPath = getDirectoryPath(fullPath); + if (fullPath !== baseFullPath) { + // Add folder in the base folder + const baseFolder = this.ensureFolder(baseFullPath); + this.addFileOrFolderInFolder(baseFolder, folder); + } + else { + // root folder + Debug.assert(this.fs.size === 0); + this.fs.set(path, folder); + } + } + Debug.assert(isFolder(folder)); + return folder; + } + + private addFileOrFolderInFolder(folder: Folder, fileOrFolder: File | Folder) { + folder.entries.push(fileOrFolder); + this.fs.set(fileOrFolder.path, fileOrFolder); + + if (isFile(fileOrFolder)) { + this.invokeFileWatcher(fileOrFolder.fullPath, FileWatcherEventKind.Created); + } + this.invokeDirectoryWatcher(folder.fullPath, fileOrFolder.fullPath); + } + + private removeFileOrFolder(fileOrFolder: File | Folder, isRemovableLeafFolder: (folder: Folder) => boolean) { + const basePath = getDirectoryPath(fileOrFolder.path); + const baseFolder = this.fs.get(basePath) as Folder; + if (basePath !== fileOrFolder.path) { + Debug.assert(!!baseFolder); + filterMutate(baseFolder.entries, entry => entry !== fileOrFolder); + } + this.fs.delete(fileOrFolder.path); + + if (isFile(fileOrFolder)) { + this.invokeFileWatcher(fileOrFolder.fullPath, FileWatcherEventKind.Deleted); + } + else { + Debug.assert(fileOrFolder.entries.length === 0); + invokeDirectoryWatcher(this.watchedDirectories.get(fileOrFolder.path), () => this.getRelativePathToDirectory(fileOrFolder.fullPath, fileOrFolder.fullPath)); + invokeDirectoryWatcher(this.watchedDirectoriesRecursive.get(fileOrFolder.path), () => this.getRelativePathToDirectory(fileOrFolder.fullPath, fileOrFolder.fullPath)); + } + + if (basePath !== fileOrFolder.path) { + if (baseFolder.entries.length === 0 && isRemovableLeafFolder(baseFolder)) { + this.removeFileOrFolder(baseFolder, isRemovableLeafFolder); + } + else { + this.invokeRecursiveDirectoryWatcher(baseFolder.fullPath, fileOrFolder.fullPath); + } + } + } + + private invokeFileWatcher(fileFullPath: string, eventId: FileWatcherEventKind) { + const callbacks = this.watchedFiles.get(this.toPath(fileFullPath)); + invokeFileWatcher(callbacks, getBaseFileName(fileFullPath), eventId); + } + + private getRelativePathToDirectory(directoryFullPath: string, fileFullPath: string) { + return getRelativePathToDirectoryOrUrl(directoryFullPath, fileFullPath, this.currentDirectory, this.getCanonicalFileName, /*isAbsolutePathAnUrl*/ false); + } + + private invokeDirectoryWatcher(folderFullPath: string, fileName: string) { + invokeDirectoryWatcher(this.watchedDirectories.get(this.toPath(folderFullPath)), () => this.getRelativePathToDirectory(folderFullPath, fileName)); + this.invokeRecursiveDirectoryWatcher(folderFullPath, fileName); + } + + private invokeRecursiveDirectoryWatcher(fullPath: string, fileName: string) { + invokeDirectoryWatcher(this.watchedDirectoriesRecursive.get(this.toPath(fullPath)), () => this.getRelativePathToDirectory(fullPath, fileName)); + const basePath = getDirectoryPath(fullPath); + if (this.getCanonicalFileName(fullPath) !== this.getCanonicalFileName(basePath)) { + this.invokeRecursiveDirectoryWatcher(basePath, fileName); + } + } + + private toFile(fileOrFolder: FileOrFolder): File { + const fullPath = getNormalizedAbsolutePath(fileOrFolder.path, this.currentDirectory); + return { + path: this.toPath(fullPath), + content: fileOrFolder.content, + fullPath, + fileSize: fileOrFolder.fileSize + }; + } + + private toFolder(path: string): Folder { + const fullPath = getNormalizedAbsolutePath(path, this.currentDirectory); + return { + path: this.toPath(fullPath), + entries: [], + fullPath + }; + } + + fileExists(s: string) { + const path = this.toFullPath(s); + return isFile(this.fs.get(path)); + } + + getFileSize(s: string) { + const path = this.toFullPath(s); + const entry = this.fs.get(path); + if (isFile(entry)) { + return entry.fileSize ? entry.fileSize : entry.content.length; + } + return undefined; + } + + directoryExists(s: string) { + const path = this.toFullPath(s); + return isFolder(this.fs.get(path)); + } + + getDirectories(s: string) { + const path = this.toFullPath(s); + const folder = this.fs.get(path); + if (isFolder(folder)) { + return mapDefined(folder.entries, entry => isFolder(entry) ? getBaseFileName(entry.fullPath) : undefined); + } + Debug.fail(folder ? "getDirectories called on file" : "getDirectories called on missing folder"); + return []; + } + + readDirectory(path: string, extensions?: ReadonlyArray, exclude?: ReadonlyArray, include?: ReadonlyArray, depth?: number): string[] { + return ts.matchFiles(this.toFullPath(path), extensions, exclude, include, this.useCaseSensitiveFileNames, this.getCurrentDirectory(), depth, (dir) => { + const directories: string[] = []; + const files: string[] = []; + const dirEntry = this.fs.get(this.toPath(dir)); + if (isFolder(dirEntry)) { + dirEntry.entries.forEach((entry) => { + if (isFolder(entry)) { + directories.push(getBaseFileName(entry.fullPath)); + } + else if (isFile(entry)) { + files.push(getBaseFileName(entry.fullPath)); + } + else { + Debug.fail("Unknown entry"); + } + }); + } + return { directories, files }; + }); + } + + watchDirectory(directoryName: string, callback: DirectoryWatcherCallback, recursive: boolean): DirectoryWatcher { + const path = this.toFullPath(directoryName); + const map = recursive ? this.watchedDirectoriesRecursive : this.watchedDirectories; + map.add(path, callback); + return { + referenceCount: 0, + directoryName, + close: () => map.remove(path, callback) + }; + } + + createHash(s: string): string { + return Harness.mockHash(s); + } + + watchFile(fileName: string, callback: FileWatcherCallback) { + const path = this.toFullPath(fileName); + this.watchedFiles.add(path, callback); + return { close: () => this.watchedFiles.remove(path, callback) }; + } + + // TOOD: record and invoke callbacks to simulate timer events + setTimeout(callback: TimeOutCallback, _time: number, ...args: any[]) { + return this.timeoutCallbacks.register(callback, args); + } + + clearTimeout(timeoutId: any): void { + this.timeoutCallbacks.unregister(timeoutId); + } + + checkTimeoutQueueLengthAndRun(expected: number) { + this.checkTimeoutQueueLength(expected); + this.runQueuedTimeoutCallbacks(); + } + + checkTimeoutQueueLength(expected: number) { + const callbacksCount = this.timeoutCallbacks.count(); + assert.equal(callbacksCount, expected, `expected ${expected} timeout callbacks queued but found ${callbacksCount}.`); + } + + runQueuedTimeoutCallbacks() { + this.timeoutCallbacks.invoke(); + } + + runQueuedImmediateCallbacks() { + this.immediateCallbacks.invoke(); + } + + setImmediate(callback: TimeOutCallback, _time: number, ...args: any[]) { + return this.immediateCallbacks.register(callback, args); + } + + clearImmediate(timeoutId: any): void { + this.immediateCallbacks.unregister(timeoutId); + } + + createDirectory(directoryName: string): void { + const folder = this.toFolder(directoryName); + + // base folder has to be present + const base = getDirectoryPath(folder.fullPath); + const baseFolder = this.fs.get(base) as Folder; + Debug.assert(isFolder(baseFolder)); + + Debug.assert(!this.fs.get(folder.path), isFile(this.fs.get(folder.path)) ? `Found the file ${folder.path}` : `Found the folder ${folder.path}`); + this.addFileOrFolderInFolder(baseFolder, folder); + } + + writeFile(path: string, content: string): void { + const file = this.toFile({ path, content }); + + // base folder has to be present + const base = getDirectoryPath(file.fullPath); + const folder = this.fs.get(base) as Folder; + Debug.assert(isFolder(folder)); + + this.addFileOrFolderInFolder(folder, file); + } + + write(message: string) { + this.output.push(message); + } + + getOutput(): ReadonlyArray { + return this.output; + } + + clearOutput() { + clear(this.output); + } + + readonly readFile = (s: string) => (this.fs.get(this.toFullPath(s))).content; + readonly resolvePath = (s: string) => s; + readonly getExecutingFilePath = () => this.executingFilePath; + readonly getCurrentDirectory = () => this.currentDirectory; + readonly exit = notImplemented; + readonly getEnvironmentVariable = notImplemented; + } +} From c814d8e847e559f14ed44ced735a8f7ebd79d42c Mon Sep 17 00:00:00 2001 From: Sheetal Nandi Date: Wed, 26 Jul 2017 15:12:03 -0700 Subject: [PATCH 039/109] Add tests for the tsc --watch --- Jakefile.js | 2 + src/compiler/program.ts | 24 +- src/compiler/tsc.ts | 3 + src/compiler/tscLib.ts | 224 ++--- src/harness/tsconfig.json | 2 + src/harness/unittests/tscWatchMode.ts | 1000 +++++++++++++++++++++ src/harness/virtualFileSystemWithWatch.ts | 55 +- src/services/services.ts | 19 +- 8 files changed, 1204 insertions(+), 125 deletions(-) create mode 100644 src/harness/unittests/tscWatchMode.ts diff --git a/Jakefile.js b/Jakefile.js index 31243f77c4284..ba3e18f3e56c4 100644 --- a/Jakefile.js +++ b/Jakefile.js @@ -91,6 +91,7 @@ var languageServiceLibrarySources = filesFromConfig(path.join(serverDirectory, " var harnessCoreSources = [ "harness.ts", "virtualFileSystem.ts", + "virtualFileSystemWithWatch.ts", "sourceMapRecorder.ts", "harnessLanguageService.ts", "fourslash.ts", @@ -128,6 +129,7 @@ var harnessSources = harnessCoreSources.concat([ "convertCompilerOptionsFromJson.ts", "convertTypeAcquisitionFromJson.ts", "tsserverProjectSystem.ts", + "tscWatchMode.ts", "compileOnSave.ts", "typingsInstaller.ts", "projectErrors.ts", diff --git a/src/compiler/program.ts b/src/compiler/program.ts index e2bfa647eb7d6..80bd089429a77 100644 --- a/src/compiler/program.ts +++ b/src/compiler/program.ts @@ -387,7 +387,8 @@ namespace ts { allDiagnostics?: Diagnostic[]; } - export function isProgramUptoDate(program: Program, rootFileNames: string[], newOptions: CompilerOptions, getSourceVersion: (path: Path) => string): boolean { + export function isProgramUptoDate(program: Program, rootFileNames: string[], newOptions: CompilerOptions, + getSourceVersion: (path: Path) => string, fileExists: (fileName: string) => boolean): boolean { // If we haven't create a program yet, then it is not up-to-date if (!program) { return false; @@ -398,14 +399,18 @@ namespace ts { return false; } - const fileNames = concatenate(rootFileNames, map(program.getSourceFiles(), sourceFile => sourceFile.fileName)); // If any file is not up-to-date, then the whole program is not up-to-date - for (const fileName of fileNames) { - if (!sourceFileUpToDate(program.getSourceFile(fileName))) { + for (const file of program.getSourceFiles()) { + if (!sourceFileUpToDate(program.getSourceFile(file.fileName))) { return false; } } + // If any of the missing file paths are now created + if (program.getMissingFilePaths().some(missingFilePath => fileExists(missingFilePath))) { + return false; + } + const currentOptions = program.getCompilerOptions(); // If the compilation settings do no match, then the program is not up-to-date if (!compareDataObjects(currentOptions, newOptions)) { @@ -445,14 +450,10 @@ namespace ts { /** * Updates the existing missing file watches with the new set of missing files after new program is created - * @param program - * @param existingMap - * @param createMissingFileWatch - * @param closeExistingFileWatcher */ export function updateMissingFilePathsWatch(program: Program, existingMap: Map, createMissingFileWatch: (missingFilePath: Path) => FileWatcher, - closeExistingFileWatcher: (missingFilePath: Path, fileWatcher: FileWatcher) => void) { + closeExistingMissingFilePathFileWatcher: (missingFilePath: Path, fileWatcher: FileWatcher) => void) { const missingFilePaths = program.getMissingFilePaths(); const newMissingFilePathMap = arrayToSet(missingFilePaths); @@ -463,12 +464,15 @@ namespace ts { createMissingFileWatch, // Files that are no longer missing (e.g. because they are no longer required) // should no longer be watched. - closeExistingFileWatcher + closeExistingMissingFilePathFileWatcher ); } export type WildCardDirectoryWatchers = { watcher: FileWatcher, recursive: boolean }; + /** + * Updates the existing wild card directory watcyhes with the new set of wild card directories from the config file after new program is created + */ export function updateWatchingWildcardDirectories(existingWatchedForWildcards: Map, wildcardDirectories: Map, watchDirectory: (directory: string, recursive: boolean) => FileWatcher, closeDirectoryWatcher: (directory: string, watcher: FileWatcher, recursive: boolean, recursiveChanged: boolean) => void) { diff --git a/src/compiler/tsc.ts b/src/compiler/tsc.ts index ec93b61bbdf53..3a672fca72728 100644 --- a/src/compiler/tsc.ts +++ b/src/compiler/tsc.ts @@ -1,3 +1,6 @@ /// +if (ts.sys.tryEnableSourceMapsForHost && /^development$/i.test(ts.sys.getEnvironmentVariable("NODE_ENV"))) { + ts.sys.tryEnableSourceMapsForHost(); +} ts.executeCommandLine(ts.sys.args); diff --git a/src/compiler/tscLib.ts b/src/compiler/tscLib.ts index 3dba8961674bd..b92349061761e 100644 --- a/src/compiler/tscLib.ts +++ b/src/compiler/tscLib.ts @@ -12,35 +12,49 @@ namespace ts { value: string; } - const defaultFormatDiagnosticsHost: FormatDiagnosticsHost = { + export interface FormatDiagnosticsHostWithWrite extends FormatDiagnosticsHost { + write?(s: string): void; + } + + const defaultFormatDiagnosticsHost: FormatDiagnosticsHostWithWrite = sys ? { getCurrentDirectory: () => sys.getCurrentDirectory(), getNewLine: () => sys.newLine, - getCanonicalFileName: createGetCanonicalFileName(sys.useCaseSensitiveFileNames) - }; + getCanonicalFileName: createGetCanonicalFileName(sys.useCaseSensitiveFileNames), + write: s => sys.write(s) + } : undefined; + + function getDefaultFormatDiagnosticsHost(system: System): FormatDiagnosticsHostWithWrite { + return system === sys ? defaultFormatDiagnosticsHost : { + getCurrentDirectory: () => system.getCurrentDirectory(), + getNewLine: () => system.newLine, + getCanonicalFileName: createGetCanonicalFileName(system.useCaseSensitiveFileNames), + write: s => system.write(s) + }; + } let reportDiagnosticWorker = reportDiagnosticSimply; - function reportDiagnostic(diagnostic: Diagnostic, host: FormatDiagnosticsHost) { + function reportDiagnostic(diagnostic: Diagnostic, host: FormatDiagnosticsHostWithWrite) { reportDiagnosticWorker(diagnostic, host || defaultFormatDiagnosticsHost); } - function reportDiagnostics(diagnostics: Diagnostic[], host: FormatDiagnosticsHost): void { + function reportDiagnostics(diagnostics: Diagnostic[], host: FormatDiagnosticsHostWithWrite): void { for (const diagnostic of diagnostics) { reportDiagnostic(diagnostic, host); } } - function reportEmittedFiles(files: string[]): void { + function reportEmittedFiles(files: string[], system: System): void { if (!files || files.length === 0) { return; } - const currentDir = sys.getCurrentDirectory(); + const currentDir = system.getCurrentDirectory(); for (const file of files) { const filepath = getNormalizedAbsolutePath(file, currentDir); - sys.write(`TSFILE: ${filepath}${sys.newLine}`); + system.write(`TSFILE: ${filepath}${system.newLine}`); } } @@ -57,15 +71,15 @@ namespace ts { return diagnostic.messageText; } - function reportDiagnosticSimply(diagnostic: Diagnostic, host: FormatDiagnosticsHost): void { - sys.write(ts.formatDiagnostics([diagnostic], host)); + function reportDiagnosticSimply(diagnostic: Diagnostic, host: FormatDiagnosticsHostWithWrite): void { + host.write(ts.formatDiagnostics([diagnostic], host)); } - function reportDiagnosticWithColorAndContext(diagnostic: Diagnostic, host: FormatDiagnosticsHost): void { - sys.write(ts.formatDiagnosticsWithColorAndContext([diagnostic], host) + sys.newLine); + function reportDiagnosticWithColorAndContext(diagnostic: Diagnostic, host: FormatDiagnosticsHostWithWrite): void { + host.write(ts.formatDiagnosticsWithColorAndContext([diagnostic], host) + host.getNewLine()); } - function reportWatchDiagnostic(diagnostic: Diagnostic) { + function reportWatchDiagnostic(diagnostic: Diagnostic, system: System) { let output = new Date().toLocaleTimeString() + " - "; if (diagnostic.file) { @@ -73,9 +87,9 @@ namespace ts { output += `${ diagnostic.file.fileName }(${ loc.line + 1 },${ loc.character + 1 }): `; } - output += `${ flattenDiagnosticMessageText(diagnostic.messageText, sys.newLine) }${ sys.newLine + sys.newLine + sys.newLine }`; + output += `${ flattenDiagnosticMessageText(diagnostic.messageText, system.newLine) }${ system.newLine + system.newLine + system.newLine }`; - sys.write(output); + system.write(output); } function padLeft(s: string, length: number) { @@ -175,7 +189,7 @@ namespace ts { const configParseResult = parseConfigFile(configFileName, commandLineOptions, sys); if (isWatchSet(configParseResult.options)) { reportWatchModeWithoutSysSupport(); - createWatchModeWithConfigFile(configParseResult, commandLineOptions); + createWatchModeWithConfigFile(configParseResult, commandLineOptions, sys); } else { performCompilation(configParseResult.fileNames, configParseResult.options); @@ -184,7 +198,7 @@ namespace ts { else { if (isWatchSet(commandLine.options)) { reportWatchModeWithoutSysSupport(); - createWatchModeWithoutConfigFile(commandLine.fileNames, commandLineOptions); + createWatchModeWithoutConfigFile(commandLine.fileNames, commandLineOptions, sys); } else { performCompilation(commandLine.fileNames, commandLineOptions); @@ -204,7 +218,7 @@ namespace ts { } const compilerHost = createCompilerHost(compilerOptions); - const compileResult = compile(rootFileNames, compilerOptions, compilerHost); + const compileResult = compile(rootFileNames, compilerOptions, compilerHost, sys); return sys.exit(compileResult.exitStatus); } } @@ -216,16 +230,16 @@ namespace ts { } /* @internal */ - export function createWatchModeWithConfigFile(configParseResult: ParsedCommandLine, optionsToExtend?: CompilerOptions) { - return createWatchMode(configParseResult.fileNames, configParseResult.options, configParseResult.options.configFilePath, configParseResult.configFileSpecs, configParseResult.wildcardDirectories, optionsToExtend); + export function createWatchModeWithConfigFile(configParseResult: ParsedCommandLine, optionsToExtend: CompilerOptions, system: System) { + return createWatchMode(configParseResult.fileNames, configParseResult.options, system, configParseResult.options.configFilePath, configParseResult.configFileSpecs, configParseResult.wildcardDirectories, optionsToExtend); } /* @internal */ - export function createWatchModeWithoutConfigFile(rootFileNames: string[], compilerOptions: CompilerOptions) { - return createWatchMode(rootFileNames, compilerOptions); + export function createWatchModeWithoutConfigFile(rootFileNames: string[], compilerOptions: CompilerOptions, system: System) { + return createWatchMode(rootFileNames, compilerOptions, system); } - function createWatchMode(rootFileNames: string[], compilerOptions: CompilerOptions, configFileName?: string, configFileSpecs?: ConfigFileSpecs, configFileWildCardDirectories?: MapLike, optionsToExtendForConfigFile?: CompilerOptions) { + function createWatchMode(rootFileNames: string[], compilerOptions: CompilerOptions, system: System, configFileName?: string, configFileSpecs?: ConfigFileSpecs, configFileWildCardDirectories?: MapLike, optionsToExtendForConfigFile?: CompilerOptions) { let program: Program; let needsReload: boolean; // true if the config file changed and needs to reload it from the disk let missingFilesMap: Map; // Map of file watchers for the missing files @@ -238,11 +252,11 @@ namespace ts { let host: System; if (configFileName) { - host = createCachedSystem(sys); - configFileWatcher = sys.watchFile(configFileName, onConfigFileChanged); + host = createCachedSystem(system); + configFileWatcher = system.watchFile(configFileName, onConfigFileChanged); } else { - host = sys; + host = system; } const currentDirectory = host.getCurrentDirectory(); const getCanonicalFileName = createGetCanonicalFileName(host.useCaseSensitiveFileNames); @@ -259,21 +273,23 @@ namespace ts { // Update the wild card directory watch watchConfigFileWildCardDirectories(); + return () => program; + function synchronizeProgram() { writeLog(`Synchronizing program`); - if (isProgramUptoDate(program, rootFileNames, compilerOptions, getSourceVersion)) { + if (isProgramUptoDate(program, rootFileNames, compilerOptions, getSourceVersion, fileExists)) { return; } // Create the compiler host const compilerHost = createWatchedCompilerHost(compilerOptions); - program = compile(rootFileNames, compilerOptions, compilerHost, program).program; + program = compile(rootFileNames, compilerOptions, compilerHost, system, program).program; // Update watches missingFilesMap = updateMissingFilePathsWatch(program, missingFilesMap, watchMissingFilePath, closeMissingFilePathWatcher); - reportWatchDiagnostic(createCompilerDiagnostic(Diagnostics.Compilation_complete_Watching_for_file_changes)); + reportWatchDiagnostic(createCompilerDiagnostic(Diagnostics.Compilation_complete_Watching_for_file_changes), system); } function createWatchedCompilerHost(options: CompilerOptions): CompilerHost { @@ -302,14 +318,14 @@ namespace ts { }; // TODO: cache module resolution - //if (host.resolveModuleNames) { - // compilerHost.resolveModuleNames = (moduleNames, containingFile) => host.resolveModuleNames(moduleNames, containingFile); - //} - //if (host.resolveTypeReferenceDirectives) { - // compilerHost.resolveTypeReferenceDirectives = (typeReferenceDirectiveNames, containingFile) => { - // return host.resolveTypeReferenceDirectives(typeReferenceDirectiveNames, containingFile); - // }; - //} + // if (host.resolveModuleNames) { + // compilerHost.resolveModuleNames = (moduleNames, containingFile) => host.resolveModuleNames(moduleNames, containingFile); + // } + // if (host.resolveTypeReferenceDirectives) { + // compilerHost.resolveTypeReferenceDirectives = (typeReferenceDirectiveNames, containingFile) => { + // return host.resolveTypeReferenceDirectives(typeReferenceDirectiveNames, containingFile); + // }; + // } function ensureDirectoriesExist(directoryPath: string) { if (directoryPath.length > getRootLength(directoryPath) && !host.directoryExists(directoryPath)) { @@ -342,7 +358,7 @@ namespace ts { const seenFiles = createMap(); let emitSkipped: boolean; - let diagnostics: Diagnostic[]; + const diagnostics: Diagnostic[] = []; const emittedFiles: string[] = program.getCompilerOptions().listEmittedFiles ? [] : undefined; let sourceMaps: SourceMapData[]; while (filesPendingToEmit.length) { @@ -366,7 +382,7 @@ namespace ts { emitSkipped = true; } - diagnostics = concatenate(diagnostics, emitOutput.diagnostics); + diagnostics.push(...emitOutput.diagnostics); sourceMaps = concatenate(sourceMaps, emitOutput.sourceMaps); // If it emitted more than one source files, just mark all those source files as seen if (emitOutput.emittedSourceFiles && emitOutput.emittedSourceFiles.length > 1) { @@ -377,7 +393,7 @@ namespace ts { for (const outputFile of emitOutput.outputFiles) { const error = writeFile(outputFile.name, outputFile.text, outputFile.writeByteOrderMark); if (error) { - (diagnostics || (diagnostics = [])).push(error); + diagnostics.push(error); } if (emittedFiles) { emittedFiles.push(outputFile.name); @@ -413,41 +429,43 @@ namespace ts { } // Create new source file if requested or the versions dont match - if (!hostSourceFile) { + if (!hostSourceFile || shouldCreateNewSourceFile || hostSourceFile.version.toString() !== hostSourceFile.sourceFile.version) { changedFilePaths.push(path); - const sourceFile = getSourceFile(fileName, languageVersion, onError); - if (sourceFile) { - sourceFile.version = "0"; - const fileWatcher = watchSourceFileForChanges(sourceFile.path); - sourceFilesCache.set(path, { sourceFile, version: 0, fileWatcher }); + + const sourceFile = getNewSourceFile(); + if (hostSourceFile) { + if (shouldCreateNewSourceFile) { + hostSourceFile.version++; + } + if (sourceFile) { + hostSourceFile.sourceFile = sourceFile; + sourceFile.version = hostSourceFile.version.toString(); + if (!hostSourceFile.fileWatcher) { + hostSourceFile.fileWatcher = watchSourceFileForChanges(path); + } + } + else { + // There is no source file on host any more, close the watch, missing file paths will track it + hostSourceFile.fileWatcher.close(); + sourceFilesCache.set(path, hostSourceFile.version.toString()); + } } else { - sourceFilesCache.set(path, "0"); + let fileWatcher: FileWatcher; + if (sourceFile) { + sourceFile.version = "0"; + fileWatcher = watchSourceFileForChanges(path); + sourceFilesCache.set(path, { sourceFile, version: 0, fileWatcher }); + } + else { + sourceFilesCache.set(path, "0"); + } } return sourceFile; } - else if (shouldCreateNewSourceFile || hostSourceFile.version.toString() !== hostSourceFile.sourceFile.version) { - changedFilePaths.push(path); - if (shouldCreateNewSourceFile) { - hostSourceFile.version++; - } - const newSourceFile = getSourceFile(fileName, languageVersion, onError); - if (newSourceFile) { - newSourceFile.version = hostSourceFile.version.toString(); - hostSourceFile.sourceFile = newSourceFile; - } - else { - // File doesnt exist any more - hostSourceFile.fileWatcher.close(); - sourceFilesCache.set(path, hostSourceFile.version.toString()); - } - - return newSourceFile; - } - return hostSourceFile.sourceFile; - function getSourceFile(fileName: string, languageVersion: ScriptTarget, onError?: (message: string) => void): SourceFile { + function getNewSourceFile() { let text: string; try { performance.mark("beforeIORead"); @@ -459,7 +477,6 @@ namespace ts { if (onError) { onError(e.message); } - text = ""; } return text !== undefined ? createSourceFile(fileName, text, languageVersion) : undefined; @@ -496,14 +513,14 @@ namespace ts { // operations (such as saving all modified files in an editor) a chance to complete before we kick // off a new compilation. function scheduleProgramUpdate() { - if (!sys.setTimeout || !sys.clearTimeout) { + if (!system.setTimeout || !system.clearTimeout) { return; } if (timerToUpdateProgram) { - sys.clearTimeout(timerToUpdateProgram); + system.clearTimeout(timerToUpdateProgram); } - timerToUpdateProgram = sys.setTimeout(updateProgram, 250); + timerToUpdateProgram = system.setTimeout(updateProgram, 250); } function scheduleProgramReload() { @@ -514,7 +531,7 @@ namespace ts { function updateProgram() { timerToUpdateProgram = undefined; - reportWatchDiagnostic(createCompilerDiagnostic(Diagnostics.File_change_detected_Starting_incremental_compilation)); + reportWatchDiagnostic(createCompilerDiagnostic(Diagnostics.File_change_detected_Starting_incremental_compilation), system); if (needsReload) { reloadConfigFile(); @@ -526,8 +543,6 @@ namespace ts { function reloadConfigFile() { writeLog(`Reloading config file: ${configFileName}`); - reportWatchDiagnostic(createCompilerDiagnostic(Diagnostics.File_change_detected_Starting_incremental_compilation)); - needsReload = false; const cachedHost = host as CachedSystem; @@ -550,6 +565,8 @@ namespace ts { function onSourceFileChange(fileName: string, path: Path, eventKind: FileWatcherEventKind) { writeLog(`Source file path : ${path} changed: ${FileWatcherEventKind[eventKind]}, fileName: ${fileName}`); + + updateCachedSystem(fileName, path); const hostSourceFile = sourceFilesCache.get(path); if (hostSourceFile) { // Update the cache @@ -570,10 +587,18 @@ namespace ts { } } } + // Update the program scheduleProgramUpdate(); } + function updateCachedSystem(fileName: string, path: Path) { + if (configFileName) { + const absoluteNormalizedPath = getNormalizedAbsolutePath(fileName, getDirectoryPath(path)); + (host as CachedSystem).addOrDeleteFileOrFolder(normalizePath(absoluteNormalizedPath)); + } + } + function watchMissingFilePath(missingFilePath: Path) { return host.watchFile(missingFilePath, (fileName, eventKind) => onMissingFileChange(fileName, missingFilePath, eventKind)); } @@ -588,10 +613,7 @@ namespace ts { closeMissingFilePathWatcher(missingFilePath, missingFilesMap.get(missingFilePath)); missingFilesMap.delete(missingFilePath); - if (configFileName) { - const absoluteNormalizedPath = getNormalizedAbsolutePath(filename, getDirectoryPath(missingFilePath)); - (host as CachedSystem).addOrDeleteFileOrFolder(normalizePath(absoluteNormalizedPath)); - } + updateCachedSystem(filename, missingFilePath); // Delete the entry in the source files cache so that new source file is created removeSourceFile(missingFilePath); @@ -621,10 +643,12 @@ namespace ts { function onFileAddOrRemoveInWatchedDirectory(fileName: string) { Debug.assert(!!configFileName); - (host as CachedSystem).addOrDeleteFileOrFolder(fileName); + + const path = toPath(fileName, currentDirectory, getCanonicalFileName); // Since the file existance changed, update the sourceFiles cache - removeSourceFile(toPath(fileName, currentDirectory, getCanonicalFileName)); + updateCachedSystem(fileName, path); + removeSourceFile(path); // If a change was made inside "folder/file", node will trigger the callback twice: // one with the fileName being "folder/file", and the other one with "folder". @@ -640,7 +664,7 @@ namespace ts { if (!needsReload) { const result = getFileNamesFromConfigSpecs(configFileSpecs, getDirectoryPath(configFileName), compilerOptions, host, /*extraFileExtensions*/ []); if (!configFileSpecs.filesSpecs) { - reportDiagnostics([getErrorForNoInputFiles(configFileSpecs, configFileName)], /*host*/ undefined); + reportDiagnostics([getErrorForNoInputFiles(configFileSpecs, configFileName)], getDefaultFormatDiagnosticsHost(system)); } rootFileNames = result.fileNames; @@ -662,7 +686,7 @@ namespace ts { } function computeHash(data: string) { - return sys.createHash ? sys.createHash(data) : data; + return system.createHash ? system.createHash(data) : data; } } @@ -718,35 +742,35 @@ namespace ts { } /* @internal */ - export function parseConfigFile(configFileName: string, optionsToExtend: CompilerOptions, host: System): ParsedCommandLine { + export function parseConfigFile(configFileName: string, optionsToExtend: CompilerOptions, system: System): ParsedCommandLine { let configFileText: string; try { - configFileText = host.readFile(configFileName); + configFileText = system.readFile(configFileName); } catch (e) { const error = createCompilerDiagnostic(Diagnostics.Cannot_read_file_0_Colon_1, configFileName, e.message); - reportWatchDiagnostic(error); - host.exit(ExitStatus.DiagnosticsPresent_OutputsSkipped); + reportWatchDiagnostic(error, system); + system.exit(ExitStatus.DiagnosticsPresent_OutputsSkipped); return; } if (!configFileText) { const error = createCompilerDiagnostic(Diagnostics.File_0_not_found, configFileName); - reportDiagnostics([error], /* compilerHost */ undefined); - host.exit(ExitStatus.DiagnosticsPresent_OutputsSkipped); + reportDiagnostics([error], getDefaultFormatDiagnosticsHost(system)); + system.exit(ExitStatus.DiagnosticsPresent_OutputsSkipped); return; } const result = parseJsonText(configFileName, configFileText); - reportDiagnostics(result.parseDiagnostics, /* compilerHost */ undefined); + reportDiagnostics(result.parseDiagnostics, getDefaultFormatDiagnosticsHost(system)); - const cwd = host.getCurrentDirectory(); - const configParseResult = parseJsonSourceFileConfigFileContent(result, host, getNormalizedAbsolutePath(getDirectoryPath(configFileName), cwd), optionsToExtend, getNormalizedAbsolutePath(configFileName, cwd)); - reportDiagnostics(configParseResult.errors, /* compilerHost */ undefined); + const cwd = system.getCurrentDirectory(); + const configParseResult = parseJsonSourceFileConfigFileContent(result, system, getNormalizedAbsolutePath(getDirectoryPath(configFileName), cwd), optionsToExtend, getNormalizedAbsolutePath(configFileName, cwd)); + reportDiagnostics(configParseResult.errors, getDefaultFormatDiagnosticsHost(system)); return configParseResult; } - function compile(fileNames: string[], compilerOptions: CompilerOptions, compilerHost: CompilerHost, oldProgram?: Program) { + function compile(fileNames: string[], compilerOptions: CompilerOptions, compilerHost: CompilerHost, system: System, oldProgram?: Program) { const hasDiagnostics = compilerOptions.diagnostics || compilerOptions.extendedDiagnostics; let statistics: Statistic[]; if (hasDiagnostics) { @@ -759,12 +783,12 @@ namespace ts { if (compilerOptions.listFiles) { forEach(program.getSourceFiles(), file => { - sys.write(file.fileName + sys.newLine); + system.write(file.fileName + system.newLine); }); } if (hasDiagnostics) { - const memoryUsed = sys.getMemoryUsage ? sys.getMemoryUsage() : -1; + const memoryUsed = system.getMemoryUsage ? system.getMemoryUsage() : -1; reportCountStatistic("Files", program.getSourceFiles().length); reportCountStatistic("Lines", countLines(program)); reportCountStatistic("Nodes", program.getNodeCount()); @@ -830,9 +854,9 @@ namespace ts { } diagnostics = diagnostics.concat(emitOutput.diagnostics); - reportDiagnostics(sortAndDeduplicateDiagnostics(diagnostics), compilerHost); + reportDiagnostics(sortAndDeduplicateDiagnostics(diagnostics), getDefaultFormatDiagnosticsHost(system)); - reportEmittedFiles(emitOutput.emittedFiles); + reportEmittedFiles(emitOutput.emittedFiles, system); if (emitOutput.emitSkipped && diagnostics.length > 0) { // If the emitter didn't emit anything, then pass that value along. return ExitStatus.DiagnosticsPresent_OutputsSkipped; @@ -859,7 +883,7 @@ namespace ts { } for (const { name, value } of statistics) { - sys.write(padRight(name + ":", nameSize + 2) + padLeft(value.toString(), valueSize) + sys.newLine); + system.write(padRight(name + ":", nameSize + 2) + padLeft(value.toString(), valueSize) + system.newLine); } } @@ -1012,7 +1036,3 @@ namespace ts { if (ts.Debug.isDebugging) { ts.Debug.enableDebugInfo(); } - -if (ts.sys.tryEnableSourceMapsForHost && /^development$/i.test(ts.sys.getEnvironmentVariable("NODE_ENV"))) { - ts.sys.tryEnableSourceMapsForHost(); -} diff --git a/src/harness/tsconfig.json b/src/harness/tsconfig.json index dfdd0a2fee853..7c3ad0bc5a35c 100644 --- a/src/harness/tsconfig.json +++ b/src/harness/tsconfig.json @@ -93,6 +93,7 @@ "rwcRunner.ts", "test262Runner.ts", "runner.ts", + "virtualFileSystemWithWatch.ts", "../server/protocol.ts", "../server/session.ts", "../server/client.ts", @@ -116,6 +117,7 @@ "./unittests/convertCompilerOptionsFromJson.ts", "./unittests/convertTypeAcquisitionFromJson.ts", "./unittests/tsserverProjectSystem.ts", + "./unittests/tscWatchMode.ts", "./unittests/matchFiles.ts", "./unittests/initializeTSConfig.ts", "./unittests/compileOnSave.ts", diff --git a/src/harness/unittests/tscWatchMode.ts b/src/harness/unittests/tscWatchMode.ts new file mode 100644 index 0000000000000..630d9a71d201a --- /dev/null +++ b/src/harness/unittests/tscWatchMode.ts @@ -0,0 +1,1000 @@ +/// +/// +/// + +namespace ts.tscWatch { + + export import WatchedSystem = ts.TestFSWithWatch.TestServerHost; + export type TestServerHostCreationParameters = ts.TestFSWithWatch.TestServerHostCreationParameters; + export type File = ts.TestFSWithWatch.File; + export type FileOrFolder = ts.TestFSWithWatch.FileOrFolder; + export type Folder = ts.TestFSWithWatch.Folder; + export type FSEntry = ts.TestFSWithWatch.FSEntry; + export import createWatchedSystem = ts.TestFSWithWatch.createWatchedSystem; + export import checkFileNames = ts.TestFSWithWatch.checkFileNames; + export import libFile = ts.TestFSWithWatch.libFile; + export import checkWatchedFiles = ts.TestFSWithWatch.checkWatchedFiles; + export import checkWatchedDirectories = ts.TestFSWithWatch.checkWatchedDirectories; + export import checkOutputContains = ts.TestFSWithWatch.checkOutputContains; + export import checkOutputDoesNotContain = ts.TestFSWithWatch.checkOutputDoesNotContain; + + export function checkProgramActualFiles(program: Program, expectedFiles: string[]) { + checkFileNames(`Program actual files`, program.getSourceFiles().map(file => file.fileName), expectedFiles); + } + + export function checkProgramRootFiles(program: Program, expectedFiles: string[]) { + checkFileNames(`Program rootFileNames`, program.getRootFileNames(), expectedFiles); + } + + export function createWatchWithConfig(configFilePath: string, host: WatchedSystem) { + const configFileResult = parseConfigFile(configFilePath, {}, host); + return createWatchModeWithConfigFile(configFileResult, {}, host); + } + + describe("tsc-watch program updates", () => { + const commonFile1: FileOrFolder = { + path: "/a/b/commonFile1.ts", + content: "let x = 1" + }; + const commonFile2: FileOrFolder = { + path: "/a/b/commonFile2.ts", + content: "let y = 1" + }; + + it("create watch without config file", () => { + const appFile: FileOrFolder = { + path: "/a/b/c/app.ts", + content: ` + import {f} from "./module" + console.log(f) + ` + }; + + const moduleFile: FileOrFolder = { + path: "/a/b/c/module.d.ts", + content: `export let x: number` + }; + const host = createWatchedSystem([appFile, moduleFile, libFile]); + const watch = createWatchModeWithoutConfigFile([appFile.path], {}, host); + + checkProgramActualFiles(watch(), [appFile.path, libFile.path, moduleFile.path]); + + // TODO: Should we watch creation of config files in the root file's file hierarchy? + + // const configFileLocations = ["/a/b/c/", "/a/b/", "/a/", "/"]; + // const configFiles = flatMap(configFileLocations, location => [location + "tsconfig.json", location + "jsconfig.json"]); + // checkWatchedFiles(host, configFiles.concat(libFile.path, moduleFile.path)); + }); + + it("can handle tsconfig file name with difference casing", () => { + const f1 = { + path: "/a/b/app.ts", + content: "let x = 1" + }; + const config = { + path: "/a/b/tsconfig.json", + content: JSON.stringify({ + include: ["app.ts"] + }) + }; + + const host = createWatchedSystem([f1, config], { useCaseSensitiveFileNames: false }); + const upperCaseConfigFilePath = combinePaths(getDirectoryPath(config.path).toUpperCase(), getBaseFileName(config.path)); + const watch = createWatchWithConfig(upperCaseConfigFilePath, host); + checkProgramActualFiles(watch(), [f1.path]); + }); + + it("create configured project without file list", () => { + const configFile: FileOrFolder = { + path: "/a/b/tsconfig.json", + content: ` + { + "compilerOptions": {}, + "exclude": [ + "e" + ] + }` + }; + const file1: FileOrFolder = { + path: "/a/b/c/f1.ts", + content: "let x = 1" + }; + const file2: FileOrFolder = { + path: "/a/b/d/f2.ts", + content: "let y = 1" + }; + const file3: FileOrFolder = { + path: "/a/b/e/f3.ts", + content: "let z = 1" + }; + + const host = createWatchedSystem([configFile, libFile, file1, file2, file3]); + const configFileResult = parseConfigFile(configFile.path, {}, host); + assert.equal(configFileResult.errors.length, 0, `expect no errors in config file, got ${JSON.stringify(configFileResult.errors)}`); + + const watch = createWatchModeWithConfigFile(configFileResult, {}, host); + + checkProgramActualFiles(watch(), [file1.path, libFile.path, file2.path]); + checkProgramRootFiles(watch(), [file1.path, file2.path]); + checkWatchedFiles(host, [configFile.path, file1.path, file2.path, libFile.path]); + checkWatchedDirectories(host, [getDirectoryPath(configFile.path)], /*recursive*/ true); + }); + + // TODO: if watching for config file creation + // it("add and then remove a config file in a folder with loose files", () => { + // }); + + it("add new files to a configured program without file list", () => { + const configFile: FileOrFolder = { + path: "/a/b/tsconfig.json", + content: `{}` + }; + const host = createWatchedSystem([commonFile1, libFile, configFile]); + const watch = createWatchWithConfig(configFile.path, host); + checkWatchedDirectories(host, ["/a/b"], /*recursive*/ true); + + checkProgramRootFiles(watch(), [commonFile1.path]); + + // add a new ts file + host.reloadFS([commonFile1, commonFile2, libFile, configFile]); + host.checkTimeoutQueueLengthAndRun(1); + checkProgramRootFiles(watch(), [commonFile1.path, commonFile2.path]); + }); + + it("should ignore non-existing files specified in the config file", () => { + const configFile: FileOrFolder = { + path: "/a/b/tsconfig.json", + content: `{ + "compilerOptions": {}, + "files": [ + "commonFile1.ts", + "commonFile3.ts" + ] + }` + }; + const host = createWatchedSystem([commonFile1, commonFile2, configFile]); + const watch = createWatchWithConfig(configFile.path, host); + + const commonFile3 = "/a/b/commonFile3.ts"; + checkProgramRootFiles(watch(), [commonFile1.path, commonFile3]); + checkProgramActualFiles(watch(), [commonFile1.path]); + }); + + it("handle recreated files correctly", () => { + const configFile: FileOrFolder = { + path: "/a/b/tsconfig.json", + content: `{}` + }; + const host = createWatchedSystem([commonFile1, commonFile2, configFile]); + const watch = createWatchWithConfig(configFile.path, host); + checkProgramRootFiles(watch(), [commonFile1.path, commonFile2.path]); + + // delete commonFile2 + host.reloadFS([commonFile1, configFile]); + host.checkTimeoutQueueLengthAndRun(1); + checkProgramRootFiles(watch(), [commonFile1.path]); + + // re-add commonFile2 + host.reloadFS([commonFile1, commonFile2, configFile]); + host.checkTimeoutQueueLengthAndRun(1); + checkProgramRootFiles(watch(), [commonFile1.path, commonFile2.path]); + }); + + it("handles the missing files - that were added to program because they were added with /// { + const file1: FileOrFolder = { + path: "/a/b/commonFile1.ts", + content: `/// + let x = y` + }; + const host = createWatchedSystem([file1, libFile]); + const watch = createWatchModeWithoutConfigFile([file1.path], {}, host); + + checkProgramRootFiles(watch(), [file1.path]); + checkProgramActualFiles(watch(), [file1.path, libFile.path]); + const errors = [ + `a/b/commonFile1.ts(1,22): error TS6053: File '${commonFile2.path}' not found.${host.newLine}`, + `a/b/commonFile1.ts(2,29): error TS2304: Cannot find name 'y'.${host.newLine}` + ]; + checkOutputContains(host, errors); + host.clearOutput(); + + host.reloadFS([file1, commonFile2, libFile]); + host.runQueuedTimeoutCallbacks(); + checkProgramRootFiles(watch(), [file1.path]); + checkProgramActualFiles(watch(), [file1.path, libFile.path, commonFile2.path]); + checkOutputDoesNotContain(host, errors); + }); + + it("should reflect change in config file", () => { + const configFile: FileOrFolder = { + path: "/a/b/tsconfig.json", + content: `{ + "compilerOptions": {}, + "files": ["${commonFile1.path}", "${commonFile2.path}"] + }` + }; + const files = [commonFile1, commonFile2, configFile]; + const host = createWatchedSystem(files); + const watch = createWatchWithConfig(configFile.path, host); + + checkProgramRootFiles(watch(), [commonFile1.path, commonFile2.path]); + configFile.content = `{ + "compilerOptions": {}, + "files": ["${commonFile1.path}"] + }`; + + host.reloadFS(files); + host.checkTimeoutQueueLengthAndRun(1); // reload the configured project + checkProgramRootFiles(watch(), [commonFile1.path]); + }); + + it("files explicitly excluded in config file", () => { + const configFile: FileOrFolder = { + path: "/a/b/tsconfig.json", + content: `{ + "compilerOptions": {}, + "exclude": ["/a/c"] + }` + }; + const excludedFile1: FileOrFolder = { + path: "/a/c/excluedFile1.ts", + content: `let t = 1;` + }; + + const host = createWatchedSystem([commonFile1, commonFile2, excludedFile1, configFile]); + const watch = createWatchWithConfig(configFile.path, host); + checkProgramRootFiles(watch(), [commonFile1.path, commonFile2.path]); + }); + + it("should properly handle module resolution changes in config file", () => { + const file1: FileOrFolder = { + path: "/a/b/file1.ts", + content: `import { T } from "module1";` + }; + const nodeModuleFile: FileOrFolder = { + path: "/a/b/node_modules/module1.ts", + content: `export interface T {}` + }; + const classicModuleFile: FileOrFolder = { + path: "/a/module1.ts", + content: `export interface T {}` + }; + const configFile: FileOrFolder = { + path: "/a/b/tsconfig.json", + content: `{ + "compilerOptions": { + "moduleResolution": "node" + }, + "files": ["${file1.path}"] + }` + }; + const files = [file1, nodeModuleFile, classicModuleFile, configFile]; + const host = createWatchedSystem(files); + const watch = createWatchWithConfig(configFile.path, host); + checkProgramRootFiles(watch(), [file1.path]); + checkProgramActualFiles(watch(), [file1.path, nodeModuleFile.path]); + + configFile.content = `{ + "compilerOptions": { + "moduleResolution": "classic" + }, + "files": ["${file1.path}"] + }`; + host.reloadFS(files); + host.checkTimeoutQueueLengthAndRun(1); + checkProgramRootFiles(watch(), [file1.path]); + checkProgramActualFiles(watch(), [file1.path, classicModuleFile.path]); + }); + + it("should tolerate config file errors and still try to build a project", () => { + const configFile: FileOrFolder = { + path: "/a/b/tsconfig.json", + content: `{ + "compilerOptions": { + "target": "es6", + "allowAnything": true + }, + "someOtherProperty": {} + }` + }; + const host = createWatchedSystem([commonFile1, commonFile2, libFile, configFile]); + const watch = createWatchWithConfig(configFile.path, host); + checkProgramRootFiles(watch(), [commonFile1.path, commonFile2.path]); + }); + + it("changes in files are reflected in project structure", () => { + const file1 = { + path: "/a/b/f1.ts", + content: `export * from "./f2"` + }; + const file2 = { + path: "/a/b/f2.ts", + content: `export let x = 1` + }; + const file3 = { + path: "/a/c/f3.ts", + content: `export let y = 1;` + }; + const host = createWatchedSystem([file1, file2, file3]); + const watch = createWatchModeWithoutConfigFile([file1.path], {}, host); + checkProgramRootFiles(watch(), [file1.path]); + checkProgramActualFiles(watch(), [file1.path, file2.path]); + + const modifiedFile2 = { + path: file2.path, + content: `export * from "../c/f3"` // now inferred project should inclule file3 + }; + + host.reloadFS([file1, modifiedFile2, file3]); + host.checkTimeoutQueueLengthAndRun(1); + checkProgramRootFiles(watch(), [file1.path]); + checkProgramActualFiles(watch(), [file1.path, modifiedFile2.path, file3.path]); + }); + + it("deleted files affect project structure", () => { + const file1 = { + path: "/a/b/f1.ts", + content: `export * from "./f2"` + }; + const file2 = { + path: "/a/b/f2.ts", + content: `export * from "../c/f3"` + }; + const file3 = { + path: "/a/c/f3.ts", + content: `export let y = 1;` + }; + const host = createWatchedSystem([file1, file2, file3]); + const watch = createWatchModeWithoutConfigFile([file1.path], {}, host); + checkProgramActualFiles(watch(), [file1.path, file2.path, file3.path]); + + host.reloadFS([file1, file3]); + host.checkTimeoutQueueLengthAndRun(1); + + checkProgramActualFiles(watch(), [file1.path]); + }); + + it("deleted files affect project structure - 2", () => { + const file1 = { + path: "/a/b/f1.ts", + content: `export * from "./f2"` + }; + const file2 = { + path: "/a/b/f2.ts", + content: `export * from "../c/f3"` + }; + const file3 = { + path: "/a/c/f3.ts", + content: `export let y = 1;` + }; + const host = createWatchedSystem([file1, file2, file3]); + const watch = createWatchModeWithoutConfigFile([file1.path, file3.path], {}, host); + checkProgramActualFiles(watch(), [file1.path, file2.path, file3.path]); + + host.reloadFS([file1, file3]); + host.checkTimeoutQueueLengthAndRun(1); + + checkProgramActualFiles(watch(), [file1.path, file3.path]); + }); + + it("config file includes the file", () => { + const file1 = { + path: "/a/b/f1.ts", + content: "export let x = 5" + }; + const file2 = { + path: "/a/c/f2.ts", + content: `import {x} from "../b/f1"` + }; + const file3 = { + path: "/a/c/f3.ts", + content: "export let y = 1" + }; + const configFile = { + path: "/a/c/tsconfig.json", + content: JSON.stringify({ compilerOptions: {}, files: ["f2.ts", "f3.ts"] }) + }; + + const host = createWatchedSystem([file1, file2, file3, configFile]); + const watch = createWatchWithConfig(configFile.path, host); + + checkProgramRootFiles(watch(), [file2.path, file3.path]); + checkProgramActualFiles(watch(), [file1.path, file2.path, file3.path]); + }); + + it("correctly migrate files between projects", () => { + const file1 = { + path: "/a/b/f1.ts", + content: ` + export * from "../c/f2"; + export * from "../d/f3";` + }; + const file2 = { + path: "/a/c/f2.ts", + content: "export let x = 1;" + }; + const file3 = { + path: "/a/d/f3.ts", + content: "export let y = 1;" + }; + const host = createWatchedSystem([file1, file2, file3]); + const watch = createWatchModeWithoutConfigFile([file2.path, file3.path], {}, host); + checkProgramActualFiles(watch(), [file2.path, file3.path]); + + const watch2 = createWatchModeWithoutConfigFile([file1.path], {}, host); + checkProgramActualFiles(watch2(), [file1.path, file2.path, file3.path]); + + // Previous program shouldnt be updated + checkProgramActualFiles(watch(), [file2.path, file3.path]); + host.checkTimeoutQueueLength(0); + }); + + it("can correctly update configured project when set of root files has changed (new file on disk)", () => { + const file1 = { + path: "/a/b/f1.ts", + content: "let x = 1" + }; + const file2 = { + path: "/a/b/f2.ts", + content: "let y = 1" + }; + const configFile = { + path: "/a/b/tsconfig.json", + content: JSON.stringify({ compilerOptions: {} }) + }; + + const host = createWatchedSystem([file1, configFile]); + const watch = createWatchWithConfig(configFile.path, host); + checkProgramActualFiles(watch(), [file1.path]); + + host.reloadFS([file1, file2, configFile]); + host.checkTimeoutQueueLengthAndRun(1); + + checkProgramActualFiles(watch(), [file1.path, file2.path]); + checkProgramRootFiles(watch(), [file1.path, file2.path]); + }); + + it("can correctly update configured project when set of root files has changed (new file in list of files)", () => { + const file1 = { + path: "/a/b/f1.ts", + content: "let x = 1" + }; + const file2 = { + path: "/a/b/f2.ts", + content: "let y = 1" + }; + const configFile = { + path: "/a/b/tsconfig.json", + content: JSON.stringify({ compilerOptions: {}, files: ["f1.ts"] }) + }; + + const host = createWatchedSystem([file1, file2, configFile]); + const watch = createWatchWithConfig(configFile.path, host); + + checkProgramActualFiles(watch(), [file1.path]); + + const modifiedConfigFile = { + path: configFile.path, + content: JSON.stringify({ compilerOptions: {}, files: ["f1.ts", "f2.ts"] }) + }; + + host.reloadFS([file1, file2, modifiedConfigFile]); + host.checkTimeoutQueueLengthAndRun(1); + checkProgramRootFiles(watch(), [file1.path, file2.path]); + checkProgramActualFiles(watch(), [file1.path, file2.path]); + }); + + it("can update configured project when set of root files was not changed", () => { + const file1 = { + path: "/a/b/f1.ts", + content: "let x = 1" + }; + const file2 = { + path: "/a/b/f2.ts", + content: "let y = 1" + }; + const configFile = { + path: "/a/b/tsconfig.json", + content: JSON.stringify({ compilerOptions: {}, files: ["f1.ts", "f2.ts"] }) + }; + + const host = createWatchedSystem([file1, file2, configFile]); + const watch = createWatchWithConfig(configFile.path, host); + checkProgramActualFiles(watch(), [file1.path, file2.path]); + + const modifiedConfigFile = { + path: configFile.path, + content: JSON.stringify({ compilerOptions: { outFile: "out.js" }, files: ["f1.ts", "f2.ts"] }) + }; + + host.reloadFS([file1, file2, modifiedConfigFile]); + host.checkTimeoutQueueLengthAndRun(1); + checkProgramRootFiles(watch(), [file1.path, file2.path]); + checkProgramActualFiles(watch(), [file1.path, file2.path]); + }); + + it("config file is deleted", () => { + const file1 = { + path: "/a/b/f1.ts", + content: "let x = 1;" + }; + const file2 = { + path: "/a/b/f2.ts", + content: "let y = 2;" + }; + const config = { + path: "/a/b/tsconfig.json", + content: JSON.stringify({ compilerOptions: {} }) + }; + const host = createWatchedSystem([file1, file2, config]); + const watch = createWatchWithConfig(config.path, host); + + checkProgramActualFiles(watch(), [file1.path, file2.path]); + + host.clearOutput(); + host.reloadFS([file1, file2]); + host.checkTimeoutQueueLengthAndRun(1); + + assert.equal(host.exitCode, ExitStatus.DiagnosticsPresent_OutputsSkipped); + checkOutputContains(host, [`error TS6053: File '${config.path}' not found.${host.newLine}`]); + }); + + it("Proper errors: document is not contained in project", () => { + const file1 = { + path: "/a/b/app.ts", + content: "" + }; + const corruptedConfig = { + path: "/a/b/tsconfig.json", + content: "{" + }; + const host = createWatchedSystem([file1, corruptedConfig]); + const watch = createWatchWithConfig(corruptedConfig.path, host); + + checkProgramActualFiles(watch(), [file1.path]); + }); + + it("correctly handles changes in lib section of config file", () => { + const libES5 = { + path: "/compiler/lib.es5.d.ts", + content: "declare const eval: any" + }; + const libES2015Promise = { + path: "/compiler/lib.es2015.promise.d.ts", + content: "declare class Promise {}" + }; + const app = { + path: "/src/app.ts", + content: "var x: Promise;" + }; + const config1 = { + path: "/src/tsconfig.json", + content: JSON.stringify( + { + "compilerOptions": { + "module": "commonjs", + "target": "es5", + "noImplicitAny": true, + "sourceMap": false, + "lib": [ + "es5" + ] + } + }) + }; + const config2 = { + path: config1.path, + content: JSON.stringify( + { + "compilerOptions": { + "module": "commonjs", + "target": "es5", + "noImplicitAny": true, + "sourceMap": false, + "lib": [ + "es5", + "es2015.promise" + ] + } + }) + }; + const host = createWatchedSystem([libES5, libES2015Promise, app, config1], { executingFilePath: "/compiler/tsc.js" }); + const watch = createWatchWithConfig(config1.path, host); + + checkProgramActualFiles(watch(), [libES5.path, app.path]); + + host.reloadFS([libES5, libES2015Promise, app, config2]); + host.checkTimeoutQueueLengthAndRun(1); + checkProgramActualFiles(watch(), [libES5.path, libES2015Promise.path, app.path]); + }); + + it("should handle non-existing directories in config file", () => { + const f = { + path: "/a/src/app.ts", + content: "let x = 1;" + }; + const config = { + path: "/a/tsconfig.json", + content: JSON.stringify({ + compilerOptions: {}, + include: [ + "src/**/*", + "notexistingfolder/*" + ] + }) + }; + const host = createWatchedSystem([f, config]); + const watch = createWatchWithConfig(config.path, host); + checkProgramActualFiles(watch(), [f.path]); + }); + + it("rename a module file and rename back should restore the states for inferred projects", () => { + const moduleFile = { + path: "/a/b/moduleFile.ts", + content: "export function bar() { };" + }; + const file1 = { + path: "/a/b/file1.ts", + content: "import * as T from './moduleFile'; T.bar();" + }; + const host = createWatchedSystem([moduleFile, file1, libFile]); + createWatchModeWithoutConfigFile([file1.path], {}, host); + const error = "a/b/file1.ts(1,20): error TS2307: Cannot find module \'./moduleFile\'.\n"; + checkOutputDoesNotContain(host, [error]); + + const moduleFileOldPath = moduleFile.path; + const moduleFileNewPath = "/a/b/moduleFile1.ts"; + moduleFile.path = moduleFileNewPath; + host.reloadFS([moduleFile, file1, libFile]); + host.runQueuedTimeoutCallbacks(); + checkOutputContains(host, [error]); + + host.clearOutput(); + moduleFile.path = moduleFileOldPath; + host.reloadFS([moduleFile, file1, libFile]); + host.runQueuedTimeoutCallbacks(); + checkOutputDoesNotContain(host, [error]); + }); + + it("rename a module file and rename back should restore the states for configured projects", () => { + const moduleFile = { + path: "/a/b/moduleFile.ts", + content: "export function bar() { };" + }; + const file1 = { + path: "/a/b/file1.ts", + content: "import * as T from './moduleFile'; T.bar();" + }; + const configFile = { + path: "/a/b/tsconfig.json", + content: `{}` + }; + const host = createWatchedSystem([moduleFile, file1, configFile, libFile]); + createWatchWithConfig(configFile.path, host); + + const error = `error TS6053: File '${moduleFile.path}' not found.${host.newLine}`; + checkOutputDoesNotContain(host, [error]); + + const moduleFileOldPath = moduleFile.path; + const moduleFileNewPath = "/a/b/moduleFile1.ts"; + moduleFile.path = moduleFileNewPath; + host.reloadFS([moduleFile, file1, configFile, libFile]); + host.runQueuedTimeoutCallbacks(); + checkOutputContains(host, [error]); + + host.clearOutput(); + moduleFile.path = moduleFileOldPath; + host.reloadFS([moduleFile, file1, configFile, libFile]); + host.runQueuedTimeoutCallbacks(); + checkOutputDoesNotContain(host, [error]); + }); + + it("types should load from config file path if config exists", () => { + const f1 = { + path: "/a/b/app.ts", + content: "let x = 1" + }; + const config = { + path: "/a/b/tsconfig.json", + content: JSON.stringify({ compilerOptions: { types: ["node"], typeRoots: [] } }) + }; + const node = { + path: "/a/b/node_modules/@types/node/index.d.ts", + content: "declare var process: any" + }; + const cwd = { + path: "/a/c" + }; + const host = createWatchedSystem([f1, config, node, cwd], { currentDirectory: cwd.path }); + const watch = createWatchWithConfig(config.path, host); + + checkProgramActualFiles(watch(), [f1.path, node.path]); + }); + + it("add the missing module file for inferred project: should remove the `module not found` error", () => { + const moduleFile = { + path: "/a/b/moduleFile.ts", + content: "export function bar() { };" + }; + const file1 = { + path: "/a/b/file1.ts", + content: "import * as T from './moduleFile'; T.bar();" + }; + const host = createWatchedSystem([file1, libFile]); + createWatchModeWithoutConfigFile([file1.path], {}, host); + + const error = `a/b/file1.ts(1,20): error TS2307: Cannot find module \'./moduleFile\'.${host.newLine}`; + checkOutputContains(host, [error]); + host.clearOutput(); + + host.reloadFS([file1, moduleFile, libFile]); + host.runQueuedTimeoutCallbacks(); + checkOutputDoesNotContain(host, [error]); + }); + + it("Configure file diagnostics events are generated when the config file has errors", () => { + const file = { + path: "/a/b/app.ts", + content: "let x = 10" + }; + const configFile = { + path: "/a/b/tsconfig.json", + content: `{ + "compilerOptions": { + "foo": "bar", + "allowJS": true + } + }` + }; + + const host = createWatchedSystem([file, configFile, libFile]); + createWatchWithConfig(configFile.path, host); + checkOutputContains(host, [ + `a/b/tsconfig.json(3,29): error TS5023: Unknown compiler option \'foo\'.${host.newLine}`, + `a/b/tsconfig.json(4,29): error TS5023: Unknown compiler option \'allowJS\'.${host.newLine}` + ]); + }); + + it("Configure file diagnostics events are generated when the config file doesn't have errors", () => { + const file = { + path: "/a/b/app.ts", + content: "let x = 10" + }; + const configFile = { + path: "/a/b/tsconfig.json", + content: `{ + "compilerOptions": {} + }` + }; + + const host = createWatchedSystem([file, configFile, libFile]); + createWatchWithConfig(configFile.path, host); + checkOutputDoesNotContain(host, [ + `a/b/tsconfig.json(3,29): error TS5023: Unknown compiler option \'foo\'.${host.newLine}`, + `a/b/tsconfig.json(4,29): error TS5023: Unknown compiler option \'allowJS\'.${host.newLine}` + ]); + }); + + it("Configure file diagnostics events are generated when the config file changes", () => { + const file = { + path: "/a/b/app.ts", + content: "let x = 10" + }; + const configFile = { + path: "/a/b/tsconfig.json", + content: `{ + "compilerOptions": {} + }` + }; + + const host = createWatchedSystem([file, configFile, libFile]); + createWatchWithConfig(configFile.path, host); + const error = `a/b/tsconfig.json(3,25): error TS5023: Unknown compiler option 'haha'.${host.newLine}`; + checkOutputDoesNotContain(host, [error]); + + configFile.content = `{ + "compilerOptions": { + "haha": 123 + } + }`; + host.reloadFS([file, configFile, libFile]); + host.runQueuedTimeoutCallbacks(); + checkOutputContains(host, [error]); + + host.clearOutput(); + configFile.content = `{ + "compilerOptions": {} + }`; + host.reloadFS([file, configFile, libFile]); + host.runQueuedTimeoutCallbacks(); + checkOutputDoesNotContain(host, [error]); + }); + + it("non-existing directories listed in config file input array should be tolerated without crashing the server", () => { + const configFile = { + path: "/a/b/tsconfig.json", + content: `{ + "compilerOptions": {}, + "include": ["app/*", "test/**/*", "something"] + }` + }; + const file1 = { + path: "/a/b/file1.ts", + content: "let t = 10;" + }; + + const host = createWatchedSystem([file1, configFile, libFile]); + const watch = createWatchWithConfig(configFile.path, host); + + checkProgramActualFiles(watch(), [libFile.path]); + }); + + it("non-existing directories listed in config file input array should be able to handle @types if input file list is empty", () => { + const f = { + path: "/a/app.ts", + content: "let x = 1" + }; + const config = { + path: "/a/tsconfig.json", + content: JSON.stringify({ + compiler: {}, + files: [] + }) + }; + const t1 = { + path: "/a/node_modules/@types/typings/index.d.ts", + content: `export * from "./lib"` + }; + const t2 = { + path: "/a/node_modules/@types/typings/lib.d.ts", + content: `export const x: number` + }; + const host = createWatchedSystem([f, config, t1, t2], { currentDirectory: getDirectoryPath(f.path) }); + const watch = createWatchWithConfig(config.path, host); + + checkProgramActualFiles(watch(), [t1.path, t2.path]); + }); + + it("should support files without extensions", () => { + const f = { + path: "/a/compile", + content: "let x = 1" + }; + const host = createWatchedSystem([f, libFile]); + const watch = createWatchModeWithoutConfigFile([f.path], { allowNonTsExtensions: true }, host); + checkProgramActualFiles(watch(), [f.path, libFile.path]); + }); + + it("Options Diagnostic locations reported correctly with changes in configFile contents when options change", () => { + const file = { + path: "/a/b/app.ts", + content: "let x = 10" + }; + const configFileContentBeforeComment = `{`; + const configFileContentComment = ` + // comment + // More comment`; + const configFileContentAfterComment = ` + "compilerOptions": { + "allowJs": true, + "declaration": true + } + }`; + const configFileContentWithComment = configFileContentBeforeComment + configFileContentComment + configFileContentAfterComment; + const configFileContentWithoutCommentLine = configFileContentBeforeComment + configFileContentAfterComment; + + const line = 5; + const errors = (line: number) => [ + `a/b/tsconfig.json(${line},25): error TS5053: Option \'allowJs\' cannot be specified with option \'declaration\'.\n`, + `a/b/tsconfig.json(${line + 1},25): error TS5053: Option \'allowJs\' cannot be specified with option \'declaration\'.\n` + ]; + + const configFile = { + path: "/a/b/tsconfig.json", + content: configFileContentWithComment + }; + + const host = createWatchedSystem([file, libFile, configFile]); + createWatchWithConfig(configFile.path, host); + checkOutputContains(host, errors(line)); + checkOutputDoesNotContain(host, errors(line - 2)); + host.clearOutput(); + + configFile.content = configFileContentWithoutCommentLine; + host.reloadFS([file, configFile]); + host.runQueuedTimeoutCallbacks(); + checkOutputContains(host, errors(line - 2)); + checkOutputDoesNotContain(host, errors(line)); + }); + }); + + describe("tsc-watch emit", () => { + it("emit with outFile or out setting projectUsesOutFile should not be returned if not set", () => { + const f1 = { + path: "/a/a.ts", + content: "let x = 1" + }; + const f2 = { + path: "/a/b.ts", + content: "let y = 1" + }; + const config = { + path: "/a/tsconfig.json", + content: JSON.stringify({ + compilerOptions: { listEmittedFiles: true } + }) + }; + const files = [f1, f2, config, libFile]; + const host = createWatchedSystem([f1, f2, config, libFile]); + createWatchWithConfig(config.path, host); + const emittedF1 = `TSFILE: ${f1.path.replace(".ts", ".js")}${host.newLine}`; + const emittedF2 = `TSFILE: ${f2.path.replace(".ts", ".js")}${host.newLine}`; + checkOutputContains(host, [emittedF1, emittedF2]); + host.clearOutput(); + + f1.content = "let x = 11"; + host.reloadFS(files); + host.runQueuedTimeoutCallbacks(); + checkOutputContains(host, [emittedF1]); + checkOutputDoesNotContain(host, [emittedF2]); + }); + + it("emit with outFile or out setting projectUsesOutFile should be true if out is set", () => { + const outJS = "/a/out.js"; + const f1 = { + path: "/a/a.ts", + content: "let x = 1" + }; + const f2 = { + path: "/a/b.ts", + content: "let y = 1" + }; + const config = { + path: "/a/tsconfig.json", + content: JSON.stringify({ + compilerOptions: { listEmittedFiles: true, out: outJS } + }) + }; + const files = [f1, f2, config, libFile]; + const host = createWatchedSystem([f1, f2, config, libFile]); + createWatchWithConfig(config.path, host); + const emittedF1 = `TSFILE: ${outJS}${host.newLine}`; + checkOutputContains(host, [emittedF1]); + host.clearOutput(); + + f1.content = "let x = 11"; + host.reloadFS(files); + host.runQueuedTimeoutCallbacks(); + checkOutputContains(host, [emittedF1]); + }); + + it("emit with outFile or out setting projectUsesOutFile should be true if outFile is set", () => { + const outJs = "/a/out.js"; + const f1 = { + path: "/a/a.ts", + content: "let x = 1" + }; + const f2 = { + path: "/a/b.ts", + content: "let y = 1" + }; + const config = { + path: "/a/tsconfig.json", + content: JSON.stringify({ + compilerOptions: { listEmittedFiles: true, outFile: outJs } + }) + }; + const files = [f1, f2, config, libFile]; + const host = createWatchedSystem([f1, f2, config, libFile]); + createWatchWithConfig(config.path, host); + const emittedF1 = `TSFILE: ${outJs}${host.newLine}`; + checkOutputContains(host, [emittedF1]); + host.clearOutput(); + + f1.content = "let x = 11"; + host.reloadFS(files); + host.runQueuedTimeoutCallbacks(); + checkOutputContains(host, [emittedF1]); + }); + }); +} diff --git a/src/harness/virtualFileSystemWithWatch.ts b/src/harness/virtualFileSystemWithWatch.ts index f37d685851c3c..45c0554bd9c7e 100644 --- a/src/harness/virtualFileSystemWithWatch.ts +++ b/src/harness/virtualFileSystemWithWatch.ts @@ -30,6 +30,19 @@ namespace ts.TestFSWithWatch { newLine?: string; } + export function createWatchedSystem(fileOrFolderList: FileOrFolder[], params?: TestServerHostCreationParameters): TestServerHost { + if (!params) { + params = {}; + } + const host = new TestServerHost(/*withSafelist*/ false, + params.useCaseSensitiveFileNames !== undefined ? params.useCaseSensitiveFileNames : false, + params.executingFilePath || getExecutingFilePathFromLibFile(), + params.currentDirectory || "/", + fileOrFolderList, + params.newLine); + return host; + } + export function createServerHost(fileOrFolderList: FileOrFolder[], params?: TestServerHostCreationParameters): TestServerHost { if (!params) { params = {}; @@ -112,6 +125,23 @@ namespace ts.TestFSWithWatch { checkMapKeys("watchedDirectories", recursive ? host.watchedDirectoriesRecursive : host.watchedDirectories, expectedDirectories); } + export function checkOutputContains(host: TestServerHost, expected: string[] | ReadonlyArray) { + const mapExpected = arrayToSet(expected); + for (const f of host.getOutput()) { + if (mapExpected.has(f)) { + mapExpected.delete(f); + } + } + assert.equal(mapExpected.size, 0, `Output has missing ${JSON.stringify(flatMapIter(mapExpected.keys(), key => key))} in ${JSON.stringify(host.getOutput())}`); + } + + export function checkOutputDoesNotContain(host: TestServerHost, expectedToBeAbsent: string[] | ReadonlyArray) { + const mapExpectedToBeAbsent = arrayToSet(expectedToBeAbsent); + for (const f of host.getOutput()) { + assert.isFalse(mapExpectedToBeAbsent.has(f), `Contains ${f} in ${JSON.stringify(host.getOutput())}`); + } + } + export class Callbacks { private map: TimeOutCallback[] = []; private nextId = 1; @@ -172,7 +202,7 @@ namespace ts.TestFSWithWatch { this.reloadFS(fileOrFolderList); } - private toFullPath(s: string) { + toFullPath(s: string) { const fullPath = getNormalizedAbsolutePath(s, this.currentDirectory); return this.toPath(fullPath); } @@ -345,6 +375,11 @@ namespace ts.TestFSWithWatch { return isFile(this.fs.get(path)); } + readFile(s: string) { + const fsEntry = this.fs.get(this.toFullPath(s)); + return isFile(fsEntry) ? fsEntry.content : undefined; + } + getFileSize(s: string) { const path = this.toFullPath(s); const entry = this.fs.get(path); @@ -432,7 +467,15 @@ namespace ts.TestFSWithWatch { } runQueuedTimeoutCallbacks() { - this.timeoutCallbacks.invoke(); + try { + this.timeoutCallbacks.invoke(); + } + catch (e) { + if (e.message === this.existMessage) { + return; + } + throw e; + } } runQueuedImmediateCallbacks() { @@ -482,11 +525,15 @@ namespace ts.TestFSWithWatch { clear(this.output); } - readonly readFile = (s: string) => (this.fs.get(this.toFullPath(s))).content; + readonly existMessage = "System Exit"; + exitCode: number; readonly resolvePath = (s: string) => s; readonly getExecutingFilePath = () => this.executingFilePath; readonly getCurrentDirectory = () => this.currentDirectory; - readonly exit = notImplemented; + exit(exitCode?: number) { + this.exitCode = exitCode; + throw new Error(this.existMessage); + } readonly getEnvironmentVariable = notImplemented; } } diff --git a/src/services/services.ts b/src/services/services.ts index 091364b5c50ee..8fe158a47984e 100644 --- a/src/services/services.ts +++ b/src/services/services.ts @@ -1116,7 +1116,7 @@ namespace ts { let hostCache = new HostCache(host, getCanonicalFileName); const rootFileNames = hostCache.getRootFileNames(); // If the program is already up-to-date, we can reuse it - if (isProgramUptoDate(program, rootFileNames, hostCache.compilationSettings(), (path) => hostCache.getVersion(path))) { + if (isProgramUptoDate(program, rootFileNames, hostCache.compilationSettings(), path => hostCache.getVersion(path), fileExists)) { return; } @@ -1139,14 +1139,7 @@ namespace ts { getDefaultLibFileName: (options) => host.getDefaultLibFileName(options), writeFile: noop, getCurrentDirectory: () => currentDirectory, - fileExists: (fileName): boolean => { - // stub missing host functionality - const path = toPath(fileName, currentDirectory, getCanonicalFileName); - const entry = hostCache.getEntryByPath(path); - return entry ? - !isString(entry) : - (host.fileExists && host.fileExists(fileName)); - }, + fileExists, readFile(fileName) { // stub missing host functionality const path = toPath(fileName, currentDirectory, getCanonicalFileName); @@ -1189,6 +1182,14 @@ namespace ts { program.getTypeChecker(); return; + function fileExists(fileName: string) { + const path = toPath(fileName, currentDirectory, getCanonicalFileName); + const entry = hostCache.getEntryByPath(path); + return entry ? + !isString(entry) : + (host.fileExists && host.fileExists(fileName)); + } + // Release any files we have acquired in the old program but are // not part of the new program. function onReleaseOldSourceFile(oldSourceFile: SourceFile, oldOptions: CompilerOptions) { From 2dd6aed654f4dc8f0a5bba326ef6764030a12683 Mon Sep 17 00:00:00 2001 From: Sheetal Nandi Date: Fri, 28 Jul 2017 19:34:20 -0700 Subject: [PATCH 040/109] Emit tests --- src/compiler/builder.ts | 20 +- src/harness/unittests/tscWatchMode.ts | 510 +++++++++++++++++++--- src/harness/virtualFileSystemWithWatch.ts | 5 +- 3 files changed, 460 insertions(+), 75 deletions(-) diff --git a/src/compiler/builder.ts b/src/compiler/builder.ts index 5661270e8f723..c507b1570a85e 100644 --- a/src/compiler/builder.ts +++ b/src/compiler/builder.ts @@ -294,9 +294,11 @@ namespace ts { existingMap = mutateExistingMapWithNewSet( existingMap, getReferencedFiles(program, sourceFile), - // Creating new Reference: Also add referenced by + // Creating new Reference: as sourceFile references file with path 'key' + // in other words source file (path) is referenced by 'key' key => { referencedBy.add(key, path); return true; }, - // Remove existing reference + // Remove existing reference by entry: source file doesnt reference file 'key' any more + // in other words source file (path) is not referenced by 'key' (key, _existingValue) => { referencedBy.remove(key, path); } ); references.set(path, existingMap); @@ -338,28 +340,28 @@ namespace ts { // Because if so, its own referencedBy files need to be saved as well to make the // emitting result consistent with files on disk. - const fileNamesMap = createMap(); - const setFileName = (path: Path, sourceFile: SourceFile) => { - fileNamesMap.set(path, sourceFile && shouldEmitFile(sourceFile) ? sourceFile.fileName : undefined); + const seenFileNamesMap = createMap(); + const setSeenFileName = (path: Path, sourceFile: SourceFile) => { + seenFileNamesMap.set(path, sourceFile && shouldEmitFile(sourceFile) ? sourceFile.fileName : undefined); }; // Start with the paths this file was referenced by const path = sourceFile.path; - setFileName(path, sourceFile); + setSeenFileName(path, sourceFile); const queue = getReferencedByPaths(path).slice(); while (queue.length > 0) { const currentPath = queue.pop(); - if (!fileNamesMap.has(currentPath)) { + if (!seenFileNamesMap.has(currentPath)) { const currentSourceFile = program.getSourceFileByPath(currentPath); if (currentSourceFile && updateShapeSignature(program, currentSourceFile)) { queue.push(...getReferencedByPaths(currentPath)); } - setFileName(currentPath, currentSourceFile); + setSeenFileName(currentPath, currentSourceFile); } } // Return array of values that needs emit - return flatMapIter(fileNamesMap.values(), value => value); + return flatMapIter(seenFileNamesMap.values(), value => value); } } } diff --git a/src/harness/unittests/tscWatchMode.ts b/src/harness/unittests/tscWatchMode.ts index 630d9a71d201a..1025e250d2780 100644 --- a/src/harness/unittests/tscWatchMode.ts +++ b/src/harness/unittests/tscWatchMode.ts @@ -31,6 +31,45 @@ namespace ts.tscWatch { return createWatchModeWithConfigFile(configFileResult, {}, host); } + function getEmittedLineForMultiFileOutput(file: FileOrFolder, host: WatchedSystem) { + return `TSFILE: ${file.path.replace(".ts", ".js")}${host.newLine}`; + } + + function getEmittedLineForSingleFileOutput(filename: string, host: WatchedSystem) { + return `TSFILE: ${filename}${host.newLine}`; + } + + interface FileOrFolderEmit extends FileOrFolder { + output?: string; + } + + function getFileOrFolderEmit(file: FileOrFolder, getOutput?: (file: FileOrFolder) => string): FileOrFolderEmit { + const result = file as FileOrFolderEmit; + if (getOutput) { + result.output = getOutput(file); + } + return result; + } + + function getEmittedLines(files: FileOrFolderEmit[]) { + const seen = createMap(); + const result: string[] = []; + for (const { output} of files) { + if (output && !seen.has(output)) { + seen.set(output, true); + result.push(output); + } + } + return result; + } + + function checkAffectedLines(host: WatchedSystem, affectedFiles: FileOrFolderEmit[], allEmittedFiles: string[]) { + const expectedAffectedFiles = getEmittedLines(affectedFiles); + const expectedNonAffectedFiles = mapDefined(allEmittedFiles, line => contains(expectedAffectedFiles, line) ? undefined : line); + checkOutputContains(host, expectedAffectedFiles); + checkOutputDoesNotContain(host, expectedNonAffectedFiles); + } + describe("tsc-watch program updates", () => { const commonFile1: FileOrFolder = { path: "/a/b/commonFile1.ts", @@ -908,93 +947,434 @@ namespace ts.tscWatch { }); }); - describe("tsc-watch emit", () => { - it("emit with outFile or out setting projectUsesOutFile should not be returned if not set", () => { - const f1 = { - path: "/a/a.ts", - content: "let x = 1" - }; - const f2 = { - path: "/a/b.ts", - content: "let y = 1" - }; - const config = { + describe("tsc-watch emit with outFile or out setting", () => { + function createWatchForOut(out?: string, outFile?: string) { + const host = createWatchedSystem([]); + const config: FileOrFolderEmit = { path: "/a/tsconfig.json", content: JSON.stringify({ compilerOptions: { listEmittedFiles: true } }) }; - const files = [f1, f2, config, libFile]; - const host = createWatchedSystem([f1, f2, config, libFile]); - createWatchWithConfig(config.path, host); - const emittedF1 = `TSFILE: ${f1.path.replace(".ts", ".js")}${host.newLine}`; - const emittedF2 = `TSFILE: ${f2.path.replace(".ts", ".js")}${host.newLine}`; - checkOutputContains(host, [emittedF1, emittedF2]); - host.clearOutput(); - f1.content = "let x = 11"; - host.reloadFS(files); - host.runQueuedTimeoutCallbacks(); - checkOutputContains(host, [emittedF1]); - checkOutputDoesNotContain(host, [emittedF2]); - }); - - it("emit with outFile or out setting projectUsesOutFile should be true if out is set", () => { - const outJS = "/a/out.js"; - const f1 = { + let getOutput: (file: FileOrFolder) => string; + if (out) { + config.content = JSON.stringify({ + compilerOptions: { listEmittedFiles: true, out } + }); + getOutput = __ => getEmittedLineForSingleFileOutput(out, host); + } + else if (outFile) { + config.content = JSON.stringify({ + compilerOptions: { listEmittedFiles: true, outFile } + }); + getOutput = __ => getEmittedLineForSingleFileOutput(outFile, host); + } + else { + getOutput = file => getEmittedLineForMultiFileOutput(file, host); + } + + const f1 = getFileOrFolderEmit({ path: "/a/a.ts", content: "let x = 1" - }; - const f2 = { + }, getOutput); + const f2 = getFileOrFolderEmit({ path: "/a/b.ts", content: "let y = 1" - }; - const config = { - path: "/a/tsconfig.json", - content: JSON.stringify({ - compilerOptions: { listEmittedFiles: true, out: outJS } - }) - }; + }, getOutput); + const files = [f1, f2, config, libFile]; - const host = createWatchedSystem([f1, f2, config, libFile]); + host.reloadFS(files); createWatchWithConfig(config.path, host); - const emittedF1 = `TSFILE: ${outJS}${host.newLine}`; - checkOutputContains(host, [emittedF1]); + + const allEmittedLines = getEmittedLines(files); + checkOutputContains(host, allEmittedLines); host.clearOutput(); f1.content = "let x = 11"; host.reloadFS(files); host.runQueuedTimeoutCallbacks(); - checkOutputContains(host, [emittedF1]); + checkAffectedLines(host, [f1], allEmittedLines); + } + + it("projectUsesOutFile should not be returned if not set", () => { + createWatchForOut(); }); - it("emit with outFile or out setting projectUsesOutFile should be true if outFile is set", () => { + it("projectUsesOutFile should be true if out is set", () => { const outJs = "/a/out.js"; - const f1 = { - path: "/a/a.ts", - content: "let x = 1" - }; - const f2 = { - path: "/a/b.ts", - content: "let y = 1" - }; - const config = { - path: "/a/tsconfig.json", - content: JSON.stringify({ - compilerOptions: { listEmittedFiles: true, outFile: outJs } - }) - }; - const files = [f1, f2, config, libFile]; - const host = createWatchedSystem([f1, f2, config, libFile]); - createWatchWithConfig(config.path, host); - const emittedF1 = `TSFILE: ${outJs}${host.newLine}`; - checkOutputContains(host, [emittedF1]); - host.clearOutput(); + createWatchForOut(outJs); + }); - f1.content = "let x = 11"; + it("projectUsesOutFile should be true if outFile is set", () => { + const outJs = "/a/out.js"; + createWatchForOut(/*out*/ undefined, outJs); + }); + }); + + describe("tsc-watch emit for configured projects", () => { + const file1Consumer1Path = "/a/b/file1Consumer1.ts"; + const moduleFile1Path = "/a/b/moduleFile1.ts"; + function getInitialState(configObj: any = {}, fileNames?: string[]) { + const host = createWatchedSystem([]); + const getOutputName = (file: FileOrFolder) => getEmittedLineForMultiFileOutput(file, host); + const moduleFile1 = getFileOrFolderEmit({ + path: moduleFile1Path, + content: "export function Foo() { };", + }, getOutputName); + + const file1Consumer1 = getFileOrFolderEmit({ + path: file1Consumer1Path, + content: `import {Foo} from "./moduleFile1"; export var y = 10;`, + }, getOutputName); + + const file1Consumer2 = getFileOrFolderEmit({ + path: "/a/b/file1Consumer2.ts", + content: `import {Foo} from "./moduleFile1"; let z = 10;`, + }, getOutputName); + + const moduleFile2 = getFileOrFolderEmit({ + path: "/a/b/moduleFile2.ts", + content: `export var Foo4 = 10;`, + }, getOutputName); + + const globalFile3 = getFileOrFolderEmit({ + path: "/a/b/globalFile3.ts", + content: `interface GlobalFoo { age: number }` + }); + + (configObj.compilerOptions || (configObj.compilerOptions = {})).listEmittedFiles = true; + const configFile = getFileOrFolderEmit({ + path: "/a/b/tsconfig.json", + content: JSON.stringify(configObj) + }); + + const files = [moduleFile1, file1Consumer1, file1Consumer2, globalFile3, moduleFile2, configFile, libFile]; + let allEmittedFiles = getEmittedLines(files); host.reloadFS(files); - host.runQueuedTimeoutCallbacks(); - checkOutputContains(host, [emittedF1]); + + // Initial compile + createWatchWithConfig(configFile.path, host); + if (fileNames) { + checkAffectedLines(host, map(fileNames, name => find(files, file => file.path === name)), allEmittedFiles); + } + else { + checkOutputContains(host, allEmittedFiles); + } + host.clearOutput(); + + return { + moduleFile1, file1Consumer1, file1Consumer2, moduleFile2, globalFile3, configFile, + files, + verifyAffectedFiles, + verifyAffectedAllFiles, + getOutputName + }; + + function verifyAffectedAllFiles() { + host.reloadFS(files); + host.checkTimeoutQueueLengthAndRun(1); + checkOutputContains(host, allEmittedFiles); + host.clearOutput(); + } + + function verifyAffectedFiles(expected: FileOrFolderEmit[], filesToReload?: FileOrFolderEmit[]) { + if (!filesToReload) { + filesToReload = files; + } + else if (filesToReload.length > files.length) { + allEmittedFiles = getEmittedLines(filesToReload); + } + host.reloadFS(filesToReload); + host.checkTimeoutQueueLengthAndRun(1); + checkAffectedLines(host, expected, allEmittedFiles); + host.clearOutput(); + } + } + + it("should contains only itself if a module file's shape didn't change, and all files referencing it if its shape changed", () => { + const { + moduleFile1, file1Consumer1, file1Consumer2, + verifyAffectedFiles + } = getInitialState(); + + // Change the content of moduleFile1 to `export var T: number;export function Foo() { };` + moduleFile1.content = `export var T: number;export function Foo() { };`; + verifyAffectedFiles([moduleFile1, file1Consumer1, file1Consumer2]); + + // Change the content of moduleFile1 to `export var T: number;export function Foo() { console.log('hi'); };` + moduleFile1.content = `export var T: number;export function Foo() { console.log('hi'); };`; + verifyAffectedFiles([moduleFile1]); }); + + it("should be up-to-date with the reference map changes", () => { + const { + moduleFile1, file1Consumer1, file1Consumer2, + verifyAffectedFiles + } = getInitialState(); + + // Change file1Consumer1 content to `export let y = Foo();` + file1Consumer1.content = `export let y = Foo();`; + verifyAffectedFiles([file1Consumer1]); + + // Change the content of moduleFile1 to `export var T: number;export function Foo() { };` + moduleFile1.content = `export var T: number;export function Foo() { };`; + verifyAffectedFiles([moduleFile1, file1Consumer2]); + + // Add the import statements back to file1Consumer1 + file1Consumer1.content = `import {Foo} from "./moduleFile1";let y = Foo();`; + verifyAffectedFiles([file1Consumer1]); + + // Change the content of moduleFile1 to `export var T: number;export var T2: string;export function Foo() { };` + moduleFile1.content = `export var T: number;export var T2: string;export function Foo() { };`; + verifyAffectedFiles([moduleFile1, file1Consumer2, file1Consumer1]); + + // Multiple file edits in one go: + + // Change file1Consumer1 content to `export let y = Foo();` + // Change the content of moduleFile1 to `export var T: number;export function Foo() { };` + file1Consumer1.content = `export let y = Foo();`; + moduleFile1.content = `export var T: number;export function Foo() { };`; + verifyAffectedFiles([moduleFile1, file1Consumer1, file1Consumer2]); + }); + + it("should be up-to-date with deleted files", () => { + const { + moduleFile1, file1Consumer1, file1Consumer2, + files, + verifyAffectedFiles + } = getInitialState(); + + // Change the content of moduleFile1 to `export var T: number;export function Foo() { };` + moduleFile1.content = `export var T: number;export function Foo() { };`; + + // Delete file1Consumer2 + const filesToLoad = mapDefined(files, file => file === file1Consumer2 ? undefined : file); + verifyAffectedFiles([moduleFile1, file1Consumer1], filesToLoad); + }); + + it("should be up-to-date with newly created files", () => { + const { + moduleFile1, file1Consumer1, file1Consumer2, + files, + verifyAffectedFiles, + getOutputName + } = getInitialState(); + + const file1Consumer3 = getFileOrFolderEmit({ + path: "/a/b/file1Consumer3.ts", + content: `import {Foo} from "./moduleFile1"; let y = Foo();` + }, getOutputName); + moduleFile1.content = `export var T: number;export function Foo() { };`; + verifyAffectedFiles([moduleFile1, file1Consumer1, file1Consumer3, file1Consumer2], files.concat(file1Consumer3)); + }); + + it("should detect changes in non-root files", () => { + const { + moduleFile1, file1Consumer1, + verifyAffectedFiles, + } = getInitialState({ files: [file1Consumer1Path] }, [file1Consumer1Path, moduleFile1Path]); + + moduleFile1.content = `export var T: number;export function Foo() { };`; + verifyAffectedFiles([moduleFile1, file1Consumer1]); + + // change file1 internal, and verify only file1 is affected + moduleFile1.content += "var T1: number;"; + verifyAffectedFiles([moduleFile1]); + }); + + it("should return all files if a global file changed shape", () => { + const { + globalFile3, verifyAffectedAllFiles + } = getInitialState(); + + globalFile3.content += "var T2: string;"; + verifyAffectedAllFiles(); + }); + + //it("should save with base tsconfig.json", () => { + // configFile = { + // path: "/a/b/tsconfig.json", + // content: `{ + // "extends": "/a/tsconfig.json" + // }` + // }; + + // const configFile2: FileOrFolder = { + // path: "/a/tsconfig.json", + // content: `{ + // "compileOnSave": true + // }` + // }; + + // const host = createServerHost([moduleFile1, file1Consumer1, file1Consumer2, configFile2, configFile, libFile]); + // const typingsInstaller = createTestTypingsInstaller(host); + // const session = createSession(host, typingsInstaller); + + // openFilesForSession([moduleFile1, file1Consumer1], session); + // sendAffectedFileRequestAndCheckResult(session, moduleFile1FileListRequest, [{ projectFileName: configFile.path, files: [moduleFile1, file1Consumer1, file1Consumer2] }]); + //}); + + //it("should always return the file itself if '--isolatedModules' is specified", () => { + // configFile = { + // path: "/a/b/tsconfig.json", + // content: `{ + // "compileOnSave": true, + // "compilerOptions": { + // "isolatedModules": true + // } + // }` + // }; + + // const host = createServerHost([moduleFile1, file1Consumer1, configFile, libFile]); + // const typingsInstaller = createTestTypingsInstaller(host); + // const session = createSession(host, typingsInstaller); + // openFilesForSession([moduleFile1], session); + + // const file1ChangeShapeRequest = makeSessionRequest(CommandNames.Change, { + // file: moduleFile1.path, + // line: 1, + // offset: 27, + // endLine: 1, + // endOffset: 27, + // insertString: `Point,` + // }); + // session.executeCommand(file1ChangeShapeRequest); + // sendAffectedFileRequestAndCheckResult(session, moduleFile1FileListRequest, [{ projectFileName: configFile.path, files: [moduleFile1] }]); + //}); + + //it("should always return the file itself if '--out' or '--outFile' is specified", () => { + // configFile = { + // path: "/a/b/tsconfig.json", + // content: `{ + // "compileOnSave": true, + // "compilerOptions": { + // "module": "system", + // "outFile": "/a/b/out.js" + // } + // }` + // }; + + // const host = createServerHost([moduleFile1, file1Consumer1, configFile, libFile]); + // const typingsInstaller = createTestTypingsInstaller(host); + // const session = createSession(host, typingsInstaller); + // openFilesForSession([moduleFile1], session); + + // const file1ChangeShapeRequest = makeSessionRequest(CommandNames.Change, { + // file: moduleFile1.path, + // line: 1, + // offset: 27, + // endLine: 1, + // endOffset: 27, + // insertString: `Point,` + // }); + // session.executeCommand(file1ChangeShapeRequest); + // sendAffectedFileRequestAndCheckResult(session, moduleFile1FileListRequest, [{ projectFileName: configFile.path, files: [moduleFile1] }]); + //}); + + //it("should return cascaded affected file list", () => { + // const file1Consumer1Consumer1: FileOrFolder = { + // path: "/a/b/file1Consumer1Consumer1.ts", + // content: `import {y} from "./file1Consumer1";` + // }; + // const host = createServerHost([moduleFile1, file1Consumer1, file1Consumer1Consumer1, globalFile3, configFile, libFile]); + // const typingsInstaller = createTestTypingsInstaller(host); + // const session = createSession(host, typingsInstaller); + + // openFilesForSession([moduleFile1, file1Consumer1], session); + // sendAffectedFileRequestAndCheckResult(session, moduleFile1FileListRequest, [{ projectFileName: configFile.path, files: [moduleFile1, file1Consumer1, file1Consumer1Consumer1] }]); + + // const changeFile1Consumer1ShapeRequest = makeSessionRequest(CommandNames.Change, { + // file: file1Consumer1.path, + // line: 2, + // offset: 1, + // endLine: 2, + // endOffset: 1, + // insertString: `export var T: number;` + // }); + // session.executeCommand(changeModuleFile1ShapeRequest1); + // session.executeCommand(changeFile1Consumer1ShapeRequest); + // sendAffectedFileRequestAndCheckResult(session, moduleFile1FileListRequest, [{ projectFileName: configFile.path, files: [moduleFile1, file1Consumer1, file1Consumer1Consumer1] }]); + //}); + + //it("should work fine for files with circular references", () => { + // const file1: FileOrFolder = { + // path: "/a/b/file1.ts", + // content: ` + // /// + // export var t1 = 10;` + // }; + // const file2: FileOrFolder = { + // path: "/a/b/file2.ts", + // content: ` + // /// + // export var t2 = 10;` + // }; + // const host = createServerHost([file1, file2, configFile]); + // const typingsInstaller = createTestTypingsInstaller(host); + // const session = createSession(host, typingsInstaller); + + // openFilesForSession([file1, file2], session); + // const file1AffectedListRequest = makeSessionRequest(CommandNames.CompileOnSaveAffectedFileList, { file: file1.path }); + // sendAffectedFileRequestAndCheckResult(session, file1AffectedListRequest, [{ projectFileName: configFile.path, files: [file1, file2] }]); + //}); + + //it("should return results for all projects if not specifying projectFileName", () => { + // const file1: FileOrFolder = { path: "/a/b/file1.ts", content: "export var t = 10;" }; + // const file2: FileOrFolder = { path: "/a/b/file2.ts", content: `import {t} from "./file1"; var t2 = 11;` }; + // const file3: FileOrFolder = { path: "/a/c/file2.ts", content: `import {t} from "../b/file1"; var t3 = 11;` }; + // const configFile1: FileOrFolder = { path: "/a/b/tsconfig.json", content: `{ "compileOnSave": true }` }; + // const configFile2: FileOrFolder = { path: "/a/c/tsconfig.json", content: `{ "compileOnSave": true }` }; + + // const host = createServerHost([file1, file2, file3, configFile1, configFile2]); + // const session = createSession(host); + + // openFilesForSession([file1, file2, file3], session); + // const file1AffectedListRequest = makeSessionRequest(CommandNames.CompileOnSaveAffectedFileList, { file: file1.path }); + + // sendAffectedFileRequestAndCheckResult(session, file1AffectedListRequest, [ + // { projectFileName: configFile1.path, files: [file1, file2] }, + // { projectFileName: configFile2.path, files: [file1, file3] } + // ]); + //}); + + //it("should detect removed code file", () => { + // const referenceFile1: FileOrFolder = { + // path: "/a/b/referenceFile1.ts", + // content: ` + // /// + // export var x = Foo();` + // }; + // const host = createServerHost([moduleFile1, referenceFile1, configFile]); + // const session = createSession(host); + + // openFilesForSession([referenceFile1], session); + // host.reloadFS([referenceFile1, configFile]); + + // const request = makeSessionRequest(CommandNames.CompileOnSaveAffectedFileList, { file: referenceFile1.path }); + // sendAffectedFileRequestAndCheckResult(session, request, [ + // { projectFileName: configFile.path, files: [referenceFile1] } + // ]); + // const requestForMissingFile = makeSessionRequest(CommandNames.CompileOnSaveAffectedFileList, { file: moduleFile1.path }); + // sendAffectedFileRequestAndCheckResult(session, requestForMissingFile, []); + //}); + + //it("should detect non-existing code file", () => { + // const referenceFile1: FileOrFolder = { + // path: "/a/b/referenceFile1.ts", + // content: ` + // /// + // export var x = Foo();` + // }; + // const host = createServerHost([referenceFile1, configFile]); + // const session = createSession(host); + + // openFilesForSession([referenceFile1], session); + // const request = makeSessionRequest(CommandNames.CompileOnSaveAffectedFileList, { file: referenceFile1.path }); + // sendAffectedFileRequestAndCheckResult(session, request, [ + // { projectFileName: configFile.path, files: [referenceFile1] } + // ]); + //}); }); + } diff --git a/src/harness/virtualFileSystemWithWatch.ts b/src/harness/virtualFileSystemWithWatch.ts index 45c0554bd9c7e..95ef3266fb153 100644 --- a/src/harness/virtualFileSystemWithWatch.ts +++ b/src/harness/virtualFileSystemWithWatch.ts @@ -125,11 +125,14 @@ namespace ts.TestFSWithWatch { checkMapKeys("watchedDirectories", recursive ? host.watchedDirectoriesRecursive : host.watchedDirectories, expectedDirectories); } - export function checkOutputContains(host: TestServerHost, expected: string[] | ReadonlyArray) { + export function checkOutputContains(host: TestServerHost, expected: ReadonlyArray) { const mapExpected = arrayToSet(expected); + const mapSeen = createMap(); for (const f of host.getOutput()) { + assert.isUndefined(mapSeen.get(f), `Already found ${f} in ${JSON.stringify(host.getOutput())}`); if (mapExpected.has(f)) { mapExpected.delete(f); + mapSeen.set(f, true); } } assert.equal(mapExpected.size, 0, `Output has missing ${JSON.stringify(flatMapIter(mapExpected.keys(), key => key))} in ${JSON.stringify(host.getOutput())}`); From 89c61e797c794515a06a3719ca95e37d97783d6e Mon Sep 17 00:00:00 2001 From: Sheetal Nandi Date: Thu, 3 Aug 2017 00:27:46 -0700 Subject: [PATCH 041/109] Modify the api in builder so that it tracks changed files --- src/compiler/builder.ts | 186 ++++++++------ src/compiler/core.ts | 8 +- src/compiler/tscLib.ts | 74 +++--- src/compiler/utilities.ts | 20 +- src/harness/unittests/tscWatchMode.ts | 354 ++++++++++++-------------- src/server/project.ts | 8 +- src/services/services.ts | 4 +- src/services/types.ts | 2 +- 8 files changed, 336 insertions(+), 320 deletions(-) diff --git a/src/compiler/builder.ts b/src/compiler/builder.ts index c507b1570a85e..489111f26db83 100644 --- a/src/compiler/builder.ts +++ b/src/compiler/builder.ts @@ -18,13 +18,14 @@ namespace ts { text: string; } - export interface Builder { + export interface Builder { /** * This is the callback when file infos in the builder are updated */ onProgramUpdateGraph(program: Program): void; getFilesAffectedBy(program: Program, path: Path): string[]; - emitFile(program: Program, path: Path): T | EmitOutput; + emitFile(program: Program, path: Path): EmitOutput; + emitChangedFiles(program: Program): EmitOutputDetailed[]; clear(): void; } @@ -38,19 +39,7 @@ namespace ts { getFilesAffectedByUpdatedShape(program: Program, sourceFile: SourceFile, singleFileResult: string[]): string[]; } - export function getDetailedEmitOutput(program: Program, sourceFile: SourceFile, emitOnlyDtsFiles: boolean, - cancellationToken ?: CancellationToken, customTransformers ?: CustomTransformers): EmitOutputDetailed { - return getEmitOutput(/*detailed*/ true, program, sourceFile, emitOnlyDtsFiles, - cancellationToken, customTransformers) as EmitOutputDetailed; - } - - export function getFileEmitOutput(program: Program, sourceFile: SourceFile, emitOnlyDtsFiles: boolean, - cancellationToken?: CancellationToken, customTransformers?: CustomTransformers): EmitOutput { - return getEmitOutput(/*detailed*/ false, program, sourceFile, emitOnlyDtsFiles, - cancellationToken, customTransformers); - } - - function getEmitOutput(isDetailed: boolean, program: Program, sourceFile: SourceFile, emitOnlyDtsFiles: boolean, + export function getFileEmitOutput(program: Program, sourceFile: SourceFile, emitOnlyDtsFiles: boolean, isDetailed: boolean, cancellationToken?: CancellationToken, customTransformers?: CustomTransformers): EmitOutput | EmitOutputDetailed { const outputFiles: OutputFile[] = []; let emittedSourceFiles: SourceFile[]; @@ -75,20 +64,23 @@ namespace ts { } } - export function createBuilder( + export function createBuilder( getCanonicalFileName: (fileName: string) => string, - getEmitOutput: (program: Program, sourceFile: SourceFile, emitOnlyDtsFiles?: boolean) => T, + getEmitOutput: (program: Program, sourceFile: SourceFile, emitOnlyDtsFiles: boolean, isDetailed: boolean) => EmitOutput | EmitOutputDetailed, computeHash: (data: string) => string, shouldEmitFile: (sourceFile: SourceFile) => boolean - ): Builder { + ): Builder { let isModuleEmit: boolean | undefined; // Last checked shape signature for the file info - let fileInfos: Map; + type FileInfo = { version: string; signature: string; }; + let fileInfos: Map; + let changedFilesSinceLastEmit: Map; let emitHandler: EmitHandler; return { onProgramUpdateGraph, getFilesAffectedBy, emitFile, + emitChangedFiles, clear }; @@ -100,19 +92,37 @@ namespace ts { fileInfos = undefined; } - fileInfos = mutateExistingMap( + changedFilesSinceLastEmit = changedFilesSinceLastEmit || createMap(); + fileInfos = mutateExistingMapWithSameExistingValues( fileInfos, arrayToMap(program.getSourceFiles(), sourceFile => sourceFile.path), - (_path, sourceFile) => { - emitHandler.addScriptInfo(program, sourceFile); - return ""; - }, - (path: Path, _value) => emitHandler.removeScriptInfo(path), - /*isSameValue*/ undefined, - /*OnDeleteExistingMismatchValue*/ undefined, - (_prevValue, sourceFile) => emitHandler.updateScriptInfo(program, sourceFile) + // Add new file info + (_path, sourceFile) => addNewFileInfo(program, sourceFile), + // 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) ); } + function addNewFileInfo(program: Program, sourceFile: SourceFile): FileInfo { + changedFilesSinceLastEmit.set(sourceFile.path, true); + emitHandler.addScriptInfo(program, sourceFile); + return { version: sourceFile.version, signature: undefined }; + } + + function removeExistingFileInfo(path: Path, _existingFileInfo: FileInfo) { + changedFilesSinceLastEmit.set(path, true); + emitHandler.removeScriptInfo(path); + } + + function updateExistingFileInfo(program: Program, existingInfo: FileInfo, sourceFile: SourceFile) { + if (existingInfo.version !== sourceFile.version) { + changedFilesSinceLastEmit.set(sourceFile.path, true); + existingInfo.version = sourceFile.version; + emitHandler.updateScriptInfo(program, sourceFile); + } + } + function ensureProgramGraph(program: Program) { if (!emitHandler) { createProgramGraph(program); @@ -130,20 +140,53 @@ namespace ts { const sourceFile = program.getSourceFile(path); const singleFileResult = sourceFile && shouldEmitFile(sourceFile) ? [sourceFile.fileName] : []; - if (!fileInfos || !fileInfos.has(path) || !updateShapeSignature(program, sourceFile)) { + const info = fileInfos && fileInfos.get(path); + if (!info || !updateShapeSignature(program, sourceFile, info)) { return singleFileResult; } + Debug.assert(!!sourceFile); return emitHandler.getFilesAffectedByUpdatedShape(program, sourceFile, singleFileResult); } - function emitFile(program: Program, path: Path): T | EmitOutput { + function emitFile(program: Program, path: Path) { ensureProgramGraph(program); if (!fileInfos || !fileInfos.has(path)) { return { outputFiles: [], emitSkipped: true }; } - return getEmitOutput(program, program.getSourceFileByPath(path), /*emitOnlyDtsFiles*/ false); + return getEmitOutput(program, program.getSourceFileByPath(path), /*emitOnlyDtsFiles*/ false, /*isDetailed*/ false); + } + + function emitChangedFiles(program: Program): EmitOutputDetailed[] { + ensureProgramGraph(program); + const result: EmitOutputDetailed[] = []; + if (changedFilesSinceLastEmit) { + const seenFiles = createMap(); + changedFilesSinceLastEmit.forEach((__value, path: Path) => { + const affectedFiles = getFilesAffectedBy(program, path); + for (const file of affectedFiles) { + if (!seenFiles.has(file)) { + const sourceFile = program.getSourceFile(file); + seenFiles.set(file, sourceFile); + if (sourceFile) { + const emitOutput = getEmitOutput(program, sourceFile, /*emitOnlyDtsFiles*/ false, /*isDetailed*/ true) as EmitOutputDetailed; + result.push(emitOutput); + + // mark all the emitted source files as seen + if (emitOutput.emittedSourceFiles) { + for (const file of emitOutput.emittedSourceFiles) { + seenFiles.set(file.fileName, file); + } + } + } + } + } + }); + + changedFilesSinceLastEmit = undefined; + } + return result; } function clear() { @@ -152,10 +195,6 @@ namespace ts { fileInfos = undefined; } - function isExternalModuleOrHasOnlyAmbientExternalModules(sourceFile: SourceFile) { - return sourceFile && (isExternalModule(sourceFile) || containsOnlyAmbientModules(sourceFile)); - } - /** * For script files that contains only ambient external modules, although they are not actually external module files, * they can only be consumed via importing elements from them. Regular script files cannot consume them. Therefore, @@ -174,20 +213,21 @@ namespace ts { /** * @return {boolean} indicates if the shape signature has changed since last update. */ - function updateShapeSignature(program: Program, sourceFile: SourceFile) { - const path = sourceFile.path; - - const prevSignature = fileInfos.get(path); - let latestSignature = prevSignature; + function updateShapeSignature(program: Program, sourceFile: SourceFile, info: FileInfo) { + const prevSignature = info.signature; + let latestSignature: string; if (sourceFile.isDeclarationFile) { latestSignature = computeHash(sourceFile.text); - fileInfos.set(path, latestSignature); + info.signature = latestSignature; } else { - const emitOutput = getEmitOutput(program, sourceFile, /*emitOnlyDtsFiles*/ true); + const emitOutput = getEmitOutput(program, sourceFile, /*emitOnlyDtsFiles*/ true, /*isDetailed*/ false); if (emitOutput.outputFiles && emitOutput.outputFiles.length > 0) { latestSignature = computeHash(emitOutput.outputFiles[0].text); - fileInfos.set(path, latestSignature); + info.signature = latestSignature; + } + else { + latestSignature = prevSignature; } } @@ -201,6 +241,7 @@ namespace ts { */ function getReferencedFiles(program: Program, sourceFile: SourceFile): Map { const referencedFiles = createMap(); + // We need to use a set here since the code can contain the same import twice, // but that will only be one dependency. // To avoid invernal conversion, the key of the referencedFiles map must be of type Path @@ -281,44 +322,45 @@ namespace ts { function getModuleEmitHandler(): EmitHandler { const references = createMap>(); const referencedBy = createMultiMap(); - const scriptVersionForReferences = createMap(); return { - addScriptInfo, + addScriptInfo: setReferences, removeScriptInfo, - updateScriptInfo, + updateScriptInfo: setReferences, getFilesAffectedByUpdatedShape }; - function setReferences(program: Program, sourceFile: SourceFile, existingMap: Map) { + function setReferences(program: Program, sourceFile: SourceFile) { const path = sourceFile.path; - existingMap = mutateExistingMapWithNewSet( - existingMap, - getReferencedFiles(program, sourceFile), - // Creating new Reference: as sourceFile references file with path 'key' - // in other words source file (path) is referenced by 'key' - key => { referencedBy.add(key, path); return true; }, - // Remove existing reference by entry: source file doesnt reference file 'key' any more - // in other words source file (path) is not referenced by 'key' - (key, _existingValue) => { referencedBy.remove(key, path); } + references.set(path, + mutateExistingMapWithNewSet( + // Existing references + references.get(path), + // Updated references + getReferencedFiles(program, sourceFile), + // Creating new Reference: as sourceFile references file with path 'key' + // in other words source file (path) is referenced by 'key' + key => { referencedBy.add(key, path); return true; }, + // Remove existing reference by entry: source file doesnt reference file 'key' any more + // in other words source file (path) is not referenced by 'key' + (key, _existingValue) => { referencedBy.remove(key, path); } + ) ); - references.set(path, existingMap); - scriptVersionForReferences.set(path, sourceFile.version); - } - - function addScriptInfo(program: Program, sourceFile: SourceFile) { - setReferences(program, sourceFile, undefined); } function removeScriptInfo(path: Path) { + // Remove existing references + references.forEach((_value, key) => { + referencedBy.remove(key, path); + }); references.delete(path); - scriptVersionForReferences.delete(path); - } - function updateScriptInfo(program: Program, sourceFile: SourceFile) { - const path = sourceFile.path; - const lastUpdatedVersion = scriptVersionForReferences.get(path); - if (lastUpdatedVersion !== sourceFile.version) { - setReferences(program, sourceFile, references.get(path)); + // Delete the entry and add files referencing this file, as chagned files too + const referencedByPaths = referencedBy.get(path); + if (referencedByPaths) { + for (const path of referencedByPaths) { + changedFilesSinceLastEmit.set(path, true); + } + referencedBy.delete(path); } } @@ -327,7 +369,7 @@ namespace ts { } function getFilesAffectedByUpdatedShape(program: Program, sourceFile: SourceFile, singleFileResult: string[]): string[] { - if (!isExternalModuleOrHasOnlyAmbientExternalModules(sourceFile)) { + if (!isExternalModule(sourceFile) && !containsOnlyAmbientModules(sourceFile)) { return getAllEmittableFiles(program); } @@ -353,7 +395,7 @@ namespace ts { const currentPath = queue.pop(); if (!seenFileNamesMap.has(currentPath)) { const currentSourceFile = program.getSourceFileByPath(currentPath); - if (currentSourceFile && updateShapeSignature(program, currentSourceFile)) { + if (currentSourceFile && updateShapeSignature(program, currentSourceFile, fileInfos.get(currentPath))) { queue.push(...getReferencedByPaths(currentPath)); } setSeenFileName(currentPath, currentSourceFile); @@ -361,7 +403,7 @@ namespace ts { } // Return array of values that needs emit - return flatMapIter(seenFileNamesMap.values(), value => value); + return flatMapIter(seenFileNamesMap.values()); } } } diff --git a/src/compiler/core.ts b/src/compiler/core.ts index cbccdcbed02c6..2a41427f2fdbc 100644 --- a/src/compiler/core.ts +++ b/src/compiler/core.ts @@ -473,12 +473,14 @@ namespace ts { return result; } - export function flatMapIter(iter: Iterator, mapfn: (x: T) => U | U[] | undefined): U[] { - const result: U[] = []; + export function flatMapIter(iter: Iterator): T[]; + export function flatMapIter(iter: Iterator, mapfn: (x: T) => U | U[] | undefined): U[]; + export function flatMapIter(iter: Iterator, mapfn?: (x: any) => any): any[] { + const result = []; while (true) { const { value, done } = iter.next(); if (done) break; - const res = mapfn(value); + const res = mapfn ? mapfn(value) : value; if (res) { if (isArray(res)) { result.push(...res); diff --git a/src/compiler/tscLib.ts b/src/compiler/tscLib.ts index b92349061761e..13c1b27a5b094 100644 --- a/src/compiler/tscLib.ts +++ b/src/compiler/tscLib.ts @@ -248,7 +248,6 @@ namespace ts { let timerToUpdateProgram: any; // timer callback to recompile the program const sourceFilesCache = createMap(); // Cache that stores the source file and version info - let changedFilePaths: Path[] = []; let host: System; if (configFileName) { @@ -262,7 +261,7 @@ namespace ts { const getCanonicalFileName = createGetCanonicalFileName(host.useCaseSensitiveFileNames); // There is no extra check needed since we can just rely on the program to decide emit - const builder = createBuilder(getCanonicalFileName, getDetailedEmitOutput, computeHash, _sourceFile => true); + const builder = createBuilder(getCanonicalFileName, getFileEmitOutput, computeHash, _sourceFile => true); if (compilerOptions.pretty) { reportDiagnosticWorker = reportDiagnosticWithColorAndContext; @@ -352,51 +351,44 @@ namespace ts { function emitWithBuilder(program: Program): EmitResult { builder.onProgramUpdateGraph(program); - const filesPendingToEmit = changedFilePaths; - changedFilePaths = []; - - const seenFiles = createMap(); - - let emitSkipped: boolean; - const diagnostics: Diagnostic[] = []; const emittedFiles: string[] = program.getCompilerOptions().listEmittedFiles ? [] : undefined; let sourceMaps: SourceMapData[]; - while (filesPendingToEmit.length) { - const filePath = filesPendingToEmit.pop(); - const affectedFiles = builder.getFilesAffectedBy(program, filePath); - for (const file of affectedFiles) { - if (!seenFiles.has(file)) { - seenFiles.set(file, true); - const sourceFile = program.getSourceFile(file); - if (sourceFile) { - writeFiles(builder.emitFile(program, sourceFile.path)); + let emitSkipped: boolean; + let diagnostics: Diagnostic[]; + + const result = builder.emitChangedFiles(program); + switch (result.length) { + case 0: + emitSkipped = true; + break; + case 1: + const emitOutput = result[0]; + ({ diagnostics, sourceMaps, emitSkipped } = emitOutput); + writeOutputFiles(emitOutput.outputFiles); + break; + default: + for (const emitOutput of result) { + if (emitOutput.emitSkipped) { + emitSkipped = true; } + diagnostics = concatenate(diagnostics, emitOutput.diagnostics); + sourceMaps = concatenate(sourceMaps, emitOutput.sourceMaps); + writeOutputFiles(emitOutput.outputFiles); } - } } - return { emitSkipped, diagnostics, emittedFiles, sourceMaps }; - - function writeFiles(emitOutput: EmitOutputDetailed) { - if (emitOutput.emitSkipped) { - emitSkipped = true; - } + return { emitSkipped, diagnostics: diagnostics || [], emittedFiles, sourceMaps }; - diagnostics.push(...emitOutput.diagnostics); - sourceMaps = concatenate(sourceMaps, emitOutput.sourceMaps); - // If it emitted more than one source files, just mark all those source files as seen - if (emitOutput.emittedSourceFiles && emitOutput.emittedSourceFiles.length > 1) { - for (const file of emitOutput.emittedSourceFiles) { - seenFiles.set(file.fileName, true); - } - } - for (const outputFile of emitOutput.outputFiles) { - const error = writeFile(outputFile.name, outputFile.text, outputFile.writeByteOrderMark); - if (error) { - diagnostics.push(error); - } - if (emittedFiles) { - emittedFiles.push(outputFile.name); + function writeOutputFiles(outputFiles: OutputFile[]) { + if (outputFiles) { + for (const outputFile of outputFiles) { + const error = writeFile(outputFile.name, outputFile.text, outputFile.writeByteOrderMark); + if (error) { + diagnostics.push(error); + } + if (emittedFiles) { + emittedFiles.push(outputFile.name); + } } } } @@ -430,8 +422,6 @@ namespace ts { // Create new source file if requested or the versions dont match if (!hostSourceFile || shouldCreateNewSourceFile || hostSourceFile.version.toString() !== hostSourceFile.sourceFile.version) { - changedFilePaths.push(path); - const sourceFile = getNewSourceFile(); if (hostSourceFile) { if (shouldCreateNewSourceFile) { diff --git a/src/compiler/utilities.ts b/src/compiler/utilities.ts index 08816382ecf64..0a38e6af58b06 100644 --- a/src/compiler/utilities.ts +++ b/src/compiler/utilities.ts @@ -3656,13 +3656,27 @@ namespace ts { ); } + export function mutateExistingMapWithSameExistingValues( + existingMap: Map, newMap: Map, + createNewValue: (key: string, valueInNewMap: U) => T, + onDeleteExistingValue: (key: string, existingValue: T) => void, + onExistingValue?: (existingValue: T, valueInNewMap: U) => void + ): Map { + return mutateExistingMap( + existingMap, newMap, + createNewValue, onDeleteExistingValue, + /*isSameValue*/ undefined, /*onDeleteExistingMismatchValue*/ undefined, + onExistingValue + ); + } + export function mutateExistingMap( existingMap: Map, newMap: Map, createNewValue: (key: string, valueInNewMap: U) => T, onDeleteExistingValue: (key: string, existingValue: T) => void, isSameValue?: (existingValue: T, valueInNewMap: U) => boolean, OnDeleteExistingMismatchValue?: (key: string, existingValue: T) => void, - onSameExistingValue?: (existingValue: T, valueInNewMap: U) => void + onExistingValue?: (existingValue: T, valueInNewMap: U) => void ): Map { // If there are new values update them if (newMap) { @@ -3680,8 +3694,8 @@ namespace ts { existingMap.delete(key); OnDeleteExistingMismatchValue(key, existingValue); } - else if (onSameExistingValue) { - onSameExistingValue(existingValue, valueInNewMap); + else if (onExistingValue) { + onExistingValue(existingValue, valueInNewMap); } }); } diff --git a/src/harness/unittests/tscWatchMode.ts b/src/harness/unittests/tscWatchMode.ts index 1025e250d2780..7622bc2556be8 100644 --- a/src/harness/unittests/tscWatchMode.ts +++ b/src/harness/unittests/tscWatchMode.ts @@ -1015,9 +1015,24 @@ namespace ts.tscWatch { describe("tsc-watch emit for configured projects", () => { const file1Consumer1Path = "/a/b/file1Consumer1.ts"; const moduleFile1Path = "/a/b/moduleFile1.ts"; - function getInitialState(configObj: any = {}, fileNames?: string[]) { + const configFilePath = "/a/b/tsconfig.json"; + type InitialStateParams = { + /** custom config file options */ + configObj?: any; + /** list of the files that will be emitted for first compilation */ + firstCompilationEmitFiles?: string[]; + /** get the emit file for file - default is multi file emit line */ + getEmitLine?(file: FileOrFolder, host: WatchedSystem): string; + /** Additional files and folders to add */ + getAdditionalFileOrFolder?(): FileOrFolder[]; + /** initial list of files to emit if not the default list */ + firstReloadFileList?: string[]; + }; + function getInitialState({ configObj = {}, firstCompilationEmitFiles, getEmitLine, getAdditionalFileOrFolder, firstReloadFileList }: InitialStateParams = {}) { const host = createWatchedSystem([]); - const getOutputName = (file: FileOrFolder) => getEmittedLineForMultiFileOutput(file, host); + const getOutputName = getEmitLine ? (file: FileOrFolder) => getEmitLine(file, host) : + (file: FileOrFolder) => getEmittedLineForMultiFileOutput(file, host); + const moduleFile1 = getFileOrFolderEmit({ path: moduleFile1Path, content: "export function Foo() { };", @@ -1043,20 +1058,24 @@ namespace ts.tscWatch { content: `interface GlobalFoo { age: number }` }); + const additionalFiles = getAdditionalFileOrFolder ? + map(getAdditionalFileOrFolder(), file => getFileOrFolderEmit(file, getOutputName)) : + []; + (configObj.compilerOptions || (configObj.compilerOptions = {})).listEmittedFiles = true; const configFile = getFileOrFolderEmit({ - path: "/a/b/tsconfig.json", + path: configFilePath, content: JSON.stringify(configObj) }); - const files = [moduleFile1, file1Consumer1, file1Consumer2, globalFile3, moduleFile2, configFile, libFile]; + const files = [moduleFile1, file1Consumer1, file1Consumer2, globalFile3, moduleFile2, configFile, libFile, ...additionalFiles]; let allEmittedFiles = getEmittedLines(files); - host.reloadFS(files); + host.reloadFS(firstReloadFileList ? getFiles(firstReloadFileList) : files); // Initial compile createWatchWithConfig(configFile.path, host); - if (fileNames) { - checkAffectedLines(host, map(fileNames, name => find(files, file => file.path === name)), allEmittedFiles); + if (firstCompilationEmitFiles) { + checkAffectedLines(host, getFiles(firstCompilationEmitFiles), allEmittedFiles); } else { checkOutputContains(host, allEmittedFiles); @@ -1066,11 +1085,20 @@ namespace ts.tscWatch { return { moduleFile1, file1Consumer1, file1Consumer2, moduleFile2, globalFile3, configFile, files, + getFile, verifyAffectedFiles, verifyAffectedAllFiles, getOutputName }; + function getFiles(filelist: string[]) { + return map(filelist, getFile); + } + + function getFile(fileName: string) { + return find(files, file => file.path === fileName); + } + function verifyAffectedAllFiles() { host.reloadFS(files); host.checkTimeoutQueueLengthAndRun(1); @@ -1172,8 +1200,8 @@ namespace ts.tscWatch { it("should detect changes in non-root files", () => { const { moduleFile1, file1Consumer1, - verifyAffectedFiles, - } = getInitialState({ files: [file1Consumer1Path] }, [file1Consumer1Path, moduleFile1Path]); + verifyAffectedFiles + } = getInitialState({ configObj: { files: [file1Consumer1Path] }, firstCompilationEmitFiles: [file1Consumer1Path, moduleFile1Path] }); moduleFile1.content = `export var T: number;export function Foo() { };`; verifyAffectedFiles([moduleFile1, file1Consumer1]); @@ -1192,189 +1220,129 @@ namespace ts.tscWatch { verifyAffectedAllFiles(); }); - //it("should save with base tsconfig.json", () => { - // configFile = { - // path: "/a/b/tsconfig.json", - // content: `{ - // "extends": "/a/tsconfig.json" - // }` - // }; - - // const configFile2: FileOrFolder = { - // path: "/a/tsconfig.json", - // content: `{ - // "compileOnSave": true - // }` - // }; - - // const host = createServerHost([moduleFile1, file1Consumer1, file1Consumer2, configFile2, configFile, libFile]); - // const typingsInstaller = createTestTypingsInstaller(host); - // const session = createSession(host, typingsInstaller); - - // openFilesForSession([moduleFile1, file1Consumer1], session); - // sendAffectedFileRequestAndCheckResult(session, moduleFile1FileListRequest, [{ projectFileName: configFile.path, files: [moduleFile1, file1Consumer1, file1Consumer2] }]); - //}); - - //it("should always return the file itself if '--isolatedModules' is specified", () => { - // configFile = { - // path: "/a/b/tsconfig.json", - // content: `{ - // "compileOnSave": true, - // "compilerOptions": { - // "isolatedModules": true - // } - // }` - // }; - - // const host = createServerHost([moduleFile1, file1Consumer1, configFile, libFile]); - // const typingsInstaller = createTestTypingsInstaller(host); - // const session = createSession(host, typingsInstaller); - // openFilesForSession([moduleFile1], session); - - // const file1ChangeShapeRequest = makeSessionRequest(CommandNames.Change, { - // file: moduleFile1.path, - // line: 1, - // offset: 27, - // endLine: 1, - // endOffset: 27, - // insertString: `Point,` - // }); - // session.executeCommand(file1ChangeShapeRequest); - // sendAffectedFileRequestAndCheckResult(session, moduleFile1FileListRequest, [{ projectFileName: configFile.path, files: [moduleFile1] }]); - //}); - - //it("should always return the file itself if '--out' or '--outFile' is specified", () => { - // configFile = { - // path: "/a/b/tsconfig.json", - // content: `{ - // "compileOnSave": true, - // "compilerOptions": { - // "module": "system", - // "outFile": "/a/b/out.js" - // } - // }` - // }; - - // const host = createServerHost([moduleFile1, file1Consumer1, configFile, libFile]); - // const typingsInstaller = createTestTypingsInstaller(host); - // const session = createSession(host, typingsInstaller); - // openFilesForSession([moduleFile1], session); - - // const file1ChangeShapeRequest = makeSessionRequest(CommandNames.Change, { - // file: moduleFile1.path, - // line: 1, - // offset: 27, - // endLine: 1, - // endOffset: 27, - // insertString: `Point,` - // }); - // session.executeCommand(file1ChangeShapeRequest); - // sendAffectedFileRequestAndCheckResult(session, moduleFile1FileListRequest, [{ projectFileName: configFile.path, files: [moduleFile1] }]); - //}); - - //it("should return cascaded affected file list", () => { - // const file1Consumer1Consumer1: FileOrFolder = { - // path: "/a/b/file1Consumer1Consumer1.ts", - // content: `import {y} from "./file1Consumer1";` - // }; - // const host = createServerHost([moduleFile1, file1Consumer1, file1Consumer1Consumer1, globalFile3, configFile, libFile]); - // const typingsInstaller = createTestTypingsInstaller(host); - // const session = createSession(host, typingsInstaller); - - // openFilesForSession([moduleFile1, file1Consumer1], session); - // sendAffectedFileRequestAndCheckResult(session, moduleFile1FileListRequest, [{ projectFileName: configFile.path, files: [moduleFile1, file1Consumer1, file1Consumer1Consumer1] }]); - - // const changeFile1Consumer1ShapeRequest = makeSessionRequest(CommandNames.Change, { - // file: file1Consumer1.path, - // line: 2, - // offset: 1, - // endLine: 2, - // endOffset: 1, - // insertString: `export var T: number;` - // }); - // session.executeCommand(changeModuleFile1ShapeRequest1); - // session.executeCommand(changeFile1Consumer1ShapeRequest); - // sendAffectedFileRequestAndCheckResult(session, moduleFile1FileListRequest, [{ projectFileName: configFile.path, files: [moduleFile1, file1Consumer1, file1Consumer1Consumer1] }]); - //}); - - //it("should work fine for files with circular references", () => { - // const file1: FileOrFolder = { - // path: "/a/b/file1.ts", - // content: ` - // /// - // export var t1 = 10;` - // }; - // const file2: FileOrFolder = { - // path: "/a/b/file2.ts", - // content: ` - // /// - // export var t2 = 10;` - // }; - // const host = createServerHost([file1, file2, configFile]); - // const typingsInstaller = createTestTypingsInstaller(host); - // const session = createSession(host, typingsInstaller); - - // openFilesForSession([file1, file2], session); - // const file1AffectedListRequest = makeSessionRequest(CommandNames.CompileOnSaveAffectedFileList, { file: file1.path }); - // sendAffectedFileRequestAndCheckResult(session, file1AffectedListRequest, [{ projectFileName: configFile.path, files: [file1, file2] }]); - //}); - - //it("should return results for all projects if not specifying projectFileName", () => { - // const file1: FileOrFolder = { path: "/a/b/file1.ts", content: "export var t = 10;" }; - // const file2: FileOrFolder = { path: "/a/b/file2.ts", content: `import {t} from "./file1"; var t2 = 11;` }; - // const file3: FileOrFolder = { path: "/a/c/file2.ts", content: `import {t} from "../b/file1"; var t3 = 11;` }; - // const configFile1: FileOrFolder = { path: "/a/b/tsconfig.json", content: `{ "compileOnSave": true }` }; - // const configFile2: FileOrFolder = { path: "/a/c/tsconfig.json", content: `{ "compileOnSave": true }` }; - - // const host = createServerHost([file1, file2, file3, configFile1, configFile2]); - // const session = createSession(host); - - // openFilesForSession([file1, file2, file3], session); - // const file1AffectedListRequest = makeSessionRequest(CommandNames.CompileOnSaveAffectedFileList, { file: file1.path }); - - // sendAffectedFileRequestAndCheckResult(session, file1AffectedListRequest, [ - // { projectFileName: configFile1.path, files: [file1, file2] }, - // { projectFileName: configFile2.path, files: [file1, file3] } - // ]); - //}); - - //it("should detect removed code file", () => { - // const referenceFile1: FileOrFolder = { - // path: "/a/b/referenceFile1.ts", - // content: ` - // /// - // export var x = Foo();` - // }; - // const host = createServerHost([moduleFile1, referenceFile1, configFile]); - // const session = createSession(host); - - // openFilesForSession([referenceFile1], session); - // host.reloadFS([referenceFile1, configFile]); - - // const request = makeSessionRequest(CommandNames.CompileOnSaveAffectedFileList, { file: referenceFile1.path }); - // sendAffectedFileRequestAndCheckResult(session, request, [ - // { projectFileName: configFile.path, files: [referenceFile1] } - // ]); - // const requestForMissingFile = makeSessionRequest(CommandNames.CompileOnSaveAffectedFileList, { file: moduleFile1.path }); - // sendAffectedFileRequestAndCheckResult(session, requestForMissingFile, []); - //}); - - //it("should detect non-existing code file", () => { - // const referenceFile1: FileOrFolder = { - // path: "/a/b/referenceFile1.ts", - // content: ` - // /// - // export var x = Foo();` - // }; - // const host = createServerHost([referenceFile1, configFile]); - // const session = createSession(host); - - // openFilesForSession([referenceFile1], session); - // const request = makeSessionRequest(CommandNames.CompileOnSaveAffectedFileList, { file: referenceFile1.path }); - // sendAffectedFileRequestAndCheckResult(session, request, [ - // { projectFileName: configFile.path, files: [referenceFile1] } - // ]); - //}); - }); + it("should always return the file itself if '--isolatedModules' is specified", () => { + const { + moduleFile1, verifyAffectedFiles + } = getInitialState({ configObj: { compilerOptions: { isolatedModules: true } } }); + + moduleFile1.content = `export var T: number;export function Foo() { };`; + verifyAffectedFiles([moduleFile1]); + }); + + it("should always return the file itself if '--out' or '--outFile' is specified", () => { + const outFilePath = "/a/b/out.js"; + const { + moduleFile1, verifyAffectedFiles + } = getInitialState({ + configObj: { compilerOptions: { module: "system", outFile: outFilePath } }, + getEmitLine: (_, host) => getEmittedLineForSingleFileOutput(outFilePath, host) + }); + + moduleFile1.content = `export var T: number;export function Foo() { };`; + verifyAffectedFiles([moduleFile1]); + }); + + it("should return cascaded affected file list", () => { + const file1Consumer1Consumer1: FileOrFolder = { + path: "/a/b/file1Consumer1Consumer1.ts", + content: `import {y} from "./file1Consumer1";` + }; + const { + moduleFile1, file1Consumer1, file1Consumer2, verifyAffectedFiles, getFile + } = getInitialState({ + getAdditionalFileOrFolder: () => [file1Consumer1Consumer1] + }); + + const file1Consumer1Consumer1Emit = getFile(file1Consumer1Consumer1.path); + file1Consumer1.content += "export var T: number;"; + verifyAffectedFiles([file1Consumer1, file1Consumer1Consumer1Emit]); + + // Doesnt change the shape of file1Consumer1 + moduleFile1.content = `export var T: number;export function Foo() { };`; + verifyAffectedFiles([moduleFile1, file1Consumer1, file1Consumer2]); + + // Change both files before the timeout + file1Consumer1.content += "export var T2: number;"; + moduleFile1.content = `export var T2: number;export function Foo() { };`; + verifyAffectedFiles([moduleFile1, file1Consumer1, file1Consumer2, file1Consumer1Consumer1Emit]); + }); + + it("should work fine for files with circular references", () => { + // TODO: do not exit on such errors? Just continue to watch the files for update in watch mode + const file1: FileOrFolder = { + path: "/a/b/file1.ts", + content: ` + /// + export var t1 = 10;` + }; + const file2: FileOrFolder = { + path: "/a/b/file2.ts", + content: ` + /// + export var t2 = 10;` + }; + const { + configFile, + getFile, + verifyAffectedFiles + } = getInitialState({ + firstCompilationEmitFiles: [file1.path, file2.path], + getAdditionalFileOrFolder: () => [file1, file2], + firstReloadFileList: [libFile.path, file1.path, file2.path, configFilePath] + }); + const file1Emit = getFile(file1.path), file2Emit = getFile(file2.path); + + file1Emit.content += "export var t3 = 10;"; + verifyAffectedFiles([file1Emit, file2Emit], [file1, file2, libFile, configFile]); + + }); + + it("should detect removed code file", () => { + const referenceFile1: FileOrFolder = { + path: "/a/b/referenceFile1.ts", + content: ` + /// + export var x = Foo();` + }; + const { + configFile, + getFile, + verifyAffectedFiles + } = getInitialState({ + firstCompilationEmitFiles: [referenceFile1.path, moduleFile1Path], + getAdditionalFileOrFolder: () => [referenceFile1], + firstReloadFileList: [libFile.path, referenceFile1.path, moduleFile1Path, configFilePath] + }); + + const referenceFile1Emit = getFile(referenceFile1.path); + verifyAffectedFiles([referenceFile1Emit], [libFile, referenceFile1Emit, configFile]); + }); + + it("should detect non-existing code file", () => { + const referenceFile1: FileOrFolder = { + path: "/a/b/referenceFile1.ts", + content: ` + /// + export var x = Foo();` + }; + const { + configFile, + moduleFile2, + getFile, + verifyAffectedFiles + } = getInitialState({ + firstCompilationEmitFiles: [referenceFile1.path], + getAdditionalFileOrFolder: () => [referenceFile1], + firstReloadFileList: [libFile.path, referenceFile1.path, configFilePath] + }); + + const referenceFile1Emit = getFile(referenceFile1.path); + referenceFile1Emit.content += "export var yy = Foo();"; + verifyAffectedFiles([referenceFile1Emit], [libFile, referenceFile1Emit, configFile]); + + // Create module File2 and see both files are saved + verifyAffectedFiles([referenceFile1Emit, moduleFile2], [libFile, moduleFile2, referenceFile1Emit, configFile]); + }); + }); } diff --git a/src/server/project.ts b/src/server/project.ts index 942ff9f71a41f..02e2fb20759a1 100644 --- a/src/server/project.ts +++ b/src/server/project.ts @@ -130,7 +130,7 @@ namespace ts.server { /*@internal*/ lsHost: LSHost; - builder: Builder; + builder: Builder; /** * Set of files names that were updated since the last call to getChangesSinceVersion. */ @@ -250,7 +250,7 @@ namespace ts.server { if (!this.builder) { this.builder = createBuilder( this.projectService.toCanonicalFileName, - (_program, sourceFile, emitOnlyDts) => this.getFileEmitOutput(sourceFile, emitOnlyDts), + (_program, sourceFile, emitOnlyDts, isDetailed) => this.getFileEmitOutput(sourceFile, emitOnlyDts, isDetailed), data => this.projectService.host.createHash(data), sourceFile => !this.projectService.getScriptInfoForPath(sourceFile.path).hasMixedContent ); @@ -418,11 +418,11 @@ namespace ts.server { }); } - private getFileEmitOutput(sourceFile: SourceFile, emitOnlyDtsFiles: boolean) { + private getFileEmitOutput(sourceFile: SourceFile, emitOnlyDtsFiles: boolean, isDetailed: boolean) { if (!this.languageServiceEnabled) { return undefined; } - return this.getLanguageService().getEmitOutput(sourceFile.fileName, emitOnlyDtsFiles); + return this.getLanguageService().getEmitOutput(sourceFile.fileName, emitOnlyDtsFiles, isDetailed); } getFileNames(excludeFilesFromExternalLibraries?: boolean, excludeConfigFiles?: boolean) { diff --git a/src/services/services.ts b/src/services/services.ts index 8fe158a47984e..40a446e0967a4 100644 --- a/src/services/services.ts +++ b/src/services/services.ts @@ -1468,12 +1468,12 @@ namespace ts { return ts.NavigateTo.getNavigateToItems(sourceFiles, program.getTypeChecker(), cancellationToken, searchValue, maxResultCount, excludeDtsFiles); } - function getEmitOutput(fileName: string, emitOnlyDtsFiles?: boolean): EmitOutput { + function getEmitOutput(fileName: string, emitOnlyDtsFiles?: boolean, isDetailed?: boolean) { synchronizeHostData(); const sourceFile = getValidSourceFile(fileName); const customTransformers = host.getCustomTransformers && host.getCustomTransformers(); - return getFileEmitOutput(program, sourceFile, emitOnlyDtsFiles, cancellationToken, customTransformers); + return getFileEmitOutput(program, sourceFile, emitOnlyDtsFiles, isDetailed, cancellationToken, customTransformers); } // Signature help diff --git a/src/services/types.ts b/src/services/types.ts index 34d79b311ca74..e17df08378ad0 100644 --- a/src/services/types.ts +++ b/src/services/types.ts @@ -275,7 +275,7 @@ namespace ts { getApplicableRefactors(fileName: string, positionOrRaneg: number | TextRange): ApplicableRefactorInfo[]; getEditsForRefactor(fileName: string, formatOptions: FormatCodeSettings, positionOrRange: number | TextRange, refactorName: string, actionName: string): RefactorEditInfo | undefined; - getEmitOutput(fileName: string, emitOnlyDtsFiles?: boolean): EmitOutput; + getEmitOutput(fileName: string, emitOnlyDtsFiles?: boolean, isDetailed?: boolean): EmitOutput | EmitOutputDetailed; getProgram(): Program; From bb91b32a4d0e3be9a4567034ad53ecf4461bff12 Mon Sep 17 00:00:00 2001 From: Sheetal Nandi Date: Thu, 3 Aug 2017 13:39:39 -0700 Subject: [PATCH 042/109] Add tests to verify emitted files --- src/compiler/tscLib.ts | 2 +- src/compiler/utilities.ts | 7 +- src/harness/unittests/tscWatchMode.ts | 119 ++++++++++++++++++++++++++ 3 files changed, 122 insertions(+), 6 deletions(-) diff --git a/src/compiler/tscLib.ts b/src/compiler/tscLib.ts index 13c1b27a5b094..36c9428cecd22 100644 --- a/src/compiler/tscLib.ts +++ b/src/compiler/tscLib.ts @@ -292,7 +292,7 @@ namespace ts { } function createWatchedCompilerHost(options: CompilerOptions): CompilerHost { - const newLine = getNewLineCharacter(options); + const newLine = getNewLineCharacter(options, system); const realpath = host.realpath && ((path: string) => host.realpath(path)); return { diff --git a/src/compiler/utilities.ts b/src/compiler/utilities.ts index 0a38e6af58b06..bea28ed27028a 100644 --- a/src/compiler/utilities.ts +++ b/src/compiler/utilities.ts @@ -3209,17 +3209,14 @@ namespace ts { const carriageReturnLineFeed = "\r\n"; const lineFeed = "\n"; - export function getNewLineCharacter(options: CompilerOptions | PrinterOptions): string { + export function getNewLineCharacter(options: CompilerOptions | PrinterOptions, system?: System): string { switch (options.newLine) { case NewLineKind.CarriageReturnLineFeed: return carriageReturnLineFeed; case NewLineKind.LineFeed: return lineFeed; } - if (sys) { - return sys.newLine; - } - return carriageReturnLineFeed; + return system ? system.newLine : sys ? sys.newLine : carriageReturnLineFeed; } /** diff --git a/src/harness/unittests/tscWatchMode.ts b/src/harness/unittests/tscWatchMode.ts index 7622bc2556be8..a55cfb133d78e 100644 --- a/src/harness/unittests/tscWatchMode.ts +++ b/src/harness/unittests/tscWatchMode.ts @@ -1345,4 +1345,123 @@ namespace ts.tscWatch { verifyAffectedFiles([referenceFile1Emit, moduleFile2], [libFile, moduleFile2, referenceFile1Emit, configFile]); }); }); + + describe("tsc-watch emit file content", () => { + interface EmittedFile extends FileOrFolder { + shouldBeWritten: boolean; + } + function getEmittedFiles(files: FileOrFolderEmit[], contents: string[]): EmittedFile[] { + return map(contents, (content, index) => { + return { + content, + path: changeExtension(files[index].path, Extension.Js), + shouldBeWritten: true + }; + } + ); + } + function verifyEmittedFiles(host: WatchedSystem, emittedFiles: EmittedFile[]) { + for (const { path, content, shouldBeWritten } of emittedFiles) { + if (shouldBeWritten) { + assert.isTrue(host.fileExists(path), `Expected file ${path} to be present`); + assert.equal(host.readFile(path), content, `Contents of file ${path} do not match`); + } + else { + assert.isNotTrue(host.fileExists(path), `Expected file ${path} to be absent`); + } + } + } + + function verifyEmittedFileContents(newLine: string, inputFiles: FileOrFolder[], initialEmittedFileContents: string[], + modifyFiles: (files: FileOrFolderEmit[], emitedFiles: EmittedFile[]) => FileOrFolderEmit[], configFile?: FileOrFolder) { + const host = createWatchedSystem([], { newLine }); + const files = concatenate( + map(inputFiles, file => getFileOrFolderEmit(file, fileToConvert => getEmittedLineForMultiFileOutput(fileToConvert, host))), + configFile ? [libFile, configFile] : [libFile] + ); + const allEmittedFiles = getEmittedLines(files); + host.reloadFS(files); + + // Initial compile + if (configFile) { + createWatchWithConfig(configFile.path, host); + } + else { + // First file as the root + createWatchModeWithoutConfigFile([files[0].path], { listEmittedFiles: true }, host); + } + checkOutputContains(host, allEmittedFiles); + + const emittedFiles = getEmittedFiles(files, initialEmittedFileContents); + verifyEmittedFiles(host, emittedFiles); + host.clearOutput(); + + const affectedFiles = modifyFiles(files, emittedFiles); + host.reloadFS(files); + host.checkTimeoutQueueLengthAndRun(1); + checkAffectedLines(host, affectedFiles, allEmittedFiles); + + verifyEmittedFiles(host, emittedFiles); + } + + function verifyNewLine(newLine: string) { + const lines = ["var x = 1;", "var y = 2;"]; + const fileContent = lines.join(newLine); + const f = { + path: "/a/app.ts", + content: fileContent + }; + + verifyEmittedFileContents(newLine, [f], [fileContent + newLine], modifyFiles); + + function modifyFiles(files: FileOrFolderEmit[], emittedFiles: EmittedFile[]) { + files[0].content = fileContent + newLine + "var z = 3;"; + emittedFiles[0].content = files[0].content + newLine; + return [files[0]]; + } + } + + it("handles new lines: \\n", () => { + verifyNewLine("\n"); + }); + + it("handles new lines: \\r\\n", () => { + verifyNewLine("\r\n"); + }); + + it("should emit specified file", () => { + const file1 = { + path: "/a/b/f1.ts", + content: `export function Foo() { return 10; }` + }; + + const file2 = { + path: "/a/b/f2.ts", + content: `import {Foo} from "./f1"; export let y = Foo();` + }; + + const file3 = { + path: "/a/b/f3.ts", + content: `import {y} from "./f2"; let x = y;` + }; + + const configFile = { + path: "/a/b/tsconfig.json", + content: JSON.stringify({ compilerOptions: { listEmittedFiles: true } }) + }; + + verifyEmittedFileContents("\r\n", [file1, file2, file3], [ + `"use strict";\r\nexports.__esModule = true;\r\nfunction Foo() { return 10; }\r\nexports.Foo = Foo;\r\n`, + `"use strict";\r\nexports.__esModule = true;\r\nvar f1_1 = require("./f1");\r\nexports.y = f1_1.Foo();\r\n`, + `"use strict";\r\nexports.__esModule = true;\r\nvar f2_1 = require("./f2");\r\nvar x = f2_1.y;\r\n` + ], modifyFiles, configFile); + + function modifyFiles(files: FileOrFolderEmit[], emittedFiles: EmittedFile[]) { + files[0].content += `export function foo2() { return 2; }`; + emittedFiles[0].content += `function foo2() { return 2; }\r\nexports.foo2 = foo2;\r\n`; + emittedFiles[2].shouldBeWritten = false; + return files.slice(0, 2); + } + }); + }); } From 46e3d1c1d9054f55b0aa8ea5d1a062f6c4f3c2b2 Mon Sep 17 00:00:00 2001 From: Sheetal Nandi Date: Thu, 3 Aug 2017 19:14:47 -0700 Subject: [PATCH 043/109] Refactoring so that instead of just using from tsc --watch the new api is accessible all the time --- src/compiler/program.ts | 24 +- src/compiler/tscLib.ts | 733 ++------------------------ src/compiler/tsconfig.json | 1 + src/compiler/watchedProgram.ts | 673 +++++++++++++++++++++++ src/harness/unittests/tscWatchMode.ts | 101 ++-- 5 files changed, 795 insertions(+), 737 deletions(-) create mode 100644 src/compiler/watchedProgram.ts diff --git a/src/compiler/program.ts b/src/compiler/program.ts index 80bd089429a77..8ce51ac5e8c04 100644 --- a/src/compiler/program.ts +++ b/src/compiler/program.ts @@ -228,19 +228,25 @@ namespace ts { let output = ""; for (const diagnostic of diagnostics) { - if (diagnostic.file) { - const { line, character } = getLineAndCharacterOfPosition(diagnostic.file, diagnostic.start); - const fileName = diagnostic.file.fileName; - const relativeFileName = convertToRelativePath(fileName, host.getCurrentDirectory(), fileName => host.getCanonicalFileName(fileName)); - output += `${relativeFileName}(${line + 1},${character + 1}): `; - } - - const category = DiagnosticCategory[diagnostic.category].toLowerCase(); - output += `${category} TS${diagnostic.code}: ${flattenDiagnosticMessageText(diagnostic.messageText, host.getNewLine())}${host.getNewLine()}`; + output += formatDiagnostic(diagnostic, host); } return output; } + export function formatDiagnostic(diagnostic: Diagnostic, host: FormatDiagnosticsHost): string { + const category = DiagnosticCategory[diagnostic.category].toLowerCase(); + const errorMessage = `${category} TS${diagnostic.code}: ${flattenDiagnosticMessageText(diagnostic.messageText, host.getNewLine())}${host.getNewLine()}`; + + if (diagnostic.file) { + const { line, character } = getLineAndCharacterOfPosition(diagnostic.file, diagnostic.start); + const fileName = diagnostic.file.fileName; + const relativeFileName = convertToRelativePath(fileName, host.getCurrentDirectory(), fileName => host.getCanonicalFileName(fileName)); + return `${relativeFileName}(${line + 1},${character + 1}): ` + errorMessage; + } + + return errorMessage; + } + const redForegroundEscapeSequence = "\u001b[91m"; const yellowForegroundEscapeSequence = "\u001b[93m"; const blueForegroundEscapeSequence = "\u001b[93m"; diff --git a/src/compiler/tscLib.ts b/src/compiler/tscLib.ts index 36c9428cecd22..bd4a64d8899c2 100644 --- a/src/compiler/tscLib.ts +++ b/src/compiler/tscLib.ts @@ -1,63 +1,13 @@ /// +/// /// namespace ts { - export interface CompilerHost { - /** If this is the emit based on the graph builder, use it to emit */ - emitWithBuilder?(program: Program): EmitResult; - } - interface Statistic { name: string; value: string; } - export interface FormatDiagnosticsHostWithWrite extends FormatDiagnosticsHost { - write?(s: string): void; - } - - const defaultFormatDiagnosticsHost: FormatDiagnosticsHostWithWrite = sys ? { - getCurrentDirectory: () => sys.getCurrentDirectory(), - getNewLine: () => sys.newLine, - getCanonicalFileName: createGetCanonicalFileName(sys.useCaseSensitiveFileNames), - write: s => sys.write(s) - } : undefined; - - function getDefaultFormatDiagnosticsHost(system: System): FormatDiagnosticsHostWithWrite { - return system === sys ? defaultFormatDiagnosticsHost : { - getCurrentDirectory: () => system.getCurrentDirectory(), - getNewLine: () => system.newLine, - getCanonicalFileName: createGetCanonicalFileName(system.useCaseSensitiveFileNames), - write: s => system.write(s) - }; - } - - let reportDiagnosticWorker = reportDiagnosticSimply; - - function reportDiagnostic(diagnostic: Diagnostic, host: FormatDiagnosticsHostWithWrite) { - reportDiagnosticWorker(diagnostic, host || defaultFormatDiagnosticsHost); - } - - function reportDiagnostics(diagnostics: Diagnostic[], host: FormatDiagnosticsHostWithWrite): void { - for (const diagnostic of diagnostics) { - reportDiagnostic(diagnostic, host); - } - } - - function reportEmittedFiles(files: string[], system: System): void { - if (!files || files.length === 0) { - return; - } - - const currentDir = system.getCurrentDirectory(); - - for (const file of files) { - const filepath = getNormalizedAbsolutePath(file, currentDir); - - system.write(`TSFILE: ${filepath}${system.newLine}`); - } - } - function countLines(program: Program): number { let count = 0; forEach(program.getSourceFiles(), file => { @@ -71,25 +21,11 @@ namespace ts { return diagnostic.messageText; } - function reportDiagnosticSimply(diagnostic: Diagnostic, host: FormatDiagnosticsHostWithWrite): void { - host.write(ts.formatDiagnostics([diagnostic], host)); - } - - function reportDiagnosticWithColorAndContext(diagnostic: Diagnostic, host: FormatDiagnosticsHostWithWrite): void { - host.write(ts.formatDiagnosticsWithColorAndContext([diagnostic], host) + host.getNewLine()); - } - - function reportWatchDiagnostic(diagnostic: Diagnostic, system: System) { - let output = new Date().toLocaleTimeString() + " - "; - - if (diagnostic.file) { - const loc = getLineAndCharacterOfPosition(diagnostic.file, diagnostic.start); - output += `${ diagnostic.file.fileName }(${ loc.line + 1 },${ loc.character + 1 }): `; + let reportDiagnostic = createDiagnosticReporter(sys, reportDiagnosticSimply); + function udpateReportDiagnostic(options: CompilerOptions) { + if (options.pretty) { + reportDiagnostic = createDiagnosticReporter(sys, reportDiagnosticWithColorAndContext); } - - output += `${ flattenDiagnosticMessageText(diagnostic.messageText, system.newLine) }${ system.newLine + system.newLine + system.newLine }`; - - system.write(output); } function padLeft(s: string, length: number) { @@ -118,7 +54,7 @@ namespace ts { let configFileName: string; if (commandLine.options.locale) { if (!isJSONSupported()) { - reportDiagnostic(createCompilerDiagnostic(Diagnostics.The_current_host_does_not_support_the_0_option, "--locale"), /* host */ undefined); + reportDiagnostic(createCompilerDiagnostic(Diagnostics.The_current_host_does_not_support_the_0_option, "--locale")); return sys.exit(ExitStatus.DiagnosticsPresent_OutputsSkipped); } validateLocaleAndSetLanguage(commandLine.options.locale, sys, commandLine.errors); @@ -127,7 +63,7 @@ namespace ts { // If there are any errors due to command line parsing and/or // setting up localization, report them and quit. if (commandLine.errors.length > 0) { - reportDiagnostics(commandLine.errors, /*host*/ undefined); + reportDiagnostics(commandLine.errors, reportDiagnostic); return sys.exit(ExitStatus.DiagnosticsPresent_OutputsSkipped); } @@ -149,11 +85,11 @@ namespace ts { if (commandLine.options.project) { if (!isJSONSupported()) { - reportDiagnostic(createCompilerDiagnostic(Diagnostics.The_current_host_does_not_support_the_0_option, "--project"), /* host */ undefined); + reportDiagnostic(createCompilerDiagnostic(Diagnostics.The_current_host_does_not_support_the_0_option, "--project")); return sys.exit(ExitStatus.DiagnosticsPresent_OutputsSkipped); } if (commandLine.fileNames.length !== 0) { - reportDiagnostic(createCompilerDiagnostic(Diagnostics.Option_project_cannot_be_mixed_with_source_files_on_a_command_line), /* host */ undefined); + reportDiagnostic(createCompilerDiagnostic(Diagnostics.Option_project_cannot_be_mixed_with_source_files_on_a_command_line)); return sys.exit(ExitStatus.DiagnosticsPresent_OutputsSkipped); } @@ -161,14 +97,14 @@ namespace ts { if (!fileOrDirectory /* current directory "." */ || sys.directoryExists(fileOrDirectory)) { configFileName = combinePaths(fileOrDirectory, "tsconfig.json"); if (!sys.fileExists(configFileName)) { - reportDiagnostic(createCompilerDiagnostic(Diagnostics.Cannot_find_a_tsconfig_json_file_at_the_specified_directory_Colon_0, commandLine.options.project), /* host */ undefined); + reportDiagnostic(createCompilerDiagnostic(Diagnostics.Cannot_find_a_tsconfig_json_file_at_the_specified_directory_Colon_0, commandLine.options.project)); return sys.exit(ExitStatus.DiagnosticsPresent_OutputsSkipped); } } else { configFileName = fileOrDirectory; if (!sys.fileExists(configFileName)) { - reportDiagnostic(createCompilerDiagnostic(Diagnostics.The_specified_path_does_not_exist_Colon_0, commandLine.options.project), /* host */ undefined); + reportDiagnostic(createCompilerDiagnostic(Diagnostics.The_specified_path_does_not_exist_Colon_0, commandLine.options.project)); return sys.exit(ExitStatus.DiagnosticsPresent_OutputsSkipped); } } @@ -186,599 +122,70 @@ namespace ts { const commandLineOptions = commandLine.options; if (configFileName) { - const configParseResult = parseConfigFile(configFileName, commandLineOptions, sys); + const reportWatchDiagnostic = createWatchDiagnosticReporter(); + const configParseResult = parseConfigFile(configFileName, commandLineOptions, sys, reportDiagnostic, reportWatchDiagnostic); + udpateReportDiagnostic(configParseResult.options); if (isWatchSet(configParseResult.options)) { reportWatchModeWithoutSysSupport(); - createWatchModeWithConfigFile(configParseResult, commandLineOptions, sys); + createWatchModeWithConfigFile(configParseResult, commandLineOptions, createWatchingSystemHost(reportWatchDiagnostic)); } else { performCompilation(configParseResult.fileNames, configParseResult.options); } } else { - if (isWatchSet(commandLine.options)) { + udpateReportDiagnostic(commandLineOptions); + if (isWatchSet(commandLineOptions)) { reportWatchModeWithoutSysSupport(); - createWatchModeWithoutConfigFile(commandLine.fileNames, commandLineOptions, sys); + createWatchModeWithoutConfigFile(commandLine.fileNames, commandLineOptions, createWatchingSystemHost()); } else { performCompilation(commandLine.fileNames, commandLineOptions); } } - - function reportWatchModeWithoutSysSupport() { - if (!sys.watchFile || !sys.watchDirectory) { - reportDiagnostic(createCompilerDiagnostic(Diagnostics.The_current_host_does_not_support_the_0_option, "--watch"), /* host */ undefined); - sys.exit(ExitStatus.DiagnosticsPresent_OutputsSkipped); - } - } - - function performCompilation(rootFileNames: string[], compilerOptions: CompilerOptions) { - if (compilerOptions.pretty) { - reportDiagnosticWorker = reportDiagnosticWithColorAndContext; - } - - const compilerHost = createCompilerHost(compilerOptions); - const compileResult = compile(rootFileNames, compilerOptions, compilerHost, sys); - return sys.exit(compileResult.exitStatus); - } - } - - interface HostFileInfo { - version: number; - sourceFile: SourceFile; - fileWatcher: FileWatcher; - } - - /* @internal */ - export function createWatchModeWithConfigFile(configParseResult: ParsedCommandLine, optionsToExtend: CompilerOptions, system: System) { - return createWatchMode(configParseResult.fileNames, configParseResult.options, system, configParseResult.options.configFilePath, configParseResult.configFileSpecs, configParseResult.wildcardDirectories, optionsToExtend); } - /* @internal */ - export function createWatchModeWithoutConfigFile(rootFileNames: string[], compilerOptions: CompilerOptions, system: System) { - return createWatchMode(rootFileNames, compilerOptions, system); - } - - function createWatchMode(rootFileNames: string[], compilerOptions: CompilerOptions, system: System, configFileName?: string, configFileSpecs?: ConfigFileSpecs, configFileWildCardDirectories?: MapLike, optionsToExtendForConfigFile?: CompilerOptions) { - let program: Program; - let needsReload: boolean; // true if the config file changed and needs to reload it from the disk - let missingFilesMap: Map; // Map of file watchers for the missing files - let configFileWatcher: FileWatcher; // watcher for the config file - let watchedWildCardDirectories: Map; // map of watchers for the wild card directories in the config file - let timerToUpdateProgram: any; // timer callback to recompile the program - - const sourceFilesCache = createMap(); // Cache that stores the source file and version info - - let host: System; - if (configFileName) { - host = createCachedSystem(system); - configFileWatcher = system.watchFile(configFileName, onConfigFileChanged); - } - else { - host = system; - } - const currentDirectory = host.getCurrentDirectory(); - const getCanonicalFileName = createGetCanonicalFileName(host.useCaseSensitiveFileNames); - - // There is no extra check needed since we can just rely on the program to decide emit - const builder = createBuilder(getCanonicalFileName, getFileEmitOutput, computeHash, _sourceFile => true); - - if (compilerOptions.pretty) { - reportDiagnosticWorker = reportDiagnosticWithColorAndContext; - } - - synchronizeProgram(); - - // Update the wild card directory watch - watchConfigFileWildCardDirectories(); - - return () => program; - - function synchronizeProgram() { - writeLog(`Synchronizing program`); - - if (isProgramUptoDate(program, rootFileNames, compilerOptions, getSourceVersion, fileExists)) { - return; - } - - // Create the compiler host - const compilerHost = createWatchedCompilerHost(compilerOptions); - program = compile(rootFileNames, compilerOptions, compilerHost, system, program).program; - - // Update watches - missingFilesMap = updateMissingFilePathsWatch(program, missingFilesMap, watchMissingFilePath, closeMissingFilePathWatcher); - - reportWatchDiagnostic(createCompilerDiagnostic(Diagnostics.Compilation_complete_Watching_for_file_changes), system); - } - - function createWatchedCompilerHost(options: CompilerOptions): CompilerHost { - const newLine = getNewLineCharacter(options, system); - const realpath = host.realpath && ((path: string) => host.realpath(path)); - - return { - getSourceFile: getVersionedSourceFile, - getSourceFileByPath: getVersionedSourceFileByPath, - getDefaultLibLocation, - getDefaultLibFileName: options => combinePaths(getDefaultLibLocation(), getDefaultLibFileName(options)), - writeFile: (_fileName, _data, _writeByteOrderMark, _onError?, _sourceFiles?) => { }, - getCurrentDirectory: memoize(() => host.getCurrentDirectory()), - useCaseSensitiveFileNames: () => host.useCaseSensitiveFileNames, - getCanonicalFileName, - getNewLine: () => newLine, - fileExists, - readFile: fileName => host.readFile(fileName), - trace: (s: string) => host.write(s + newLine), - directoryExists: directoryName => host.directoryExists(directoryName), - getEnvironmentVariable: name => host.getEnvironmentVariable ? host.getEnvironmentVariable(name) : "", - getDirectories: (path: string) => host.getDirectories(path), - realpath, - onReleaseOldSourceFile, - emitWithBuilder - }; - - // TODO: cache module resolution - // if (host.resolveModuleNames) { - // compilerHost.resolveModuleNames = (moduleNames, containingFile) => host.resolveModuleNames(moduleNames, containingFile); - // } - // if (host.resolveTypeReferenceDirectives) { - // compilerHost.resolveTypeReferenceDirectives = (typeReferenceDirectiveNames, containingFile) => { - // return host.resolveTypeReferenceDirectives(typeReferenceDirectiveNames, containingFile); - // }; - // } - - function ensureDirectoriesExist(directoryPath: string) { - if (directoryPath.length > getRootLength(directoryPath) && !host.directoryExists(directoryPath)) { - const parentDirectory = getDirectoryPath(directoryPath); - ensureDirectoriesExist(parentDirectory); - host.createDirectory(directoryPath); - } - } - - function writeFile(fileName: string, data: string, writeByteOrderMark: boolean) { - try { - performance.mark("beforeIOWrite"); - ensureDirectoriesExist(getDirectoryPath(normalizePath(fileName))); - - host.writeFile(fileName, data, writeByteOrderMark); - - performance.mark("afterIOWrite"); - performance.measure("I/O Write", "beforeIOWrite", "afterIOWrite"); - } - catch (e) { - return createCompilerDiagnostic(Diagnostics.Could_not_write_file_0_Colon_1, fileName, e); - } - } - - function emitWithBuilder(program: Program): EmitResult { - builder.onProgramUpdateGraph(program); - const emittedFiles: string[] = program.getCompilerOptions().listEmittedFiles ? [] : undefined; - let sourceMaps: SourceMapData[]; - let emitSkipped: boolean; - let diagnostics: Diagnostic[]; - - const result = builder.emitChangedFiles(program); - switch (result.length) { - case 0: - emitSkipped = true; - break; - case 1: - const emitOutput = result[0]; - ({ diagnostics, sourceMaps, emitSkipped } = emitOutput); - writeOutputFiles(emitOutput.outputFiles); - break; - default: - for (const emitOutput of result) { - if (emitOutput.emitSkipped) { - emitSkipped = true; - } - diagnostics = concatenate(diagnostics, emitOutput.diagnostics); - sourceMaps = concatenate(sourceMaps, emitOutput.sourceMaps); - writeOutputFiles(emitOutput.outputFiles); - } - } - - return { emitSkipped, diagnostics: diagnostics || [], emittedFiles, sourceMaps }; - - function writeOutputFiles(outputFiles: OutputFile[]) { - if (outputFiles) { - for (const outputFile of outputFiles) { - const error = writeFile(outputFile.name, outputFile.text, outputFile.writeByteOrderMark); - if (error) { - diagnostics.push(error); - } - if (emittedFiles) { - emittedFiles.push(outputFile.name); - } - } - } - } - } - } - - function fileExists(fileName: string) { - const path = toPath(fileName, currentDirectory, getCanonicalFileName); - const hostSourceFileInfo = sourceFilesCache.get(path); - if (hostSourceFileInfo !== undefined) { - return !isString(hostSourceFileInfo); - } - - return host.fileExists(fileName); - } - - function getDefaultLibLocation(): string { - return getDirectoryPath(normalizePath(host.getExecutingFilePath())); - } - - function getVersionedSourceFile(fileName: string, languageVersion: ScriptTarget, onError?: (message: string) => void, shouldCreateNewSourceFile?: boolean): SourceFile { - return getVersionedSourceFileByPath(fileName, toPath(fileName, currentDirectory, getCanonicalFileName), languageVersion, onError, shouldCreateNewSourceFile); - } - - function getVersionedSourceFileByPath(fileName: string, path: Path, languageVersion: ScriptTarget, onError?: (message: string) => void, shouldCreateNewSourceFile?: boolean): SourceFile { - const hostSourceFile = sourceFilesCache.get(path); - // No source file on the host - if (isString(hostSourceFile)) { - return undefined; - } - - // Create new source file if requested or the versions dont match - if (!hostSourceFile || shouldCreateNewSourceFile || hostSourceFile.version.toString() !== hostSourceFile.sourceFile.version) { - const sourceFile = getNewSourceFile(); - if (hostSourceFile) { - if (shouldCreateNewSourceFile) { - hostSourceFile.version++; - } - if (sourceFile) { - hostSourceFile.sourceFile = sourceFile; - sourceFile.version = hostSourceFile.version.toString(); - if (!hostSourceFile.fileWatcher) { - hostSourceFile.fileWatcher = watchSourceFileForChanges(path); - } - } - else { - // There is no source file on host any more, close the watch, missing file paths will track it - hostSourceFile.fileWatcher.close(); - sourceFilesCache.set(path, hostSourceFile.version.toString()); - } - } - else { - let fileWatcher: FileWatcher; - if (sourceFile) { - sourceFile.version = "0"; - fileWatcher = watchSourceFileForChanges(path); - sourceFilesCache.set(path, { sourceFile, version: 0, fileWatcher }); - } - else { - sourceFilesCache.set(path, "0"); - } - } - return sourceFile; - } - return hostSourceFile.sourceFile; - - function getNewSourceFile() { - let text: string; - try { - performance.mark("beforeIORead"); - text = host.readFile(fileName, compilerOptions.charset); - performance.mark("afterIORead"); - performance.measure("I/O Read", "beforeIORead", "afterIORead"); - } - catch (e) { - if (onError) { - onError(e.message); - } - } - - return text !== undefined ? createSourceFile(fileName, text, languageVersion) : undefined; - } - } - - function removeSourceFile(path: Path) { - const hostSourceFile = sourceFilesCache.get(path); - if (hostSourceFile !== undefined) { - if (!isString(hostSourceFile)) { - hostSourceFile.fileWatcher.close(); - } - sourceFilesCache.delete(path); - } - } - - function getSourceVersion(path: Path): string { - const hostSourceFile = sourceFilesCache.get(path); - return !hostSourceFile || isString(hostSourceFile) ? undefined : hostSourceFile.version.toString(); - } - - function onReleaseOldSourceFile(oldSourceFile: SourceFile, _oldOptions: CompilerOptions) { - const hostSourceFileInfo = sourceFilesCache.get(oldSourceFile.path); - // If this is the source file thats in the cache and new program doesnt need it, - // remove the cached entry. - // Note we arent deleting entry if file became missing in new program or - // there was version update and new source file was created. - if (hostSourceFileInfo && !isString(hostSourceFileInfo) && hostSourceFileInfo.sourceFile === oldSourceFile) { - sourceFilesCache.delete(oldSourceFile.path); - } - } - - // 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. - function scheduleProgramUpdate() { - if (!system.setTimeout || !system.clearTimeout) { - return; - } - - if (timerToUpdateProgram) { - system.clearTimeout(timerToUpdateProgram); - } - timerToUpdateProgram = system.setTimeout(updateProgram, 250); - } - - function scheduleProgramReload() { - Debug.assert(!!configFileName); - needsReload = true; - scheduleProgramUpdate(); - } - - function updateProgram() { - timerToUpdateProgram = undefined; - reportWatchDiagnostic(createCompilerDiagnostic(Diagnostics.File_change_detected_Starting_incremental_compilation), system); - - if (needsReload) { - reloadConfigFile(); - } - else { - synchronizeProgram(); - } - } - - function reloadConfigFile() { - writeLog(`Reloading config file: ${configFileName}`); - needsReload = false; - - const cachedHost = host as CachedSystem; - cachedHost.clearCache(); - const configParseResult = parseConfigFile(configFileName, optionsToExtendForConfigFile, cachedHost); - rootFileNames = configParseResult.fileNames; - compilerOptions = configParseResult.options; - configFileSpecs = configParseResult.configFileSpecs; - configFileWildCardDirectories = configParseResult.wildcardDirectories; - - synchronizeProgram(); - - // Update the wild card directory watch - watchConfigFileWildCardDirectories(); - } - - function watchSourceFileForChanges(path: Path) { - return host.watchFile(path, (fileName, eventKind) => onSourceFileChange(fileName, path, eventKind)); - } - - function onSourceFileChange(fileName: string, path: Path, eventKind: FileWatcherEventKind) { - writeLog(`Source file path : ${path} changed: ${FileWatcherEventKind[eventKind]}, fileName: ${fileName}`); - - updateCachedSystem(fileName, path); - const hostSourceFile = sourceFilesCache.get(path); - if (hostSourceFile) { - // Update the cache - if (eventKind === FileWatcherEventKind.Deleted) { - if (!isString(hostSourceFile)) { - hostSourceFile.fileWatcher.close(); - sourceFilesCache.set(path, (hostSourceFile.version++).toString()); - } - } - else { - // Deleted file created - if (isString(hostSourceFile)) { - sourceFilesCache.delete(path); - } - else { - // file changed - just update the version - hostSourceFile.version++; - } - } - } - - // Update the program - scheduleProgramUpdate(); - } - - function updateCachedSystem(fileName: string, path: Path) { - if (configFileName) { - const absoluteNormalizedPath = getNormalizedAbsolutePath(fileName, getDirectoryPath(path)); - (host as CachedSystem).addOrDeleteFileOrFolder(normalizePath(absoluteNormalizedPath)); - } - } - - function watchMissingFilePath(missingFilePath: Path) { - return host.watchFile(missingFilePath, (fileName, eventKind) => onMissingFileChange(fileName, missingFilePath, eventKind)); - } - - function closeMissingFilePathWatcher(_missingFilePath: Path, fileWatcher: FileWatcher) { - fileWatcher.close(); - } - - function onMissingFileChange(filename: string, missingFilePath: Path, eventKind: FileWatcherEventKind) { - writeLog(`Missing file path : ${missingFilePath} changed: ${FileWatcherEventKind[eventKind]}, fileName: ${filename}`); - if (eventKind === FileWatcherEventKind.Created && missingFilesMap.has(missingFilePath)) { - closeMissingFilePathWatcher(missingFilePath, missingFilesMap.get(missingFilePath)); - missingFilesMap.delete(missingFilePath); - - updateCachedSystem(filename, missingFilePath); - - // Delete the entry in the source files cache so that new source file is created - removeSourceFile(missingFilePath); - - // When a missing file is created, we should update the graph. - scheduleProgramUpdate(); - } - } - - function watchConfigFileWildCardDirectories() { - const wildcards = createMapFromTemplate(configFileWildCardDirectories); - watchedWildCardDirectories = updateWatchingWildcardDirectories( - watchedWildCardDirectories, wildcards, - watchWildCardDirectory, stopWatchingWildCardDirectory - ); - } - - function watchWildCardDirectory(directory: string, recursive: boolean) { - return host.watchDirectory(directory, fileName => - onFileAddOrRemoveInWatchedDirectory(getNormalizedAbsolutePath(fileName, directory)), - recursive); - } - - function stopWatchingWildCardDirectory(_directory: string, fileWatcher: FileWatcher, _recursive: boolean, _recursiveChanged: boolean) { - fileWatcher.close(); - } - - function onFileAddOrRemoveInWatchedDirectory(fileName: string) { - Debug.assert(!!configFileName); - - const path = toPath(fileName, currentDirectory, getCanonicalFileName); - - // Since the file existance changed, update the sourceFiles cache - updateCachedSystem(fileName, path); - removeSourceFile(path); - - // If a change was made inside "folder/file", node will trigger the callback twice: - // one with the fileName being "folder/file", and the other one with "folder". - // We don't respond to the second one. - if (fileName && !isSupportedSourceFileName(fileName, compilerOptions)) { - writeLog(`Project: ${configFileName} Detected file add/remove of non supported extension: ${fileName}`); - return; - } - - writeLog(`Project: ${configFileName} Detected file add/remove of supported extension: ${fileName}`); - - // Reload is pending, do the reload - if (!needsReload) { - const result = getFileNamesFromConfigSpecs(configFileSpecs, getDirectoryPath(configFileName), compilerOptions, host, /*extraFileExtensions*/ []); - if (!configFileSpecs.filesSpecs) { - reportDiagnostics([getErrorForNoInputFiles(configFileSpecs, configFileName)], getDefaultFormatDiagnosticsHost(system)); - } - rootFileNames = result.fileNames; - - // Schedule Update the program - scheduleProgramUpdate(); - } - } - - function onConfigFileChanged(fileName: string, eventKind: FileWatcherEventKind) { - writeLog(`Config file : ${configFileName} changed: ${FileWatcherEventKind[eventKind]}, fileName: ${fileName}`); - scheduleProgramReload(); + function reportWatchModeWithoutSysSupport() { + if (!sys.watchFile || !sys.watchDirectory) { + reportDiagnostic(createCompilerDiagnostic(Diagnostics.The_current_host_does_not_support_the_0_option, "--watch")); + sys.exit(ExitStatus.DiagnosticsPresent_OutputsSkipped); } + } - function writeLog(s: string) { - const hasDiagnostics = compilerOptions.diagnostics || compilerOptions.extendedDiagnostics; - if (hasDiagnostics) { - host.write(s); - } - } + function performCompilation(rootFileNames: string[], compilerOptions: CompilerOptions) { + const compilerHost = createCompilerHost(compilerOptions); + enableStatistics(compilerOptions); - function computeHash(data: string) { - return system.createHash ? system.createHash(data) : data; - } - } + const program = createProgram(rootFileNames, compilerOptions, compilerHost); + const exitStatus = compileProgram(sys, program, () => program.emit(), reportDiagnostic); - interface CachedSystem extends System { - addOrDeleteFileOrFolder(fileOrFolder: string): void; - clearCache(): void; + reportStatistics(program); + return sys.exit(exitStatus); } - function createCachedSystem(host: System): CachedSystem { - const getFileSize = host.getFileSize ? (path: string) => host.getFileSize(path) : undefined; - const watchFile = host.watchFile ? (path: string, callback: FileWatcherCallback, pollingInterval?: number) => host.watchFile(path, callback, pollingInterval) : undefined; - const watchDirectory = host.watchDirectory ? (path: string, callback: DirectoryWatcherCallback, recursive?: boolean) => host.watchDirectory(path, callback, recursive) : undefined; - const getModifiedTime = host.getModifiedTime ? (path: string) => host.getModifiedTime(path) : undefined; - const createHash = host.createHash ? (data: string) => host.createHash(data) : undefined; - const getMemoryUsage = host.getMemoryUsage ? () => host.getMemoryUsage() : undefined; - const realpath = host.realpath ? (path: string) => host.realpath(path) : undefined; - const tryEnableSourceMapsForHost = host.tryEnableSourceMapsForHost ? () => host.tryEnableSourceMapsForHost() : undefined; - const setTimeout = host.setTimeout ? (callback: (...args: any[]) => void, ms: number, ...args: any[]) => host.setTimeout(callback, ms, ...args) : undefined; - const clearTimeout = host.clearTimeout ? (timeoutId: any) => host.clearTimeout(timeoutId) : undefined; - - const cachedHost = createCachedHost(host); - return { - args: host.args, - newLine: host.newLine, - useCaseSensitiveFileNames: host.useCaseSensitiveFileNames, - write: s => host.write(s), - readFile: (path, encoding?) => host.readFile(path, encoding), - getFileSize, - writeFile: (fileName, data, writeByteOrderMark?) => cachedHost.writeFile(fileName, data, writeByteOrderMark), - watchFile, - watchDirectory, - resolvePath: path => host.resolvePath(path), - fileExists: fileName => cachedHost.fileExists(fileName), - directoryExists: dir => cachedHost.directoryExists(dir), - createDirectory: dir => cachedHost.createDirectory(dir), - getExecutingFilePath: () => host.getExecutingFilePath(), - getCurrentDirectory: () => cachedHost.getCurrentDirectory(), - getDirectories: dir => cachedHost.getDirectories(dir), - readDirectory: (path, extensions, excludes, includes, depth) => cachedHost.readDirectory(path, extensions, excludes, includes, depth), - getModifiedTime, - createHash, - getMemoryUsage, - exit: exitCode => host.exit(exitCode), - realpath, - getEnvironmentVariable: name => host.getEnvironmentVariable(name), - tryEnableSourceMapsForHost, - debugMode: host.debugMode, - setTimeout, - clearTimeout, - addOrDeleteFileOrFolder: fileOrFolder => cachedHost.addOrDeleteFileOrFolder(fileOrFolder), - clearCache: () => cachedHost.clearCache() + function createWatchingSystemHost(reportWatchDiagnostic?: DiagnosticReporter) { + const watchingHost = ts.createWatchingSystemHost(/*pretty*/ undefined, sys, parseConfigFile, reportDiagnostic, reportWatchDiagnostic); + watchingHost.beforeCompile = enableStatistics; + const afterCompile = watchingHost.afterCompile; + watchingHost.afterCompile = (host, program, builder) => { + afterCompile(host, program, builder); + reportStatistics(program); }; + return watchingHost; } - /* @internal */ - export function parseConfigFile(configFileName: string, optionsToExtend: CompilerOptions, system: System): ParsedCommandLine { - let configFileText: string; - try { - configFileText = system.readFile(configFileName); - } - catch (e) { - const error = createCompilerDiagnostic(Diagnostics.Cannot_read_file_0_Colon_1, configFileName, e.message); - reportWatchDiagnostic(error, system); - system.exit(ExitStatus.DiagnosticsPresent_OutputsSkipped); - return; - } - if (!configFileText) { - const error = createCompilerDiagnostic(Diagnostics.File_0_not_found, configFileName); - reportDiagnostics([error], getDefaultFormatDiagnosticsHost(system)); - system.exit(ExitStatus.DiagnosticsPresent_OutputsSkipped); - return; + function enableStatistics(compilerOptions: CompilerOptions) { + if (compilerOptions.diagnostics || compilerOptions.extendedDiagnostics) { + performance.enable(); } - - const result = parseJsonText(configFileName, configFileText); - reportDiagnostics(result.parseDiagnostics, getDefaultFormatDiagnosticsHost(system)); - - const cwd = system.getCurrentDirectory(); - const configParseResult = parseJsonSourceFileConfigFileContent(result, system, getNormalizedAbsolutePath(getDirectoryPath(configFileName), cwd), optionsToExtend, getNormalizedAbsolutePath(configFileName, cwd)); - reportDiagnostics(configParseResult.errors, getDefaultFormatDiagnosticsHost(system)); - - return configParseResult; } - function compile(fileNames: string[], compilerOptions: CompilerOptions, compilerHost: CompilerHost, system: System, oldProgram?: Program) { - const hasDiagnostics = compilerOptions.diagnostics || compilerOptions.extendedDiagnostics; + function reportStatistics(program: Program) { let statistics: Statistic[]; - if (hasDiagnostics) { - performance.enable(); + const compilerOptions = program.getCompilerOptions(); + if (compilerOptions.diagnostics || compilerOptions.extendedDiagnostics) { statistics = []; - } - - const program = createProgram(fileNames, compilerOptions, compilerHost, oldProgram); - const exitStatus = compileProgram(); - - if (compilerOptions.listFiles) { - forEach(program.getSourceFiles(), file => { - system.write(file.fileName + system.newLine); - }); - } - - if (hasDiagnostics) { - const memoryUsed = system.getMemoryUsage ? system.getMemoryUsage() : -1; + const memoryUsed = sys.getMemoryUsage ? sys.getMemoryUsage() : -1; reportCountStatistic("Files", program.getSourceFiles().length); reportCountStatistic("Lines", countLines(program)); reportCountStatistic("Nodes", program.getNodeCount()); @@ -815,50 +222,6 @@ namespace ts { performance.disable(); } - return { program, exitStatus }; - - function compileProgram(): ExitStatus { - let diagnostics: Diagnostic[]; - - // First get and report any syntactic errors. - diagnostics = program.getSyntacticDiagnostics(); - - // If we didn't have any syntactic errors, then also try getting the global and - // semantic errors. - if (diagnostics.length === 0) { - diagnostics = program.getOptionsDiagnostics().concat(program.getGlobalDiagnostics()); - - if (diagnostics.length === 0) { - diagnostics = program.getSemanticDiagnostics(); - } - } - - // Emit and report any errors we ran into. - let emitOutput: EmitResult; - if (compilerHost.emitWithBuilder) { - emitOutput = compilerHost.emitWithBuilder(program); - } - else { - // Emit whole program - emitOutput = program.emit(); - } - diagnostics = diagnostics.concat(emitOutput.diagnostics); - - reportDiagnostics(sortAndDeduplicateDiagnostics(diagnostics), getDefaultFormatDiagnosticsHost(system)); - - reportEmittedFiles(emitOutput.emittedFiles, system); - if (emitOutput.emitSkipped && diagnostics.length > 0) { - // If the emitter didn't emit anything, then pass that value along. - return ExitStatus.DiagnosticsPresent_OutputsSkipped; - } - else if (diagnostics.length > 0) { - // The emitter emitted something, inform the caller if that happened in the presence - // of diagnostics or not. - return ExitStatus.DiagnosticsPresent_OutputsGenerated; - } - return ExitStatus.Success; - } - function reportStatistics() { let nameSize = 0; let valueSize = 0; @@ -873,7 +236,7 @@ namespace ts { } for (const { name, value } of statistics) { - system.write(padRight(name + ":", nameSize + 2) + padLeft(value.toString(), valueSize) + system.newLine); + sys.write(padRight(name + ":", nameSize + 2) + padLeft(value.toString(), valueSize) + sys.newLine); } } @@ -1012,11 +375,11 @@ namespace ts { const currentDirectory = sys.getCurrentDirectory(); const file = normalizePath(combinePaths(currentDirectory, "tsconfig.json")); if (sys.fileExists(file)) { - reportDiagnostic(createCompilerDiagnostic(Diagnostics.A_tsconfig_json_file_is_already_defined_at_Colon_0, file), /* host */ undefined); + reportDiagnostic(createCompilerDiagnostic(Diagnostics.A_tsconfig_json_file_is_already_defined_at_Colon_0, file)); } else { sys.writeFile(file, generateTSConfig(options, fileNames, sys.newLine)); - reportDiagnostic(createCompilerDiagnostic(Diagnostics.Successfully_created_a_tsconfig_json_file), /* host */ undefined); + reportDiagnostic(createCompilerDiagnostic(Diagnostics.Successfully_created_a_tsconfig_json_file)); } return; diff --git a/src/compiler/tsconfig.json b/src/compiler/tsconfig.json index 360819f2b3659..fd92a6008cdce 100644 --- a/src/compiler/tsconfig.json +++ b/src/compiler/tsconfig.json @@ -37,6 +37,7 @@ "emitter.ts", "program.ts", "builder.ts", + "watchedProgram.ts", "commandLineParser.ts", "tscLib.ts", "tsc.ts", diff --git a/src/compiler/watchedProgram.ts b/src/compiler/watchedProgram.ts new file mode 100644 index 0000000000000..22625282a8de3 --- /dev/null +++ b/src/compiler/watchedProgram.ts @@ -0,0 +1,673 @@ +/// +/// + +namespace ts { + export type DiagnosticReporter = (diagnostic: Diagnostic) => void; + export type DiagnosticWorker = (diagnostic: Diagnostic, host: FormatDiagnosticsHost, system: System) => void; + export type ParseConfigFile = (configFileName: string, optionsToExtend: CompilerOptions, system: System, reportDiagnostic: DiagnosticReporter, reportWatchDiagnostic: DiagnosticReporter) => ParsedCommandLine; + export interface WatchingSystemHost { + // FS system to use + system: System; + + // parse config file + parseConfigFile: ParseConfigFile; + + // Reporting errors + reportDiagnostic: DiagnosticReporter; + reportWatchDiagnostic: DiagnosticReporter; + + // Callbacks to do custom action before creating program and after creating program + beforeCompile(compilerOptions: CompilerOptions): void; + afterCompile(host: System, program: Program, builder: Builder): void; + } + + const defaultFormatDiagnosticsHost: FormatDiagnosticsHost = sys ? { + getCurrentDirectory: () => sys.getCurrentDirectory(), + getNewLine: () => sys.newLine, + getCanonicalFileName: createGetCanonicalFileName(sys.useCaseSensitiveFileNames) + } : undefined; + + export function createDiagnosticReporter(system = sys, worker = reportDiagnosticSimply, formatDiagnosticsHost?: FormatDiagnosticsHost): DiagnosticReporter { + return diagnostic => worker(diagnostic, getFormatDiagnosticsHost(), system); + + function getFormatDiagnosticsHost() { + return formatDiagnosticsHost || (formatDiagnosticsHost = system === sys ? defaultFormatDiagnosticsHost : { + getCurrentDirectory: () => system.getCurrentDirectory(), + getNewLine: () => system.newLine, + getCanonicalFileName: createGetCanonicalFileName(system.useCaseSensitiveFileNames), + }); + } + } + + export function createWatchDiagnosticReporter(system = sys): DiagnosticReporter { + return diagnostic => { + let output = new Date().toLocaleTimeString() + " - "; + output += `${flattenDiagnosticMessageText(diagnostic.messageText, system.newLine)}${system.newLine + system.newLine + system.newLine}`; + system.write(output); + }; + } + + export function reportDiagnostics(diagnostics: Diagnostic[], reportDiagnostic: DiagnosticReporter): void { + for (const diagnostic of diagnostics) { + reportDiagnostic(diagnostic); + } + } + + export function reportDiagnosticSimply(diagnostic: Diagnostic, host: FormatDiagnosticsHost, system: System): void { + system.write(ts.formatDiagnostic(diagnostic, host)); + } + + export function reportDiagnosticWithColorAndContext(diagnostic: Diagnostic, host: FormatDiagnosticsHost, system: System): void { + system.write(ts.formatDiagnosticsWithColorAndContext([diagnostic], host) + host.getNewLine()); + } + + export function parseConfigFile(configFileName: string, optionsToExtend: CompilerOptions, system: System, reportDiagnostic: DiagnosticReporter, reportWatchDiagnostic: DiagnosticReporter): ParsedCommandLine { + let configFileText: string; + try { + configFileText = system.readFile(configFileName); + } + catch (e) { + const error = createCompilerDiagnostic(Diagnostics.Cannot_read_file_0_Colon_1, configFileName, e.message); + reportWatchDiagnostic(error); + system.exit(ExitStatus.DiagnosticsPresent_OutputsSkipped); + return; + } + if (!configFileText) { + const error = createCompilerDiagnostic(Diagnostics.File_0_not_found, configFileName); + reportDiagnostics([error], reportDiagnostic); + system.exit(ExitStatus.DiagnosticsPresent_OutputsSkipped); + return; + } + + const result = parseJsonText(configFileName, configFileText); + reportDiagnostics(result.parseDiagnostics, reportDiagnostic); + + const cwd = system.getCurrentDirectory(); + const configParseResult = parseJsonSourceFileConfigFileContent(result, system, getNormalizedAbsolutePath(getDirectoryPath(configFileName), cwd), optionsToExtend, getNormalizedAbsolutePath(configFileName, cwd)); + reportDiagnostics(configParseResult.errors, reportDiagnostic); + + return configParseResult; + } + + function reportEmittedFiles(files: string[], system: System): void { + if (!files || files.length === 0) { + return; + } + const currentDir = system.getCurrentDirectory(); + for (const file of files) { + const filepath = getNormalizedAbsolutePath(file, currentDir); + system.write(`TSFILE: ${filepath}${system.newLine}`); + } + } + + export function compileProgram(system: System, program: Program, emitProgram: () => EmitResult, + reportDiagnostic: DiagnosticReporter): ExitStatus { + let diagnostics: Diagnostic[]; + + // First get and report any syntactic errors. + diagnostics = program.getSyntacticDiagnostics(); + + // If we didn't have any syntactic errors, then also try getting the global and + // semantic errors. + if (diagnostics.length === 0) { + diagnostics = program.getOptionsDiagnostics().concat(program.getGlobalDiagnostics()); + + if (diagnostics.length === 0) { + diagnostics = program.getSemanticDiagnostics(); + } + } + + // Emit and report any errors we ran into. + const emitOutput = emitProgram(); + diagnostics = diagnostics.concat(emitOutput.diagnostics); + + reportDiagnostics(sortAndDeduplicateDiagnostics(diagnostics), reportDiagnostic); + reportEmittedFiles(emitOutput.emittedFiles, system); + + if (program.getCompilerOptions().listFiles) { + forEach(program.getSourceFiles(), file => { + system.write(file.fileName + system.newLine); + }); + } + + if (emitOutput.emitSkipped && diagnostics.length > 0) { + // If the emitter didn't emit anything, then pass that value along. + return ExitStatus.DiagnosticsPresent_OutputsSkipped; + } + else if (diagnostics.length > 0) { + // The emitter emitted something, inform the caller if that happened in the presence + // of diagnostics or not. + return ExitStatus.DiagnosticsPresent_OutputsGenerated; + } + return ExitStatus.Success; + } + + function emitWatchedProgram(host: System, program: Program, builder: Builder) { + const emittedFiles: string[] = program.getCompilerOptions().listEmittedFiles ? [] : undefined; + let sourceMaps: SourceMapData[]; + let emitSkipped: boolean; + let diagnostics: Diagnostic[]; + + const result = builder.emitChangedFiles(program); + switch (result.length) { + case 0: + emitSkipped = true; + break; + case 1: + const emitOutput = result[0]; + ({ diagnostics, sourceMaps, emitSkipped } = emitOutput); + writeOutputFiles(emitOutput.outputFiles); + break; + default: + for (const emitOutput of result) { + if (emitOutput.emitSkipped) { + emitSkipped = true; + } + diagnostics = concatenate(diagnostics, emitOutput.diagnostics); + sourceMaps = concatenate(sourceMaps, emitOutput.sourceMaps); + writeOutputFiles(emitOutput.outputFiles); + } + } + + return { emitSkipped, diagnostics: diagnostics || [], emittedFiles, sourceMaps }; + + + function ensureDirectoriesExist(directoryPath: string) { + if (directoryPath.length > getRootLength(directoryPath) && !host.directoryExists(directoryPath)) { + const parentDirectory = getDirectoryPath(directoryPath); + ensureDirectoriesExist(parentDirectory); + host.createDirectory(directoryPath); + } + } + + function writeFile(fileName: string, data: string, writeByteOrderMark: boolean) { + try { + performance.mark("beforeIOWrite"); + ensureDirectoriesExist(getDirectoryPath(normalizePath(fileName))); + + host.writeFile(fileName, data, writeByteOrderMark); + + performance.mark("afterIOWrite"); + performance.measure("I/O Write", "beforeIOWrite", "afterIOWrite"); + } + catch (e) { + return createCompilerDiagnostic(Diagnostics.Could_not_write_file_0_Colon_1, fileName, e); + } + } + + function writeOutputFiles(outputFiles: OutputFile[]) { + if (outputFiles) { + for (const outputFile of outputFiles) { + const error = writeFile(outputFile.name, outputFile.text, outputFile.writeByteOrderMark); + if (error) { + diagnostics.push(error); + } + if (emittedFiles) { + emittedFiles.push(outputFile.name); + } + } + } + } + } + + export function createWatchingSystemHost(pretty?: DiagnosticStyle, system = sys, + parseConfigFile?: ParseConfigFile, reportDiagnostic?: DiagnosticReporter, + reportWatchDiagnostic?: DiagnosticReporter + ): WatchingSystemHost { + reportDiagnostic = reportDiagnostic || createDiagnosticReporter(system, pretty ? reportDiagnosticWithColorAndContext : reportDiagnosticSimply); + reportWatchDiagnostic = reportWatchDiagnostic || createWatchDiagnosticReporter(system); + parseConfigFile = parseConfigFile || ts.parseConfigFile; + return { + system, + parseConfigFile, + reportDiagnostic, + reportWatchDiagnostic, + beforeCompile: noop, + afterCompile: compileWatchedProgram, + }; + + function compileWatchedProgram(host: System, program: Program, builder: Builder) { + return compileProgram(system, program, () => emitWatchedProgram(host, program, builder), reportDiagnostic); + } + } + + export function createWatchModeWithConfigFile(configParseResult: ParsedCommandLine, optionsToExtend: CompilerOptions = {}, watchingHost?: WatchingSystemHost) { + return createWatchMode(configParseResult.fileNames, configParseResult.options, watchingHost, configParseResult.options.configFilePath, configParseResult.configFileSpecs, configParseResult.wildcardDirectories, optionsToExtend); + } + + export function createWatchModeWithoutConfigFile(rootFileNames: string[], compilerOptions: CompilerOptions, watchingHost?: WatchingSystemHost) { + return createWatchMode(rootFileNames, compilerOptions, watchingHost); + } + + interface HostFileInfo { + version: number; + sourceFile: SourceFile; + fileWatcher: FileWatcher; + } + + function createWatchMode(rootFileNames: string[], compilerOptions: CompilerOptions, watchingHost?: WatchingSystemHost, configFileName?: string, configFileSpecs?: ConfigFileSpecs, configFileWildCardDirectories?: MapLike, optionsToExtendForConfigFile?: CompilerOptions) { + let program: Program; + let needsReload: boolean; // true if the config file changed and needs to reload it from the disk + let missingFilesMap: Map; // Map of file watchers for the missing files + let configFileWatcher: FileWatcher; // watcher for the config file + let watchedWildCardDirectories: Map; // map of watchers for the wild card directories in the config file + let timerToUpdateProgram: any; // timer callback to recompile the program + + const sourceFilesCache = createMap(); // Cache that stores the source file and version info + watchingHost = watchingHost || createWatchingSystemHost(compilerOptions.pretty); + const { system, parseConfigFile, reportDiagnostic, reportWatchDiagnostic, beforeCompile, afterCompile } = watchingHost; + + let host: System; + if (configFileName) { + host = createCachedSystem(system); + configFileWatcher = system.watchFile(configFileName, onConfigFileChanged); + } + else { + host = system; + } + const currentDirectory = host.getCurrentDirectory(); + const getCanonicalFileName = createGetCanonicalFileName(host.useCaseSensitiveFileNames); + + // There is no extra check needed since we can just rely on the program to decide emit + const builder = createBuilder(getCanonicalFileName, getFileEmitOutput, computeHash, _sourceFile => true); + + synchronizeProgram(); + + // Update the wild card directory watch + watchConfigFileWildCardDirectories(); + + return () => program; + + function synchronizeProgram() { + writeLog(`Synchronizing program`); + + if (isProgramUptoDate(program, rootFileNames, compilerOptions, getSourceVersion, fileExists)) { + return; + } + + // Create the compiler host + const compilerHost = createWatchedCompilerHost(compilerOptions); + beforeCompile(compilerOptions); + + // Compile the program + program = createProgram(rootFileNames, compilerOptions, compilerHost, program); + builder.onProgramUpdateGraph(program); + + // Update watches + missingFilesMap = updateMissingFilePathsWatch(program, missingFilesMap, watchMissingFilePath, closeMissingFilePathWatcher); + + afterCompile(host, program, builder); + reportWatchDiagnostic(createCompilerDiagnostic(Diagnostics.Compilation_complete_Watching_for_file_changes)); + } + + function createWatchedCompilerHost(options: CompilerOptions): CompilerHost { + const newLine = getNewLineCharacter(options, system); + const realpath = host.realpath && ((path: string) => host.realpath(path)); + + return { + getSourceFile: getVersionedSourceFile, + getSourceFileByPath: getVersionedSourceFileByPath, + getDefaultLibLocation, + getDefaultLibFileName: options => combinePaths(getDefaultLibLocation(), getDefaultLibFileName(options)), + writeFile: (_fileName, _data, _writeByteOrderMark, _onError?, _sourceFiles?) => { }, + getCurrentDirectory: memoize(() => host.getCurrentDirectory()), + useCaseSensitiveFileNames: () => host.useCaseSensitiveFileNames, + getCanonicalFileName, + getNewLine: () => newLine, + fileExists, + readFile: fileName => host.readFile(fileName), + trace: (s: string) => host.write(s + newLine), + directoryExists: directoryName => host.directoryExists(directoryName), + getEnvironmentVariable: name => host.getEnvironmentVariable ? host.getEnvironmentVariable(name) : "", + getDirectories: (path: string) => host.getDirectories(path), + realpath, + onReleaseOldSourceFile, + }; + + // TODO: cache module resolution + // if (host.resolveModuleNames) { + // compilerHost.resolveModuleNames = (moduleNames, containingFile) => host.resolveModuleNames(moduleNames, containingFile); + // } + // if (host.resolveTypeReferenceDirectives) { + // compilerHost.resolveTypeReferenceDirectives = (typeReferenceDirectiveNames, containingFile) => { + // return host.resolveTypeReferenceDirectives(typeReferenceDirectiveNames, containingFile); + // }; + // } + } + + function fileExists(fileName: string) { + const path = toPath(fileName, currentDirectory, getCanonicalFileName); + const hostSourceFileInfo = sourceFilesCache.get(path); + if (hostSourceFileInfo !== undefined) { + return !isString(hostSourceFileInfo); + } + + return host.fileExists(fileName); + } + + function getDefaultLibLocation(): string { + return getDirectoryPath(normalizePath(host.getExecutingFilePath())); + } + + function getVersionedSourceFile(fileName: string, languageVersion: ScriptTarget, onError?: (message: string) => void, shouldCreateNewSourceFile?: boolean): SourceFile { + return getVersionedSourceFileByPath(fileName, toPath(fileName, currentDirectory, getCanonicalFileName), languageVersion, onError, shouldCreateNewSourceFile); + } + + function getVersionedSourceFileByPath(fileName: string, path: Path, languageVersion: ScriptTarget, onError?: (message: string) => void, shouldCreateNewSourceFile?: boolean): SourceFile { + const hostSourceFile = sourceFilesCache.get(path); + // No source file on the host + if (isString(hostSourceFile)) { + return undefined; + } + + // Create new source file if requested or the versions dont match + if (!hostSourceFile || shouldCreateNewSourceFile || hostSourceFile.version.toString() !== hostSourceFile.sourceFile.version) { + const sourceFile = getNewSourceFile(); + if (hostSourceFile) { + if (shouldCreateNewSourceFile) { + hostSourceFile.version++; + } + if (sourceFile) { + hostSourceFile.sourceFile = sourceFile; + sourceFile.version = hostSourceFile.version.toString(); + if (!hostSourceFile.fileWatcher) { + hostSourceFile.fileWatcher = watchSourceFileForChanges(path); + } + } + else { + // There is no source file on host any more, close the watch, missing file paths will track it + hostSourceFile.fileWatcher.close(); + sourceFilesCache.set(path, hostSourceFile.version.toString()); + } + } + else { + let fileWatcher: FileWatcher; + if (sourceFile) { + sourceFile.version = "0"; + fileWatcher = watchSourceFileForChanges(path); + sourceFilesCache.set(path, { sourceFile, version: 0, fileWatcher }); + } + else { + sourceFilesCache.set(path, "0"); + } + } + return sourceFile; + } + return hostSourceFile.sourceFile; + + function getNewSourceFile() { + let text: string; + try { + performance.mark("beforeIORead"); + text = host.readFile(fileName, compilerOptions.charset); + performance.mark("afterIORead"); + performance.measure("I/O Read", "beforeIORead", "afterIORead"); + } + catch (e) { + if (onError) { + onError(e.message); + } + } + + return text !== undefined ? createSourceFile(fileName, text, languageVersion) : undefined; + } + } + + function removeSourceFile(path: Path) { + const hostSourceFile = sourceFilesCache.get(path); + if (hostSourceFile !== undefined) { + if (!isString(hostSourceFile)) { + hostSourceFile.fileWatcher.close(); + } + sourceFilesCache.delete(path); + } + } + + function getSourceVersion(path: Path): string { + const hostSourceFile = sourceFilesCache.get(path); + return !hostSourceFile || isString(hostSourceFile) ? undefined : hostSourceFile.version.toString(); + } + + function onReleaseOldSourceFile(oldSourceFile: SourceFile, _oldOptions: CompilerOptions) { + const hostSourceFileInfo = sourceFilesCache.get(oldSourceFile.path); + // If this is the source file thats in the cache and new program doesnt need it, + // remove the cached entry. + // Note we arent deleting entry if file became missing in new program or + // there was version update and new source file was created. + if (hostSourceFileInfo && !isString(hostSourceFileInfo) && hostSourceFileInfo.sourceFile === oldSourceFile) { + sourceFilesCache.delete(oldSourceFile.path); + } + } + + // 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. + function scheduleProgramUpdate() { + if (!system.setTimeout || !system.clearTimeout) { + return; + } + + if (timerToUpdateProgram) { + system.clearTimeout(timerToUpdateProgram); + } + timerToUpdateProgram = system.setTimeout(updateProgram, 250); + } + + function scheduleProgramReload() { + Debug.assert(!!configFileName); + needsReload = true; + scheduleProgramUpdate(); + } + + function updateProgram() { + timerToUpdateProgram = undefined; + reportWatchDiagnostic(createCompilerDiagnostic(Diagnostics.File_change_detected_Starting_incremental_compilation)); + + if (needsReload) { + reloadConfigFile(); + } + else { + synchronizeProgram(); + } + } + + function reloadConfigFile() { + writeLog(`Reloading config file: ${configFileName}`); + needsReload = false; + + const cachedHost = host as CachedSystem; + cachedHost.clearCache(); + const configParseResult = parseConfigFile(configFileName, optionsToExtendForConfigFile, cachedHost, reportDiagnostic, reportWatchDiagnostic); + rootFileNames = configParseResult.fileNames; + compilerOptions = configParseResult.options; + configFileSpecs = configParseResult.configFileSpecs; + configFileWildCardDirectories = configParseResult.wildcardDirectories; + + synchronizeProgram(); + + // Update the wild card directory watch + watchConfigFileWildCardDirectories(); + } + + function watchSourceFileForChanges(path: Path) { + return host.watchFile(path, (fileName, eventKind) => onSourceFileChange(fileName, path, eventKind)); + } + + function onSourceFileChange(fileName: string, path: Path, eventKind: FileWatcherEventKind) { + writeLog(`Source file path : ${path} changed: ${FileWatcherEventKind[eventKind]}, fileName: ${fileName}`); + + updateCachedSystem(fileName, path); + const hostSourceFile = sourceFilesCache.get(path); + if (hostSourceFile) { + // Update the cache + if (eventKind === FileWatcherEventKind.Deleted) { + if (!isString(hostSourceFile)) { + hostSourceFile.fileWatcher.close(); + sourceFilesCache.set(path, (hostSourceFile.version++).toString()); + } + } + else { + // Deleted file created + if (isString(hostSourceFile)) { + sourceFilesCache.delete(path); + } + else { + // file changed - just update the version + hostSourceFile.version++; + } + } + } + + // Update the program + scheduleProgramUpdate(); + } + + function updateCachedSystem(fileName: string, path: Path) { + if (configFileName) { + const absoluteNormalizedPath = getNormalizedAbsolutePath(fileName, getDirectoryPath(path)); + (host as CachedSystem).addOrDeleteFileOrFolder(normalizePath(absoluteNormalizedPath)); + } + } + + function watchMissingFilePath(missingFilePath: Path) { + return host.watchFile(missingFilePath, (fileName, eventKind) => onMissingFileChange(fileName, missingFilePath, eventKind)); + } + + function closeMissingFilePathWatcher(_missingFilePath: Path, fileWatcher: FileWatcher) { + fileWatcher.close(); + } + + function onMissingFileChange(filename: string, missingFilePath: Path, eventKind: FileWatcherEventKind) { + writeLog(`Missing file path : ${missingFilePath} changed: ${FileWatcherEventKind[eventKind]}, fileName: ${filename}`); + if (eventKind === FileWatcherEventKind.Created && missingFilesMap.has(missingFilePath)) { + closeMissingFilePathWatcher(missingFilePath, missingFilesMap.get(missingFilePath)); + missingFilesMap.delete(missingFilePath); + + updateCachedSystem(filename, missingFilePath); + + // Delete the entry in the source files cache so that new source file is created + removeSourceFile(missingFilePath); + + // When a missing file is created, we should update the graph. + scheduleProgramUpdate(); + } + } + + function watchConfigFileWildCardDirectories() { + const wildcards = createMapFromTemplate(configFileWildCardDirectories); + watchedWildCardDirectories = updateWatchingWildcardDirectories( + watchedWildCardDirectories, wildcards, + watchWildCardDirectory, stopWatchingWildCardDirectory + ); + } + + function watchWildCardDirectory(directory: string, recursive: boolean) { + return host.watchDirectory(directory, fileName => + onFileAddOrRemoveInWatchedDirectory(getNormalizedAbsolutePath(fileName, directory)), + recursive); + } + + function stopWatchingWildCardDirectory(_directory: string, fileWatcher: FileWatcher, _recursive: boolean, _recursiveChanged: boolean) { + fileWatcher.close(); + } + + function onFileAddOrRemoveInWatchedDirectory(fileName: string) { + Debug.assert(!!configFileName); + + const path = toPath(fileName, currentDirectory, getCanonicalFileName); + + // Since the file existance changed, update the sourceFiles cache + updateCachedSystem(fileName, path); + removeSourceFile(path); + + // If a change was made inside "folder/file", node will trigger the callback twice: + // one with the fileName being "folder/file", and the other one with "folder". + // We don't respond to the second one. + if (fileName && !isSupportedSourceFileName(fileName, compilerOptions)) { + writeLog(`Project: ${configFileName} Detected file add/remove of non supported extension: ${fileName}`); + return; + } + + writeLog(`Project: ${configFileName} Detected file add/remove of supported extension: ${fileName}`); + + // Reload is pending, do the reload + if (!needsReload) { + const result = getFileNamesFromConfigSpecs(configFileSpecs, getDirectoryPath(configFileName), compilerOptions, host, /*extraFileExtension*/ []); + if (!configFileSpecs.filesSpecs) { + reportDiagnostic(getErrorForNoInputFiles(configFileSpecs, configFileName)); + } + rootFileNames = result.fileNames; + + // Schedule Update the program + scheduleProgramUpdate(); + } + } + + function onConfigFileChanged(fileName: string, eventKind: FileWatcherEventKind) { + writeLog(`Config file : ${configFileName} changed: ${FileWatcherEventKind[eventKind]}, fileName: ${fileName}`); + scheduleProgramReload(); + } + + function writeLog(s: string) { + const hasDiagnostics = compilerOptions.diagnostics || compilerOptions.extendedDiagnostics; + if (hasDiagnostics) { + host.write(s); + } + } + + function computeHash(data: string) { + return system.createHash ? system.createHash(data) : data; + } + } + + interface CachedSystem extends System { + addOrDeleteFileOrFolder(fileOrFolder: string): void; + clearCache(): void; + } + + function createCachedSystem(host: System): CachedSystem { + const getFileSize = host.getFileSize ? (path: string) => host.getFileSize(path) : undefined; + const watchFile = host.watchFile ? (path: string, callback: FileWatcherCallback, pollingInterval?: number) => host.watchFile(path, callback, pollingInterval) : undefined; + const watchDirectory = host.watchDirectory ? (path: string, callback: DirectoryWatcherCallback, recursive?: boolean) => host.watchDirectory(path, callback, recursive) : undefined; + const getModifiedTime = host.getModifiedTime ? (path: string) => host.getModifiedTime(path) : undefined; + const createHash = host.createHash ? (data: string) => host.createHash(data) : undefined; + const getMemoryUsage = host.getMemoryUsage ? () => host.getMemoryUsage() : undefined; + const realpath = host.realpath ? (path: string) => host.realpath(path) : undefined; + const tryEnableSourceMapsForHost = host.tryEnableSourceMapsForHost ? () => host.tryEnableSourceMapsForHost() : undefined; + const setTimeout = host.setTimeout ? (callback: (...args: any[]) => void, ms: number, ...args: any[]) => host.setTimeout(callback, ms, ...args) : undefined; + const clearTimeout = host.clearTimeout ? (timeoutId: any) => host.clearTimeout(timeoutId) : undefined; + + const cachedHost = createCachedHost(host); + return { + args: host.args, + newLine: host.newLine, + useCaseSensitiveFileNames: host.useCaseSensitiveFileNames, + write: s => host.write(s), + readFile: (path, encoding?) => host.readFile(path, encoding), + getFileSize, + writeFile: (fileName, data, writeByteOrderMark?) => cachedHost.writeFile(fileName, data, writeByteOrderMark), + watchFile, + watchDirectory, + resolvePath: path => host.resolvePath(path), + fileExists: fileName => cachedHost.fileExists(fileName), + directoryExists: dir => cachedHost.directoryExists(dir), + createDirectory: dir => cachedHost.createDirectory(dir), + getExecutingFilePath: () => host.getExecutingFilePath(), + getCurrentDirectory: () => cachedHost.getCurrentDirectory(), + getDirectories: dir => cachedHost.getDirectories(dir), + readDirectory: (path, extensions, excludes, includes, depth) => cachedHost.readDirectory(path, extensions, excludes, includes, depth), + getModifiedTime, + createHash, + getMemoryUsage, + exit: exitCode => host.exit(exitCode), + realpath, + getEnvironmentVariable: name => host.getEnvironmentVariable(name), + tryEnableSourceMapsForHost, + debugMode: host.debugMode, + setTimeout, + clearTimeout, + addOrDeleteFileOrFolder: fileOrFolder => cachedHost.addOrDeleteFileOrFolder(fileOrFolder), + clearCache: () => cachedHost.clearCache() + }; + } +} diff --git a/src/harness/unittests/tscWatchMode.ts b/src/harness/unittests/tscWatchMode.ts index a55cfb133d78e..608fc30c6b936 100644 --- a/src/harness/unittests/tscWatchMode.ts +++ b/src/harness/unittests/tscWatchMode.ts @@ -26,9 +26,23 @@ namespace ts.tscWatch { checkFileNames(`Program rootFileNames`, program.getRootFileNames(), expectedFiles); } - export function createWatchWithConfig(configFilePath: string, host: WatchedSystem) { - const configFileResult = parseConfigFile(configFilePath, {}, host); - return createWatchModeWithConfigFile(configFileResult, {}, host); + function createWatchingSystemHost(system: WatchedSystem) { + return ts.createWatchingSystemHost(/*pretty*/ undefined, system); + } + + function parseConfigFile(configFileName: string, watchingSystemHost: WatchingSystemHost) { + return ts.parseConfigFile(configFileName, {}, watchingSystemHost.system, watchingSystemHost.reportDiagnostic, watchingSystemHost.reportWatchDiagnostic); + } + + function createWatchModeWithConfigFile(configFilePath: string, host: WatchedSystem) { + const watchingSystemHost = createWatchingSystemHost(host); + const configFileResult = parseConfigFile(configFilePath, watchingSystemHost); + return ts.createWatchModeWithConfigFile(configFileResult, {}, watchingSystemHost); + } + + function createWatchModeWithoutConfigFile(fileNames: string[], host: WatchedSystem, options: CompilerOptions = {}) { + const watchingSystemHost = createWatchingSystemHost(host); + return ts.createWatchModeWithoutConfigFile(fileNames, options, watchingSystemHost); } function getEmittedLineForMultiFileOutput(file: FileOrFolder, host: WatchedSystem) { @@ -94,7 +108,7 @@ namespace ts.tscWatch { content: `export let x: number` }; const host = createWatchedSystem([appFile, moduleFile, libFile]); - const watch = createWatchModeWithoutConfigFile([appFile.path], {}, host); + const watch = createWatchModeWithoutConfigFile([appFile.path], host); checkProgramActualFiles(watch(), [appFile.path, libFile.path, moduleFile.path]); @@ -119,7 +133,7 @@ namespace ts.tscWatch { const host = createWatchedSystem([f1, config], { useCaseSensitiveFileNames: false }); const upperCaseConfigFilePath = combinePaths(getDirectoryPath(config.path).toUpperCase(), getBaseFileName(config.path)); - const watch = createWatchWithConfig(upperCaseConfigFilePath, host); + const watch = createWatchModeWithConfigFile(upperCaseConfigFilePath, host); checkProgramActualFiles(watch(), [f1.path]); }); @@ -148,10 +162,11 @@ namespace ts.tscWatch { }; const host = createWatchedSystem([configFile, libFile, file1, file2, file3]); - const configFileResult = parseConfigFile(configFile.path, {}, host); + const watchingSystemHost = createWatchingSystemHost(host); + const configFileResult = parseConfigFile(configFile.path, watchingSystemHost); assert.equal(configFileResult.errors.length, 0, `expect no errors in config file, got ${JSON.stringify(configFileResult.errors)}`); - const watch = createWatchModeWithConfigFile(configFileResult, {}, host); + const watch = ts.createWatchModeWithConfigFile(configFileResult, {}, watchingSystemHost); checkProgramActualFiles(watch(), [file1.path, libFile.path, file2.path]); checkProgramRootFiles(watch(), [file1.path, file2.path]); @@ -169,7 +184,7 @@ namespace ts.tscWatch { content: `{}` }; const host = createWatchedSystem([commonFile1, libFile, configFile]); - const watch = createWatchWithConfig(configFile.path, host); + const watch = createWatchModeWithConfigFile(configFile.path, host); checkWatchedDirectories(host, ["/a/b"], /*recursive*/ true); checkProgramRootFiles(watch(), [commonFile1.path]); @@ -192,7 +207,7 @@ namespace ts.tscWatch { }` }; const host = createWatchedSystem([commonFile1, commonFile2, configFile]); - const watch = createWatchWithConfig(configFile.path, host); + const watch = createWatchModeWithConfigFile(configFile.path, host); const commonFile3 = "/a/b/commonFile3.ts"; checkProgramRootFiles(watch(), [commonFile1.path, commonFile3]); @@ -205,7 +220,7 @@ namespace ts.tscWatch { content: `{}` }; const host = createWatchedSystem([commonFile1, commonFile2, configFile]); - const watch = createWatchWithConfig(configFile.path, host); + const watch = createWatchModeWithConfigFile(configFile.path, host); checkProgramRootFiles(watch(), [commonFile1.path, commonFile2.path]); // delete commonFile2 @@ -226,7 +241,7 @@ namespace ts.tscWatch { let x = y` }; const host = createWatchedSystem([file1, libFile]); - const watch = createWatchModeWithoutConfigFile([file1.path], {}, host); + const watch = createWatchModeWithoutConfigFile([file1.path], host); checkProgramRootFiles(watch(), [file1.path]); checkProgramActualFiles(watch(), [file1.path, libFile.path]); @@ -254,7 +269,7 @@ namespace ts.tscWatch { }; const files = [commonFile1, commonFile2, configFile]; const host = createWatchedSystem(files); - const watch = createWatchWithConfig(configFile.path, host); + const watch = createWatchModeWithConfigFile(configFile.path, host); checkProgramRootFiles(watch(), [commonFile1.path, commonFile2.path]); configFile.content = `{ @@ -281,7 +296,7 @@ namespace ts.tscWatch { }; const host = createWatchedSystem([commonFile1, commonFile2, excludedFile1, configFile]); - const watch = createWatchWithConfig(configFile.path, host); + const watch = createWatchModeWithConfigFile(configFile.path, host); checkProgramRootFiles(watch(), [commonFile1.path, commonFile2.path]); }); @@ -309,7 +324,7 @@ namespace ts.tscWatch { }; const files = [file1, nodeModuleFile, classicModuleFile, configFile]; const host = createWatchedSystem(files); - const watch = createWatchWithConfig(configFile.path, host); + const watch = createWatchModeWithConfigFile(configFile.path, host); checkProgramRootFiles(watch(), [file1.path]); checkProgramActualFiles(watch(), [file1.path, nodeModuleFile.path]); @@ -337,7 +352,7 @@ namespace ts.tscWatch { }` }; const host = createWatchedSystem([commonFile1, commonFile2, libFile, configFile]); - const watch = createWatchWithConfig(configFile.path, host); + const watch = createWatchModeWithConfigFile(configFile.path, host); checkProgramRootFiles(watch(), [commonFile1.path, commonFile2.path]); }); @@ -355,7 +370,7 @@ namespace ts.tscWatch { content: `export let y = 1;` }; const host = createWatchedSystem([file1, file2, file3]); - const watch = createWatchModeWithoutConfigFile([file1.path], {}, host); + const watch = createWatchModeWithoutConfigFile([file1.path], host); checkProgramRootFiles(watch(), [file1.path]); checkProgramActualFiles(watch(), [file1.path, file2.path]); @@ -384,7 +399,7 @@ namespace ts.tscWatch { content: `export let y = 1;` }; const host = createWatchedSystem([file1, file2, file3]); - const watch = createWatchModeWithoutConfigFile([file1.path], {}, host); + const watch = createWatchModeWithoutConfigFile([file1.path], host); checkProgramActualFiles(watch(), [file1.path, file2.path, file3.path]); host.reloadFS([file1, file3]); @@ -407,7 +422,7 @@ namespace ts.tscWatch { content: `export let y = 1;` }; const host = createWatchedSystem([file1, file2, file3]); - const watch = createWatchModeWithoutConfigFile([file1.path, file3.path], {}, host); + const watch = createWatchModeWithoutConfigFile([file1.path, file3.path], host); checkProgramActualFiles(watch(), [file1.path, file2.path, file3.path]); host.reloadFS([file1, file3]); @@ -435,7 +450,7 @@ namespace ts.tscWatch { }; const host = createWatchedSystem([file1, file2, file3, configFile]); - const watch = createWatchWithConfig(configFile.path, host); + const watch = createWatchModeWithConfigFile(configFile.path, host); checkProgramRootFiles(watch(), [file2.path, file3.path]); checkProgramActualFiles(watch(), [file1.path, file2.path, file3.path]); @@ -457,10 +472,10 @@ namespace ts.tscWatch { content: "export let y = 1;" }; const host = createWatchedSystem([file1, file2, file3]); - const watch = createWatchModeWithoutConfigFile([file2.path, file3.path], {}, host); + const watch = createWatchModeWithoutConfigFile([file2.path, file3.path], host); checkProgramActualFiles(watch(), [file2.path, file3.path]); - const watch2 = createWatchModeWithoutConfigFile([file1.path], {}, host); + const watch2 = createWatchModeWithoutConfigFile([file1.path], host); checkProgramActualFiles(watch2(), [file1.path, file2.path, file3.path]); // Previous program shouldnt be updated @@ -483,7 +498,7 @@ namespace ts.tscWatch { }; const host = createWatchedSystem([file1, configFile]); - const watch = createWatchWithConfig(configFile.path, host); + const watch = createWatchModeWithConfigFile(configFile.path, host); checkProgramActualFiles(watch(), [file1.path]); host.reloadFS([file1, file2, configFile]); @@ -508,7 +523,7 @@ namespace ts.tscWatch { }; const host = createWatchedSystem([file1, file2, configFile]); - const watch = createWatchWithConfig(configFile.path, host); + const watch = createWatchModeWithConfigFile(configFile.path, host); checkProgramActualFiles(watch(), [file1.path]); @@ -538,7 +553,7 @@ namespace ts.tscWatch { }; const host = createWatchedSystem([file1, file2, configFile]); - const watch = createWatchWithConfig(configFile.path, host); + const watch = createWatchModeWithConfigFile(configFile.path, host); checkProgramActualFiles(watch(), [file1.path, file2.path]); const modifiedConfigFile = { @@ -566,7 +581,7 @@ namespace ts.tscWatch { content: JSON.stringify({ compilerOptions: {} }) }; const host = createWatchedSystem([file1, file2, config]); - const watch = createWatchWithConfig(config.path, host); + const watch = createWatchModeWithConfigFile(config.path, host); checkProgramActualFiles(watch(), [file1.path, file2.path]); @@ -588,7 +603,7 @@ namespace ts.tscWatch { content: "{" }; const host = createWatchedSystem([file1, corruptedConfig]); - const watch = createWatchWithConfig(corruptedConfig.path, host); + const watch = createWatchModeWithConfigFile(corruptedConfig.path, host); checkProgramActualFiles(watch(), [file1.path]); }); @@ -638,7 +653,7 @@ namespace ts.tscWatch { }) }; const host = createWatchedSystem([libES5, libES2015Promise, app, config1], { executingFilePath: "/compiler/tsc.js" }); - const watch = createWatchWithConfig(config1.path, host); + const watch = createWatchModeWithConfigFile(config1.path, host); checkProgramActualFiles(watch(), [libES5.path, app.path]); @@ -663,7 +678,7 @@ namespace ts.tscWatch { }) }; const host = createWatchedSystem([f, config]); - const watch = createWatchWithConfig(config.path, host); + const watch = createWatchModeWithConfigFile(config.path, host); checkProgramActualFiles(watch(), [f.path]); }); @@ -677,7 +692,7 @@ namespace ts.tscWatch { content: "import * as T from './moduleFile'; T.bar();" }; const host = createWatchedSystem([moduleFile, file1, libFile]); - createWatchModeWithoutConfigFile([file1.path], {}, host); + createWatchModeWithoutConfigFile([file1.path], host); const error = "a/b/file1.ts(1,20): error TS2307: Cannot find module \'./moduleFile\'.\n"; checkOutputDoesNotContain(host, [error]); @@ -709,7 +724,7 @@ namespace ts.tscWatch { content: `{}` }; const host = createWatchedSystem([moduleFile, file1, configFile, libFile]); - createWatchWithConfig(configFile.path, host); + createWatchModeWithConfigFile(configFile.path, host); const error = `error TS6053: File '${moduleFile.path}' not found.${host.newLine}`; checkOutputDoesNotContain(host, [error]); @@ -745,7 +760,7 @@ namespace ts.tscWatch { path: "/a/c" }; const host = createWatchedSystem([f1, config, node, cwd], { currentDirectory: cwd.path }); - const watch = createWatchWithConfig(config.path, host); + const watch = createWatchModeWithConfigFile(config.path, host); checkProgramActualFiles(watch(), [f1.path, node.path]); }); @@ -760,7 +775,7 @@ namespace ts.tscWatch { content: "import * as T from './moduleFile'; T.bar();" }; const host = createWatchedSystem([file1, libFile]); - createWatchModeWithoutConfigFile([file1.path], {}, host); + createWatchModeWithoutConfigFile([file1.path], host); const error = `a/b/file1.ts(1,20): error TS2307: Cannot find module \'./moduleFile\'.${host.newLine}`; checkOutputContains(host, [error]); @@ -787,7 +802,7 @@ namespace ts.tscWatch { }; const host = createWatchedSystem([file, configFile, libFile]); - createWatchWithConfig(configFile.path, host); + createWatchModeWithConfigFile(configFile.path, host); checkOutputContains(host, [ `a/b/tsconfig.json(3,29): error TS5023: Unknown compiler option \'foo\'.${host.newLine}`, `a/b/tsconfig.json(4,29): error TS5023: Unknown compiler option \'allowJS\'.${host.newLine}` @@ -807,7 +822,7 @@ namespace ts.tscWatch { }; const host = createWatchedSystem([file, configFile, libFile]); - createWatchWithConfig(configFile.path, host); + createWatchModeWithConfigFile(configFile.path, host); checkOutputDoesNotContain(host, [ `a/b/tsconfig.json(3,29): error TS5023: Unknown compiler option \'foo\'.${host.newLine}`, `a/b/tsconfig.json(4,29): error TS5023: Unknown compiler option \'allowJS\'.${host.newLine}` @@ -827,7 +842,7 @@ namespace ts.tscWatch { }; const host = createWatchedSystem([file, configFile, libFile]); - createWatchWithConfig(configFile.path, host); + createWatchModeWithConfigFile(configFile.path, host); const error = `a/b/tsconfig.json(3,25): error TS5023: Unknown compiler option 'haha'.${host.newLine}`; checkOutputDoesNotContain(host, [error]); @@ -863,7 +878,7 @@ namespace ts.tscWatch { }; const host = createWatchedSystem([file1, configFile, libFile]); - const watch = createWatchWithConfig(configFile.path, host); + const watch = createWatchModeWithConfigFile(configFile.path, host); checkProgramActualFiles(watch(), [libFile.path]); }); @@ -889,7 +904,7 @@ namespace ts.tscWatch { content: `export const x: number` }; const host = createWatchedSystem([f, config, t1, t2], { currentDirectory: getDirectoryPath(f.path) }); - const watch = createWatchWithConfig(config.path, host); + const watch = createWatchModeWithConfigFile(config.path, host); checkProgramActualFiles(watch(), [t1.path, t2.path]); }); @@ -900,7 +915,7 @@ namespace ts.tscWatch { content: "let x = 1" }; const host = createWatchedSystem([f, libFile]); - const watch = createWatchModeWithoutConfigFile([f.path], { allowNonTsExtensions: true }, host); + const watch = createWatchModeWithoutConfigFile([f.path], host, { allowNonTsExtensions: true }); checkProgramActualFiles(watch(), [f.path, libFile.path]); }); @@ -934,7 +949,7 @@ namespace ts.tscWatch { }; const host = createWatchedSystem([file, libFile, configFile]); - createWatchWithConfig(configFile.path, host); + createWatchModeWithConfigFile(configFile.path, host); checkOutputContains(host, errors(line)); checkOutputDoesNotContain(host, errors(line - 2)); host.clearOutput(); @@ -985,7 +1000,7 @@ namespace ts.tscWatch { const files = [f1, f2, config, libFile]; host.reloadFS(files); - createWatchWithConfig(config.path, host); + createWatchModeWithConfigFile(config.path, host); const allEmittedLines = getEmittedLines(files); checkOutputContains(host, allEmittedLines); @@ -1073,7 +1088,7 @@ namespace ts.tscWatch { host.reloadFS(firstReloadFileList ? getFiles(firstReloadFileList) : files); // Initial compile - createWatchWithConfig(configFile.path, host); + createWatchModeWithConfigFile(configFile.path, host); if (firstCompilationEmitFiles) { checkAffectedLines(host, getFiles(firstCompilationEmitFiles), allEmittedFiles); } @@ -1384,11 +1399,11 @@ namespace ts.tscWatch { // Initial compile if (configFile) { - createWatchWithConfig(configFile.path, host); + createWatchModeWithConfigFile(configFile.path, host); } else { // First file as the root - createWatchModeWithoutConfigFile([files[0].path], { listEmittedFiles: true }, host); + createWatchModeWithoutConfigFile([files[0].path], host, { listEmittedFiles: true }); } checkOutputContains(host, allEmittedFiles); From 031a63762ff5e12d51d2c716339b2abb1efffdc8 Mon Sep 17 00:00:00 2001 From: Sheetal Nandi Date: Thu, 3 Aug 2017 21:10:26 -0700 Subject: [PATCH 044/109] Switch back to have tsc.ts the only file thats different in tsc.js generation now that api has tests --- src/compiler/tsc.ts | 392 +++++++++++++++++++++++++- src/compiler/tscLib.ts | 391 ------------------------- src/compiler/tsconfig.json | 1 - src/harness/unittests/tscWatchMode.ts | 2 +- 4 files changed, 392 insertions(+), 394 deletions(-) delete mode 100644 src/compiler/tscLib.ts diff --git a/src/compiler/tsc.ts b/src/compiler/tsc.ts index 3a672fca72728..b89a41a2d2302 100644 --- a/src/compiler/tsc.ts +++ b/src/compiler/tsc.ts @@ -1,4 +1,394 @@ -/// +/// +/// +/// + +namespace ts { + interface Statistic { + name: string; + value: string; + } + + function countLines(program: Program): number { + let count = 0; + forEach(program.getSourceFiles(), file => { + count += getLineStarts(file).length; + }); + return count; + } + + function getDiagnosticText(_message: DiagnosticMessage, ..._args: any[]): string { + const diagnostic = createCompilerDiagnostic.apply(undefined, arguments); + return diagnostic.messageText; + } + + let reportDiagnostic = createDiagnosticReporter(sys, reportDiagnosticSimply); + function udpateReportDiagnostic(options: CompilerOptions) { + if (options.pretty) { + reportDiagnostic = createDiagnosticReporter(sys, reportDiagnosticWithColorAndContext); + } + } + + function padLeft(s: string, length: number) { + while (s.length < length) { + s = " " + s; + } + return s; + } + + function padRight(s: string, length: number) { + while (s.length < length) { + s = s + " "; + } + + return s; + } + + function isJSONSupported() { + return typeof JSON === "object" && typeof JSON.parse === "function"; + } + + export function executeCommandLine(args: string[]): void { + const commandLine = parseCommandLine(args); + + // Configuration file name (if any) + let configFileName: string; + if (commandLine.options.locale) { + if (!isJSONSupported()) { + reportDiagnostic(createCompilerDiagnostic(Diagnostics.The_current_host_does_not_support_the_0_option, "--locale")); + return sys.exit(ExitStatus.DiagnosticsPresent_OutputsSkipped); + } + validateLocaleAndSetLanguage(commandLine.options.locale, sys, commandLine.errors); + } + + // If there are any errors due to command line parsing and/or + // setting up localization, report them and quit. + if (commandLine.errors.length > 0) { + reportDiagnostics(commandLine.errors, reportDiagnostic); + return sys.exit(ExitStatus.DiagnosticsPresent_OutputsSkipped); + } + + if (commandLine.options.init) { + writeConfigFile(commandLine.options, commandLine.fileNames); + return sys.exit(ExitStatus.Success); + } + + if (commandLine.options.version) { + printVersion(); + return sys.exit(ExitStatus.Success); + } + + if (commandLine.options.help || commandLine.options.all) { + printVersion(); + printHelp(commandLine.options.all); + return sys.exit(ExitStatus.Success); + } + + if (commandLine.options.project) { + if (!isJSONSupported()) { + reportDiagnostic(createCompilerDiagnostic(Diagnostics.The_current_host_does_not_support_the_0_option, "--project")); + return sys.exit(ExitStatus.DiagnosticsPresent_OutputsSkipped); + } + if (commandLine.fileNames.length !== 0) { + reportDiagnostic(createCompilerDiagnostic(Diagnostics.Option_project_cannot_be_mixed_with_source_files_on_a_command_line)); + return sys.exit(ExitStatus.DiagnosticsPresent_OutputsSkipped); + } + + const fileOrDirectory = normalizePath(commandLine.options.project); + if (!fileOrDirectory /* current directory "." */ || sys.directoryExists(fileOrDirectory)) { + configFileName = combinePaths(fileOrDirectory, "tsconfig.json"); + if (!sys.fileExists(configFileName)) { + reportDiagnostic(createCompilerDiagnostic(Diagnostics.Cannot_find_a_tsconfig_json_file_at_the_specified_directory_Colon_0, commandLine.options.project)); + return sys.exit(ExitStatus.DiagnosticsPresent_OutputsSkipped); + } + } + else { + configFileName = fileOrDirectory; + if (!sys.fileExists(configFileName)) { + reportDiagnostic(createCompilerDiagnostic(Diagnostics.The_specified_path_does_not_exist_Colon_0, commandLine.options.project)); + return sys.exit(ExitStatus.DiagnosticsPresent_OutputsSkipped); + } + } + } + else if (commandLine.fileNames.length === 0 && isJSONSupported()) { + const searchPath = normalizePath(sys.getCurrentDirectory()); + configFileName = findConfigFile(searchPath, sys.fileExists); + } + + if (commandLine.fileNames.length === 0 && !configFileName) { + printVersion(); + printHelp(commandLine.options.all); + return sys.exit(ExitStatus.Success); + } + + const commandLineOptions = commandLine.options; + if (configFileName) { + const reportWatchDiagnostic = createWatchDiagnosticReporter(); + const configParseResult = parseConfigFile(configFileName, commandLineOptions, sys, reportDiagnostic, reportWatchDiagnostic); + udpateReportDiagnostic(configParseResult.options); + if (isWatchSet(configParseResult.options)) { + reportWatchModeWithoutSysSupport(); + createWatchModeWithConfigFile(configParseResult, commandLineOptions, createWatchingSystemHost(reportWatchDiagnostic)); + } + else { + performCompilation(configParseResult.fileNames, configParseResult.options); + } + } + else { + udpateReportDiagnostic(commandLineOptions); + if (isWatchSet(commandLineOptions)) { + reportWatchModeWithoutSysSupport(); + createWatchModeWithoutConfigFile(commandLine.fileNames, commandLineOptions, createWatchingSystemHost()); + } + else { + performCompilation(commandLine.fileNames, commandLineOptions); + } + } + } + + function reportWatchModeWithoutSysSupport() { + if (!sys.watchFile || !sys.watchDirectory) { + reportDiagnostic(createCompilerDiagnostic(Diagnostics.The_current_host_does_not_support_the_0_option, "--watch")); + sys.exit(ExitStatus.DiagnosticsPresent_OutputsSkipped); + } + } + + function performCompilation(rootFileNames: string[], compilerOptions: CompilerOptions) { + const compilerHost = createCompilerHost(compilerOptions); + enableStatistics(compilerOptions); + + const program = createProgram(rootFileNames, compilerOptions, compilerHost); + const exitStatus = compileProgram(sys, program, () => program.emit(), reportDiagnostic); + + reportStatistics(program); + return sys.exit(exitStatus); + } + + function createWatchingSystemHost(reportWatchDiagnostic?: DiagnosticReporter) { + const watchingHost = ts.createWatchingSystemHost(/*pretty*/ undefined, sys, parseConfigFile, reportDiagnostic, reportWatchDiagnostic); + watchingHost.beforeCompile = enableStatistics; + const afterCompile = watchingHost.afterCompile; + watchingHost.afterCompile = (host, program, builder) => { + afterCompile(host, program, builder); + reportStatistics(program); + }; + return watchingHost; + } + + function enableStatistics(compilerOptions: CompilerOptions) { + if (compilerOptions.diagnostics || compilerOptions.extendedDiagnostics) { + performance.enable(); + } + } + + function reportStatistics(program: Program) { + let statistics: Statistic[]; + const compilerOptions = program.getCompilerOptions(); + if (compilerOptions.diagnostics || compilerOptions.extendedDiagnostics) { + statistics = []; + const memoryUsed = sys.getMemoryUsage ? sys.getMemoryUsage() : -1; + reportCountStatistic("Files", program.getSourceFiles().length); + reportCountStatistic("Lines", countLines(program)); + reportCountStatistic("Nodes", program.getNodeCount()); + reportCountStatistic("Identifiers", program.getIdentifierCount()); + reportCountStatistic("Symbols", program.getSymbolCount()); + reportCountStatistic("Types", program.getTypeCount()); + + if (memoryUsed >= 0) { + reportStatisticalValue("Memory used", Math.round(memoryUsed / 1000) + "K"); + } + + const programTime = performance.getDuration("Program"); + const bindTime = performance.getDuration("Bind"); + const checkTime = performance.getDuration("Check"); + const emitTime = performance.getDuration("Emit"); + if (compilerOptions.extendedDiagnostics) { + performance.forEachMeasure((name, duration) => reportTimeStatistic(`${name} time`, duration)); + } + else { + // Individual component times. + // Note: To match the behavior of previous versions of the compiler, the reported parse time includes + // I/O read time and processing time for triple-slash references and module imports, and the reported + // emit time includes I/O write time. We preserve this behavior so we can accurately compare times. + reportTimeStatistic("I/O read", performance.getDuration("I/O Read")); + reportTimeStatistic("I/O write", performance.getDuration("I/O Write")); + reportTimeStatistic("Parse time", programTime); + reportTimeStatistic("Bind time", bindTime); + reportTimeStatistic("Check time", checkTime); + reportTimeStatistic("Emit time", emitTime); + } + reportTimeStatistic("Total time", programTime + bindTime + checkTime + emitTime); + reportStatistics(); + + performance.disable(); + } + + function reportStatistics() { + let nameSize = 0; + let valueSize = 0; + for (const { name, value } of statistics) { + if (name.length > nameSize) { + nameSize = name.length; + } + + if (value.length > valueSize) { + valueSize = value.length; + } + } + + for (const { name, value } of statistics) { + sys.write(padRight(name + ":", nameSize + 2) + padLeft(value.toString(), valueSize) + sys.newLine); + } + } + + function reportStatisticalValue(name: string, value: string) { + statistics.push({ name, value }); + } + + function reportCountStatistic(name: string, count: number) { + reportStatisticalValue(name, "" + count); + } + + function reportTimeStatistic(name: string, time: number) { + reportStatisticalValue(name, (time / 1000).toFixed(2) + "s"); + } + } + + function printVersion() { + sys.write(getDiagnosticText(Diagnostics.Version_0, ts.version) + sys.newLine); + } + + function printHelp(showAllOptions: boolean) { + const output: string[] = []; + + // We want to align our "syntax" and "examples" commands to a certain margin. + const syntaxLength = getDiagnosticText(Diagnostics.Syntax_Colon_0, "").length; + const examplesLength = getDiagnosticText(Diagnostics.Examples_Colon_0, "").length; + let marginLength = Math.max(syntaxLength, examplesLength); + + // Build up the syntactic skeleton. + let syntax = makePadding(marginLength - syntaxLength); + syntax += "tsc [" + getDiagnosticText(Diagnostics.options) + "] [" + getDiagnosticText(Diagnostics.file) + " ...]"; + + output.push(getDiagnosticText(Diagnostics.Syntax_Colon_0, syntax)); + output.push(sys.newLine + sys.newLine); + + // Build up the list of examples. + const padding = makePadding(marginLength); + output.push(getDiagnosticText(Diagnostics.Examples_Colon_0, makePadding(marginLength - examplesLength) + "tsc hello.ts") + sys.newLine); + output.push(padding + "tsc --outFile file.js file.ts" + sys.newLine); + output.push(padding + "tsc @args.txt" + sys.newLine); + output.push(sys.newLine); + + output.push(getDiagnosticText(Diagnostics.Options_Colon) + sys.newLine); + + // Sort our options by their names, (e.g. "--noImplicitAny" comes before "--watch") + const optsList = showAllOptions ? + optionDeclarations.slice().sort((a, b) => compareValues(a.name.toLowerCase(), b.name.toLowerCase())) : + filter(optionDeclarations.slice(), v => v.showInSimplifiedHelpView); + + // We want our descriptions to align at the same column in our output, + // so we keep track of the longest option usage string. + marginLength = 0; + const usageColumn: string[] = []; // Things like "-d, --declaration" go in here. + const descriptionColumn: string[] = []; + + const optionsDescriptionMap = createMap(); // Map between option.description and list of option.type if it is a kind + + for (let i = 0; i < optsList.length; i++) { + const option = optsList[i]; + + // If an option lacks a description, + // it is not officially supported. + if (!option.description) { + continue; + } + + let usageText = " "; + if (option.shortName) { + usageText += "-" + option.shortName; + usageText += getParamType(option); + usageText += ", "; + } + + usageText += "--" + option.name; + usageText += getParamType(option); + + usageColumn.push(usageText); + let description: string; + + if (option.name === "lib") { + description = getDiagnosticText(option.description); + const element = (option).element; + const typeMap = >element.type; + optionsDescriptionMap.set(description, arrayFrom(typeMap.keys()).map(key => `'${key}'`)); + } + else { + description = getDiagnosticText(option.description); + } + + descriptionColumn.push(description); + + // Set the new margin for the description column if necessary. + marginLength = Math.max(usageText.length, marginLength); + } + + // Special case that can't fit in the loop. + const usageText = " @<" + getDiagnosticText(Diagnostics.file) + ">"; + usageColumn.push(usageText); + descriptionColumn.push(getDiagnosticText(Diagnostics.Insert_command_line_options_and_files_from_a_file)); + marginLength = Math.max(usageText.length, marginLength); + + // Print out each row, aligning all the descriptions on the same column. + for (let i = 0; i < usageColumn.length; i++) { + const usage = usageColumn[i]; + const description = descriptionColumn[i]; + const kindsList = optionsDescriptionMap.get(description); + output.push(usage + makePadding(marginLength - usage.length + 2) + description + sys.newLine); + + if (kindsList) { + output.push(makePadding(marginLength + 4)); + for (const kind of kindsList) { + output.push(kind + " "); + } + output.push(sys.newLine); + } + } + + for (const line of output) { + sys.write(line); + } + return; + + function getParamType(option: CommandLineOption) { + if (option.paramType !== undefined) { + return " " + getDiagnosticText(option.paramType); + } + return ""; + } + + function makePadding(paddingLength: number): string { + return Array(paddingLength + 1).join(" "); + } + } + + function writeConfigFile(options: CompilerOptions, fileNames: string[]) { + const currentDirectory = sys.getCurrentDirectory(); + const file = normalizePath(combinePaths(currentDirectory, "tsconfig.json")); + if (sys.fileExists(file)) { + reportDiagnostic(createCompilerDiagnostic(Diagnostics.A_tsconfig_json_file_is_already_defined_at_Colon_0, file)); + } + else { + sys.writeFile(file, generateTSConfig(options, fileNames, sys.newLine)); + reportDiagnostic(createCompilerDiagnostic(Diagnostics.Successfully_created_a_tsconfig_json_file)); + } + + return; + } +} + +if (ts.Debug.isDebugging) { + ts.Debug.enableDebugInfo(); +} if (ts.sys.tryEnableSourceMapsForHost && /^development$/i.test(ts.sys.getEnvironmentVariable("NODE_ENV"))) { ts.sys.tryEnableSourceMapsForHost(); diff --git a/src/compiler/tscLib.ts b/src/compiler/tscLib.ts deleted file mode 100644 index bd4a64d8899c2..0000000000000 --- a/src/compiler/tscLib.ts +++ /dev/null @@ -1,391 +0,0 @@ -/// -/// -/// - -namespace ts { - interface Statistic { - name: string; - value: string; - } - - function countLines(program: Program): number { - let count = 0; - forEach(program.getSourceFiles(), file => { - count += getLineStarts(file).length; - }); - return count; - } - - function getDiagnosticText(_message: DiagnosticMessage, ..._args: any[]): string { - const diagnostic = createCompilerDiagnostic.apply(undefined, arguments); - return diagnostic.messageText; - } - - let reportDiagnostic = createDiagnosticReporter(sys, reportDiagnosticSimply); - function udpateReportDiagnostic(options: CompilerOptions) { - if (options.pretty) { - reportDiagnostic = createDiagnosticReporter(sys, reportDiagnosticWithColorAndContext); - } - } - - function padLeft(s: string, length: number) { - while (s.length < length) { - s = " " + s; - } - return s; - } - - function padRight(s: string, length: number) { - while (s.length < length) { - s = s + " "; - } - - return s; - } - - function isJSONSupported() { - return typeof JSON === "object" && typeof JSON.parse === "function"; - } - - export function executeCommandLine(args: string[]): void { - const commandLine = parseCommandLine(args); - - // Configuration file name (if any) - let configFileName: string; - if (commandLine.options.locale) { - if (!isJSONSupported()) { - reportDiagnostic(createCompilerDiagnostic(Diagnostics.The_current_host_does_not_support_the_0_option, "--locale")); - return sys.exit(ExitStatus.DiagnosticsPresent_OutputsSkipped); - } - validateLocaleAndSetLanguage(commandLine.options.locale, sys, commandLine.errors); - } - - // If there are any errors due to command line parsing and/or - // setting up localization, report them and quit. - if (commandLine.errors.length > 0) { - reportDiagnostics(commandLine.errors, reportDiagnostic); - return sys.exit(ExitStatus.DiagnosticsPresent_OutputsSkipped); - } - - if (commandLine.options.init) { - writeConfigFile(commandLine.options, commandLine.fileNames); - return sys.exit(ExitStatus.Success); - } - - if (commandLine.options.version) { - printVersion(); - return sys.exit(ExitStatus.Success); - } - - if (commandLine.options.help || commandLine.options.all) { - printVersion(); - printHelp(commandLine.options.all); - return sys.exit(ExitStatus.Success); - } - - if (commandLine.options.project) { - if (!isJSONSupported()) { - reportDiagnostic(createCompilerDiagnostic(Diagnostics.The_current_host_does_not_support_the_0_option, "--project")); - return sys.exit(ExitStatus.DiagnosticsPresent_OutputsSkipped); - } - if (commandLine.fileNames.length !== 0) { - reportDiagnostic(createCompilerDiagnostic(Diagnostics.Option_project_cannot_be_mixed_with_source_files_on_a_command_line)); - return sys.exit(ExitStatus.DiagnosticsPresent_OutputsSkipped); - } - - const fileOrDirectory = normalizePath(commandLine.options.project); - if (!fileOrDirectory /* current directory "." */ || sys.directoryExists(fileOrDirectory)) { - configFileName = combinePaths(fileOrDirectory, "tsconfig.json"); - if (!sys.fileExists(configFileName)) { - reportDiagnostic(createCompilerDiagnostic(Diagnostics.Cannot_find_a_tsconfig_json_file_at_the_specified_directory_Colon_0, commandLine.options.project)); - return sys.exit(ExitStatus.DiagnosticsPresent_OutputsSkipped); - } - } - else { - configFileName = fileOrDirectory; - if (!sys.fileExists(configFileName)) { - reportDiagnostic(createCompilerDiagnostic(Diagnostics.The_specified_path_does_not_exist_Colon_0, commandLine.options.project)); - return sys.exit(ExitStatus.DiagnosticsPresent_OutputsSkipped); - } - } - } - else if (commandLine.fileNames.length === 0 && isJSONSupported()) { - const searchPath = normalizePath(sys.getCurrentDirectory()); - configFileName = findConfigFile(searchPath, sys.fileExists); - } - - if (commandLine.fileNames.length === 0 && !configFileName) { - printVersion(); - printHelp(commandLine.options.all); - return sys.exit(ExitStatus.Success); - } - - const commandLineOptions = commandLine.options; - if (configFileName) { - const reportWatchDiagnostic = createWatchDiagnosticReporter(); - const configParseResult = parseConfigFile(configFileName, commandLineOptions, sys, reportDiagnostic, reportWatchDiagnostic); - udpateReportDiagnostic(configParseResult.options); - if (isWatchSet(configParseResult.options)) { - reportWatchModeWithoutSysSupport(); - createWatchModeWithConfigFile(configParseResult, commandLineOptions, createWatchingSystemHost(reportWatchDiagnostic)); - } - else { - performCompilation(configParseResult.fileNames, configParseResult.options); - } - } - else { - udpateReportDiagnostic(commandLineOptions); - if (isWatchSet(commandLineOptions)) { - reportWatchModeWithoutSysSupport(); - createWatchModeWithoutConfigFile(commandLine.fileNames, commandLineOptions, createWatchingSystemHost()); - } - else { - performCompilation(commandLine.fileNames, commandLineOptions); - } - } - } - - function reportWatchModeWithoutSysSupport() { - if (!sys.watchFile || !sys.watchDirectory) { - reportDiagnostic(createCompilerDiagnostic(Diagnostics.The_current_host_does_not_support_the_0_option, "--watch")); - sys.exit(ExitStatus.DiagnosticsPresent_OutputsSkipped); - } - } - - function performCompilation(rootFileNames: string[], compilerOptions: CompilerOptions) { - const compilerHost = createCompilerHost(compilerOptions); - enableStatistics(compilerOptions); - - const program = createProgram(rootFileNames, compilerOptions, compilerHost); - const exitStatus = compileProgram(sys, program, () => program.emit(), reportDiagnostic); - - reportStatistics(program); - return sys.exit(exitStatus); - } - - function createWatchingSystemHost(reportWatchDiagnostic?: DiagnosticReporter) { - const watchingHost = ts.createWatchingSystemHost(/*pretty*/ undefined, sys, parseConfigFile, reportDiagnostic, reportWatchDiagnostic); - watchingHost.beforeCompile = enableStatistics; - const afterCompile = watchingHost.afterCompile; - watchingHost.afterCompile = (host, program, builder) => { - afterCompile(host, program, builder); - reportStatistics(program); - }; - return watchingHost; - } - - function enableStatistics(compilerOptions: CompilerOptions) { - if (compilerOptions.diagnostics || compilerOptions.extendedDiagnostics) { - performance.enable(); - } - } - - function reportStatistics(program: Program) { - let statistics: Statistic[]; - const compilerOptions = program.getCompilerOptions(); - if (compilerOptions.diagnostics || compilerOptions.extendedDiagnostics) { - statistics = []; - const memoryUsed = sys.getMemoryUsage ? sys.getMemoryUsage() : -1; - reportCountStatistic("Files", program.getSourceFiles().length); - reportCountStatistic("Lines", countLines(program)); - reportCountStatistic("Nodes", program.getNodeCount()); - reportCountStatistic("Identifiers", program.getIdentifierCount()); - reportCountStatistic("Symbols", program.getSymbolCount()); - reportCountStatistic("Types", program.getTypeCount()); - - if (memoryUsed >= 0) { - reportStatisticalValue("Memory used", Math.round(memoryUsed / 1000) + "K"); - } - - const programTime = performance.getDuration("Program"); - const bindTime = performance.getDuration("Bind"); - const checkTime = performance.getDuration("Check"); - const emitTime = performance.getDuration("Emit"); - if (compilerOptions.extendedDiagnostics) { - performance.forEachMeasure((name, duration) => reportTimeStatistic(`${name} time`, duration)); - } - else { - // Individual component times. - // Note: To match the behavior of previous versions of the compiler, the reported parse time includes - // I/O read time and processing time for triple-slash references and module imports, and the reported - // emit time includes I/O write time. We preserve this behavior so we can accurately compare times. - reportTimeStatistic("I/O read", performance.getDuration("I/O Read")); - reportTimeStatistic("I/O write", performance.getDuration("I/O Write")); - reportTimeStatistic("Parse time", programTime); - reportTimeStatistic("Bind time", bindTime); - reportTimeStatistic("Check time", checkTime); - reportTimeStatistic("Emit time", emitTime); - } - reportTimeStatistic("Total time", programTime + bindTime + checkTime + emitTime); - reportStatistics(); - - performance.disable(); - } - - function reportStatistics() { - let nameSize = 0; - let valueSize = 0; - for (const { name, value } of statistics) { - if (name.length > nameSize) { - nameSize = name.length; - } - - if (value.length > valueSize) { - valueSize = value.length; - } - } - - for (const { name, value } of statistics) { - sys.write(padRight(name + ":", nameSize + 2) + padLeft(value.toString(), valueSize) + sys.newLine); - } - } - - function reportStatisticalValue(name: string, value: string) { - statistics.push({ name, value }); - } - - function reportCountStatistic(name: string, count: number) { - reportStatisticalValue(name, "" + count); - } - - function reportTimeStatistic(name: string, time: number) { - reportStatisticalValue(name, (time / 1000).toFixed(2) + "s"); - } - } - - function printVersion() { - sys.write(getDiagnosticText(Diagnostics.Version_0, ts.version) + sys.newLine); - } - - function printHelp(showAllOptions: boolean) { - const output: string[] = []; - - // We want to align our "syntax" and "examples" commands to a certain margin. - const syntaxLength = getDiagnosticText(Diagnostics.Syntax_Colon_0, "").length; - const examplesLength = getDiagnosticText(Diagnostics.Examples_Colon_0, "").length; - let marginLength = Math.max(syntaxLength, examplesLength); - - // Build up the syntactic skeleton. - let syntax = makePadding(marginLength - syntaxLength); - syntax += "tsc [" + getDiagnosticText(Diagnostics.options) + "] [" + getDiagnosticText(Diagnostics.file) + " ...]"; - - output.push(getDiagnosticText(Diagnostics.Syntax_Colon_0, syntax)); - output.push(sys.newLine + sys.newLine); - - // Build up the list of examples. - const padding = makePadding(marginLength); - output.push(getDiagnosticText(Diagnostics.Examples_Colon_0, makePadding(marginLength - examplesLength) + "tsc hello.ts") + sys.newLine); - output.push(padding + "tsc --outFile file.js file.ts" + sys.newLine); - output.push(padding + "tsc @args.txt" + sys.newLine); - output.push(sys.newLine); - - output.push(getDiagnosticText(Diagnostics.Options_Colon) + sys.newLine); - - // Sort our options by their names, (e.g. "--noImplicitAny" comes before "--watch") - const optsList = showAllOptions ? - optionDeclarations.slice().sort((a, b) => compareValues(a.name.toLowerCase(), b.name.toLowerCase())) : - filter(optionDeclarations.slice(), v => v.showInSimplifiedHelpView); - - // We want our descriptions to align at the same column in our output, - // so we keep track of the longest option usage string. - marginLength = 0; - const usageColumn: string[] = []; // Things like "-d, --declaration" go in here. - const descriptionColumn: string[] = []; - - const optionsDescriptionMap = createMap(); // Map between option.description and list of option.type if it is a kind - - for (let i = 0; i < optsList.length; i++) { - const option = optsList[i]; - - // If an option lacks a description, - // it is not officially supported. - if (!option.description) { - continue; - } - - let usageText = " "; - if (option.shortName) { - usageText += "-" + option.shortName; - usageText += getParamType(option); - usageText += ", "; - } - - usageText += "--" + option.name; - usageText += getParamType(option); - - usageColumn.push(usageText); - let description: string; - - if (option.name === "lib") { - description = getDiagnosticText(option.description); - const element = (option).element; - const typeMap = >element.type; - optionsDescriptionMap.set(description, arrayFrom(typeMap.keys()).map(key => `'${key}'`)); - } - else { - description = getDiagnosticText(option.description); - } - - descriptionColumn.push(description); - - // Set the new margin for the description column if necessary. - marginLength = Math.max(usageText.length, marginLength); - } - - // Special case that can't fit in the loop. - const usageText = " @<" + getDiagnosticText(Diagnostics.file) + ">"; - usageColumn.push(usageText); - descriptionColumn.push(getDiagnosticText(Diagnostics.Insert_command_line_options_and_files_from_a_file)); - marginLength = Math.max(usageText.length, marginLength); - - // Print out each row, aligning all the descriptions on the same column. - for (let i = 0; i < usageColumn.length; i++) { - const usage = usageColumn[i]; - const description = descriptionColumn[i]; - const kindsList = optionsDescriptionMap.get(description); - output.push(usage + makePadding(marginLength - usage.length + 2) + description + sys.newLine); - - if (kindsList) { - output.push(makePadding(marginLength + 4)); - for (const kind of kindsList) { - output.push(kind + " "); - } - output.push(sys.newLine); - } - } - - for (const line of output) { - sys.write(line); - } - return; - - function getParamType(option: CommandLineOption) { - if (option.paramType !== undefined) { - return " " + getDiagnosticText(option.paramType); - } - return ""; - } - - function makePadding(paddingLength: number): string { - return Array(paddingLength + 1).join(" "); - } - } - - function writeConfigFile(options: CompilerOptions, fileNames: string[]) { - const currentDirectory = sys.getCurrentDirectory(); - const file = normalizePath(combinePaths(currentDirectory, "tsconfig.json")); - if (sys.fileExists(file)) { - reportDiagnostic(createCompilerDiagnostic(Diagnostics.A_tsconfig_json_file_is_already_defined_at_Colon_0, file)); - } - else { - sys.writeFile(file, generateTSConfig(options, fileNames, sys.newLine)); - reportDiagnostic(createCompilerDiagnostic(Diagnostics.Successfully_created_a_tsconfig_json_file)); - } - - return; - } -} - -if (ts.Debug.isDebugging) { - ts.Debug.enableDebugInfo(); -} diff --git a/src/compiler/tsconfig.json b/src/compiler/tsconfig.json index fd92a6008cdce..08d50e5eb82e6 100644 --- a/src/compiler/tsconfig.json +++ b/src/compiler/tsconfig.json @@ -39,7 +39,6 @@ "builder.ts", "watchedProgram.ts", "commandLineParser.ts", - "tscLib.ts", "tsc.ts", "diagnosticInformationMap.generated.ts" ] diff --git a/src/harness/unittests/tscWatchMode.ts b/src/harness/unittests/tscWatchMode.ts index 608fc30c6b936..7c535e4068774 100644 --- a/src/harness/unittests/tscWatchMode.ts +++ b/src/harness/unittests/tscWatchMode.ts @@ -1,5 +1,5 @@ /// -/// +/// /// namespace ts.tscWatch { From 0d5e6c9de57d1ea447c5505d6a676473725411ac Mon Sep 17 00:00:00 2001 From: Sheetal Nandi Date: Fri, 4 Aug 2017 01:14:54 -0700 Subject: [PATCH 045/109] Use cache for module resolution even in watch mode --- src/compiler/resolutionCache.ts | 198 ++++++++++++++++++++++++++++++++ src/compiler/tsconfig.json | 1 + src/compiler/watchedProgram.ts | 33 +++--- src/server/lsHost.ts | 164 +------------------------- src/server/project.ts | 25 +++- 5 files changed, 244 insertions(+), 177 deletions(-) create mode 100644 src/compiler/resolutionCache.ts diff --git a/src/compiler/resolutionCache.ts b/src/compiler/resolutionCache.ts new file mode 100644 index 0000000000000..f92464e5a3e63 --- /dev/null +++ b/src/compiler/resolutionCache.ts @@ -0,0 +1,198 @@ +/// +/// + +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; + clear(): void; + } + + type NameResolutionWithFailedLookupLocations = { failedLookupLocations: string[], isInvalidated?: boolean }; + type ResolverWithGlobalCache = (primaryResult: ResolvedModuleWithFailedLookupLocations, moduleName: string, compilerOptions: CompilerOptions, host: ModuleResolutionHost) => ResolvedModuleWithFailedLookupLocations | undefined; + + /*@internal*/ + export function resolveWithGlobalCache(primaryResult: ResolvedModuleWithFailedLookupLocations, moduleName: string, compilerOptions: CompilerOptions, host: ModuleResolutionHost, globalCache: string | undefined, projectName: string): ResolvedModuleWithFailedLookupLocations | undefined { + if (!isExternalModuleNameRelative(moduleName) && !(primaryResult.resolvedModule && extensionIsTypeScript(primaryResult.resolvedModule.extension)) && globalCache !== undefined) { + // otherwise try to load typings from @types + + // create different collection of failed lookup locations for second pass + // if it will fail and we've already found something during the first pass - we don't want to pollute its results + const { resolvedModule, failedLookupLocations } = loadModuleFromGlobalCache(moduleName, projectName, compilerOptions, host, globalCache); + if (resolvedModule) { + return { resolvedModule, failedLookupLocations: primaryResult.failedLookupLocations.concat(failedLookupLocations) }; + } + } + } + + /*@internal*/ + export function createResolutionCache( + toPath: (fileName: string) => Path, + getCompilerOptions: () => CompilerOptions, + resolveWithGlobalCache?: ResolverWithGlobalCache): ResolutionCache { + + let host: ModuleResolutionHost; + let filesWithChangedSetOfUnresolvedImports: Path[]; + const resolvedModuleNames = createMap>(); + const resolvedTypeReferenceDirectives = createMap>(); + + return { + setModuleResolutionHost, + startRecordingFilesWithChangedResolutions, + finishRecordingFilesWithChangedResolutions, + resolveModuleNames, + resolveTypeReferenceDirectives, + invalidateResolutionOfDeletedFile, + clear + }; + + function setModuleResolutionHost(updatedHost: ModuleResolutionHost) { + host = updatedHost; + } + + function clear() { + resolvedModuleNames.clear(); + resolvedTypeReferenceDirectives.clear(); + } + + function startRecordingFilesWithChangedResolutions() { + filesWithChangedSetOfUnresolvedImports = []; + } + + function finishRecordingFilesWithChangedResolutions() { + const collected = filesWithChangedSetOfUnresolvedImports; + filesWithChangedSetOfUnresolvedImports = undefined; + return collected; + } + + 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 + // otherwise try to load typings from @types + return (resolveWithGlobalCache && resolveWithGlobalCache(primaryResult, moduleName, compilerOptions, host)) || primaryResult; + } + + function resolveNamesWithLocalCache( + names: string[], + containingFile: string, + cache: Map>, + loader: (name: string, containingFile: string, options: CompilerOptions, host: ModuleResolutionHost) => T, + getResult: (s: T) => R, + getResultFileName: (result: R) => string | undefined, + logChanges: boolean): R[] { + + const path = toPath(containingFile); + const currentResolutionsInFile = cache.get(path); + + const newResolutions: Map = createMap(); + const resolvedModules: R[] = []; + const compilerOptions = getCompilerOptions(); + + for (const name of names) { + // check if this is a duplicate entry in the list + let resolution = newResolutions.get(name); + if (!resolution) { + const existingResolution = currentResolutionsInFile && currentResolutionsInFile.get(name); + if (moduleResolutionIsValid(existingResolution)) { + // ok, it is safe to use existing name resolution results + resolution = existingResolution; + } + else { + resolution = loader(name, containingFile, compilerOptions, host); + newResolutions.set(name, resolution); + } + if (logChanges && filesWithChangedSetOfUnresolvedImports && !resolutionIsEqualTo(existingResolution, resolution)) { + filesWithChangedSetOfUnresolvedImports.push(path); + // reset log changes to avoid recording the same file multiple times + logChanges = false; + } + } + + Debug.assert(resolution !== undefined); + + resolvedModules.push(getResult(resolution)); + } + + // replace old results with a new one + cache.set(path, newResolutions); + return resolvedModules; + + function resolutionIsEqualTo(oldResolution: T, newResolution: T): boolean { + if (oldResolution === newResolution) { + return true; + } + if (!oldResolution || !newResolution || oldResolution.isInvalidated) { + return false; + } + const oldResult = getResult(oldResolution); + const newResult = getResult(newResolution); + if (oldResult === newResult) { + return true; + } + if (!oldResult || !newResult) { + return false; + } + return getResultFileName(oldResult) === getResultFileName(newResult); + } + + function moduleResolutionIsValid(resolution: T): boolean { + if (!resolution || resolution.isInvalidated) { + return false; + } + + const result = getResult(resolution); + if (result) { + return true; + } + + // consider situation if we have no candidate locations as valid resolution. + // after all there is no point to invalidate it if we have no idea where to look for the module. + return resolution.failedLookupLocations.length === 0; + } + } + + + function resolveTypeReferenceDirectives(typeDirectiveNames: string[], containingFile: string): ResolvedTypeReferenceDirective[] { + return resolveNamesWithLocalCache(typeDirectiveNames, containingFile, resolvedTypeReferenceDirectives, resolveTypeReferenceDirective, + m => m.resolvedTypeReferenceDirective, r => r.resolvedFileName, /*logChanges*/ false); + } + + function resolveModuleNames(moduleNames: string[], containingFile: string, logChanges: boolean): ResolvedModuleFull[] { + return resolveNamesWithLocalCache(moduleNames, containingFile, resolvedModuleNames, resolveModuleName, + m => m.resolvedModule, r => r.resolvedFileName, logChanges); + } + + function invalidateResolutionCacheOfDeletedFile( + deletedFilePath: Path, + cache: Map>, + getResult: (s: T) => R, + getResultFileName: (result: R) => string | undefined) { + cache.forEach((value, path) => { + if (path === deletedFilePath) { + cache.delete(path); + } + else if (value) { + value.forEach((resolution) => { + if (resolution && !resolution.isInvalidated) { + const result = getResult(resolution); + if (result) { + if (getResultFileName(result) === deletedFilePath) { + resolution.isInvalidated = true; + } + } + } + }); + } + }); + } + + function invalidateResolutionOfDeletedFile(filePath: Path) { + invalidateResolutionCacheOfDeletedFile(filePath, resolvedModuleNames, m => m.resolvedModule, r => r.resolvedFileName); + invalidateResolutionCacheOfDeletedFile(filePath, resolvedTypeReferenceDirectives, m => m.resolvedTypeReferenceDirective, r => r.resolvedFileName); + } + } +} diff --git a/src/compiler/tsconfig.json b/src/compiler/tsconfig.json index 08d50e5eb82e6..1001f51f330b9 100644 --- a/src/compiler/tsconfig.json +++ b/src/compiler/tsconfig.json @@ -37,6 +37,7 @@ "emitter.ts", "program.ts", "builder.ts", + "resolutionCache.ts", "watchedProgram.ts", "commandLineParser.ts", "tsc.ts", diff --git a/src/compiler/watchedProgram.ts b/src/compiler/watchedProgram.ts index 22625282a8de3..8bf5c54245672 100644 --- a/src/compiler/watchedProgram.ts +++ b/src/compiler/watchedProgram.ts @@ -1,5 +1,6 @@ /// /// +/// namespace ts { export type DiagnosticReporter = (diagnostic: Diagnostic) => void; @@ -254,6 +255,7 @@ namespace ts { let timerToUpdateProgram: any; // timer callback to recompile the program const sourceFilesCache = createMap(); // Cache that stores the source file and version info + watchingHost = watchingHost || createWatchingSystemHost(compilerOptions.pretty); const { system, parseConfigFile, reportDiagnostic, reportWatchDiagnostic, beforeCompile, afterCompile } = watchingHost; @@ -268,6 +270,9 @@ namespace ts { const currentDirectory = host.getCurrentDirectory(); const getCanonicalFileName = createGetCanonicalFileName(host.useCaseSensitiveFileNames); + // Cache for the module resolution + const resolutionCache = createResolutionCache(fileName => toPath(fileName), () => compilerOptions); + // There is no extra check needed since we can just rely on the program to decide emit const builder = createBuilder(getCanonicalFileName, getFileEmitOutput, computeHash, _sourceFile => true); @@ -287,6 +292,10 @@ namespace ts { // Create the compiler host const compilerHost = createWatchedCompilerHost(compilerOptions); + resolutionCache.setModuleResolutionHost(compilerHost); + if (changesAffectModuleResolution(program && program.getCompilerOptions(), compilerOptions)) { + resolutionCache.clear(); + } beforeCompile(compilerOptions); // Compile the program @@ -321,22 +330,18 @@ namespace ts { getEnvironmentVariable: name => host.getEnvironmentVariable ? host.getEnvironmentVariable(name) : "", getDirectories: (path: string) => host.getDirectories(path), realpath, - onReleaseOldSourceFile, + resolveTypeReferenceDirectives: (typeDirectiveNames, containingFile) => resolutionCache.resolveTypeReferenceDirectives(typeDirectiveNames, containingFile), + resolveModuleNames: (moduleNames, containingFile) => resolutionCache.resolveModuleNames(moduleNames, containingFile, /*logChanges*/ false), + onReleaseOldSourceFile }; + } - // TODO: cache module resolution - // if (host.resolveModuleNames) { - // compilerHost.resolveModuleNames = (moduleNames, containingFile) => host.resolveModuleNames(moduleNames, containingFile); - // } - // if (host.resolveTypeReferenceDirectives) { - // compilerHost.resolveTypeReferenceDirectives = (typeReferenceDirectiveNames, containingFile) => { - // return host.resolveTypeReferenceDirectives(typeReferenceDirectiveNames, containingFile); - // }; - // } + function toPath(fileName: string) { + return ts.toPath(fileName, currentDirectory, getCanonicalFileName); } function fileExists(fileName: string) { - const path = toPath(fileName, currentDirectory, getCanonicalFileName); + const path = toPath(fileName); const hostSourceFileInfo = sourceFilesCache.get(path); if (hostSourceFileInfo !== undefined) { return !isString(hostSourceFileInfo); @@ -350,7 +355,7 @@ namespace ts { } function getVersionedSourceFile(fileName: string, languageVersion: ScriptTarget, onError?: (message: string) => void, shouldCreateNewSourceFile?: boolean): SourceFile { - return getVersionedSourceFileByPath(fileName, toPath(fileName, currentDirectory, getCanonicalFileName), languageVersion, onError, shouldCreateNewSourceFile); + return getVersionedSourceFileByPath(fileName, toPath(fileName), languageVersion, onError, shouldCreateNewSourceFile); } function getVersionedSourceFileByPath(fileName: string, path: Path, languageVersion: ScriptTarget, onError?: (message: string) => void, shouldCreateNewSourceFile?: boolean): SourceFile { @@ -418,6 +423,7 @@ namespace ts { if (hostSourceFile !== undefined) { if (!isString(hostSourceFile)) { hostSourceFile.fileWatcher.close(); + resolutionCache.invalidateResolutionOfDeletedFile(path); } sourceFilesCache.delete(path); } @@ -501,6 +507,7 @@ namespace ts { if (hostSourceFile) { // Update the cache if (eventKind === FileWatcherEventKind.Deleted) { + resolutionCache.invalidateResolutionOfDeletedFile(path); if (!isString(hostSourceFile)) { hostSourceFile.fileWatcher.close(); sourceFilesCache.set(path, (hostSourceFile.version++).toString()); @@ -574,7 +581,7 @@ namespace ts { function onFileAddOrRemoveInWatchedDirectory(fileName: string) { Debug.assert(!!configFileName); - const path = toPath(fileName, currentDirectory, getCanonicalFileName); + const path = toPath(fileName); // Since the file existance changed, update the sourceFiles cache updateCachedSystem(fileName, path); diff --git a/src/server/lsHost.ts b/src/server/lsHost.ts index 692547e4915d9..274c3c3aef682 100644 --- a/src/server/lsHost.ts +++ b/src/server/lsHost.ts @@ -1,6 +1,7 @@ /// /// /// +/// namespace ts.server { export class CachedServerHost implements ServerHost { @@ -103,15 +104,10 @@ namespace ts.server { } - type NameResolutionWithFailedLookupLocations = { failedLookupLocations: string[], isInvalidated?: boolean }; export class LSHost implements LanguageServiceHost, ModuleResolutionHost { - private compilationSettings: CompilerOptions; - private readonly resolvedModuleNames = createMap>(); - private readonly resolvedTypeReferenceDirectives = createMap>(); - - private filesWithChangedSetOfUnresolvedImports: Path[]; + /*@internal*/ + compilationSettings: CompilerOptions; - private resolveModuleName: typeof resolveModuleName; readonly trace: (s: string) => void; readonly realpath?: (path: string) => string; /** @@ -130,25 +126,6 @@ namespace ts.server { this.trace = s => host.trace(s); } - this.resolveModuleName = (moduleName, containingFile, compilerOptions, host) => { - const globalCache = this.project.getTypeAcquisition().enable - ? this.project.projectService.typingsInstaller.globalTypingsCacheLocation - : undefined; - const primaryResult = resolveModuleName(moduleName, containingFile, compilerOptions, host); - // return result immediately only if it is .ts, .tsx or .d.ts - if (!isExternalModuleNameRelative(moduleName) && !(primaryResult.resolvedModule && extensionIsTypeScript(primaryResult.resolvedModule.extension)) && globalCache !== undefined) { - // otherwise try to load typings from @types - - // create different collection of failed lookup locations for second pass - // if it will fail and we've already found something during the first pass - we don't want to pollute its results - const { resolvedModule, failedLookupLocations } = loadModuleFromGlobalCache(moduleName, this.project.getProjectName(), compilerOptions, host, globalCache); - if (resolvedModule) { - return { resolvedModule, failedLookupLocations: primaryResult.failedLookupLocations.concat(failedLookupLocations) }; - } - } - return primaryResult; - }; - if (this.host.realpath) { this.realpath = path => this.host.realpath(path); } @@ -156,99 +133,9 @@ namespace ts.server { dispose() { this.project = undefined; - this.resolveModuleName = undefined; this.host = undefined; } - public startRecordingFilesWithChangedResolutions() { - this.filesWithChangedSetOfUnresolvedImports = []; - } - - public finishRecordingFilesWithChangedResolutions() { - const collected = this.filesWithChangedSetOfUnresolvedImports; - this.filesWithChangedSetOfUnresolvedImports = undefined; - return collected; - } - - private resolveNamesWithLocalCache( - names: string[], - containingFile: string, - cache: Map>, - loader: (name: string, containingFile: string, options: CompilerOptions, host: ModuleResolutionHost) => T, - getResult: (s: T) => R, - getResultFileName: (result: R) => string | undefined, - logChanges: boolean): R[] { - - const path = this.project.projectService.toPath(containingFile); - const currentResolutionsInFile = cache.get(path); - - const newResolutions: Map = createMap(); - const resolvedModules: R[] = []; - const compilerOptions = this.getCompilationSettings(); - - for (const name of names) { - // check if this is a duplicate entry in the list - let resolution = newResolutions.get(name); - if (!resolution) { - const existingResolution = currentResolutionsInFile && currentResolutionsInFile.get(name); - if (moduleResolutionIsValid(existingResolution)) { - // ok, it is safe to use existing name resolution results - resolution = existingResolution; - } - else { - resolution = loader(name, containingFile, compilerOptions, this); - newResolutions.set(name, resolution); - } - if (logChanges && this.filesWithChangedSetOfUnresolvedImports && !resolutionIsEqualTo(existingResolution, resolution)) { - this.filesWithChangedSetOfUnresolvedImports.push(path); - // reset log changes to avoid recording the same file multiple times - logChanges = false; - } - } - - Debug.assert(resolution !== undefined); - - resolvedModules.push(getResult(resolution)); - } - - // replace old results with a new one - cache.set(path, newResolutions); - return resolvedModules; - - function resolutionIsEqualTo(oldResolution: T, newResolution: T): boolean { - if (oldResolution === newResolution) { - return true; - } - if (!oldResolution || !newResolution || oldResolution.isInvalidated) { - return false; - } - const oldResult = getResult(oldResolution); - const newResult = getResult(newResolution); - if (oldResult === newResult) { - return true; - } - if (!oldResult || !newResult) { - return false; - } - return getResultFileName(oldResult) === getResultFileName(newResult); - } - - function moduleResolutionIsValid(resolution: T): boolean { - if (!resolution || resolution.isInvalidated) { - return false; - } - - const result = getResult(resolution); - if (result) { - return true; - } - - // consider situation if we have no candidate locations as valid resolution. - // after all there is no point to invalidate it if we have no idea where to look for the module. - return resolution.failedLookupLocations.length === 0; - } - } - getNewLine() { return this.host.newLine; } @@ -270,13 +157,11 @@ namespace ts.server { } resolveTypeReferenceDirectives(typeDirectiveNames: string[], containingFile: string): ResolvedTypeReferenceDirective[] { - return this.resolveNamesWithLocalCache(typeDirectiveNames, containingFile, this.resolvedTypeReferenceDirectives, resolveTypeReferenceDirective, - m => m.resolvedTypeReferenceDirective, r => r.resolvedFileName, /*logChanges*/ false); + return this.project.resolutionCache.resolveTypeReferenceDirectives(typeDirectiveNames, containingFile); } resolveModuleNames(moduleNames: string[], containingFile: string): ResolvedModuleFull[] { - return this.resolveNamesWithLocalCache(moduleNames, containingFile, this.resolvedModuleNames, this.resolveModuleName, - m => m.resolvedModule, r => r.resolvedFileName, /*logChanges*/ true); + return this.project.resolutionCache.resolveModuleNames(moduleNames, containingFile, /*logChanges*/ true); } getDefaultLibFileName() { @@ -339,44 +224,5 @@ namespace ts.server { getDirectories(path: string): string[] { return this.host.getDirectories(path); } - - notifyFileRemoved(info: ScriptInfo) { - this.invalidateResolutionOfDeletedFile(info, this.resolvedModuleNames, - m => m.resolvedModule, r => r.resolvedFileName); - this.invalidateResolutionOfDeletedFile(info, this.resolvedTypeReferenceDirectives, - m => m.resolvedTypeReferenceDirective, r => r.resolvedFileName); - } - - private invalidateResolutionOfDeletedFile( - deletedInfo: ScriptInfo, - cache: Map>, - getResult: (s: T) => R, - getResultFileName: (result: R) => string | undefined) { - cache.forEach((value, path) => { - if (path === deletedInfo.path) { - cache.delete(path); - } - else if (value) { - value.forEach((resolution) => { - if (resolution && !resolution.isInvalidated) { - const result = getResult(resolution); - if (result) { - if (getResultFileName(result) === deletedInfo.path) { - resolution.isInvalidated = true; - } - } - } - }); - } - }); - } - - setCompilationSettings(opt: CompilerOptions) { - if (changesAffectModuleResolution(this.compilationSettings, opt)) { - this.resolvedModuleNames.clear(); - this.resolvedTypeReferenceDirectives.clear(); - } - this.compilationSettings = opt; - } } } diff --git a/src/server/project.ts b/src/server/project.ts index 02e2fb20759a1..ebad00e03be7b 100644 --- a/src/server/project.ts +++ b/src/server/project.ts @@ -127,6 +127,9 @@ namespace ts.server { public languageServiceEnabled = true; + /*@internal*/ + resolutionCache: ResolutionCache; + /*@internal*/ lsHost: LSHost; @@ -211,7 +214,14 @@ namespace ts.server { this.setInternalCompilerOptionsForEmittingJsFiles(); this.lsHost = new LSHost(host, this, this.projectService.cancellationToken); - this.lsHost.setCompilationSettings(this.compilerOptions); + this.resolutionCache = createResolutionCache( + fileName => this.projectService.toPath(fileName), + () => this.compilerOptions, + (primaryResult, moduleName, compilerOptions, host) => resolveWithGlobalCache(primaryResult, moduleName, compilerOptions, host, + this.getTypeAcquisition().enable ? this.projectService.typingsInstaller.globalTypingsCacheLocation : undefined, this.getProjectName()) + ); + this.lsHost.compilationSettings = this.compilerOptions; + this.resolutionCache.setModuleResolutionHost(this.lsHost); this.languageService = createLanguageService(this.lsHost, this.documentRegistry); @@ -349,6 +359,7 @@ namespace ts.server { this.rootFilesMap = undefined; this.program = undefined; this.builder = undefined; + this.resolutionCache = undefined; this.cachedUnresolvedImportsPerFile = undefined; this.projectErrors = undefined; this.lsHost.dispose(); @@ -518,7 +529,7 @@ namespace ts.server { if (this.isRoot(info)) { this.removeRoot(info); } - this.lsHost.notifyFileRemoved(info); + this.resolutionCache.invalidateResolutionOfDeletedFile(info.path); this.cachedUnresolvedImportsPerFile.remove(info.path); if (detachFromProject) { @@ -573,11 +584,11 @@ namespace ts.server { * @returns: true if set of files in the project stays the same and false - otherwise. */ updateGraph(): boolean { - this.lsHost.startRecordingFilesWithChangedResolutions(); + this.resolutionCache.startRecordingFilesWithChangedResolutions(); let hasChanges = this.updateGraphWorker(); - const changedFiles: ReadonlyArray = this.lsHost.finishRecordingFilesWithChangedResolutions() || emptyArray; + const changedFiles: ReadonlyArray = this.resolutionCache.finishRecordingFilesWithChangedResolutions() || emptyArray; for (const file of changedFiles) { // delete cached information for changed files @@ -759,9 +770,13 @@ namespace ts.server { this.cachedUnresolvedImportsPerFile.clear(); this.lastCachedUnresolvedImportsList = undefined; } + const oldOptions = this.compilerOptions; this.compilerOptions = compilerOptions; this.setInternalCompilerOptionsForEmittingJsFiles(); - this.lsHost.setCompilationSettings(compilerOptions); + if (changesAffectModuleResolution(oldOptions, compilerOptions)) { + this.resolutionCache.clear(); + } + this.lsHost.compilationSettings = this.compilerOptions; this.markAsDirty(); } From 2762232e038a243715589408e810c86d26be75ad Mon Sep 17 00:00:00 2001 From: Sheetal Nandi Date: Fri, 4 Aug 2017 01:15:02 -0700 Subject: [PATCH 046/109] Test for the module resolution caching --- src/harness/unittests/tscWatchMode.ts | 145 ++++++++++++++++++++++++-- 1 file changed, 138 insertions(+), 7 deletions(-) diff --git a/src/harness/unittests/tscWatchMode.ts b/src/harness/unittests/tscWatchMode.ts index 7c535e4068774..c9f6dcc053f1c 100644 --- a/src/harness/unittests/tscWatchMode.ts +++ b/src/harness/unittests/tscWatchMode.ts @@ -68,7 +68,7 @@ namespace ts.tscWatch { function getEmittedLines(files: FileOrFolderEmit[]) { const seen = createMap(); const result: string[] = []; - for (const { output} of files) { + for (const { output } of files) { if (output && !seen.has(output)) { seen.set(output, true); result.push(output); @@ -1367,12 +1367,12 @@ namespace ts.tscWatch { } function getEmittedFiles(files: FileOrFolderEmit[], contents: string[]): EmittedFile[] { return map(contents, (content, index) => { - return { - content, - path: changeExtension(files[index].path, Extension.Js), - shouldBeWritten: true - }; - } + return { + content, + path: changeExtension(files[index].path, Extension.Js), + shouldBeWritten: true + }; + } ); } function verifyEmittedFiles(host: WatchedSystem, emittedFiles: EmittedFile[]) { @@ -1479,4 +1479,135 @@ namespace ts.tscWatch { } }); }); + + describe("tsc-watch module resolution caching", () => { + it("works", () => { + const root = { + path: "/a/d/f0.ts", + content: `import {x} from "f1"` + }; + + const imported = { + path: "/a/f1.ts", + content: `foo()` + }; + + const f1IsNotModule = `a/d/f0.ts(1,17): error TS2306: File '${imported.path}' is not a module.\n`; + const cannotFindFoo = `a/f1.ts(1,1): error TS2304: Cannot find name 'foo'.\n`; + const cannotAssignValue = "a/d/f0.ts(2,21): error TS2322: Type '1' is not assignable to type 'string'.\n"; + + const files = [root, imported, libFile]; + const host = createWatchedSystem(files); + createWatchModeWithoutConfigFile([root.path], host, { module: ModuleKind.AMD }); + + // ensure that imported file was found + checkOutputContains(host, [f1IsNotModule, cannotFindFoo]); + host.clearOutput(); + + const originalFileExists = host.fileExists; + { + const newContent = `import {x} from "f1" + var x: string = 1;`; + root.content = newContent; + host.reloadFS(files); + + // patch fileExists to make sure that disk is not touched + host.fileExists = notImplemented; + + // trigger synchronization to make sure that import will be fetched from the cache + host.runQueuedTimeoutCallbacks(); + + // ensure file has correct number of errors after edit + checkOutputContains(host, [f1IsNotModule, cannotAssignValue]); + host.clearOutput(); + } + { + let fileExistsIsCalled = false; + host.fileExists = (fileName): boolean => { + if (fileName === "lib.d.ts") { + return false; + } + fileExistsIsCalled = true; + assert.isTrue(fileName.indexOf("/f2.") !== -1); + return originalFileExists.call(host, fileName); + }; + + root.content = `import {x} from "f2"`; + host.reloadFS(files); + + // trigger synchronization to make sure that LSHost will try to find 'f2' module on disk + host.runQueuedTimeoutCallbacks(); + + // ensure file has correct number of errors after edit + const cannotFindModuleF2 = `a/d/f0.ts(1,17): error TS2307: Cannot find module 'f2'.\n`; + checkOutputContains(host, [cannotFindModuleF2]); + host.clearOutput(); + + assert.isTrue(fileExistsIsCalled); + } + { + let fileExistsCalled = false; + host.fileExists = (fileName): boolean => { + if (fileName === "lib.d.ts") { + return false; + } + fileExistsCalled = true; + assert.isTrue(fileName.indexOf("/f1.") !== -1); + return originalFileExists.call(host, fileName); + }; + + const newContent = `import {x} from "f1"`; + root.content = newContent; + + host.reloadFS(files); + host.runQueuedTimeoutCallbacks(); + + checkOutputContains(host, [f1IsNotModule, cannotFindFoo]); + assert.isTrue(fileExistsCalled); + } + }); + + it("loads missing files from disk", () => { + const root = { + path: `/a/foo.ts`, + content: `import {x} from "bar"` + }; + + const imported = { + path: `/a/bar.d.ts`, + content: `export const y = 1;` + }; + + const files = [root, libFile]; + const host = createWatchedSystem(files); + const originalFileExists = host.fileExists; + + let fileExistsCalledForBar = false; + host.fileExists = fileName => { + if (fileName === "lib.d.ts") { + return false; + } + if (!fileExistsCalledForBar) { + fileExistsCalledForBar = fileName.indexOf("/bar.") !== -1; + } + + return originalFileExists.call(host, fileName); + }; + + createWatchModeWithoutConfigFile([root.path], host, { module: ModuleKind.AMD }); + + const barNotFound = `a/foo.ts(1,17): error TS2307: Cannot find module 'bar'.\n`; + assert.isTrue(fileExistsCalledForBar, "'fileExists' should be called"); + checkOutputContains(host, [barNotFound]); + host.clearOutput(); + + fileExistsCalledForBar = false; + root.content = `import {y} from "bar"`; + host.reloadFS(files.concat(imported)); + + host.runQueuedTimeoutCallbacks(); + assert.isTrue(fileExistsCalledForBar, "'fileExists' should be called."); + checkOutputDoesNotContain(host, [barNotFound]); + }); + }); } From 65a6ee07e981d996c12e4df66214469fc1e56471 Mon Sep 17 00:00:00 2001 From: Sheetal Nandi Date: Fri, 4 Aug 2017 23:55:13 -0700 Subject: [PATCH 047/109] Add test that fails because we dont watch module resolutions failed paths --- src/harness/unittests/tscWatchMode.ts | 47 +++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) diff --git a/src/harness/unittests/tscWatchMode.ts b/src/harness/unittests/tscWatchMode.ts index c9f6dcc053f1c..8d6587fc2b389 100644 --- a/src/harness/unittests/tscWatchMode.ts +++ b/src/harness/unittests/tscWatchMode.ts @@ -1609,5 +1609,52 @@ namespace ts.tscWatch { assert.isTrue(fileExistsCalledForBar, "'fileExists' should be called."); checkOutputDoesNotContain(host, [barNotFound]); }); + + it("should compile correctly when resolved module goes missing and then comes back (module is not part of the root)", () => { + const root = { + path: `/a/foo.ts`, + content: `import {x} from "bar"` + }; + + const imported = { + path: `/a/bar.d.ts`, + content: `export const y = 1;` + }; + + const files = [root, libFile]; + const filesWithImported = files.concat(imported); + const host = createWatchedSystem(filesWithImported); + const originalFileExists = host.fileExists; + let fileExistsCalledForBar = false; + host.fileExists = fileName => { + if (fileName === "lib.d.ts") { + return false; + } + if (!fileExistsCalledForBar) { + fileExistsCalledForBar = fileName.indexOf("/bar.") !== -1; + } + return originalFileExists.call(host, fileName); + }; + + createWatchModeWithoutConfigFile([root.path], host, { module: ModuleKind.AMD }); + + const barNotFound = `a/foo.ts(1,17): error TS2307: Cannot find module 'bar'.\n`; + assert.isTrue(fileExistsCalledForBar, "'fileExists' should be called"); + checkOutputDoesNotContain(host, [barNotFound]); + host.clearOutput(); + + fileExistsCalledForBar = false; + host.reloadFS(files); + host.runQueuedTimeoutCallbacks(); + assert.isTrue(fileExistsCalledForBar, "'fileExists' should be called."); + checkOutputContains(host, [barNotFound]); + host.clearOutput(); + + fileExistsCalledForBar = false; + host.reloadFS(filesWithImported); + host.checkTimeoutQueueLengthAndRun(1); + assert.isTrue(fileExistsCalledForBar, "'fileExists' should be called."); + checkOutputDoesNotContain(host, [barNotFound]); + }); }); } From d55150cbd34cbfab15d91d9a18c17e3cc2a92ff6 Mon Sep 17 00:00:00 2001 From: Sheetal Nandi Date: Fri, 4 Aug 2017 23:09:11 -0700 Subject: [PATCH 048/109] Implementation of watching the failed lookup locations --- src/compiler/resolutionCache.ts | 88 ++++++++++++++++++++++++++++++++- src/compiler/watchedProgram.ts | 13 ++++- src/server/project.ts | 4 ++ 3 files changed, 102 insertions(+), 3 deletions(-) diff --git a/src/compiler/resolutionCache.ts b/src/compiler/resolutionCache.ts index f92464e5a3e63..f2d20ab19a2ab 100644 --- a/src/compiler/resolutionCache.ts +++ b/src/compiler/resolutionCache.ts @@ -33,13 +33,20 @@ namespace ts { export function createResolutionCache( toPath: (fileName: string) => Path, getCompilerOptions: () => CompilerOptions, + clearProgramAndScheduleUpdate: () => void, + watchForFailedLookupLocation: (fileName: string, callback: FileWatcherCallback) => FileWatcher, + log: (s: string) => void, resolveWithGlobalCache?: ResolverWithGlobalCache): ResolutionCache { let host: ModuleResolutionHost; let filesWithChangedSetOfUnresolvedImports: Path[]; + const resolvedModuleNames = createMap>(); const resolvedTypeReferenceDirectives = createMap>(); + type FailedLookupLocationsWatcher = { fileWatcher: FileWatcher; refCount: number }; + const failedLookupLocationsWatches = createMap(); + return { setModuleResolutionHost, startRecordingFilesWithChangedResolutions, @@ -55,6 +62,11 @@ namespace ts { } function clear() { + failedLookupLocationsWatches.forEach((failedLookupLocationWatcher, failedLookupLocationPath: Path) => { + log(`Watcher: FailedLookupLocations: Status: ForceClose: LocationPath: ${failedLookupLocationPath}, refCount: ${failedLookupLocationWatcher.refCount}`); + failedLookupLocationWatcher.fileWatcher.close(); + }); + failedLookupLocationsWatches.clear(); resolvedModuleNames.clear(); resolvedTypeReferenceDirectives.clear(); } @@ -103,8 +115,9 @@ namespace ts { } else { resolution = loader(name, containingFile, compilerOptions, host); - newResolutions.set(name, resolution); + updateFailedLookupLocationWatches(containingFile, name, existingResolution && existingResolution.failedLookupLocations, resolution.failedLookupLocations); } + newResolutions.set(name, resolution); if (logChanges && filesWithChangedSetOfUnresolvedImports && !resolutionIsEqualTo(existingResolution, resolution)) { filesWithChangedSetOfUnresolvedImports.push(path); // reset log changes to avoid recording the same file multiple times @@ -155,7 +168,6 @@ namespace ts { } } - function resolveTypeReferenceDirectives(typeDirectiveNames: string[], containingFile: string): ResolvedTypeReferenceDirective[] { return resolveNamesWithLocalCache(typeDirectiveNames, containingFile, resolvedTypeReferenceDirectives, resolveTypeReferenceDirective, m => m.resolvedTypeReferenceDirective, r => r.resolvedFileName, /*logChanges*/ false); @@ -166,6 +178,75 @@ namespace ts { m => m.resolvedModule, r => r.resolvedFileName, logChanges); } + function watchFailedLookupLocation(failedLookupLocation: string, failedLookupLocationPath: Path, containingFile: string, name: string) { + const failedLookupLocationWatcher = failedLookupLocationsWatches.get(failedLookupLocationPath); + if (failedLookupLocationWatcher) { + failedLookupLocationWatcher.refCount++; + log(`Watcher: FailedLookupLocations: Status: Using existing watcher: Location: ${failedLookupLocation}, containingFile: ${containingFile}, name: ${name} refCount: ${failedLookupLocationWatcher.refCount}`); + } + else { + const fileWatcher = watchForFailedLookupLocation(failedLookupLocation, (__fileName, eventKind) => { + log(`Watcher: FailedLookupLocations: Status: ${FileWatcherEventKind[eventKind]}: Location: ${failedLookupLocation}, containingFile: ${containingFile}, name: ${name}`); + // There is some kind of change in the failed lookup location, update the program + clearProgramAndScheduleUpdate(); + }); + failedLookupLocationsWatches.set(failedLookupLocationPath, { fileWatcher, refCount: 1 }); + } + } + + function closeFailedLookupLocationWatcher(failedLookupLocation: string, failedLookupLocationPath: Path, containingFile: string, name: string) { + const failedLookupLocationWatcher = failedLookupLocationsWatches.get(failedLookupLocationPath); + Debug.assert(!!failedLookupLocationWatcher); + failedLookupLocationWatcher.refCount--; + if (failedLookupLocationWatcher.refCount) { + log(`Watcher: FailedLookupLocations: Status: Removing existing watcher: Location: ${failedLookupLocation}, containingFile: ${containingFile}, name: ${name}: refCount: ${failedLookupLocationWatcher.refCount}`); + } + else { + log(`Watcher: FailedLookupLocations: Status: Closing the file watcher: Location: ${failedLookupLocation}, containingFile: ${containingFile}, name: ${name}`); + failedLookupLocationWatcher.fileWatcher.close(); + failedLookupLocationsWatches.delete(failedLookupLocationPath); + } + } + + type FailedLookupLocationAction = (failedLookupLocation: string, failedLookupLocationPath: Path, containingFile: string, name: string) => void; + function withFailedLookupLocations(failedLookupLocations: string[], containingFile: string, name: string, fn: FailedLookupLocationAction) { + forEach(failedLookupLocations, failedLookupLocation => { + fn(failedLookupLocation, toPath(failedLookupLocation), containingFile, name); + }); + } + + function updateFailedLookupLocationWatches(containingFile: string, name: string, existingFailedLookupLocations: string[], failedLookupLocations: string[]) { + if (failedLookupLocations) { + if (existingFailedLookupLocations) { + const existingWatches = arrayToMap(existingFailedLookupLocations, failedLookupLocation => toPath(failedLookupLocation)); + for (const failedLookupLocation of failedLookupLocations) { + const failedLookupLocationPath = toPath(failedLookupLocation); + if (existingWatches && existingWatches.has(failedLookupLocationPath)) { + // still has same failed lookup location, keep the was + existingWatches.delete(failedLookupLocationPath); + } + else { + // Create new watch + watchFailedLookupLocation(failedLookupLocation, failedLookupLocationPath, containingFile, name); + } + } + + // Close all the watches that are still present in the existingWatches since those are not the locations looked up for buy new resolution + existingWatches.forEach((failedLookupLocation, failedLookupLocationPath: Path) => + closeFailedLookupLocationWatcher(failedLookupLocation, failedLookupLocationPath, containingFile, name) + ); + } + else { + // Watch all the failed lookup locations + withFailedLookupLocations(failedLookupLocations, containingFile, name, watchFailedLookupLocation); + } + } + else { + // Close existing watches for the failed locations + withFailedLookupLocations(existingFailedLookupLocations, containingFile, name, closeFailedLookupLocationWatcher); + } + } + function invalidateResolutionCacheOfDeletedFile( deletedFilePath: Path, cache: Map>, @@ -174,6 +255,9 @@ namespace ts { cache.forEach((value, path) => { if (path === deletedFilePath) { cache.delete(path); + value.forEach((resolution, name) => { + withFailedLookupLocations(resolution.failedLookupLocations, path, name, closeFailedLookupLocationWatcher); + }); } else if (value) { value.forEach((resolution) => { diff --git a/src/compiler/watchedProgram.ts b/src/compiler/watchedProgram.ts index 8bf5c54245672..6b16474e26897 100644 --- a/src/compiler/watchedProgram.ts +++ b/src/compiler/watchedProgram.ts @@ -271,7 +271,13 @@ namespace ts { const getCanonicalFileName = createGetCanonicalFileName(host.useCaseSensitiveFileNames); // Cache for the module resolution - const resolutionCache = createResolutionCache(fileName => toPath(fileName), () => compilerOptions); + const resolutionCache = createResolutionCache( + fileName => toPath(fileName), + () => compilerOptions, + () => clearExistingProgramAndScheduleProgramUpdate(), + (fileName, callback) => system.watchFile(fileName, callback), + s => writeLog(s) + ); // There is no extra check needed since we can just rely on the program to decide emit const builder = createBuilder(getCanonicalFileName, getFileEmitOutput, computeHash, _sourceFile => true); @@ -465,6 +471,11 @@ namespace ts { scheduleProgramUpdate(); } + function clearExistingProgramAndScheduleProgramUpdate() { + program = undefined; + scheduleProgramUpdate(); + } + function updateProgram() { timerToUpdateProgram = undefined; reportWatchDiagnostic(createCompilerDiagnostic(Diagnostics.File_change_detected_Starting_incremental_compilation)); diff --git a/src/server/project.ts b/src/server/project.ts index ebad00e03be7b..c2fab978931d6 100644 --- a/src/server/project.ts +++ b/src/server/project.ts @@ -217,6 +217,9 @@ namespace ts.server { this.resolutionCache = createResolutionCache( fileName => this.projectService.toPath(fileName), () => this.compilerOptions, + () => this.markAsDirty(), + (fileName, callback) => host.watchFile(fileName, callback), + s => this.projectService.logger.info(s), (primaryResult, moduleName, compilerOptions, host) => resolveWithGlobalCache(primaryResult, moduleName, compilerOptions, host, this.getTypeAcquisition().enable ? this.projectService.typingsInstaller.globalTypingsCacheLocation : undefined, this.getProjectName()) ); @@ -359,6 +362,7 @@ namespace ts.server { this.rootFilesMap = undefined; this.program = undefined; this.builder = undefined; + this.resolutionCache.clear(); this.resolutionCache = undefined; this.cachedUnresolvedImportsPerFile = undefined; this.projectErrors = undefined; From 8dc62484ec32820e749aaa918af863397459bc7d Mon Sep 17 00:00:00 2001 From: Sheetal Nandi Date: Sat, 5 Aug 2017 02:27:27 -0700 Subject: [PATCH 049/109] Partial implementation for invalidating the program (instead of source file as that would involve more work) so the files are picked up --- src/compiler/resolutionCache.ts | 36 ++++++++++--- src/compiler/watchedProgram.ts | 50 +++++++++++++++---- .../unittests/tsserverProjectSystem.ts | 3 +- src/server/editorServices.ts | 3 +- src/server/project.ts | 20 +++++++- 5 files changed, 91 insertions(+), 21 deletions(-) diff --git a/src/compiler/resolutionCache.ts b/src/compiler/resolutionCache.ts index f2d20ab19a2ab..6ca318f7183a5 100644 --- a/src/compiler/resolutionCache.ts +++ b/src/compiler/resolutionCache.ts @@ -9,6 +9,7 @@ namespace ts { resolveModuleNames(moduleNames: string[], containingFile: string, logChanges: boolean): ResolvedModuleFull[]; resolveTypeReferenceDirectives(typeDirectiveNames: string[], containingFile: string): ResolvedTypeReferenceDirective[]; invalidateResolutionOfDeletedFile(filePath: Path): void; + invalidateResolutionOfChangedFailedLookupLocation(failedLookupLocation: string): void; clear(): void; } @@ -33,8 +34,7 @@ namespace ts { export function createResolutionCache( toPath: (fileName: string) => Path, getCompilerOptions: () => CompilerOptions, - clearProgramAndScheduleUpdate: () => void, - watchForFailedLookupLocation: (fileName: string, callback: FileWatcherCallback) => FileWatcher, + watchForFailedLookupLocation: (failedLookupLocation: string, containingFile: string, name: string) => FileWatcher, log: (s: string) => void, resolveWithGlobalCache?: ResolverWithGlobalCache): ResolutionCache { @@ -54,6 +54,7 @@ namespace ts { resolveModuleNames, resolveTypeReferenceDirectives, invalidateResolutionOfDeletedFile, + invalidateResolutionOfChangedFailedLookupLocation, clear }; @@ -185,11 +186,8 @@ namespace ts { log(`Watcher: FailedLookupLocations: Status: Using existing watcher: Location: ${failedLookupLocation}, containingFile: ${containingFile}, name: ${name} refCount: ${failedLookupLocationWatcher.refCount}`); } else { - const fileWatcher = watchForFailedLookupLocation(failedLookupLocation, (__fileName, eventKind) => { - log(`Watcher: FailedLookupLocations: Status: ${FileWatcherEventKind[eventKind]}: Location: ${failedLookupLocation}, containingFile: ${containingFile}, name: ${name}`); - // There is some kind of change in the failed lookup location, update the program - clearProgramAndScheduleUpdate(); - }); + log(`Watcher: FailedLookupLocations: Status: new watch: Location: ${failedLookupLocation}, containingFile: ${containingFile}, name: ${name}`); + const fileWatcher = watchForFailedLookupLocation(failedLookupLocation, containingFile, name); failedLookupLocationsWatches.set(failedLookupLocationPath, { fileWatcher, refCount: 1 }); } } @@ -260,7 +258,7 @@ namespace ts { }); } else if (value) { - value.forEach((resolution) => { + value.forEach((resolution, __name) => { if (resolution && !resolution.isInvalidated) { const result = getResult(resolution); if (result) { @@ -274,9 +272,31 @@ namespace ts { }); } + function invalidateResolutionCacheOfChangedFailedLookupLocation( + failedLookupLocation: string, + cache: Map>) { + cache.forEach((value, _containingFilePath) => { + 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. + resolution.isInvalidated = true; + } + }); + } + }); + } + function invalidateResolutionOfDeletedFile(filePath: Path) { invalidateResolutionCacheOfDeletedFile(filePath, resolvedModuleNames, m => m.resolvedModule, r => r.resolvedFileName); invalidateResolutionCacheOfDeletedFile(filePath, resolvedTypeReferenceDirectives, m => m.resolvedTypeReferenceDirective, r => r.resolvedFileName); } + + function invalidateResolutionOfChangedFailedLookupLocation(failedLookupLocation: string) { + invalidateResolutionCacheOfChangedFailedLookupLocation(failedLookupLocation, resolvedModuleNames); + invalidateResolutionCacheOfChangedFailedLookupLocation(failedLookupLocation, resolvedTypeReferenceDirectives); + } } } diff --git a/src/compiler/watchedProgram.ts b/src/compiler/watchedProgram.ts index 6b16474e26897..c668d4a1510bd 100644 --- a/src/compiler/watchedProgram.ts +++ b/src/compiler/watchedProgram.ts @@ -255,6 +255,7 @@ namespace ts { let timerToUpdateProgram: any; // timer callback to recompile the program const sourceFilesCache = createMap(); // 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 watchingHost = watchingHost || createWatchingSystemHost(compilerOptions.pretty); const { system, parseConfigFile, reportDiagnostic, reportWatchDiagnostic, beforeCompile, afterCompile } = watchingHost; @@ -274,8 +275,7 @@ namespace ts { const resolutionCache = createResolutionCache( fileName => toPath(fileName), () => compilerOptions, - () => clearExistingProgramAndScheduleProgramUpdate(), - (fileName, callback) => system.watchFile(fileName, callback), + watchFailedLookupLocation, s => writeLog(s) ); @@ -310,6 +310,19 @@ namespace ts { // Update watches missingFilesMap = updateMissingFilePathsWatch(program, missingFilesMap, watchMissingFilePath, closeMissingFilePathWatcher); + if (missingFilePathsRequestedForRelease) { + // These are the paths that program creater told us as not in use any more but were missing on the disk. + // We didnt remove the entry for them from sourceFiles cache so that we dont have to do File IO, + // if there is already watcher for it (for missing files) + // At that point our watches were updated, hence now we know that these paths are not tracked and need to be removed + // so that at later time we have correct result of their presence + for (const missingFilePath of missingFilePathsRequestedForRelease) { + if (!missingFilesMap.has(missingFilePath)) { + sourceFilesCache.delete(missingFilePath); + } + } + missingFilePathsRequestedForRelease = undefined; + } afterCompile(host, program, builder); reportWatchDiagnostic(createCompilerDiagnostic(Diagnostics.Compilation_complete_Watching_for_file_changes)); @@ -446,8 +459,14 @@ namespace ts { // remove the cached entry. // Note we arent deleting entry if file became missing in new program or // there was version update and new source file was created. - if (hostSourceFileInfo && !isString(hostSourceFileInfo) && hostSourceFileInfo.sourceFile === oldSourceFile) { - sourceFilesCache.delete(oldSourceFile.path); + if (hostSourceFileInfo) { + // record the missing file paths so they can be removed later if watchers arent tracking them + if (isString(hostSourceFileInfo)) { + (missingFilePathsRequestedForRelease || (missingFilePathsRequestedForRelease = [])).push(oldSourceFile.path); + } + else if (hostSourceFileInfo.sourceFile === oldSourceFile) { + sourceFilesCache.delete(oldSourceFile.path); + } } } @@ -471,11 +490,6 @@ namespace ts { scheduleProgramUpdate(); } - function clearExistingProgramAndScheduleProgramUpdate() { - program = undefined; - scheduleProgramUpdate(); - } - function updateProgram() { timerToUpdateProgram = undefined; reportWatchDiagnostic(createCompilerDiagnostic(Diagnostics.File_change_detected_Starting_incremental_compilation)); @@ -547,6 +561,24 @@ namespace ts { } } + function watchFailedLookupLocation(failedLookupLocation: string, containingFile: string, name: string) { + return host.watchFile(failedLookupLocation, (fileName, eventKind) => onFailedLookupLocationChange(fileName, eventKind, failedLookupLocation, containingFile, name)); + } + + function onFailedLookupLocationChange(fileName: string, eventKind: FileWatcherEventKind, failedLookupLocation: string, containingFile: string, name: string) { + 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; + scheduleProgramUpdate(); + } + function watchMissingFilePath(missingFilePath: Path) { return host.watchFile(missingFilePath, (fileName, eventKind) => onMissingFileChange(fileName, missingFilePath, eventKind)); } diff --git a/src/harness/unittests/tsserverProjectSystem.ts b/src/harness/unittests/tsserverProjectSystem.ts index b7c71f281916a..39776e379c9d2 100644 --- a/src/harness/unittests/tsserverProjectSystem.ts +++ b/src/harness/unittests/tsserverProjectSystem.ts @@ -335,7 +335,8 @@ namespace ts.projectSystem { checkFileNames("inferred project", project.getFileNames(), [appFile.path, libFile.path, moduleFile.path]); const configFileLocations = ["/a/b/c/", "/a/b/", "/a/", "/"]; const configFiles = flatMap(configFileLocations, location => [location + "tsconfig.json", location + "jsconfig.json"]); - checkWatchedFiles(host, configFiles.concat(libFile.path, moduleFile.path)); + const moduleLookupLocations = ["/a/b/c/module.ts", "/a/b/c/module.tsx"]; + checkWatchedFiles(host, configFiles.concat(libFile.path, moduleFile.path, ...moduleLookupLocations)); }); it("can handle tsconfig file name with difference casing", () => { diff --git a/src/server/editorServices.ts b/src/server/editorServices.ts index d617bdb2b9e05..bf255c7defa17 100644 --- a/src/server/editorServices.ts +++ b/src/server/editorServices.ts @@ -247,7 +247,8 @@ namespace ts.server { WildCardDirectories = "Wild card directory", TypeRoot = "Type root of the project", ClosedScriptInfo = "Closed Script info", - ConfigFileForInferredRoot = "Config file for the inferred project root" + ConfigFileForInferredRoot = "Config file for the inferred project root", + FailedLookupLocation = "Failed lookup locations in module resolution" } /* @internal */ diff --git a/src/server/project.ts b/src/server/project.ts index c2fab978931d6..e600c3796e67d 100644 --- a/src/server/project.ts +++ b/src/server/project.ts @@ -217,8 +217,7 @@ namespace ts.server { this.resolutionCache = createResolutionCache( fileName => this.projectService.toPath(fileName), () => this.compilerOptions, - () => this.markAsDirty(), - (fileName, callback) => host.watchFile(fileName, callback), + (failedLookupLocation, containingFile, name) => this.watchFailedLookupLocation(failedLookupLocation, containingFile, name), s => this.projectService.logger.info(s), (primaryResult, moduleName, compilerOptions, host) => resolveWithGlobalCache(primaryResult, moduleName, compilerOptions, host, this.getTypeAcquisition().enable ? this.projectService.typingsInstaller.globalTypingsCacheLocation : undefined, this.getProjectName()) @@ -235,6 +234,23 @@ namespace ts.server { this.markAsDirty(); } + private watchFailedLookupLocation(failedLookupLocation: string, containingFile: string, name: string) { + // There is some kind of change in the failed lookup location, update the program + return this.projectService.addFileWatcher(WatchType.FailedLookupLocation, this, failedLookupLocation, (__fileName, eventKind) => { + this.projectService.logger.info(`Watcher: FailedLookupLocations: Status: ${FileWatcherEventKind[eventKind]}: Location: ${failedLookupLocation}, containingFile: ${containingFile}, name: ${name}`); + 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.projectService.delayUpdateProjectGraphAndInferredProjectsRefresh(this); + }); + } + private setInternalCompilerOptionsForEmittingJsFiles() { if (this.projectKind === ProjectKind.Inferred || this.projectKind === ProjectKind.External) { this.compilerOptions.noEmitForJsFiles = true; From 7474ba762cadf7aa20d9ffa6fccc3420ad79942c Mon Sep 17 00:00:00 2001 From: Sheetal Nandi Date: Sat, 5 Aug 2017 05:01:33 -0700 Subject: [PATCH 050/109] Implementation for invalidating source file containing possibly changed module resolution --- src/compiler/builder.ts | 18 ++++++++---------- src/compiler/core.ts | 2 +- src/compiler/moduleNameResolver.ts | 2 ++ src/compiler/program.ts | 19 +++++++++++++------ src/compiler/resolutionCache.ts | 24 +++++++++++++++++++----- src/compiler/types.ts | 1 + src/compiler/watchedProgram.ts | 17 +++++++---------- src/server/lsHost.ts | 4 ++++ src/server/project.ts | 11 ++++------- src/services/services.ts | 8 ++++++-- src/services/types.ts | 1 + 11 files changed, 66 insertions(+), 41 deletions(-) diff --git a/src/compiler/builder.ts b/src/compiler/builder.ts index 489111f26db83..c0af4f5b13d6e 100644 --- a/src/compiler/builder.ts +++ b/src/compiler/builder.ts @@ -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, diff --git a/src/compiler/core.ts b/src/compiler/core.ts index 2a41427f2fdbc..813d805adead8 100644 --- a/src/compiler/core.ts +++ b/src/compiler/core.ts @@ -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 { diff --git a/src/compiler/moduleNameResolver.ts b/src/compiler/moduleNameResolver.ts index 027d7ca202535..3258bba665612 100644 --- a/src/compiler/moduleNameResolver.ts +++ b/src/compiler/moduleNameResolver.ts @@ -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. diff --git a/src/compiler/program.ts b/src/compiler/program.ts index 8ce51ac5e8c04..58931379d7c46 100644 --- a/src/compiler/program.ts +++ b/src/compiler/program.ts @@ -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); diff --git a/src/compiler/resolutionCache.ts b/src/compiler/resolutionCache.ts index 6ca318f7183a5..b6f3e7547f1c9 100644 --- a/src/compiler/resolutionCache.ts +++ b/src/compiler/resolutionCache.ts @@ -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; const resolvedModuleNames = createMap>(); const resolvedTypeReferenceDirectives = createMap>(); @@ -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>, 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())).set(path, true); } } } @@ -275,14 +290,13 @@ namespace ts { function invalidateResolutionCacheOfChangedFailedLookupLocation( failedLookupLocation: string, cache: Map>) { - 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())).set(containingFile, true); } }); } diff --git a/src/compiler/types.ts b/src/compiler/types.ts index 45f5a16a80c7d..c1ab964af304b 100644 --- a/src/compiler/types.ts +++ b/src/compiler/types.ts @@ -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 */ diff --git a/src/compiler/watchedProgram.ts b/src/compiler/watchedProgram.ts index c668d4a1510bd..8628b6d049bcb 100644 --- a/src/compiler/watchedProgram.ts +++ b/src/compiler/watchedProgram.ts @@ -256,6 +256,7 @@ namespace ts { const sourceFilesCache = createMap(); // 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(); } diff --git a/src/server/lsHost.ts b/src/server/lsHost.ts index 274c3c3aef682..f4cbd402604ca 100644 --- a/src/server/lsHost.ts +++ b/src/server/lsHost.ts @@ -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 diff --git a/src/server/project.ts b/src/server/project.ts index e600c3796e67d..5ea54aca54eef 100644 --- a/src/server/project.ts +++ b/src/server/project.ts @@ -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(); diff --git a/src/services/services.ts b/src/services/services.ts index 40a446e0967a4..14b5ddd85c4f8 100644 --- a/src/services/services.ts +++ b/src/services/services.ts @@ -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); diff --git a/src/services/types.ts b/src/services/types.ts index e17df08378ad0..018e8941765ec 100644 --- a/src/services/types.ts +++ b/src/services/types.ts @@ -185,6 +185,7 @@ namespace ts { */ resolveModuleNames?(moduleNames: string[], containingFile: string): ResolvedModule[]; resolveTypeReferenceDirectives?(typeDirectiveNames: string[], containingFile: string): ResolvedTypeReferenceDirective[]; + hasInvalidatedResolution?: HasInvalidatedResolution; directoryExists?(directoryName: string): boolean; /* From 6385f7e3bb2c97118211754b2a11ff2356fe11ab Mon Sep 17 00:00:00 2001 From: Sheetal Nandi Date: Mon, 7 Aug 2017 14:47:32 -0700 Subject: [PATCH 051/109] Get semantic diagnostics for the program from builder so that it caches the errors of unchanged files --- src/compiler/builder.ts | 57 ++++++++++- src/compiler/resolutionCache.ts | 2 +- src/compiler/tsc.ts | 25 ++++- src/compiler/watchedProgram.ts | 168 +++++++++++++++----------------- 4 files changed, 157 insertions(+), 95 deletions(-) diff --git a/src/compiler/builder.ts b/src/compiler/builder.ts index c0af4f5b13d6e..7d4b1ce37a283 100644 --- a/src/compiler/builder.ts +++ b/src/compiler/builder.ts @@ -26,6 +26,7 @@ namespace ts { getFilesAffectedBy(program: Program, path: Path): string[]; emitFile(program: Program, path: Path): EmitOutput; emitChangedFiles(program: Program): EmitOutputDetailed[]; + getSemanticDiagnostics(program: Program, cancellationToken?: CancellationToken): Diagnostic[]; clear(): void; } @@ -74,6 +75,7 @@ namespace ts { // Last checked shape signature for the file info type FileInfo = { version: string; signature: string; }; let fileInfos: Map; + const semanticDiagnosticsPerFile = createMap(); let changedFilesSinceLastEmit: Map; let emitHandler: EmitHandler; return { @@ -81,6 +83,7 @@ namespace ts { getFilesAffectedBy, emitFile, emitChangedFiles, + getSemanticDiagnostics, clear }; @@ -90,6 +93,7 @@ namespace ts { isModuleEmit = currentIsModuleEmit; emitHandler = isModuleEmit ? getModuleEmitHandler() : getNonModuleEmitHandler(); fileInfos = undefined; + semanticDiagnosticsPerFile.clear(); } changedFilesSinceLastEmit = changedFilesSinceLastEmit || createMap(); @@ -104,20 +108,27 @@ namespace ts { ); } + function registerChangedFile(path: Path) { + changedFilesSinceLastEmit.set(path, true); + // All changed files need to re-evaluate its semantic diagnostics + semanticDiagnosticsPerFile.delete(path); + } + function addNewFileInfo(program: Program, sourceFile: SourceFile): FileInfo { - changedFilesSinceLastEmit.set(sourceFile.path, true); + registerChangedFile(sourceFile.path); emitHandler.addScriptInfo(program, sourceFile); return { version: sourceFile.version, signature: undefined }; } function removeExistingFileInfo(path: Path, _existingFileInfo: FileInfo) { - changedFilesSinceLastEmit.set(path, true); + registerChangedFile(path); emitHandler.removeScriptInfo(path); } function updateExistingFileInfo(program: Program, existingInfo: FileInfo, sourceFile: SourceFile, hasInvalidatedResolution: HasInvalidatedResolution) { if (existingInfo.version !== sourceFile.version || hasInvalidatedResolution(sourceFile.path)) { - changedFilesSinceLastEmit.set(sourceFile.path, true); + registerChangedFile(sourceFile.path); + semanticDiagnosticsPerFile.delete(sourceFile.path); existingInfo.version = sourceFile.version; emitHandler.updateScriptInfo(program, sourceFile); } @@ -170,6 +181,9 @@ namespace ts { const sourceFile = program.getSourceFile(file); seenFiles.set(file, sourceFile); if (sourceFile) { + // Any affected file shouldnt have the cached diagnostics + semanticDiagnosticsPerFile.delete(sourceFile.path); + const emitOutput = getEmitOutput(program, sourceFile, /*emitOnlyDtsFiles*/ false, /*isDetailed*/ true) as EmitOutputDetailed; result.push(emitOutput); @@ -189,10 +203,45 @@ namespace ts { return result; } + function getSemanticDiagnostics(program: Program, cancellationToken?: CancellationToken): Diagnostic[] { + ensureProgramGraph(program); + + // Ensure that changed files have cleared their respective + if (changedFilesSinceLastEmit) { + changedFilesSinceLastEmit.forEach((__value, path: Path) => { + const affectedFiles = getFilesAffectedBy(program, path); + for (const file of affectedFiles) { + const sourceFile = program.getSourceFile(file); + if (sourceFile) { + semanticDiagnosticsPerFile.delete(sourceFile.path); + } + } + }); + } + + let diagnostics: Diagnostic[]; + for (const sourceFile of program.getSourceFiles()) { + const path = sourceFile.path; + const cachedDiagnostics = semanticDiagnosticsPerFile.get(path); + // Report the semantic diagnostics from the cache if we already have those diagnostics present + if (cachedDiagnostics) { + diagnostics = concatenate(diagnostics, cachedDiagnostics); + } + else { + // Diagnostics werent cached, get them from program, and cache the result + const cachedDiagnostics = program.getSemanticDiagnostics(sourceFile, cancellationToken); + semanticDiagnosticsPerFile.set(path, cachedDiagnostics); + diagnostics = concatenate(diagnostics, cachedDiagnostics); + } + } + return diagnostics || emptyArray; + } + function clear() { isModuleEmit = undefined; emitHandler = undefined; fileInfos = undefined; + semanticDiagnosticsPerFile.clear(); } /** @@ -356,7 +405,7 @@ namespace ts { const referencedByPaths = referencedBy.get(path); if (referencedByPaths) { for (const path of referencedByPaths) { - changedFilesSinceLastEmit.set(path, true); + registerChangedFile(path); } referencedBy.delete(path); } diff --git a/src/compiler/resolutionCache.ts b/src/compiler/resolutionCache.ts index b6f3e7547f1c9..615340e487208 100644 --- a/src/compiler/resolutionCache.ts +++ b/src/compiler/resolutionCache.ts @@ -276,7 +276,7 @@ namespace ts { if (resolution && !resolution.isInvalidated) { const result = getResult(resolution); if (result) { - if (getResultFileName(result) === deletedFilePath) { + if (toPath(getResultFileName(result)) === deletedFilePath) { resolution.isInvalidated = true; (filesWithInvalidatedResolutions || (filesWithInvalidatedResolutions = createMap())).set(path, true); } diff --git a/src/compiler/tsc.ts b/src/compiler/tsc.ts index b89a41a2d2302..78586e5479a7e 100644 --- a/src/compiler/tsc.ts +++ b/src/compiler/tsc.ts @@ -157,7 +157,7 @@ namespace ts { enableStatistics(compilerOptions); const program = createProgram(rootFileNames, compilerOptions, compilerHost); - const exitStatus = compileProgram(sys, program, () => program.emit(), reportDiagnostic); + const exitStatus = compileProgram(program); reportStatistics(program); return sys.exit(exitStatus); @@ -174,6 +174,29 @@ namespace ts { return watchingHost; } + function compileProgram(program: Program): ExitStatus { + let diagnostics: Diagnostic[]; + + // First get and report any syntactic errors. + diagnostics = program.getSyntacticDiagnostics(); + + // If we didn't have any syntactic errors, then also try getting the global and + // semantic errors. + if (diagnostics.length === 0) { + diagnostics = program.getOptionsDiagnostics().concat(program.getGlobalDiagnostics()); + + if (diagnostics.length === 0) { + diagnostics = program.getSemanticDiagnostics(); + } + } + + // Emit and report any errors we ran into. + const { emittedFiles, emitSkipped, diagnostics: emitDiagnostics } = program.emit(); + diagnostics = diagnostics.concat(emitDiagnostics); + + return handleEmitOutputAndReportErrors(sys, program, emittedFiles, emitSkipped, diagnostics, reportDiagnostic); + } + function enableStatistics(compilerOptions: CompilerOptions) { if (compilerOptions.diagnostics || compilerOptions.extendedDiagnostics) { performance.enable(); diff --git a/src/compiler/watchedProgram.ts b/src/compiler/watchedProgram.ts index 8628b6d049bcb..d4301ea839dd0 100644 --- a/src/compiler/watchedProgram.ts +++ b/src/compiler/watchedProgram.ts @@ -101,29 +101,12 @@ namespace ts { } } - export function compileProgram(system: System, program: Program, emitProgram: () => EmitResult, - reportDiagnostic: DiagnosticReporter): ExitStatus { - let diagnostics: Diagnostic[]; - - // First get and report any syntactic errors. - diagnostics = program.getSyntacticDiagnostics(); - - // If we didn't have any syntactic errors, then also try getting the global and - // semantic errors. - if (diagnostics.length === 0) { - diagnostics = program.getOptionsDiagnostics().concat(program.getGlobalDiagnostics()); - - if (diagnostics.length === 0) { - diagnostics = program.getSemanticDiagnostics(); - } - } - - // Emit and report any errors we ran into. - const emitOutput = emitProgram(); - diagnostics = diagnostics.concat(emitOutput.diagnostics); - + export function handleEmitOutputAndReportErrors(system: System, program: Program, + emittedFiles: string[], emitSkipped: boolean, + diagnostics: Diagnostic[], reportDiagnostic: DiagnosticReporter + ): ExitStatus { reportDiagnostics(sortAndDeduplicateDiagnostics(diagnostics), reportDiagnostic); - reportEmittedFiles(emitOutput.emittedFiles, system); + reportEmittedFiles(emittedFiles, system); if (program.getCompilerOptions().listFiles) { forEach(program.getSourceFiles(), file => { @@ -131,7 +114,7 @@ namespace ts { }); } - if (emitOutput.emitSkipped && diagnostics.length > 0) { + if (emitSkipped && diagnostics.length > 0) { // If the emitter didn't emit anything, then pass that value along. return ExitStatus.DiagnosticsPresent_OutputsSkipped; } @@ -143,23 +126,47 @@ namespace ts { return ExitStatus.Success; } - function emitWatchedProgram(host: System, program: Program, builder: Builder) { - const emittedFiles: string[] = program.getCompilerOptions().listEmittedFiles ? [] : undefined; - let sourceMaps: SourceMapData[]; - let emitSkipped: boolean; - let diagnostics: Diagnostic[]; + export function createWatchingSystemHost(pretty?: DiagnosticStyle, system = sys, + parseConfigFile?: ParseConfigFile, reportDiagnostic?: DiagnosticReporter, + reportWatchDiagnostic?: DiagnosticReporter + ): WatchingSystemHost { + reportDiagnostic = reportDiagnostic || createDiagnosticReporter(system, pretty ? reportDiagnosticWithColorAndContext : reportDiagnosticSimply); + reportWatchDiagnostic = reportWatchDiagnostic || createWatchDiagnosticReporter(system); + parseConfigFile = parseConfigFile || ts.parseConfigFile; + return { + system, + parseConfigFile, + reportDiagnostic, + reportWatchDiagnostic, + beforeCompile: noop, + afterCompile: compileWatchedProgram, + }; + + function compileWatchedProgram(host: System, program: Program, builder: Builder) { + // First get and report any syntactic errors. + let diagnostics = program.getSyntacticDiagnostics(); + let reportSemanticDiagnostics = false; + + // If we didn't have any syntactic errors, then also try getting the global and + // semantic errors. + if (diagnostics.length === 0) { + diagnostics = program.getOptionsDiagnostics().concat(program.getGlobalDiagnostics()); - const result = builder.emitChangedFiles(program); - switch (result.length) { - case 0: + if (diagnostics.length === 0) { + reportSemanticDiagnostics = true; + } + } + + // Emit and report any errors we ran into. + const emittedFiles: string[] = program.getCompilerOptions().listEmittedFiles ? [] : undefined; + let sourceMaps: SourceMapData[]; + let emitSkipped: boolean; + + const result = builder.emitChangedFiles(program); + if (result.length === 0) { emitSkipped = true; - break; - case 1: - const emitOutput = result[0]; - ({ diagnostics, sourceMaps, emitSkipped } = emitOutput); - writeOutputFiles(emitOutput.outputFiles); - break; - default: + } + else { for (const emitOutput of result) { if (emitOutput.emitSkipped) { emitSkipped = true; @@ -168,70 +175,53 @@ namespace ts { sourceMaps = concatenate(sourceMaps, emitOutput.sourceMaps); writeOutputFiles(emitOutput.outputFiles); } - } - - return { emitSkipped, diagnostics: diagnostics || [], emittedFiles, sourceMaps }; - + } - function ensureDirectoriesExist(directoryPath: string) { - if (directoryPath.length > getRootLength(directoryPath) && !host.directoryExists(directoryPath)) { - const parentDirectory = getDirectoryPath(directoryPath); - ensureDirectoriesExist(parentDirectory); - host.createDirectory(directoryPath); + if (reportSemanticDiagnostics) { + diagnostics = diagnostics.concat(builder.getSemanticDiagnostics(program)); + } + return handleEmitOutputAndReportErrors(host, program, emittedFiles, emitSkipped, + diagnostics, reportDiagnostic); + + function ensureDirectoriesExist(directoryPath: string) { + if (directoryPath.length > getRootLength(directoryPath) && !host.directoryExists(directoryPath)) { + const parentDirectory = getDirectoryPath(directoryPath); + ensureDirectoriesExist(parentDirectory); + host.createDirectory(directoryPath); + } } - } - function writeFile(fileName: string, data: string, writeByteOrderMark: boolean) { - try { - performance.mark("beforeIOWrite"); - ensureDirectoriesExist(getDirectoryPath(normalizePath(fileName))); + function writeFile(fileName: string, data: string, writeByteOrderMark: boolean) { + try { + performance.mark("beforeIOWrite"); + ensureDirectoriesExist(getDirectoryPath(normalizePath(fileName))); - host.writeFile(fileName, data, writeByteOrderMark); + host.writeFile(fileName, data, writeByteOrderMark); - performance.mark("afterIOWrite"); - performance.measure("I/O Write", "beforeIOWrite", "afterIOWrite"); - } - catch (e) { - return createCompilerDiagnostic(Diagnostics.Could_not_write_file_0_Colon_1, fileName, e); + performance.mark("afterIOWrite"); + performance.measure("I/O Write", "beforeIOWrite", "afterIOWrite"); + } + catch (e) { + return createCompilerDiagnostic(Diagnostics.Could_not_write_file_0_Colon_1, fileName, e); + } } - } - function writeOutputFiles(outputFiles: OutputFile[]) { - if (outputFiles) { - for (const outputFile of outputFiles) { - const error = writeFile(outputFile.name, outputFile.text, outputFile.writeByteOrderMark); - if (error) { - diagnostics.push(error); - } - if (emittedFiles) { - emittedFiles.push(outputFile.name); + function writeOutputFiles(outputFiles: OutputFile[]) { + if (outputFiles) { + for (const outputFile of outputFiles) { + const error = writeFile(outputFile.name, outputFile.text, outputFile.writeByteOrderMark); + if (error) { + diagnostics.push(error); + } + if (emittedFiles) { + emittedFiles.push(outputFile.name); + } } } } } } - export function createWatchingSystemHost(pretty?: DiagnosticStyle, system = sys, - parseConfigFile?: ParseConfigFile, reportDiagnostic?: DiagnosticReporter, - reportWatchDiagnostic?: DiagnosticReporter - ): WatchingSystemHost { - reportDiagnostic = reportDiagnostic || createDiagnosticReporter(system, pretty ? reportDiagnosticWithColorAndContext : reportDiagnosticSimply); - reportWatchDiagnostic = reportWatchDiagnostic || createWatchDiagnosticReporter(system); - parseConfigFile = parseConfigFile || ts.parseConfigFile; - return { - system, - parseConfigFile, - reportDiagnostic, - reportWatchDiagnostic, - beforeCompile: noop, - afterCompile: compileWatchedProgram, - }; - - function compileWatchedProgram(host: System, program: Program, builder: Builder) { - return compileProgram(system, program, () => emitWatchedProgram(host, program, builder), reportDiagnostic); - } - } - export function createWatchModeWithConfigFile(configParseResult: ParsedCommandLine, optionsToExtend: CompilerOptions = {}, watchingHost?: WatchingSystemHost) { return createWatchMode(configParseResult.fileNames, configParseResult.options, watchingHost, configParseResult.options.configFilePath, configParseResult.configFileSpecs, configParseResult.wildcardDirectories, optionsToExtend); } From 65521bc25949a724fc5e53d43ea8dc6d23a5966d Mon Sep 17 00:00:00 2001 From: Sheetal Nandi Date: Mon, 7 Aug 2017 14:47:32 -0700 Subject: [PATCH 052/109] Feedback from the PR --- src/compiler/commandLineParser.ts | 6 +- src/compiler/types.ts | 8 +- src/harness/unittests/projectErrors.ts | 18 ++-- .../unittests/tsserverProjectSystem.ts | 84 ++++++++++++++----- src/server/editorServices.ts | 9 +- src/server/project.ts | 10 +++ 6 files changed, 91 insertions(+), 44 deletions(-) diff --git a/src/compiler/commandLineParser.ts b/src/compiler/commandLineParser.ts index efb574ac97107..21cf4275cca10 100644 --- a/src/compiler/commandLineParser.ts +++ b/src/compiler/commandLineParser.ts @@ -1478,7 +1478,11 @@ namespace ts { } } - export function getErrorForNoInputFiles({ includeSpecs, excludeSpecs }: ConfigFileSpecs, configFileName?: string) { + export function isErrorNoInputFiles(error: Diagnostic) { + return error.code === Diagnostics.No_inputs_were_found_in_config_file_0_Specified_include_paths_were_1_and_exclude_paths_were_2.code; + } + + export function getErrorForNoInputFiles({ includeSpecs, excludeSpecs }: ConfigFileSpecs, configFileName: string | undefined) { return createCompilerDiagnostic( Diagnostics.No_inputs_were_found_in_config_file_0_Specified_include_paths_were_1_and_exclude_paths_were_2, configFileName || "tsconfig.json", diff --git a/src/compiler/types.ts b/src/compiler/types.ts index 8929b19bfe331..ed0a812845890 100644 --- a/src/compiler/types.ts +++ b/src/compiler/types.ts @@ -3930,9 +3930,9 @@ namespace ts { } export interface ResolvedModuleWithFailedLookupLocations { - resolvedModule: ResolvedModuleFull | undefined; + readonly resolvedModule: ResolvedModuleFull | undefined; /* @internal */ - failedLookupLocations: string[]; + readonly failedLookupLocations: string[]; /*@internal*/ isInvalidated?: boolean; } @@ -3945,8 +3945,8 @@ namespace ts { } export interface ResolvedTypeReferenceDirectiveWithFailedLookupLocations { - resolvedTypeReferenceDirective: ResolvedTypeReferenceDirective; - failedLookupLocations: string[]; + readonly resolvedTypeReferenceDirective: ResolvedTypeReferenceDirective; + readonly failedLookupLocations: string[]; /*@internal*/ isInvalidated?: boolean; } diff --git a/src/harness/unittests/projectErrors.ts b/src/harness/unittests/projectErrors.ts index 0143aee770143..4d0757e3c2ffa 100644 --- a/src/harness/unittests/projectErrors.ts +++ b/src/harness/unittests/projectErrors.ts @@ -23,11 +23,9 @@ namespace ts.projectSystem { function checkDiagnosticsWithLinePos(errors: server.protocol.DiagnosticWithLinePosition[], expectedErrors: string[]) { assert.equal(errors ? errors.length : 0, expectedErrors.length, `expected ${expectedErrors.length} error in the list`); if (expectedErrors.length) { - for (let i = 0; i < errors.length; i++) { - const actualMessage = errors[i].message; - const expectedMessage = expectedErrors[i]; - assert.isTrue(actualMessage.indexOf(errors[i].message) === 0, `error message does not match, expected ${actualMessage} to start with ${expectedMessage}`); - } + zipWith(errors, expectedErrors, ({ message: actualMessage }, expectedMessage) => { + assert.isTrue(startsWith(actualMessage, actualMessage), `error message does not match, expected ${actualMessage} to start with ${expectedMessage}`); + }); } } @@ -40,12 +38,11 @@ namespace ts.projectSystem { path: "/a/b/applib.ts", content: "" }; - // only file1 exists - expect error const host = createServerHost([file1, libFile]); const session = createSession(host); const projectService = session.getProjectService(); const projectFileName = "/a/b/test.csproj"; - const compilerOptionsRequest = { + const compilerOptionsRequest: server.protocol.CompilerOptionsDiagnosticsRequest = { type: "request", command: server.CommandNames.CompilerOptionsDiagnosticsFull, seq: 2, @@ -61,19 +58,20 @@ namespace ts.projectSystem { checkNumberOfProjects(projectService, { externalProjects: 1 }); const diags = session.executeCommand(compilerOptionsRequest).response; + // only file1 exists - expect error checkDiagnosticsWithLinePos(diags, ["File '/a/b/applib.ts' not found."]); } - // only file2 exists - expect error host.reloadFS([file2, libFile]); { + // only file2 exists - expect error checkNumberOfProjects(projectService, { externalProjects: 1 }); const diags = session.executeCommand(compilerOptionsRequest).response; checkDiagnosticsWithLinePos(diags, ["File '/a/b/app.ts' not found."]); } - // both files exist - expect no errors host.reloadFS([file1, file2, libFile]); { + // both files exist - expect no errors checkNumberOfProjects(projectService, { externalProjects: 1 }); const diags = session.executeCommand(compilerOptionsRequest).response; checkDiagnosticsWithLinePos(diags, []); @@ -99,7 +97,7 @@ namespace ts.projectSystem { openFilesForSession([file1], session); checkNumberOfProjects(projectService, { configuredProjects: 1 }); const project = configuredProjectAt(projectService, 0); - const compilerOptionsRequest = { + const compilerOptionsRequest: server.protocol.CompilerOptionsDiagnosticsRequest = { type: "request", command: server.CommandNames.CompilerOptionsDiagnosticsFull, seq: 2, diff --git a/src/harness/unittests/tsserverProjectSystem.ts b/src/harness/unittests/tsserverProjectSystem.ts index fa6ffc74e6557..17953a8bcd51d 100644 --- a/src/harness/unittests/tsserverProjectSystem.ts +++ b/src/harness/unittests/tsserverProjectSystem.ts @@ -253,29 +253,19 @@ namespace ts.projectSystem { entries: FSEntry[]; } - export function isFolder(s: FSEntry): s is Folder { + export function isFolder(s: FSEntry | undefined): s is Folder { return s && isArray((s).entries); } - export function isFile(s: FSEntry): s is File { + export function isFile(s: FSEntry | undefined): s is File { return s && typeof (s).content === "string"; } - function invokeDirectoryWatcher(callbacks: DirectoryWatcherCallback[], getRelativeFilePath: () => string) { + function invokeWatcherCallbacks(callbacks: T[], invokeCallback: (cb: T) => void): void { if (callbacks) { const cbs = callbacks.slice(); for (const cb of cbs) { - const fileName = getRelativeFilePath(); - cb(fileName); - } - } - } - - function invokeFileWatcher(callbacks: FileWatcherCallback[], fileName: string, eventId: FileWatcherEventKind) { - if (callbacks) { - const cbs = callbacks.slice(); - for (const cb of cbs) { - cb(fileName, eventId); + invokeCallback(cb); } } } @@ -380,7 +370,7 @@ namespace ts.projectSystem { private readonly output: string[] = []; - private fs: Map = createMap(); + private fs = createMap(); private getCanonicalFileName: (s: string) => string; private toPath: (f: string) => Path; private timeoutCallbacks = new Callbacks(); @@ -510,8 +500,12 @@ namespace ts.projectSystem { } else { Debug.assert(fileOrFolder.entries.length === 0); - invokeDirectoryWatcher(this.watchedDirectories.get(fileOrFolder.path), () => this.getRelativePathToDirectory(fileOrFolder.fullPath, fileOrFolder.fullPath)); - invokeDirectoryWatcher(this.watchedDirectoriesRecursive.get(fileOrFolder.path), () => this.getRelativePathToDirectory(fileOrFolder.fullPath, fileOrFolder.fullPath)); + const relativePath = this.getRelativePathToDirectory(fileOrFolder.fullPath, fileOrFolder.fullPath); + // Invoke directory and recursive directory watcher for the folder + // Here we arent invoking recursive directory watchers for the base folders + // since that is something we would want to do for both file as well as folder we are deleting + invokeWatcherCallbacks(this.watchedDirectories.get(fileOrFolder.path), cb => cb(relativePath)); + invokeWatcherCallbacks(this.watchedDirectoriesRecursive.get(fileOrFolder.path), cb => cb(relativePath)); } if (basePath !== fileOrFolder.path) { @@ -524,22 +518,31 @@ namespace ts.projectSystem { } } - private invokeFileWatcher(fileFullPath: string, eventId: FileWatcherEventKind) { + private invokeFileWatcher(fileFullPath: string, eventKind: FileWatcherEventKind) { const callbacks = this.watchedFiles.get(this.toPath(fileFullPath)); - invokeFileWatcher(callbacks, getBaseFileName(fileFullPath), eventId); + const fileName = getBaseFileName(fileFullPath); + invokeWatcherCallbacks(callbacks, cb => cb(fileName, eventKind)); } private getRelativePathToDirectory(directoryFullPath: string, fileFullPath: string) { return getRelativePathToDirectoryOrUrl(directoryFullPath, fileFullPath, this.currentDirectory, this.getCanonicalFileName, /*isAbsolutePathAnUrl*/ false); } + /** + * This will call the directory watcher for the foldeFullPath and recursive directory watchers for this and base folders + */ private invokeDirectoryWatcher(folderFullPath: string, fileName: string) { - invokeDirectoryWatcher(this.watchedDirectories.get(this.toPath(folderFullPath)), () => this.getRelativePathToDirectory(folderFullPath, fileName)); + const relativePath = this.getRelativePathToDirectory(folderFullPath, fileName); + invokeWatcherCallbacks(this.watchedDirectories.get(this.toPath(folderFullPath)), cb => cb(relativePath)); this.invokeRecursiveDirectoryWatcher(folderFullPath, fileName); } + /** + * This will call the recursive directory watcher for this directory as well as all the base directories + */ private invokeRecursiveDirectoryWatcher(fullPath: string, fileName: string) { - invokeDirectoryWatcher(this.watchedDirectoriesRecursive.get(this.toPath(fullPath)), () => this.getRelativePathToDirectory(fullPath, fileName)); + const relativePath = this.getRelativePathToDirectory(fullPath, fileName); + invokeWatcherCallbacks(this.watchedDirectoriesRecursive.get(this.toPath(fullPath)), cb => cb(relativePath)); const basePath = getDirectoryPath(fullPath); if (this.getCanonicalFileName(fullPath) !== this.getCanonicalFileName(basePath)) { this.invokeRecursiveDirectoryWatcher(basePath, fileName); @@ -885,6 +888,45 @@ namespace ts.projectSystem { checkWatchedDirectories(host, [getDirectoryPath(configFile.path)], /*recursive*/ true); }); + it("create configured project with the file list", () => { + const configFile: FileOrFolder = { + path: "/a/b/tsconfig.json", + content: ` + { + "compilerOptions": {}, + "include": ["*.ts"] + }` + }; + const file1: FileOrFolder = { + path: "/a/b/f1.ts", + content: "let x = 1" + }; + const file2: FileOrFolder = { + path: "/a/b/f2.ts", + content: "let y = 1" + }; + const file3: FileOrFolder = { + path: "/a/b/c/f3.ts", + content: "let z = 1" + }; + + const host = createServerHost([configFile, libFile, file1, file2, file3]); + const projectService = createProjectService(host); + const { configFileName, configFileErrors } = projectService.openClientFile(file1.path); + + assert(configFileName, "should find config file"); + assert.isTrue(!configFileErrors, `expect no errors in config file, got ${JSON.stringify(configFileErrors)}`); + checkNumberOfInferredProjects(projectService, 0); + checkNumberOfConfiguredProjects(projectService, 1); + + const project = configuredProjectAt(projectService, 0); + checkProjectActualFiles(project, [file1.path, libFile.path, file2.path, configFile.path]); + checkProjectRootFiles(project, [file1.path, file2.path]); + // watching all files except one that was open + checkWatchedFiles(host, [configFile.path, file2.path, libFile.path]); + checkWatchedDirectories(host, [getDirectoryPath(configFile.path)], /*recursive*/ false); + }); + it("add and then remove a config file in a folder with loose files", () => { const configFile: FileOrFolder = { path: "/a/b/tsconfig.json", diff --git a/src/server/editorServices.ts b/src/server/editorServices.ts index 003bb87f6a932..b71bc93909758 100644 --- a/src/server/editorServices.ts +++ b/src/server/editorServices.ts @@ -665,14 +665,7 @@ namespace ts.server { const configFileSpecs = project.configFileSpecs; const result = getFileNamesFromConfigSpecs(configFileSpecs, getDirectoryPath(configFilename), project.getCompilerOptions(), project.getCachedServerHost(), this.hostConfiguration.extraFileExtensions); - const errors = project.getAllProjectErrors(); - const isErrorNoInputFiles = (error: Diagnostic) => error.code === Diagnostics.No_inputs_were_found_in_config_file_0_Specified_include_paths_were_1_and_exclude_paths_were_2.code; - if (result.fileNames.length !== 0) { - filterMutate(errors, error => !isErrorNoInputFiles(error)); - } - else if (!configFileSpecs.filesSpecs && !some(errors, isErrorNoInputFiles)) { - errors.push(getErrorForNoInputFiles(configFileSpecs, configFilename)); - } + project.updateErrorOnNoInputFiles(result.fileNames.length !== 0); this.updateNonInferredProjectFiles(project, result.fileNames, fileNamePropertyReader, /*clientFileName*/ undefined); this.delayUpdateProjectGraphAndInferredProjectsRefresh(project); } diff --git a/src/server/project.ts b/src/server/project.ts index 62c450f9f4f90..d36551bdb5ac3 100644 --- a/src/server/project.ts +++ b/src/server/project.ts @@ -1226,6 +1226,16 @@ namespace ts.server { getEffectiveTypeRoots() { return getEffectiveTypeRoots(this.getCompilerOptions(), this.lsHost.host) || []; } + + /*@internal*/ + updateErrorOnNoInputFiles(hasFileNames: boolean) { + if (hasFileNames) { + filterMutate(this.projectErrors, error => !isErrorNoInputFiles(error)); + } + else if (!this.configFileSpecs.filesSpecs && !some(this.projectErrors, isErrorNoInputFiles)) { + this.projectErrors.push(getErrorForNoInputFiles(this.configFileSpecs, this.getConfigFilePath())); + } + } } /** From 27988bf33a7b7c497bbb208c4300cd404ee891b8 Mon Sep 17 00:00:00 2001 From: Sheetal Nandi Date: Mon, 7 Aug 2017 14:47:32 -0700 Subject: [PATCH 053/109] More updates based on PR feedback --- .../unittests/tsserverProjectSystem.ts | 65 ++++++--- src/server/editorServices.ts | 135 ++++++++++-------- src/server/lsHost.ts | 10 +- 3 files changed, 128 insertions(+), 82 deletions(-) diff --git a/src/harness/unittests/tsserverProjectSystem.ts b/src/harness/unittests/tsserverProjectSystem.ts index 17953a8bcd51d..7d70aa74fdfd1 100644 --- a/src/harness/unittests/tsserverProjectSystem.ts +++ b/src/harness/unittests/tsserverProjectSystem.ts @@ -407,17 +407,20 @@ namespace ts.projectSystem { // Update file if (currentEntry.content !== fileOrFolder.content) { currentEntry.content = fileOrFolder.content; + currentEntry.fileSize = fileOrFolder.fileSize; this.invokeFileWatcher(currentEntry.fullPath, FileWatcherEventKind.Changed); } } else { // TODO: Changing from file => folder + Debug.fail(`Currently ${path} is file and new FS makes it folder which isnt supported yet`); } } else { // Folder if (typeof fileOrFolder.content === "string") { // TODO: Changing from folder => file + Debug.fail(`Currently ${path} is folder and new FS makes it file which isnt supported yet`); } else { // Folder update: Nothing to do. @@ -778,6 +781,20 @@ namespace ts.projectSystem { } } + type ErrorInformation = { diagnosticMessage: DiagnosticMessage, errorTextArguments?: string[] }; + function getProtocolDiagnosticMessage({ diagnosticMessage, errorTextArguments = [] }: ErrorInformation) { + return formatStringFromArgs(diagnosticMessage.message, errorTextArguments); + } + + function verifyDiagnostics(actual: server.protocol.Diagnostic[], expected: ErrorInformation[]) { + const expectedErrors = expected.map(getProtocolDiagnosticMessage); + assert.deepEqual(actual.map(diag => flattenDiagnosticMessageText(diag.text, "\n")), expectedErrors); + } + + function verifyNoDiagnostics(actual: server.protocol.Diagnostic[]) { + verifyDiagnostics(actual, []); + } + describe("tsserver-project-system", () => { const commonFile1: FileOrFolder = { path: "/a/b/commonFile1.ts", @@ -1111,10 +1128,13 @@ namespace ts.projectSystem { server.CommandNames.SemanticDiagnosticsSync, { file: file1.path } ); - let diags = session.executeCommand(getErrRequest).response; // Two errors: CommonFile2 not found and cannot find name y - assert.equal(diags.length, 2, diags.map(diag => flattenDiagnosticMessageText(diag.text, "\n")).join("\n")); + let diags: server.protocol.Diagnostic[] = session.executeCommand(getErrRequest).response; + verifyDiagnostics(diags, [ + { diagnosticMessage: Diagnostics.Cannot_find_name_0, errorTextArguments: ["y"] }, + { diagnosticMessage: Diagnostics.File_0_not_found, errorTextArguments: [commonFile2.path] } + ]); host.reloadFS([file1, commonFile2, libFile]); host.runQueuedTimeoutCallbacks(); @@ -1122,8 +1142,8 @@ namespace ts.projectSystem { assert.strictEqual(projectService.inferredProjects[0], project, "Inferred project should be same"); checkProjectRootFiles(project, [file1.path]); checkProjectActualFiles(project, [file1.path, libFile.path, commonFile2.path]); - diags = session.executeCommand(getErrRequest).response; - assert.equal(diags.length, 0); + diags = session.executeCommand(getErrRequest).response; + verifyNoDiagnostics(diags); }); it("should create new inferred projects for files excluded from a configured project", () => { @@ -3110,15 +3130,18 @@ namespace ts.projectSystem { server.CommandNames.SemanticDiagnosticsSync, { file: file1.path } ); - let diags = session.executeCommand(getErrRequest).response; - assert.equal(diags.length, 0); + let diags: server.protocol.Diagnostic[] = session.executeCommand(getErrRequest).response; + verifyNoDiagnostics(diags); const moduleFileOldPath = moduleFile.path; const moduleFileNewPath = "/a/b/moduleFile1.ts"; moduleFile.path = moduleFileNewPath; host.reloadFS([moduleFile, file1]); host.runQueuedTimeoutCallbacks(); - diags = session.executeCommand(getErrRequest).response; + diags = session.executeCommand(getErrRequest).response; + verifyDiagnostics(diags, [ + { diagnosticMessage: Diagnostics.Cannot_find_module_0, errorTextArguments: ["./moduleFile"] } + ]); assert.equal(diags.length, 1); moduleFile.path = moduleFileOldPath; @@ -3133,8 +3156,8 @@ namespace ts.projectSystem { session.executeCommand(changeRequest); host.runQueuedTimeoutCallbacks(); - diags = session.executeCommand(getErrRequest).response; - assert.equal(diags.length, 0); + diags = session.executeCommand(getErrRequest).response; + verifyNoDiagnostics(diags); }); it("should restore the states for configured projects", () => { @@ -3158,22 +3181,24 @@ namespace ts.projectSystem { server.CommandNames.SemanticDiagnosticsSync, { file: file1.path } ); - let diags = session.executeCommand(getErrRequest).response; - assert.equal(diags.length, 0); + let diags: server.protocol.Diagnostic[] = session.executeCommand(getErrRequest).response; + verifyNoDiagnostics(diags); const moduleFileOldPath = moduleFile.path; const moduleFileNewPath = "/a/b/moduleFile1.ts"; moduleFile.path = moduleFileNewPath; host.reloadFS([moduleFile, file1, configFile]); host.runQueuedTimeoutCallbacks(); - diags = session.executeCommand(getErrRequest).response; - assert.equal(diags.length, 1); + diags = session.executeCommand(getErrRequest).response; + verifyDiagnostics(diags, [ + { diagnosticMessage: Diagnostics.Cannot_find_module_0, errorTextArguments: ["./moduleFile"] } + ]); moduleFile.path = moduleFileOldPath; host.reloadFS([moduleFile, file1, configFile]); host.runQueuedTimeoutCallbacks(); - diags = session.executeCommand(getErrRequest).response; - assert.equal(diags.length, 0); + diags = session.executeCommand(getErrRequest).response; + verifyNoDiagnostics(diags); }); it("should property handle missing config files", () => { @@ -3239,8 +3264,10 @@ namespace ts.projectSystem { server.CommandNames.SemanticDiagnosticsSync, { file: file1.path } ); - let diags = session.executeCommand(getErrRequest).response; - assert.equal(diags.length, 1); + let diags: server.protocol.Diagnostic[] = session.executeCommand(getErrRequest).response; + verifyDiagnostics(diags, [ + { diagnosticMessage: Diagnostics.Cannot_find_module_0, errorTextArguments: ["./moduleFile"] } + ]); host.reloadFS([file1, moduleFile]); host.runQueuedTimeoutCallbacks(); @@ -3253,8 +3280,8 @@ namespace ts.projectSystem { session.executeCommand(changeRequest); // Recheck - diags = session.executeCommand(getErrRequest).response; - assert.equal(diags.length, 0); + diags = session.executeCommand(getErrRequest).response; + verifyNoDiagnostics(diags); }); }); diff --git a/src/server/editorServices.ts b/src/server/editorServices.ts index b71bc93909758..2d41a3ccb3008 100644 --- a/src/server/editorServices.ts +++ b/src/server/editorServices.ts @@ -283,10 +283,17 @@ namespace ts.server { type ConfigFileExistence = { /** * Cached value of existence of config file + * It is true if there is configured project open for this file. + * It can be either true or false if this is the config file that is being watched by inferred project + * to decide when to update the structure so that it knows about updating the project for its files + * (config file may include the inferred project files after the change and hence may be wont need to be in inferred project) */ exists: boolean; /** - * The value in the open files map is true if the file is inferred project root + * The value in the trackingOpenFilesMap is true if the open file is inferred project root + * and hence tracking changes to this config file + * (either config file is already present but doesnt include the open file in the project structure or config file doesnt exist) + * * Otherwise its false */ trackingOpenFilesMap: Map; @@ -345,7 +352,14 @@ namespace ts.server { private compilerOptionsForInferredProjects: CompilerOptions; private compileOnSaveForInferredProjects: boolean; private readonly projectToSizeMap: Map = createMap(); - private readonly mapOfConfigFilePresence: Map; + /** + * This is a map of config file paths existance that doesnt need query to disk + * - The entry can be present because there is inferred project that needs to watch addition of config file to folder + * In this case the exists could be true/false based on config file is present or not + * - Or it is present if we have configured project open with config file at that location + * In this case the exists property is always true + */ + private readonly mapOfConfigFilePresence = createMap(); private readonly throttledOperations: ThrottledOperations; private readonly hostConfiguration: HostConfiguration; @@ -389,7 +403,6 @@ namespace ts.server { this.currentDirectory = this.host.getCurrentDirectory(); this.toCanonicalFileName = createGetCanonicalFileName(this.host.useCaseSensitiveFileNames); - this.mapOfConfigFilePresence = createMap(); this.throttledOperations = new ThrottledOperations(this.host); this.typingsInstaller.attach(this); @@ -415,7 +428,7 @@ namespace ts.server { } ensureInferredProjectsUpToDate_TestOnly() { - this.ensureInferredProjectsUpToDate(); + this.ensureProjectStructuresUptoDate(); } getCompilerOptionsForInferredProjects() { @@ -505,7 +518,7 @@ namespace ts.server { return undefined; } if (isInferredProjectName(projectName)) { - this.ensureInferredProjectsUpToDate(); + this.ensureProjectStructuresUptoDate(); return findProjectByName(projectName, this.inferredProjects); } return this.findExternalProjectByProjectName(projectName) || this.findConfiguredProjectByProjectName(toNormalizedPath(projectName)); @@ -513,7 +526,7 @@ namespace ts.server { getDefaultProjectForFile(fileName: NormalizedPath, refreshInferredProjects: boolean) { if (refreshInferredProjects) { - this.ensureInferredProjectsUpToDate(); + this.ensureProjectStructuresUptoDate(); } const scriptInfo = this.getScriptInfoForNormalizedPath(fileName); return scriptInfo && scriptInfo.getDefaultProject(); @@ -521,9 +534,16 @@ namespace ts.server { /** * Ensures the project structures are upto date - * @param refreshInferredProjects when true updates the inferred projects even if there is no pending work + * This means, + * - if there are changedFiles (the files were updated but their containing project graph was not upto date), + * their project graph is updated + * - If there are pendingProjectUpdates (scheduled to be updated with delay so they can batch update the graph if there are several changes in short time span) + * their project graph is updated + * - If there were project graph updates and/or there was pending inferred project update and/or called forced the inferred project structure refresh + * Inferred projects are created/updated/deleted based on open files states + * @param forceInferredProjectsRefresh when true updates the inferred projects even if there is no pending work to update the files/project structures */ - private ensureInferredProjectsUpToDate(refreshInferredProjects?: boolean) { + private ensureProjectStructuresUptoDate(forceInferredProjectsRefresh?: boolean) { if (this.changedFiles) { let projectsToUpdate: Project[]; if (this.changedFiles.length === 1) { @@ -546,7 +566,7 @@ namespace ts.server { this.updateProjectGraphs(projectsToUpdate); } - if (this.pendingInferredProjectUpdate || refreshInferredProjects) { + if (this.pendingInferredProjectUpdate || forceInferredProjectsRefresh) { this.pendingInferredProjectUpdate = false; this.refreshInferredProjects(); } @@ -584,26 +604,22 @@ namespace ts.server { const info = this.getScriptInfoForNormalizedPath(fileName); if (!info) { this.logger.info(`Error: got watch notification for unknown file: ${fileName}`); - return; } - - if (eventKind === FileWatcherEventKind.Deleted) { + else if (eventKind === FileWatcherEventKind.Deleted) { // File was deleted this.handleDeletedFile(info); } - else { - if (!info.isScriptOpen()) { - if (info.containingProjects.length === 0) { - // Orphan script info, remove it as we can always reload it on next open file request - this.stopWatchingScriptInfo(info, WatcherCloseReason.OrphanScriptInfoWithChange); - this.filenameToScriptInfo.delete(info.path); - } - else { - // file has been changed which might affect the set of referenced files in projects that include - // this file and set of inferred projects - info.reloadFromFile(); - this.delayUpdateProjectGraphs(info.containingProjects); - } + else if (!info.isScriptOpen()) { + if (info.containingProjects.length === 0) { + // Orphan script info, remove it as we can always reload it on next open file request + this.stopWatchingScriptInfo(info, WatcherCloseReason.OrphanScriptInfoWithChange); + this.filenameToScriptInfo.delete(info.path); + } + else { + // file has been changed which might affect the set of referenced files in projects that include + // this file and set of inferred projects + info.reloadFromFile(); + this.delayUpdateProjectGraphs(info.containingProjects); } } } @@ -821,7 +837,7 @@ namespace ts.server { this.removeProject(project); } - // collect orphanted files and try to re-add them as newly opened + // collect orphaned files and try to re-add them as newly opened // treat orphaned files as newly opened // for all open files for (const f of this.openFiles) { @@ -866,7 +882,7 @@ namespace ts.server { return configFilePresenceInfo.exists; } - // Theorotically we should be adding watch for the directory here itself. + // Theoretically we should be adding watch for the directory here itself. // In practice there will be very few scenarios where the config file gets added // somewhere inside the another config file directory. // And technically we could handle that case in configFile's directory watcher in some cases @@ -935,25 +951,26 @@ namespace ts.server { } private logConfigFileWatchUpdate(configFileName: NormalizedPath, configFilePresenceInfo: ConfigFileExistence, status: ConfigFileWatcherStatus) { - if (this.logger.loggingEnabled()) { - const inferredRoots: string[] = []; - const otherFiles: string[] = []; - configFilePresenceInfo.trackingOpenFilesMap.forEach((value, key: Path) => { - const info = this.getScriptInfoForPath(key); - if (value) { - inferredRoots.push(info.fileName); - } - else { - otherFiles.push(info.fileName); - } - }); - const watchType = status === ConfigFileWatcherStatus.UpdatedCallback || - status === ConfigFileWatcherStatus.ReloadingFiles || - status === ConfigFileWatcherStatus.ReloadingInferredRootFiles ? - (configFilePresenceInfo.configFileWatcher ? WatchType.ConfigFileForInferredRoot : WatchType.ConfigFilePath) : - ""; - this.logger.info(`ConfigFilePresence ${watchType}:: File: ${configFileName} Currently Tracking: InferredRootFiles: ${inferredRoots} OtherFiles: ${otherFiles} Status: ${status}`); + if (!this.logger.loggingEnabled()) { + return; } + const inferredRoots: string[] = []; + const otherFiles: string[] = []; + configFilePresenceInfo.trackingOpenFilesMap.forEach((value, key: Path) => { + const info = this.getScriptInfoForPath(key); + if (value) { + inferredRoots.push(info.fileName); + } + else { + otherFiles.push(info.fileName); + } + }); + const watchType = status === ConfigFileWatcherStatus.UpdatedCallback || + status === ConfigFileWatcherStatus.ReloadingFiles || + status === ConfigFileWatcherStatus.ReloadingInferredRootFiles ? + (configFilePresenceInfo.configFileWatcher ? WatchType.ConfigFileForInferredRoot : WatchType.ConfigFilePath) : + ""; + this.logger.info(`ConfigFilePresence ${watchType}:: File: ${configFileName} Currently Tracking: InferredRootFiles: ${inferredRoots} OtherFiles: ${otherFiles} Status: ${status}`); } private closeConfigFileWatcherIfInferredRoot(configFileName: NormalizedPath, canonicalConfigFilePath: string, @@ -998,7 +1015,7 @@ namespace ts.server { */ private stopWatchingConfigFilesForClosedScriptInfo(info: ScriptInfo) { Debug.assert(!info.isScriptOpen()); - this.enumerateConfigFileLocations(info, (configFileName, canonicalConfigFilePath) => + this.forEachConfigFileLocation(info, (configFileName, canonicalConfigFilePath) => this.closeConfigFileWatchForClosedScriptInfo(configFileName, canonicalConfigFilePath, info) ); } @@ -1034,7 +1051,7 @@ namespace ts.server { /* @internal */ startWatchingConfigFilesForInferredProjectRoot(info: ScriptInfo) { Debug.assert(info.isScriptOpen()); - this.enumerateConfigFileLocations(info, (configFileName, canonicalConfigFilePath) => + this.forEachConfigFileLocation(info, (configFileName, canonicalConfigFilePath) => this.watchConfigFileForInferredProjectRoot(configFileName, canonicalConfigFilePath, info) ); } @@ -1060,7 +1077,7 @@ namespace ts.server { */ /* @internal */ stopWatchingConfigFilesForInferredProjectRoot(info: ScriptInfo, reason: WatcherCloseReason) { - this.enumerateConfigFileLocations(info, (configFileName, canonicalConfigFilePath) => + this.forEachConfigFileLocation(info, (configFileName, canonicalConfigFilePath) => this.closeWatchConfigFileForInferredProjectRoot(configFileName, canonicalConfigFilePath, info, reason) ); } @@ -1073,7 +1090,7 @@ namespace ts.server { * The server must start searching from the directory containing * the newly opened file. */ - private enumerateConfigFileLocations(info: ScriptInfo, + private forEachConfigFileLocation(info: ScriptInfo, action: (configFileName: NormalizedPath, canonicalConfigFilePath: string) => boolean | void, projectRootPath?: NormalizedPath) { let searchPath = asNormalizedPath(getDirectoryPath(info.fileName)); @@ -1113,8 +1130,8 @@ namespace ts.server { private getConfigFileNameForFile(info: ScriptInfo, projectRootPath?: NormalizedPath) { Debug.assert(info.isScriptOpen()); this.logger.info(`Search path: ${getDirectoryPath(info.fileName)}`); - const configFileName = this.enumerateConfigFileLocations(info, - (configFileName: NormalizedPath, canonicalConfigFilePath: string) => + const configFileName = this.forEachConfigFileLocation(info, + (configFileName, canonicalConfigFilePath) => this.configFileExists(configFileName, canonicalConfigFilePath, info), projectRootPath ); @@ -1684,16 +1701,18 @@ namespace ts.server { } /** - * This function is to update the project structure for every projects. + * This function is to update the project structure for every inferred project. * It is called on the premise that all the configured projects are * up to date. + * This will go through open files and assign them to inferred project if open file is not part of any other project + * After that all the inferred project graphs are updated */ private refreshInferredProjects() { this.logger.info("refreshInferredProjects: updating project structure from ..."); this.printProjects(); for (const info of this.openFiles) { - // collect all orphanted script infos from open files + // collect all orphaned script infos from open files if (info.containingProjects.length === 0) { this.assignScriptInfoToInferredProjectIfNecessary(info, /*addToListOfOpenFiles*/ false); } @@ -1826,7 +1845,7 @@ namespace ts.server { // if files were open or closed then explicitly refresh list of inferred projects // otherwise if there were only changes in files - record changed files in `changedFiles` and defer the update if (openFiles || closedFiles) { - this.ensureInferredProjectsUpToDate(/*refreshInferredProjects*/ true); + this.ensureProjectStructuresUptoDate(/*refreshInferredProjects*/ true); } } @@ -1849,7 +1868,7 @@ namespace ts.server { } this.externalProjectToConfiguredProjectMap.delete(fileName); if (shouldRefreshInferredProjects && !suppressRefresh) { - this.ensureInferredProjectsUpToDate(/*refreshInferredProjects*/ true); + this.ensureProjectStructuresUptoDate(/*refreshInferredProjects*/ true); } } else { @@ -1858,7 +1877,7 @@ namespace ts.server { if (externalProject) { this.removeProject(externalProject); if (!suppressRefresh) { - this.ensureInferredProjectsUpToDate(/*refreshInferredProjects*/ true); + this.ensureProjectStructuresUptoDate(/*refreshInferredProjects*/ true); } } } @@ -1882,7 +1901,7 @@ namespace ts.server { this.closeExternalProject(externalProjectName, /*suppressRefresh*/ true); }); - this.ensureInferredProjectsUpToDate(/*refreshInferredProjects*/ true); + this.ensureProjectStructuresUptoDate(/*refreshInferredProjects*/ true); } /** Makes a filename safe to insert in a RegExp */ @@ -2077,7 +2096,7 @@ namespace ts.server { this.createAndAddExternalProject(proj.projectFileName, rootFiles, proj.options, proj.typeAcquisition); } if (!suppressRefreshOfInferredProjects) { - this.ensureInferredProjectsUpToDate(/*refreshInferredProjects*/ true); + this.ensureProjectStructuresUptoDate(/*refreshInferredProjects*/ true); } } } diff --git a/src/server/lsHost.ts b/src/server/lsHost.ts index fb1cd80b66063..3bba55132272b 100644 --- a/src/server/lsHost.ts +++ b/src/server/lsHost.ts @@ -154,19 +154,19 @@ namespace ts.server { const path = this.toPath(fileOrFolder); const existingResult = this.cachedReadDirectoryResult.get(path); if (existingResult) { + // This was a folder already present, remove it if this doesnt exist any more if (!this.host.directoryExists(fileOrFolder)) { this.cachedReadDirectoryResult.delete(path); } } else { - // Was this earlier file + // This was earlier a file (hence not in cached directory contents) + // or we never cached the directory containing it const parentResult = this.cachedReadDirectoryResult.get(getDirectoryPath(path)); if (parentResult) { const baseName = getBaseFileName(fileOrFolder); - if (parentResult) { - parentResult.files = this.updateFileSystemEntry(parentResult.files, baseName, this.host.fileExists(path)); - parentResult.directories = this.updateFileSystemEntry(parentResult.directories, baseName, this.host.directoryExists(path)); - } + parentResult.files = this.updateFileSystemEntry(parentResult.files, baseName, this.host.fileExists(path)); + parentResult.directories = this.updateFileSystemEntry(parentResult.directories, baseName, this.host.directoryExists(path)); } } } From 02b8a7de659c09756c367fdcab5d2289cd69b982 Mon Sep 17 00:00:00 2001 From: Sheetal Nandi Date: Mon, 7 Aug 2017 14:47:32 -0700 Subject: [PATCH 054/109] More work on PR feedback --- src/compiler/types.ts | 6 ++ src/compiler/utilities.ts | 4 + src/server/editorServices.ts | 10 +- src/server/project.ts | 178 ++++++++++++++++++----------------- src/server/utilities.ts | 91 +++++++----------- 5 files changed, 144 insertions(+), 145 deletions(-) diff --git a/src/compiler/types.ts b/src/compiler/types.ts index ed0a812845890..2b680db71bcf2 100644 --- a/src/compiler/types.ts +++ b/src/compiler/types.ts @@ -3688,7 +3688,13 @@ namespace ts { export interface ConfigFileSpecs { filesSpecs: ReadonlyArray; + /** + * Present to report errors (user specified specs), validatedIncludeSpecs are used for file name matching + */ includeSpecs: ReadonlyArray; + /** + * Present to report errors (user specified specs), validatedExcludeSpecs are used for file name matching + */ excludeSpecs: ReadonlyArray; validatedIncludeSpecs: ReadonlyArray; validatedExcludeSpecs: ReadonlyArray; diff --git a/src/compiler/utilities.ts b/src/compiler/utilities.ts index 76d242e8b08d8..14cab78bc84ce 100644 --- a/src/compiler/utilities.ts +++ b/src/compiler/utilities.ts @@ -3607,6 +3607,10 @@ namespace ts { export function getCombinedLocalAndExportSymbolFlags(symbol: Symbol): SymbolFlags { return symbol.exportSymbol ? symbol.exportSymbol.flags | symbol.flags : symbol.flags; } + + export function isRecursiveDirectoryWatch(flags: WatchDirectoryFlags) { + return (flags & WatchDirectoryFlags.Recursive) !== 0; + } } namespace ts { diff --git a/src/server/editorServices.ts b/src/server/editorServices.ts index 2d41a3ccb3008..5ca8a54d71f54 100644 --- a/src/server/editorServices.ts +++ b/src/server/editorServices.ts @@ -280,7 +280,7 @@ namespace ts.server { /* @internal */ export type ServerDirectoryWatcherCallback = (path: NormalizedPath) => void; - type ConfigFileExistence = { + interface ConfigFileExistence { /** * Cached value of existence of config file * It is true if there is configured project open for this file. @@ -302,7 +302,7 @@ namespace ts.server { * The watcher is present only when there is no open configured project for this config file */ configFileWatcher?: FileWatcher; - }; + } export interface ProjectServiceOptions { host: ServerHost; @@ -1606,13 +1606,15 @@ namespace ts.server { } /* @internal */ - closeDirectoryWatcher(watchType: WatchType, project: Project, directory: string, watcher: FileWatcher, recursive: boolean, reason: WatcherCloseReason) { + closeDirectoryWatcher(watchType: WatchType, project: Project, directory: string, watcher: FileWatcher, flags: WatchDirectoryFlags, reason: WatcherCloseReason) { + const recursive = isRecursiveDirectoryWatch(flags); this.logger.info(`DirectoryWatcher ${recursive ? "recursive" : ""}:: Close: ${directory} Project: ${project.getProjectName()} WatchType: ${watchType} Reason: ${reason}`); watcher.close(); } /* @internal */ - addDirectoryWatcher(watchType: WatchType, project: Project, directory: string, cb: ServerDirectoryWatcherCallback, recursive: boolean) { + addDirectoryWatcher(watchType: WatchType, project: Project, directory: string, cb: ServerDirectoryWatcherCallback, flags: WatchDirectoryFlags) { + const recursive = isRecursiveDirectoryWatch(flags); this.logger.info(`DirectoryWatcher ${recursive ? "recursive" : ""}:: Added: ${directory} Project: ${project.getProjectName()} WatchType: ${watchType}`); return this.host.watchDirectory(directory, fileName => { const path = toNormalizedPath(getNormalizedAbsolutePath(fileName, directory)); diff --git a/src/server/project.ts b/src/server/project.ts index d36551bdb5ac3..06e4205bc8f92 100644 --- a/src/server/project.ts +++ b/src/server/project.ts @@ -327,10 +327,12 @@ namespace ts.server { this.lsHost = undefined; // Clean up file watchers waiting for missing files - cleanExistingMap(this.missingFilesMap, (missingFilePath, fileWatcher) => { - this.projectService.closeFileWatcher(WatchType.MissingFilePath, this, missingFilePath, fileWatcher, WatcherCloseReason.ProjectClose); - }); - this.missingFilesMap = undefined; + if (this.missingFilesMap) { + clearMap(this.missingFilesMap, (missingFilePath, fileWatcher) => { + this.projectService.closeFileWatcher(WatchType.MissingFilePath, this, missingFilePath, fileWatcher, WatcherCloseReason.ProjectClose); + }); + this.missingFilesMap = undefined; + } // signal language service to release source files acquired from document registry this.languageService.dispose(); @@ -642,34 +644,37 @@ namespace ts.server { const missingFilePaths = this.program.getMissingFilePaths(); const newMissingFilePathMap = arrayToSet(missingFilePaths); // Update the missing file paths watcher - this.missingFilesMap = mutateExistingMapWithNewSet( - this.missingFilesMap, newMissingFilePathMap, - // Watch the missing files - missingFilePath => { - const fileWatcher = this.projectService.addFileWatcher( - WatchType.MissingFilePath, this, missingFilePath, - (filename, eventKind) => { - if (eventKind === FileWatcherEventKind.Created && this.missingFilesMap.has(missingFilePath)) { - this.missingFilesMap.delete(missingFilePath); - this.projectService.closeFileWatcher(WatchType.MissingFilePath, this, missingFilePath, fileWatcher, WatcherCloseReason.FileCreated); - - if (this.projectKind === ProjectKind.Configured) { - const absoluteNormalizedPath = getNormalizedAbsolutePath(filename, getDirectoryPath(missingFilePath)); - (this.lsHost.host as CachedServerHost).addOrDeleteFileOrFolder(toNormalizedPath(absoluteNormalizedPath)); + mutateMap( + this.missingFilesMap || (this.missingFilesMap = createMap()), + newMissingFilePathMap, + { + // Watch the missing files + createNewValue: missingFilePath => { + const fileWatcher = this.projectService.addFileWatcher( + WatchType.MissingFilePath, this, missingFilePath, + (filename, eventKind) => { + if (eventKind === FileWatcherEventKind.Created && this.missingFilesMap.has(missingFilePath)) { + this.missingFilesMap.delete(missingFilePath); + this.projectService.closeFileWatcher(WatchType.MissingFilePath, this, missingFilePath, fileWatcher, WatcherCloseReason.FileCreated); + + if (this.projectKind === ProjectKind.Configured) { + const absoluteNormalizedPath = getNormalizedAbsolutePath(filename, getDirectoryPath(missingFilePath)); + (this.lsHost.host as CachedServerHost).addOrDeleteFileOrFolder(toNormalizedPath(absoluteNormalizedPath)); + } + + // When a missing file is created, we should update the graph. + this.markAsDirty(); + this.projectService.delayUpdateProjectGraphAndInferredProjectsRefresh(this); } - - // When a missing file is created, we should update the graph. - this.markAsDirty(); - this.projectService.delayUpdateProjectGraphAndInferredProjectsRefresh(this); } - } - ); - return fileWatcher; - }, - // Files that are no longer missing (e.g. because they are no longer required) - // should no longer be watched. - (missingFilePath, fileWatcher) => { - this.projectService.closeFileWatcher(WatchType.MissingFilePath, this, missingFilePath, fileWatcher, WatcherCloseReason.NotNeeded); + ); + return fileWatcher; + }, + // Files that are no longer missing (e.g. because they are no longer required) + // should no longer be watched. + onDeleteExistingValue: (missingFilePath, fileWatcher) => { + this.projectService.closeFileWatcher(WatchType.MissingFilePath, this, missingFilePath, fileWatcher, WatcherCloseReason.NotNeeded); + } } ); } @@ -970,7 +975,10 @@ namespace ts.server { } } - type WildCardDirectoryWatchers = { watcher: FileWatcher, recursive: boolean }; + interface WildcardDirectoryWatcher { + watcher: FileWatcher; + flags: WatchDirectoryFlags; + } /** * If a file is opened, the server will look for a tsconfig (or jsconfig) @@ -981,7 +989,7 @@ namespace ts.server { private typeAcquisition: TypeAcquisition; /* @internal */ configFileWatcher: FileWatcher; - private directoriesWatchedForWildcards: Map | undefined; + private directoriesWatchedForWildcards: Map | undefined; private typeRootsWatchers: Map | undefined; readonly canonicalConfigFilePath: NormalizedPath; @@ -1136,70 +1144,72 @@ namespace ts.server { } watchWildcards(wildcardDirectories: Map) { - this.directoriesWatchedForWildcards = mutateExistingMap( - this.directoriesWatchedForWildcards, wildcardDirectories, - // Watcher is same if the recursive flags match - ({ recursive: existingRecursive }, flag) => { - // If the recursive dont match, it needs update - const recursive = (flag & WatchDirectoryFlags.Recursive) !== 0; - return existingRecursive !== recursive; - }, - // Create new watch and recursive info - (directory, flag) => { - const recursive = (flag & WatchDirectoryFlags.Recursive) !== 0; - return { - watcher: this.projectService.addDirectoryWatcher( - WatchType.WildCardDirectories, this, directory, - path => this.projectService.onFileAddOrRemoveInWatchedDirectoryOfProject(this, path), - recursive - ), - recursive - }; - }, - // Close existing watch thats not needed any more - (directory, { watcher, recursive }) => this.projectService.closeDirectoryWatcher( - WatchType.WildCardDirectories, this, directory, watcher, recursive, WatcherCloseReason.NotNeeded - ), - // Close existing watch that doesnt match in recursive flag - (directory, { watcher, recursive }) => this.projectService.closeDirectoryWatcher( - WatchType.WildCardDirectories, this, directory, watcher, recursive, WatcherCloseReason.RecursiveChanged - ) + mutateMap( + this.directoriesWatchedForWildcards || (this.directoriesWatchedForWildcards = createMap()), + wildcardDirectories, + { + // Watcher is same if the recursive flags match + isSameValue: ({ flags: existingFlags }, flags) => existingFlags !== flags, + // Create new watch and recursive info + createNewValue: (directory, flags) => { + return { + watcher: this.projectService.addDirectoryWatcher( + WatchType.WildCardDirectories, this, directory, + path => this.projectService.onFileAddOrRemoveInWatchedDirectoryOfProject(this, path), + flags + ), + flags + }; + }, + // Close existing watch thats not needed any more or doesnt match recursive flags + onDeleteExistingValue: (directory, wildcardDirectoryWatcher, isNotSame) => + this.closeWildcardDirectoryWatcher(directory, wildcardDirectoryWatcher, isNotSame ? WatcherCloseReason.RecursiveChanged : WatcherCloseReason.NotNeeded), + } ); } + private closeWildcardDirectoryWatcher(directory: string, { watcher, flags }: WildcardDirectoryWatcher, closeReason: WatcherCloseReason) { + this.projectService.closeDirectoryWatcher(WatchType.WildCardDirectories, this, directory, watcher, flags, closeReason); + } + stopWatchingWildCards(reason: WatcherCloseReason) { - cleanExistingMap( - this.directoriesWatchedForWildcards, - (directory, { watcher, recursive }) => - this.projectService.closeDirectoryWatcher(WatchType.WildCardDirectories, this, - directory, watcher, recursive, reason) - ); - this.directoriesWatchedForWildcards = undefined; + if (this.directoriesWatchedForWildcards) { + clearMap( + this.directoriesWatchedForWildcards, + (directory, wildcardDirectoryWatcher) => this.closeWildcardDirectoryWatcher(directory, wildcardDirectoryWatcher, reason) + ); + this.directoriesWatchedForWildcards = undefined; + } } watchTypeRoots() { const newTypeRoots = arrayToSet(this.getEffectiveTypeRoots(), dir => this.projectService.toCanonicalFileName(dir)); - this.typeRootsWatchers = mutateExistingMapWithNewSet( - this.typeRootsWatchers, newTypeRoots, - // Create new watch - root => this.projectService.addDirectoryWatcher(WatchType.TypeRoot, this, root, - path => this.projectService.onTypeRootFileChanged(this, path), /*recursive*/ false - ), - // Close existing watch thats not needed any more - (directory, watcher) => this.projectService.closeDirectoryWatcher( - WatchType.TypeRoot, this, directory, watcher, /*recursive*/ false, WatcherCloseReason.NotNeeded - ) + mutateMap( + this.typeRootsWatchers || (this.typeRootsWatchers = createMap()), + newTypeRoots, + { + // Create new watch + createNewValue: root => this.projectService.addDirectoryWatcher(WatchType.TypeRoot, this, root, + path => this.projectService.onTypeRootFileChanged(this, path), WatchDirectoryFlags.None + ), + // Close existing watch thats not needed any more + onDeleteExistingValue: (directory, watcher) => this.projectService.closeDirectoryWatcher( + WatchType.TypeRoot, this, directory, watcher, WatchDirectoryFlags.None, WatcherCloseReason.NotNeeded + ) + } ); } stopWatchingTypeRoots(reason: WatcherCloseReason) { - cleanExistingMap( - this.typeRootsWatchers, - (directory, watcher) => - this.projectService.closeDirectoryWatcher(WatchType.TypeRoot, this, - directory, watcher, /*recursive*/ false, reason) - ); - this.typeRootsWatchers = undefined; + if (this.typeRootsWatchers) { + clearMap( + this.typeRootsWatchers, + (directory, watcher) => + this.projectService.closeDirectoryWatcher(WatchType.TypeRoot, this, + directory, watcher, WatchDirectoryFlags.None, reason) + ); + this.typeRootsWatchers = undefined; + } } close() { diff --git a/src/server/utilities.ts b/src/server/utilities.ts index 6c884cebf5930..edc3d0e1117cf 100644 --- a/src/server/utilities.ts +++ b/src/server/utilities.ts @@ -293,76 +293,53 @@ namespace ts.server { } } - export function cleanExistingMap( - existingMap: Map, - onDeleteExistingValue: (key: string, existingValue: T) => void) { - if (existingMap) { - // Remove all - existingMap.forEach((existingValue, key) => { - existingMap.delete(key); - onDeleteExistingValue(key, existingValue); - }); - } + /** + * clears already present map (!== undefined) by calling onDeleteExistingValue callback before deleting that key/value + */ + export function clearMap(map: Map, onDeleteExistingValue: (key: string, existingValue: T) => void) { + // Remove all + map.forEach((existingValue, key) => { + map.delete(key); + onDeleteExistingValue(key, existingValue); + }); } - export function mutateExistingMapWithNewSet( - existingMap: Map, newMap: Map, - createNewValue: (key: string) => T, - onDeleteExistingValue: (key: string, existingValue: T) => void - ): Map { - return mutateExistingMap( - existingMap, newMap, - // Same value if the value is set in the map - /*isSameValue*/(_existingValue, _valueInNewMap) => true, - /*createNewValue*/(key, _valueInNewMap) => createNewValue(key), - onDeleteExistingValue, - // Should never be called since we say yes to same values all the time - /*OnDeleteExistingMismatchValue*/(_key, _existingValue) => notImplemented() - ); + export interface MutateMapOptions { + createNewValue(key: string, valueInNewMap: U): T; + onDeleteExistingValue(key: string, existingValue: T, isNotSame?: boolean): void; + + isSameValue?(existingValue: T, valueInNewMap: U): boolean; } - export function mutateExistingMap( - existingMap: Map, newMap: Map, - isSameValue: (existingValue: T, valueInNewMap: U) => boolean, - createNewValue: (key: string, valueInNewMap: U) => T, - onDeleteExistingValue: (key: string, existingValue: T) => void, - OnDeleteExistingMismatchValue: (key: string, existingValue: T) => void - ): Map { + /** + * Mutates the map with newMap such that keys in map will be same as newMap. + */ + export function mutateMap(map: Map, newMap: ReadonlyMap, options: MutateMapOptions) { // If there are new values update them if (newMap) { - if (existingMap) { - // Needs update - existingMap.forEach((existingValue, key) => { - const valueInNewMap = newMap.get(key); - // Existing value - remove it - if (valueInNewMap === undefined) { - existingMap.delete(key); - onDeleteExistingValue(key, existingValue); - } + const { isSameValue, createNewValue, onDeleteExistingValue } = options; + // Needs update + map.forEach((existingValue, key) => { + const valueInNewMap = newMap.get(key); + if (valueInNewMap === undefined || // different value - remove it - else if (!isSameValue(existingValue, valueInNewMap)) { - existingMap.delete(key); - OnDeleteExistingMismatchValue(key, existingValue); - } - }); - } - else { - // Create new - existingMap = createMap(); - } + (isSameValue && !isSameValue(existingValue, valueInNewMap))) { + const isNotSame = valueInNewMap !== undefined; + map.delete(key); + onDeleteExistingValue(key, existingValue, isNotSame); + } + }); // Add new values that are not already present newMap.forEach((valueInNewMap, key) => { - if (!existingMap.has(key)) { + if (!map.has(key)) { // New values - existingMap.set(key, createNewValue(key, valueInNewMap)); + map.set(key, createNewValue(key, valueInNewMap)); } }); - - return existingMap; } - - cleanExistingMap(existingMap, onDeleteExistingValue); - return undefined; + else { + clearMap(map, options.onDeleteExistingValue); + } } } From f723beb24491e51aff667a31663c11e24c8a889c Mon Sep 17 00:00:00 2001 From: Sheetal Nandi Date: Mon, 7 Aug 2017 14:47:32 -0700 Subject: [PATCH 055/109] More updates per PR feedback --- src/compiler/core.ts | 13 + .../unittests/cachingInServerLSHost.ts | 2 +- .../unittests/tsserverProjectSystem.ts | 2 + src/server/editorServices.ts | 523 +++++++++--------- src/services/refactorProvider.ts | 2 +- 5 files changed, 286 insertions(+), 256 deletions(-) diff --git a/src/compiler/core.ts b/src/compiler/core.ts index 54b772cc29c4a..829a6afa4b7da 100644 --- a/src/compiler/core.ts +++ b/src/compiler/core.ts @@ -534,6 +534,19 @@ namespace ts { return result; } + export function mapDefinedIter(iter: Iterator, mapFn: (x: T) => U | undefined): U[] { + const result: U[] = []; + while (true) { + const { value, done } = iter.next(); + if (done) break; + const res = mapFn(value); + if (res !== undefined) { + result.push(res); + } + } + return result; + } + /** * Computes the first matching span of elements and returns a tuple of the first span * and the remaining elements. diff --git a/src/harness/unittests/cachingInServerLSHost.ts b/src/harness/unittests/cachingInServerLSHost.ts index eb2907e89dea6..3498030142a51 100644 --- a/src/harness/unittests/cachingInServerLSHost.ts +++ b/src/harness/unittests/cachingInServerLSHost.ts @@ -74,7 +74,7 @@ namespace ts { const projectService = new server.ProjectService(svcOpts); const rootScriptInfo = projectService.getOrCreateScriptInfo(rootFile, /* openedByClient */ true, /*containingProject*/ undefined); - const project = projectService.createInferredProjectWithRootFileIfNecessary(rootScriptInfo); + const project = projectService.assignScriptInfoToInferredProject(rootScriptInfo); project.setCompilerOptions({ module: ts.ModuleKind.AMD, noLib: true } ); return { project, diff --git a/src/harness/unittests/tsserverProjectSystem.ts b/src/harness/unittests/tsserverProjectSystem.ts index 7d70aa74fdfd1..deaa17c2afedb 100644 --- a/src/harness/unittests/tsserverProjectSystem.ts +++ b/src/harness/unittests/tsserverProjectSystem.ts @@ -263,6 +263,8 @@ namespace ts.projectSystem { function invokeWatcherCallbacks(callbacks: T[], invokeCallback: (cb: T) => void): void { if (callbacks) { + // The array copy is made to ensure that even if one of the callback removes the callbacks, + // we dont miss any callbacks following it const cbs = callbacks.slice(); for (const cb of cbs) { invokeCallback(cb); diff --git a/src/server/editorServices.ts b/src/server/editorServices.ts index 5ca8a54d71f54..4d704a365b5da 100644 --- a/src/server/editorServices.ts +++ b/src/server/editorServices.ts @@ -271,16 +271,16 @@ namespace ts.server { ReloadingFiles = "Reloading configured projects for files", ReloadingInferredRootFiles = "Reloading configured projects for only inferred root files", UpdatedCallback = "Updated the callback", - TrackingFileAdded = "Tracking file added", - TrackingFileRemoved = "Tracking file removed", - InferredRootAdded = "Inferred Root file added", - InferredRootRemoved = "Inferred Root file removed", + OpenFilesImpactedByConfigFileAdd = "File added to open files impacted by this config file", + OpenFilesImpactedByConfigFileRemove = "File removed from open files impacted by this config file", + RootOfInferredProjectTrue = "Open file was set as Inferred root", + RootOfInferredProjectFalse = "Open file was set as not inferred root", } /* @internal */ export type ServerDirectoryWatcherCallback = (path: NormalizedPath) => void; - interface ConfigFileExistence { + interface ConfigFileExistenceInfo { /** * Cached value of existence of config file * It is true if there is configured project open for this file. @@ -290,18 +290,19 @@ namespace ts.server { */ exists: boolean; /** - * The value in the trackingOpenFilesMap is true if the open file is inferred project root - * and hence tracking changes to this config file - * (either config file is already present but doesnt include the open file in the project structure or config file doesnt exist) - * - * Otherwise its false + * openFilesImpactedByConfigFiles is a map of open files that would be impacted by this config file + * because these are the paths being looked up for their default configured project location + * The value in the map is true if the open file is root of the inferred project + * It is false when the open file that would still be impacted by existance of + * this config file but it is not the root of inferred project */ - trackingOpenFilesMap: Map; + openFilesImpactedByConfigFile: Map; /** - * The file watcher corresponding to this config file for the inferred project root - * The watcher is present only when there is no open configured project for this config file + * The file watcher watching the config file because there is open script info that is root of + * inferred project and will be impacted by change in the status of the config file + * The watcher is present only when there is no open configured project for the config file */ - configFileWatcher?: FileWatcher; + configFileWatcherForRootOfInferredProject?: FileWatcher; } export interface ProjectServiceOptions { @@ -351,6 +352,9 @@ namespace ts.server { private compilerOptionsForInferredProjects: CompilerOptions; private compileOnSaveForInferredProjects: boolean; + /** + * Project size for configured or external projects + */ private readonly projectToSizeMap: Map = createMap(); /** * This is a map of config file paths existance that doesnt need query to disk @@ -359,7 +363,7 @@ namespace ts.server { * - Or it is present if we have configured project open with config file at that location * In this case the exists property is always true */ - private readonly mapOfConfigFilePresence = createMap(); + private readonly mapOfConfigFileExistenceInfo = createMap(); private readonly throttledOperations: ThrottledOperations; private readonly hostConfiguration: HostConfiguration; @@ -427,14 +431,17 @@ namespace ts.server { return this.changedFiles; } + /* @internal */ ensureInferredProjectsUpToDate_TestOnly() { this.ensureProjectStructuresUptoDate(); } + /* @internal */ getCompilerOptionsForInferredProjects() { return this.compilerOptionsForInferredProjects; } + /* @internal */ onUpdateLanguageServiceStateForProject(project: Project, languageServiceEnabled: boolean) { if (!this.eventHandler) { return; @@ -479,14 +486,13 @@ namespace ts.server { const projectName = project.getProjectName(); this.pendingProjectUpdates.set(projectName, project); this.throttledOperations.schedule(projectName, /*delay*/ 250, () => { - const project = this.pendingProjectUpdates.get(projectName); - if (project) { - this.pendingProjectUpdates.delete(projectName); + if (this.pendingProjectUpdates.delete(projectName)) { project.updateGraph(); } }); } + /* @internal */ delayUpdateProjectGraphAndInferredProjectsRefresh(project: Project) { this.delayUpdateProjectGraph(project); this.delayInferredProjectsRefresh(); @@ -513,7 +519,7 @@ namespace ts.server { this.delayUpdateProjectGraphs(this.inferredProjects); } - findProject(projectName: string): Project { + findProject(projectName: string): Project | undefined { if (projectName === undefined) { return undefined; } @@ -687,39 +693,46 @@ namespace ts.server { } private onConfigChangedForConfiguredProject(project: ConfiguredProject, eventKind: FileWatcherEventKind) { - const configFilePresenceInfo = this.mapOfConfigFilePresence.get(project.canonicalConfigFilePath); + const configFileExistenceInfo = this.mapOfConfigFileExistenceInfo.get(project.canonicalConfigFilePath); if (eventKind === FileWatcherEventKind.Deleted) { // Update the cached status - // No action needed on tracking open files since the existing config file anyways didnt affect the tracking file - configFilePresenceInfo.exists = false; + // We arent updating or removing the cached config file presence info as that will be taken care of by + // setConfigFilePresenceByClosedConfigFile when the project is closed (depending on tracking open files) + configFileExistenceInfo.exists = false; this.removeProject(project); // Reload the configured projects for the open files in the map as they are affectected by this config file - this.logConfigFileWatchUpdate(project.getConfigFilePath(), configFilePresenceInfo, ConfigFileWatcherStatus.ReloadingFiles); - // Since the configured project was deleted, we want to reload projects for all the open files - this.delayReloadConfiguredProjectForFiles(configFilePresenceInfo.trackingOpenFilesMap, /*ignoreIfNotInferredProjectRoot*/ false); + // Since the configured project was deleted, we want to reload projects for all the open files including files + // that are not root of the inferred project + this.logConfigFileWatchUpdate(project.getConfigFilePath(), project.canonicalConfigFilePath, configFileExistenceInfo, ConfigFileWatcherStatus.ReloadingFiles); + this.delayReloadConfiguredProjectForFiles(configFileExistenceInfo, /*ignoreIfNotInferredProjectRoot*/ false); } else { - this.logConfigFileWatchUpdate(project.getConfigFilePath(), configFilePresenceInfo, ConfigFileWatcherStatus.ReloadingInferredRootFiles); + this.logConfigFileWatchUpdate(project.getConfigFilePath(), project.canonicalConfigFilePath, configFileExistenceInfo, ConfigFileWatcherStatus.ReloadingInferredRootFiles); project.pendingReload = true; this.delayUpdateProjectGraph(project); - // As we scheduled the updated project graph, we would need to only schedule the project reload for the inferred project roots - this.delayReloadConfiguredProjectForFiles(configFilePresenceInfo.trackingOpenFilesMap, /*ignoreIfNotInferredProjectRoot*/ true); + // As we scheduled the update on configured project graph, + // we would need to schedule the project reload for only the root of inferred projects + this.delayReloadConfiguredProjectForFiles(configFileExistenceInfo, /*ignoreIfNotInferredProjectRoot*/ true); } } /** - * This is the callback function for the config file add/remove/change at any location that matters to open - * script info but doesnt have configured project open for the config file + * This is the callback function for the config file add/remove/change at any location + * that matters to open script info but doesnt have configured project open + * for the config file */ private onConfigFileChangeForOpenScriptInfo(configFileName: NormalizedPath, eventKind: FileWatcherEventKind) { // This callback is called only if we dont have config file project for this config file - const cononicalConfigPath = normalizedPathToPath(configFileName, this.currentDirectory, this.toCanonicalFileName); - const configFilePresenceInfo = this.mapOfConfigFilePresence.get(cononicalConfigPath); - configFilePresenceInfo.exists = (eventKind !== FileWatcherEventKind.Deleted); - this.logConfigFileWatchUpdate(configFileName, configFilePresenceInfo, ConfigFileWatcherStatus.ReloadingFiles); - // The tracking opens files would only contaion the inferred root so no need to check - this.delayReloadConfiguredProjectForFiles(configFilePresenceInfo.trackingOpenFilesMap, /*ignoreIfNotInferredProjectRoot*/ false); + const canonicalConfigPath = normalizedPathToPath(configFileName, this.currentDirectory, this.toCanonicalFileName); + const configFileExistenceInfo = this.mapOfConfigFileExistenceInfo.get(canonicalConfigPath); + configFileExistenceInfo.exists = (eventKind !== FileWatcherEventKind.Deleted); + this.logConfigFileWatchUpdate(configFileName, canonicalConfigPath, configFileExistenceInfo, ConfigFileWatcherStatus.ReloadingFiles); + + // Because there is no configured project open for the config file, the tracking open files map + // will only have open files that need the re-detection of the project and hence + // reload projects for all the tracking open files in the map + this.delayReloadConfiguredProjectForFiles(configFileExistenceInfo, /*ignoreIfNotInferredProjectRoot*/ false); } private removeProject(project: Project) { @@ -737,7 +750,7 @@ namespace ts.server { case ProjectKind.Configured: this.configuredProjects.delete((project).canonicalConfigFilePath); this.projectToSizeMap.delete((project as ConfiguredProject).canonicalConfigFilePath); - this.setConfigFilePresenceByClosedConfigFile(project); + this.setConfigFileExistenceInfoByClosedConfiguredProject(project); break; case ProjectKind.Inferred: unorderedRemoveItem(this.inferredProjects, project); @@ -745,47 +758,16 @@ namespace ts.server { } } - private assignScriptInfoToInferredProjectIfNecessary(info: ScriptInfo, addToListOfOpenFiles: boolean): void { - if (info.containingProjects.length === 0) { - // create new inferred project p with the newly opened file as root - // or add root to existing inferred project if 'useOneInferredProject' is true - this.createInferredProjectWithRootFileIfNecessary(info); - - // if useOneInferredProject is not set then try to fixup ownership of open files - // check 'defaultProject !== inferredProject' is necessary to handle cases - // when creation inferred project for some file has added other open files into this project - // (i.e.as referenced files) - // we definitely don't want to delete the project that was just created - // Also note that we need to create a copy of the array since the list of project will change - for (const inferredProject of this.inferredProjects.slice(0, this.inferredProjects.length - 1)) { - Debug.assert(!this.useSingleInferredProject); - // Remove this file from the root of inferred project if its part of more than 2 projects - // This logic is same as iterating over all open files and calling - // this.removRootOfInferredProjectIfNowPartOfOtherProject(f); - // Since this is also called from refreshInferredProject and closeOpen file - // to update inferred projects of the open file, this iteration might be faster - // instead of scanning all open files - const root = inferredProject.getRootScriptInfos(); - Debug.assert(root.length === 1); - if (root[0].containingProjects.length > 1) { - this.removeProject(inferredProject); - } - } - } - else { - for (const p of info.containingProjects) { - // file is the part of configured project - if (p.projectKind === ProjectKind.Configured) { - if (addToListOfOpenFiles) { - ((p)).addOpenRef(); - } - } + private addToListOfOpenFiles(info: ScriptInfo) { + Debug.assert(info.containingProjects.length !== 0); + for (const p of info.containingProjects) { + // file is the part of configured project, addref the project + if (p.projectKind === ProjectKind.Configured) { + ((p)).addOpenRef(); } } - if (addToListOfOpenFiles) { - this.openFiles.push(info); - } + this.openFiles.push(info); } /** @@ -837,17 +819,16 @@ namespace ts.server { this.removeProject(project); } - // collect orphaned files and try to re-add them as newly opened - // treat orphaned files as newly opened - // for all open files + // collect orphaned files and assign them to inferred project just like we treat open of a file for (const f of this.openFiles) { if (f.containingProjects.length === 0) { - this.assignScriptInfoToInferredProjectIfNecessary(f, /*addToListOfOpenFiles*/ false); + this.assignScriptInfoToInferredProject(f); } } - // Cleanup script infos that arent part of any project is postponed to - // next file open so that if file from same project is opened we wont end up creating same script infos + // Cleanup script infos that arent part of any project (eg. those could be closed script infos not referenced by any project) + // is postponed to next file open so that if file from same project is opened, + // we wont end up creating same script infos } // If the current info is being just closed - add the watcher file to track changes @@ -871,15 +852,15 @@ namespace ts.server { } private configFileExists(configFileName: NormalizedPath, canonicalConfigFilePath: string, info: ScriptInfo) { - let configFilePresenceInfo = this.mapOfConfigFilePresence.get(canonicalConfigFilePath); - if (configFilePresenceInfo) { - // By default the info is belong to the config file. - // Only adding the info as a root to inferred project will make it the root - if (!configFilePresenceInfo.trackingOpenFilesMap.has(info.path)) { - configFilePresenceInfo.trackingOpenFilesMap.set(info.path, false); - this.logConfigFileWatchUpdate(configFileName, configFilePresenceInfo, ConfigFileWatcherStatus.TrackingFileAdded); + let configFileExistenceInfo = this.mapOfConfigFileExistenceInfo.get(canonicalConfigFilePath); + if (configFileExistenceInfo) { + // By default the info would get impacted by presence of config file since its in the detection path + // Only adding the info as a root to inferred project will need the existence to be watched by file watcher + if (!configFileExistenceInfo.openFilesImpactedByConfigFile.has(info.path)) { + configFileExistenceInfo.openFilesImpactedByConfigFile.set(info.path, false); + this.logConfigFileWatchUpdate(configFileName, canonicalConfigFilePath, configFileExistenceInfo, ConfigFileWatcherStatus.OpenFilesImpactedByConfigFileAdd); } - return configFilePresenceInfo.exists; + return configFileExistenceInfo.exists; } // Theoretically we should be adding watch for the directory here itself. @@ -887,162 +868,148 @@ namespace ts.server { // somewhere inside the another config file directory. // And technically we could handle that case in configFile's directory watcher in some cases // But given that its a rare scenario it seems like too much overhead. (we werent watching those directories earlier either) - // So what we are now watching is: configFile if the project is open - // And the whole chain of config files only for the inferred project roots - // Cache the host value of file exists and add the info tio to the tracked root - const trackingOpenFilesMap = createMap(); - trackingOpenFilesMap.set(info.path, false); + // So what we are now watching is: configFile if the configured project corresponding to it is open + // Or the whole chain of config files for the roots of the inferred projects + + // Cache the host value of file exists and add the info to map of open files impacted by this config file + const openFilesImpactedByConfigFile = createMap(); + openFilesImpactedByConfigFile.set(info.path, false); const exists = this.host.fileExists(configFileName); - configFilePresenceInfo = { exists, trackingOpenFilesMap }; - this.mapOfConfigFilePresence.set(canonicalConfigFilePath, configFilePresenceInfo); - this.logConfigFileWatchUpdate(configFileName, configFilePresenceInfo, ConfigFileWatcherStatus.TrackingFileAdded); + configFileExistenceInfo = { exists, openFilesImpactedByConfigFile }; + this.mapOfConfigFileExistenceInfo.set(canonicalConfigFilePath, configFileExistenceInfo); + this.logConfigFileWatchUpdate(configFileName, canonicalConfigFilePath, configFileExistenceInfo, ConfigFileWatcherStatus.OpenFilesImpactedByConfigFileAdd); return exists; } - private setConfigFilePresenceByNewConfiguredProject(project: ConfiguredProject) { - const configFilePresenceInfo = this.mapOfConfigFilePresence.get(project.canonicalConfigFilePath); - if (configFilePresenceInfo) { - Debug.assert(configFilePresenceInfo.exists); + private setConfigFileExistenceByNewConfiguredProject(project: ConfiguredProject) { + const configFileExistenceInfo = this.mapOfConfigFileExistenceInfo.get(project.canonicalConfigFilePath); + if (configFileExistenceInfo) { + Debug.assert(configFileExistenceInfo.exists); // close existing watcher - if (configFilePresenceInfo.configFileWatcher) { + if (configFileExistenceInfo.configFileWatcherForRootOfInferredProject) { const configFileName = project.getConfigFilePath(); this.closeFileWatcher( WatchType.ConfigFileForInferredRoot, /*project*/ undefined, configFileName, - configFilePresenceInfo.configFileWatcher, WatcherCloseReason.ConfigProjectCreated + configFileExistenceInfo.configFileWatcherForRootOfInferredProject, WatcherCloseReason.ConfigProjectCreated ); - configFilePresenceInfo.configFileWatcher = undefined; - this.logConfigFileWatchUpdate(configFileName, configFilePresenceInfo, ConfigFileWatcherStatus.UpdatedCallback); + configFileExistenceInfo.configFileWatcherForRootOfInferredProject = undefined; + this.logConfigFileWatchUpdate(configFileName, project.canonicalConfigFilePath, configFileExistenceInfo, ConfigFileWatcherStatus.UpdatedCallback); } } else { - // We could be in this scenario if it is the external project tracked configured file + // We could be in this scenario if project is the configured project tracked by external project // Since that route doesnt check if the config file is present or not - this.mapOfConfigFilePresence.set(project.canonicalConfigFilePath, { + this.mapOfConfigFileExistenceInfo.set(project.canonicalConfigFilePath, { exists: true, - trackingOpenFilesMap: createMap() + openFilesImpactedByConfigFile: createMap() }); } } - private configFileExistenceTracksInferredRoot(configFilePresenceInfo: ConfigFileExistence) { - return forEachEntry(configFilePresenceInfo.trackingOpenFilesMap, (value, __key) => value); + /** + * Returns true if the configFileExistenceInfo is needed/impacted by open files that are root of inferred project + */ + private configFileExistenceImpactsRootOfInferredProject(configFileExistenceInfo: ConfigFileExistenceInfo) { + return forEachEntry(configFileExistenceInfo.openFilesImpactedByConfigFile, (isRootOfInferredProject, __key) => isRootOfInferredProject); } - private setConfigFilePresenceByClosedConfigFile(closedProject: ConfiguredProject) { - const configFilePresenceInfo = this.mapOfConfigFilePresence.get(closedProject.canonicalConfigFilePath); - Debug.assert(!!configFilePresenceInfo); - const trackingOpenFilesMap = configFilePresenceInfo.trackingOpenFilesMap; - if (trackingOpenFilesMap.size) { + private setConfigFileExistenceInfoByClosedConfiguredProject(closedProject: ConfiguredProject) { + const configFileExistenceInfo = this.mapOfConfigFileExistenceInfo.get(closedProject.canonicalConfigFilePath); + Debug.assert(!!configFileExistenceInfo); + if (configFileExistenceInfo.openFilesImpactedByConfigFile.size) { const configFileName = closedProject.getConfigFilePath(); - if (this.configFileExistenceTracksInferredRoot(configFilePresenceInfo)) { - Debug.assert(!configFilePresenceInfo.configFileWatcher); - configFilePresenceInfo.configFileWatcher = this.addFileWatcher( + // If there are open files that are impacted by this config file existence + // but none of them are root of inferred project, the config file watcher will be + // created when any of the script infos are added as root of inferred project + if (this.configFileExistenceImpactsRootOfInferredProject(configFileExistenceInfo)) { + Debug.assert(!configFileExistenceInfo.configFileWatcherForRootOfInferredProject); + configFileExistenceInfo.configFileWatcherForRootOfInferredProject = this.addFileWatcher( WatchType.ConfigFileForInferredRoot, /*project*/ undefined, configFileName, (_filename, eventKind) => this.onConfigFileChangeForOpenScriptInfo(configFileName, eventKind) ); - this.logConfigFileWatchUpdate(configFileName, configFilePresenceInfo, ConfigFileWatcherStatus.UpdatedCallback); + this.logConfigFileWatchUpdate(configFileName, closedProject.canonicalConfigFilePath, configFileExistenceInfo, ConfigFileWatcherStatus.UpdatedCallback); } } else { - // There is no one tracking anymore. Remove the status - this.mapOfConfigFilePresence.delete(closedProject.canonicalConfigFilePath); + // There is not a single file open thats tracking the status of this config file. Remove from cache + this.mapOfConfigFileExistenceInfo.delete(closedProject.canonicalConfigFilePath); } } - private logConfigFileWatchUpdate(configFileName: NormalizedPath, configFilePresenceInfo: ConfigFileExistence, status: ConfigFileWatcherStatus) { + private logConfigFileWatchUpdate(configFileName: NormalizedPath, canonicalConfigFilePath: string, configFileExistenceInfo: ConfigFileExistenceInfo, status: ConfigFileWatcherStatus) { if (!this.logger.loggingEnabled()) { return; } const inferredRoots: string[] = []; const otherFiles: string[] = []; - configFilePresenceInfo.trackingOpenFilesMap.forEach((value, key: Path) => { - const info = this.getScriptInfoForPath(key); - if (value) { - inferredRoots.push(info.fileName); - } - else { - otherFiles.push(info.fileName); - } + configFileExistenceInfo.openFilesImpactedByConfigFile.forEach((isRootOfInferredProject, key) => { + const info = this.getScriptInfoForPath(key as Path); + (isRootOfInferredProject ? inferredRoots : otherFiles).push(info.fileName); }); - const watchType = status === ConfigFileWatcherStatus.UpdatedCallback || - status === ConfigFileWatcherStatus.ReloadingFiles || - status === ConfigFileWatcherStatus.ReloadingInferredRootFiles ? - (configFilePresenceInfo.configFileWatcher ? WatchType.ConfigFileForInferredRoot : WatchType.ConfigFilePath) : - ""; - this.logger.info(`ConfigFilePresence ${watchType}:: File: ${configFileName} Currently Tracking: InferredRootFiles: ${inferredRoots} OtherFiles: ${otherFiles} Status: ${status}`); - } - - private closeConfigFileWatcherIfInferredRoot(configFileName: NormalizedPath, canonicalConfigFilePath: string, - configFilePresenceInfo: ConfigFileExistence, infoIsInferredRoot: boolean, reason: WatcherCloseReason) { - // Close the config file watcher if it was the last inferred root - if (infoIsInferredRoot && - configFilePresenceInfo.configFileWatcher && - !this.configFileExistenceTracksInferredRoot(configFilePresenceInfo)) { - this.closeFileWatcher( - WatchType.ConfigFileForInferredRoot, /*project*/ undefined, configFileName, - configFilePresenceInfo.configFileWatcher, reason - ); - configFilePresenceInfo.configFileWatcher = undefined; - } - // If this was the last tracking file open for this config file, remove the cached value - if (!configFilePresenceInfo.trackingOpenFilesMap.size && - !this.getConfiguredProjectByCanonicalConfigFilePath(canonicalConfigFilePath)) { - this.mapOfConfigFilePresence.delete(canonicalConfigFilePath); + const watches: WatchType[] = []; + if (configFileExistenceInfo.configFileWatcherForRootOfInferredProject) { + watches.push(WatchType.ConfigFileForInferredRoot); } + if (this.configuredProjects.has(canonicalConfigFilePath)) { + watches.push(WatchType.ConfigFilePath); + } + this.logger.info(`ConfigFilePresence:: Current Watches: ['${watches.join("','")}']:: File: ${configFileName} Currently impacted open files: RootsOfInferredProjects: ${inferredRoots} OtherOpenFiles: ${otherFiles} Status: ${status}`); } - private closeConfigFileWatchForClosedScriptInfo(configFileName: NormalizedPath, canonicalConfigFilePath: string, info: ScriptInfo) { - const configFilePresenceInfo = this.mapOfConfigFilePresence.get(canonicalConfigFilePath); - if (configFilePresenceInfo) { - const isInferredRoot = configFilePresenceInfo.trackingOpenFilesMap.get(info.path); - - // Delete the info from tracking - configFilePresenceInfo.trackingOpenFilesMap.delete(info.path); - this.logConfigFileWatchUpdate(configFileName, configFilePresenceInfo, ConfigFileWatcherStatus.TrackingFileRemoved); - - // Close the config file watcher if it was the last inferred root - this.closeConfigFileWatcherIfInferredRoot(configFileName, canonicalConfigFilePath, - configFilePresenceInfo, isInferredRoot, WatcherCloseReason.FileClosed + /** + * Close the config file watcher in the cached ConfigFileExistenceInfo + * if there arent any open files that are root of inferred project + */ + private closeConfigFileWatcherOfConfigFileExistenceInfo( + configFileName: NormalizedPath, configFileExistenceInfo: ConfigFileExistenceInfo, + reason: WatcherCloseReason + ) { + // Close the config file watcher if there are no more open files that are root of inferred project + if (configFileExistenceInfo.configFileWatcherForRootOfInferredProject && + !this.configFileExistenceImpactsRootOfInferredProject(configFileExistenceInfo)) { + this.closeFileWatcher( + WatchType.ConfigFileForInferredRoot, /*project*/ undefined, configFileName, + configFileExistenceInfo.configFileWatcherForRootOfInferredProject, reason ); + configFileExistenceInfo.configFileWatcherForRootOfInferredProject = undefined; } } /** * This is called on file close, so that we stop watching the config file for this script info - * @param info */ private stopWatchingConfigFilesForClosedScriptInfo(info: ScriptInfo) { Debug.assert(!info.isScriptOpen()); - this.forEachConfigFileLocation(info, (configFileName, canonicalConfigFilePath) => - this.closeConfigFileWatchForClosedScriptInfo(configFileName, canonicalConfigFilePath, info) - ); - } - - private watchConfigFileForInferredProjectRoot(configFileName: NormalizedPath, canonicalConfigFilePath: string, info: ScriptInfo) { - let configFilePresenceInfo = this.mapOfConfigFilePresence.get(canonicalConfigFilePath); - if (!configFilePresenceInfo) { - // Create the cache - configFilePresenceInfo = { - exists: this.host.fileExists(configFileName), - trackingOpenFilesMap: createMap() - }; - this.mapOfConfigFilePresence.set(canonicalConfigFilePath, configFilePresenceInfo); - } - - // Set this file as inferred root - configFilePresenceInfo.trackingOpenFilesMap.set(info.path, true); - this.logConfigFileWatchUpdate(configFileName, configFilePresenceInfo, ConfigFileWatcherStatus.InferredRootAdded); + this.forEachConfigFileLocation(info, (configFileName, canonicalConfigFilePath) => { + const configFileExistenceInfo = this.mapOfConfigFileExistenceInfo.get(canonicalConfigFilePath); + if (configFileExistenceInfo) { + const infoIsRootOfInferredProject = configFileExistenceInfo.openFilesImpactedByConfigFile.get(info.path); + + // Delete the info from map, since this file is no more open + configFileExistenceInfo.openFilesImpactedByConfigFile.delete(info.path); + this.logConfigFileWatchUpdate(configFileName, canonicalConfigFilePath, configFileExistenceInfo, ConfigFileWatcherStatus.OpenFilesImpactedByConfigFileRemove); + + // If the script info was not root of inferred project, + // there wont be config file watch open because of this script info + if (infoIsRootOfInferredProject) { + // But if it is a root, it could be the last script info that is root of inferred project + // and hence we would need to close the config file watcher + this.closeConfigFileWatcherOfConfigFileExistenceInfo( + configFileName, configFileExistenceInfo, WatcherCloseReason.FileClosed + ); + } - // If there is no configured project for this config file, create the watcher - if (!configFilePresenceInfo.configFileWatcher && - !this.getConfiguredProjectByCanonicalConfigFilePath(canonicalConfigFilePath)) { - configFilePresenceInfo.configFileWatcher = this.addFileWatcher(WatchType.ConfigFileForInferredRoot, /*project*/ undefined, configFileName, - (_fileName, eventKind) => this.onConfigFileChangeForOpenScriptInfo(configFileName, eventKind) - ); - this.logConfigFileWatchUpdate(configFileName, configFilePresenceInfo, ConfigFileWatcherStatus.UpdatedCallback); - } + // If there are no open files that are impacted by configFileExistenceInfo after closing this script info + // there is no configured project present, remove the cached existence info + if (!configFileExistenceInfo.openFilesImpactedByConfigFile.size && + !this.getConfiguredProjectByCanonicalConfigFilePath(canonicalConfigFilePath)) { + Debug.assert(!configFileExistenceInfo.configFileWatcherForRootOfInferredProject); + this.mapOfConfigFileExistenceInfo.delete(canonicalConfigFilePath); + } + } + }); } /** @@ -1051,25 +1018,30 @@ namespace ts.server { /* @internal */ startWatchingConfigFilesForInferredProjectRoot(info: ScriptInfo) { Debug.assert(info.isScriptOpen()); - this.forEachConfigFileLocation(info, (configFileName, canonicalConfigFilePath) => - this.watchConfigFileForInferredProjectRoot(configFileName, canonicalConfigFilePath, info) - ); - } - - private closeWatchConfigFileForInferredProjectRoot(configFileName: NormalizedPath, canonicalConfigFilePath: string, info: ScriptInfo, reason: WatcherCloseReason) { - const configFilePresenceInfo = this.mapOfConfigFilePresence.get(canonicalConfigFilePath); - if (configFilePresenceInfo) { - // Set this as not inferred root - if (configFilePresenceInfo.trackingOpenFilesMap.has(info.path)) { - configFilePresenceInfo.trackingOpenFilesMap.set(info.path, false); - this.logConfigFileWatchUpdate(configFileName, configFilePresenceInfo, ConfigFileWatcherStatus.InferredRootRemoved); + this.forEachConfigFileLocation(info, (configFileName, canonicalConfigFilePath) => { + let configFilePresenceInfo = this.mapOfConfigFileExistenceInfo.get(canonicalConfigFilePath); + if (!configFilePresenceInfo) { + // Create the cache + configFilePresenceInfo = { + exists: this.host.fileExists(configFileName), + openFilesImpactedByConfigFile: createMap() + }; + this.mapOfConfigFileExistenceInfo.set(canonicalConfigFilePath, configFilePresenceInfo); } - // Close the watcher if present - this.closeConfigFileWatcherIfInferredRoot(configFileName, canonicalConfigFilePath, - configFilePresenceInfo, /*infoIsInferredRoot*/ true, reason - ); - } + // Set this file as the root of inferred project + configFilePresenceInfo.openFilesImpactedByConfigFile.set(info.path, true); + this.logConfigFileWatchUpdate(configFileName, canonicalConfigFilePath, configFilePresenceInfo, ConfigFileWatcherStatus.RootOfInferredProjectTrue); + + // If there is no configured project for this config file, add the file watcher + if (!configFilePresenceInfo.configFileWatcherForRootOfInferredProject && + !this.getConfiguredProjectByCanonicalConfigFilePath(canonicalConfigFilePath)) { + configFilePresenceInfo.configFileWatcherForRootOfInferredProject = this.addFileWatcher(WatchType.ConfigFileForInferredRoot, /*project*/ undefined, configFileName, + (_fileName, eventKind) => this.onConfigFileChangeForOpenScriptInfo(configFileName, eventKind) + ); + this.logConfigFileWatchUpdate(configFileName, canonicalConfigFilePath, configFilePresenceInfo, ConfigFileWatcherStatus.UpdatedCallback); + } + }); } /** @@ -1077,9 +1049,21 @@ namespace ts.server { */ /* @internal */ stopWatchingConfigFilesForInferredProjectRoot(info: ScriptInfo, reason: WatcherCloseReason) { - this.forEachConfigFileLocation(info, (configFileName, canonicalConfigFilePath) => - this.closeWatchConfigFileForInferredProjectRoot(configFileName, canonicalConfigFilePath, info, reason) - ); + this.forEachConfigFileLocation(info, (configFileName, canonicalConfigFilePath) => { + const configFileExistenceInfo = this.mapOfConfigFileExistenceInfo.get(canonicalConfigFilePath); + if (configFileExistenceInfo && configFileExistenceInfo.openFilesImpactedByConfigFile.has(info.path)) { + Debug.assert(info.isScriptOpen()); + + // Info is not root of inferred project any more + configFileExistenceInfo.openFilesImpactedByConfigFile.set(info.path, false); + this.logConfigFileWatchUpdate(configFileName, canonicalConfigFilePath, configFileExistenceInfo, ConfigFileWatcherStatus.RootOfInferredProjectFalse); + + // Close the config file watcher + this.closeConfigFileWatcherOfConfigFileExistenceInfo( + configFileName, configFileExistenceInfo, reason + ); + } + }); } /** @@ -1165,7 +1149,6 @@ namespace ts.server { function printProjects(logger: Logger, projects: Project[], counter: number) { for (const project of projects) { - // Print shouldnt update the graph. It should emit whatever state the project is currently in logger.info(`Project '${project.getProjectName()}' (${ProjectKind[project.projectKind]}) ${counter}`); logger.info(project.filesToString()); logger.info("-----------------------------------------------"); @@ -1175,13 +1158,13 @@ namespace ts.server { } } - private findConfiguredProjectByProjectName(configFileName: NormalizedPath) { + private findConfiguredProjectByProjectName(configFileName: NormalizedPath): ConfiguredProject | undefined { // make sure that casing of config file name is consistent const canonicalConfigFilePath = asNormalizedPath(this.toCanonicalFileName(configFileName)); return this.getConfiguredProjectByCanonicalConfigFilePath(canonicalConfigFilePath); } - private getConfiguredProjectByCanonicalConfigFilePath(canonicalConfigFilePath: string) { + private getConfiguredProjectByCanonicalConfigFilePath(canonicalConfigFilePath: string): ConfiguredProject | undefined { return this.configuredProjects.get(canonicalConfigFilePath); } @@ -1259,7 +1242,7 @@ namespace ts.server { return false; } - private createAndAddExternalProject(projectFileName: string, files: protocol.ExternalFile[], options: protocol.ExternalProjectCompilerOptions, typeAcquisition: TypeAcquisition) { + private createExternalProject(projectFileName: string, files: protocol.ExternalFile[], options: protocol.ExternalProjectCompilerOptions, typeAcquisition: TypeAcquisition) { const compilerOptions = convertCompilerOptions(options); const project = new ExternalProject( projectFileName, @@ -1319,7 +1302,18 @@ namespace ts.server { } } - private createAndAddConfiguredProject(configFileName: NormalizedPath, projectOptions: ProjectOptions, configFileErrors: Diagnostic[], configFileSpecs: ConfigFileSpecs, cachedServerHost: CachedServerHost, clientFileName?: string) { + private addFilesToNonInferredProjectAndUpdateGraph(project: ConfiguredProject | ExternalProject, files: T[], propertyReader: FilePropertyReader, clientFileName: string, typeAcquisition: TypeAcquisition, configFileErrors: Diagnostic[]): void { + project.setProjectErrors(configFileErrors); + this.updateNonInferredProjectFiles(project, files, propertyReader, clientFileName); + project.setTypeAcquisition(typeAcquisition); + // This doesnt need scheduling since its either creation or reload of the project + project.updateGraph(); + } + + private createConfiguredProject(configFileName: NormalizedPath, clientFileName?: string) { + const cachedServerHost = new CachedServerHost(this.host, this.toCanonicalFileName); + const { projectOptions, configFileErrors, configFileSpecs } = this.convertConfigFileContentToProjectOptions(configFileName, cachedServerHost); + this.logger.info(`Opened configuration file ${configFileName}`); const languageServiceEnabled = !this.exceededTotalSizeLimitForNonTsFiles(configFileName, projectOptions.compilerOptions, projectOptions.files, fileNamePropertyReader); const project = new ConfiguredProject( configFileName, @@ -1332,7 +1326,7 @@ namespace ts.server { cachedServerHost); project.configFileSpecs = configFileSpecs; - // TODO: (sheetalkamat) We should also watch the configFiles that are extended + // TODO: We probably should also watch the configFiles that are extended project.configFileWatcher = this.addFileWatcher(WatchType.ConfigFilePath, project, configFileName, (_fileName, eventKind) => this.onConfigChangedForConfiguredProject(project, eventKind) ); @@ -1343,26 +1337,11 @@ namespace ts.server { this.addFilesToNonInferredProjectAndUpdateGraph(project, projectOptions.files, fileNamePropertyReader, clientFileName, projectOptions.typeAcquisition, configFileErrors); this.configuredProjects.set(project.canonicalConfigFilePath, project); - this.setConfigFilePresenceByNewConfiguredProject(project); + this.setConfigFileExistenceByNewConfiguredProject(project); this.sendProjectTelemetry(project.getConfigFilePath(), project, projectOptions); return project; } - private addFilesToNonInferredProjectAndUpdateGraph(project: ConfiguredProject | ExternalProject, files: T[], propertyReader: FilePropertyReader, clientFileName: string, typeAcquisition: TypeAcquisition, configFileErrors: Diagnostic[]): void { - project.setProjectErrors(configFileErrors); - this.updateNonInferredProjectFiles(project, files, propertyReader, clientFileName); - project.setTypeAcquisition(typeAcquisition); - // This doesnt need scheduling since its either creation or reload of the project - project.updateGraph(); - } - - private openConfigFile(configFileName: NormalizedPath, clientFileName?: string) { - const cachedServerHost = new CachedServerHost(this.host, this.toCanonicalFileName); - const { projectOptions, configFileErrors, configFileSpecs } = this.convertConfigFileContentToProjectOptions(configFileName, cachedServerHost); - this.logger.info(`Opened configuration file ${configFileName}`); - return this.createAndAddConfiguredProject(configFileName, projectOptions, configFileErrors, configFileSpecs, cachedServerHost, clientFileName); - } - private updateNonInferredProjectFiles(project: ExternalProject | ConfiguredProject, newUncheckedFiles: T[], propertyReader: FilePropertyReader, clientFileName?: string) { const projectRootFilesMap = project.getRootFilesMap(); const newRootScriptInfoMap: Map = createMap(); @@ -1474,18 +1453,44 @@ namespace ts.server { this.updateNonInferredProject(project, projectOptions.files, fileNamePropertyReader, projectOptions.compilerOptions, projectOptions.typeAcquisition, projectOptions.compileOnSave, configFileErrors); } - createInferredProjectWithRootFileIfNecessary(root: ScriptInfo) { + /*@internal*/ + assignScriptInfoToInferredProject(info: ScriptInfo) { + Debug.assert(info.containingProjects.length === 0); + + // create new inferred project p with the newly opened file as root + // or add root to existing inferred project if 'useSingleInferredProject' is true const useExistingProject = this.useSingleInferredProject && this.inferredProjects.length; const project = useExistingProject ? this.inferredProjects[0] : new InferredProject(this, this.documentRegistry, this.compilerOptionsForInferredProjects); - project.addRoot(root); + project.addRoot(info); project.updateGraph(); if (!useExistingProject) { + // Add the new project to inferred projects list this.inferredProjects.push(project); + + for (const inferredProject of this.inferredProjects.slice(0, this.inferredProjects.length - 1)) { + // Note that we need to create a copy of the array since the list of project can change + Debug.assert(inferredProject !== project); + + // Remove the inferred project if the root of it is now part of newly created inferred project + // e.g through references + // Which means if any root of inferred project is part of more than 1 project can be removed + // This logic is same as iterating over all open files and calling + // this.removeRootOfInferredProjectIfNowPartOfOtherProject(f); + // Since this is also called from refreshInferredProject and closeOpen file + // to update inferred projects of the open file, this iteration might be faster + // instead of scanning all open files + const root = inferredProject.getRootScriptInfos(); + Debug.assert(root.length === 1); + if (root[0].containingProjects.length > 1) { + this.removeProject(inferredProject); + } + } } + return project; } @@ -1632,28 +1637,33 @@ namespace ts.server { */ reloadProjects() { this.logger.info("reload projects."); - this.reloadConfiguredProjectForFiles(this.openFiles, /*delayReload*/ false); + this.reloadConfiguredsProjectForFiles(this.openFiles, /*delayReload*/ false); this.refreshInferredProjects(); } - delayReloadConfiguredProjectForFiles(openFilesMap: Map, ignoreIfNotInferredProjectRoot: boolean) { + private delayReloadConfiguredProjectForFiles(configFileExistenceInfo: ConfigFileExistenceInfo, ignoreIfNotRootOfInferredProject: boolean) { // Get open files to reload projects for - const openFiles = flatMapIter(openFilesMap.keys(), path => { - if (!ignoreIfNotInferredProjectRoot || openFilesMap.get(path)) { - return this.getScriptInfoForPath(path as Path); + const openFiles = mapDefinedIter( + configFileExistenceInfo.openFilesImpactedByConfigFile.entries(), + ([path, isRootOfInferredProject]) => { + if (!ignoreIfNotRootOfInferredProject || isRootOfInferredProject) { + const info = this.getScriptInfoForPath(path as Path); + Debug.assert(!!info); + return info; + } } - }); - this.reloadConfiguredProjectForFiles(openFiles, /*delayReload*/ true); + ); + this.reloadConfiguredsProjectForFiles(openFiles, /*delayReload*/ true); this.delayInferredProjectsRefresh(); } /** * This function goes through all the openFiles and tries to file the config file for them. * If the config file is found and it refers to existing project, it reloads it either immediately - * or schedules it for reload depending on delayedReload option + * or schedules it for reload depending on delayReload option * If the there is no existing project it just opens the configured project for the config file */ - reloadConfiguredProjectForFiles(openFiles: ScriptInfo[], delayReload: boolean) { + private reloadConfiguredsProjectForFiles(openFiles: ScriptInfo[], delayReload: boolean) { const mapUpdatedProjects = createMap(); // try to reload config file for all open files for (const info of openFiles) { @@ -1665,7 +1675,7 @@ namespace ts.server { if (configFileName) { let project = this.findConfiguredProjectByProjectName(configFileName); if (!project) { - project = this.openConfigFile(configFileName, info.fileName); + project = this.createConfiguredProject(configFileName, info.fileName); mapUpdatedProjects.set(configFileName, true); } else if (!mapUpdatedProjects.has(configFileName)) { @@ -1716,7 +1726,7 @@ namespace ts.server { for (const info of this.openFiles) { // collect all orphaned script infos from open files if (info.containingProjects.length === 0) { - this.assignScriptInfoToInferredProjectIfNecessary(info, /*addToListOfOpenFiles*/ false); + this.assignScriptInfoToInferredProject(info); } // Or remove the root of inferred project if is referenced in more than one projects else { @@ -1752,7 +1762,7 @@ namespace ts.server { if (configFileName) { project = this.findConfiguredProjectByProjectName(configFileName); if (!project) { - project = this.openConfigFile(configFileName, fileName); + project = this.createConfiguredProject(configFileName, fileName); // even if opening config file was successful, it could still // contain errors that were tolerated. @@ -1771,8 +1781,13 @@ namespace ts.server { project.markAsDirty(); } - // at this point if file is the part of some configured/external project then this project should be created - this.assignScriptInfoToInferredProjectIfNecessary(info, /*addToListOfOpenFiles*/ true); + // At this point if file is part of any any configured or external project, then it would be present in the containing projects + // So if it still doesnt have any containing projects, it needs to be part of inferred project + if (info.containingProjects.length === 0) { + this.assignScriptInfoToInferredProject(info); + } + this.addToListOfOpenFiles(info); + // Delete the orphan files here because there might be orphan script infos (which are not part of project) // when some file/s were closed which resulted in project removal. // It was then postponed to cleanup these script infos so that they can be reused if @@ -2084,7 +2099,7 @@ namespace ts.server { let project = this.findConfiguredProjectByProjectName(tsconfigFile); if (!project) { // errors are stored in the project - project = this.openConfigFile(tsconfigFile); + project = this.createConfiguredProject(tsconfigFile); } if (project && !contains(exisingConfigFiles, tsconfigFile)) { // keep project alive even if no documents are opened - its lifetime is bound to the lifetime of containing external project @@ -2095,7 +2110,7 @@ namespace ts.server { else { // no config files - remove the item from the collection this.externalProjectToConfiguredProjectMap.delete(proj.projectFileName); - this.createAndAddExternalProject(proj.projectFileName, rootFiles, proj.options, proj.typeAcquisition); + this.createExternalProject(proj.projectFileName, rootFiles, proj.options, proj.typeAcquisition); } if (!suppressRefreshOfInferredProjects) { this.ensureProjectStructuresUptoDate(/*refreshInferredProjects*/ true); diff --git a/src/services/refactorProvider.ts b/src/services/refactorProvider.ts index 432df8c53d00c..04b12f16563f6 100644 --- a/src/services/refactorProvider.ts +++ b/src/services/refactorProvider.ts @@ -35,7 +35,7 @@ namespace ts { export function getApplicableRefactors(context: RefactorContext): ApplicableRefactorInfo[] { return flatMapIter(refactors.values(), refactor => - context.cancellationToken && context.cancellationToken.isCancellationRequested() ? [] : refactor.getAvailableActions(context)); + context.cancellationToken && context.cancellationToken.isCancellationRequested() ? undefined : refactor.getAvailableActions(context)); } export function getEditsForRefactor(context: RefactorContext, refactorName: string, actionName: string): RefactorEditInfo | undefined { From b071a8610c4fc86c685605965a42047bb4b41e27 Mon Sep 17 00:00:00 2001 From: Sheetal Nandi Date: Mon, 7 Aug 2017 14:47:32 -0700 Subject: [PATCH 056/109] More work on feedback from PR --- src/server/editorServices.ts | 85 +++++++++++++++++------------------- src/server/project.ts | 11 +++-- src/server/utilities.ts | 5 +++ 3 files changed, 51 insertions(+), 50 deletions(-) diff --git a/src/server/editorServices.ts b/src/server/editorServices.ts index 4d704a365b5da..27dbee2ebf580 100644 --- a/src/server/editorServices.ts +++ b/src/server/editorServices.ts @@ -1289,8 +1289,7 @@ namespace ts.server { } const configFilePath = project instanceof server.ConfiguredProject && project.getConfigFilePath(); - const base = getBaseFileName(configFilePath); - return base === "tsconfig.json" || base === "jsconfig.json" ? base : "other"; + return getBaseConfigFileName(configFilePath) || "other"; } function convertTypeAcquisition({ enable, include, exclude }: TypeAcquisition): ProjectInfoTypeAcquisitionData { @@ -1344,13 +1343,14 @@ namespace ts.server { private updateNonInferredProjectFiles(project: ExternalProject | ConfiguredProject, newUncheckedFiles: T[], propertyReader: FilePropertyReader, clientFileName?: string) { const projectRootFilesMap = project.getRootFilesMap(); - const newRootScriptInfoMap: Map = createMap(); + const newRootScriptInfoMap = createMap(); for (const f of newUncheckedFiles) { const newRootFile = propertyReader.getFileName(f); const normalizedPath = toNormalizedPath(newRootFile); let scriptInfo: ScriptInfo | NormalizedPath; let path: Path; + // Use the project's lsHost so that it can use caching instead of reaching to disk for the query if (!project.lsHost.fileExists(newRootFile)) { path = normalizedPathToPath(normalizedPath, this.currentDirectory, this.toCanonicalFileName); const existingValue = projectRootFilesMap.get(path); @@ -1378,7 +1378,7 @@ namespace ts.server { newRootScriptInfoMap.set(path, scriptInfo); } - // project's root file map size is always going to be larger than new roots map + // project's root file map size is always going to be same or larger than new roots map // as we have already all the new files to the project if (projectRootFilesMap.size > newRootScriptInfoMap.size) { projectRootFilesMap.forEach((value, path) => { @@ -1388,12 +1388,14 @@ namespace ts.server { } else { projectRootFilesMap.delete(path); - project.markAsDirty(); } } }); } - project.markAsDirty(); // Just to ensure that even if root files dont change, the changes to the non root file are picked up + + // Just to ensure that even if root files dont change, the changes to the non root file are picked up, + // mark the project as dirty unconditionally + project.markAsDirty(); } private updateNonInferredProject(project: ExternalProject | ConfiguredProject, newUncheckedFiles: T[], propertyReader: FilePropertyReader, newOptions: CompilerOptions, newTypeAcquisition: TypeAcquisition, compileOnSave: boolean, configFileErrors: Diagnostic[]) { @@ -1408,38 +1410,22 @@ namespace ts.server { /** * Read the config file of the project again and update the project - * @param project */ /* @internal */ reloadConfiguredProject(project: ConfiguredProject) { // At this point, there is no reason to not have configFile in the host - - // note: the returned "success" is true does not mean the "configFileErrors" is empty. - // because we might have tolerated the errors and kept going. So always return the configFileErrors - // regardless the "success" here is true or not. const host = project.getCachedServerHost(); + + // Clear the cache since we are reloading the project from disk host.clearCache(); const configFileName = project.getConfigFilePath(); this.logger.info(`Reloading configured project ${configFileName}`); - const { projectOptions, configFileErrors, configFileSpecs } = this.convertConfigFileContentToProjectOptions(configFileName, host); - project.configFileSpecs = configFileSpecs; - this.updateConfiguredProject(project, projectOptions, configFileErrors); - - if (!this.eventHandler) { - return; - } - this.eventHandler({ - eventName: ConfigFileDiagEvent, - data: { configFileName, diagnostics: project.getGlobalProjectErrors() || [], triggerFile: configFileName } - }); - } + // Read updated contents from disk + const { projectOptions, configFileErrors, configFileSpecs } = this.convertConfigFileContentToProjectOptions(configFileName, host); - /** - * Updates the configured project with updated config file contents - * @param project - */ - private updateConfiguredProject(project: ConfiguredProject, projectOptions: ProjectOptions, configFileErrors: Diagnostic[]) { + // Update the project + project.configFileSpecs = configFileSpecs; if (this.exceededTotalSizeLimitForNonTsFiles(project.canonicalConfigFilePath, projectOptions.compilerOptions, projectOptions.files, fileNamePropertyReader)) { project.disableLanguageService(); project.stopWatchingWildCards(WatcherCloseReason.ProjectReloadHitMaxSize); @@ -1451,6 +1437,15 @@ namespace ts.server { project.watchTypeRoots(); } this.updateNonInferredProject(project, projectOptions.files, fileNamePropertyReader, projectOptions.compilerOptions, projectOptions.typeAcquisition, projectOptions.compileOnSave, configFileErrors); + + if (!this.eventHandler) { + return; + } + + this.eventHandler({ + eventName: ConfigFileDiagEvent, + data: { configFileName, diagnostics: project.getGlobalProjectErrors() || [], triggerFile: configFileName } + }); } /*@internal*/ @@ -1587,8 +1582,8 @@ namespace ts.server { } if (args.extraFileExtensions) { this.hostConfiguration.extraFileExtensions = args.extraFileExtensions; - // We need to update the projects because of we might interprete more/less files - // depending on whether extra files extenstions are either added or removed + // We need to update the project structures again as it is possible that existing + // project structure could have more or less files depending on extensions permitted this.reloadProjects(); this.logger.info("Host file extension mappings updated"); } @@ -1664,7 +1659,7 @@ namespace ts.server { * If the there is no existing project it just opens the configured project for the config file */ private reloadConfiguredsProjectForFiles(openFiles: ScriptInfo[], delayReload: boolean) { - const mapUpdatedProjects = createMap(); + const updatedProjects = createMap(); // try to reload config file for all open files for (const info of openFiles) { // This tries to search for a tsconfig.json for the given file. If we found it, @@ -1673,12 +1668,12 @@ namespace ts.server { // otherwise we create a new one. const configFileName = this.getConfigFileNameForFile(info); if (configFileName) { - let project = this.findConfiguredProjectByProjectName(configFileName); + const project = this.findConfiguredProjectByProjectName(configFileName); if (!project) { - project = this.createConfiguredProject(configFileName, info.fileName); - mapUpdatedProjects.set(configFileName, true); + this.createConfiguredProject(configFileName, info.fileName); + updatedProjects.set(configFileName, true); } - else if (!mapUpdatedProjects.has(configFileName)) { + else if (!updatedProjects.has(configFileName)) { if (delayReload) { project.pendingReload = true; this.delayUpdateProjectGraph(project); @@ -1686,19 +1681,22 @@ namespace ts.server { else { this.reloadConfiguredProject(project); } - mapUpdatedProjects.set(configFileName, true); + updatedProjects.set(configFileName, true); } } } } /** - * - script info can be never migrate to state - root file in inferred project, this is only a starting point - * - if script info has more that one containing projects - it is not a root file in inferred project because: - * - references in inferred project supercede the root part - * - root/reference in non-inferred project beats root in inferred project + * Remove the root of inferred project if script info is part of another project */ private removeRootOfInferredProjectIfNowPartOfOtherProject(info: ScriptInfo) { + // If the script info is root of inferred project, it could only be first containing project + // since info is added to inferred project and made root only when there are no other projects containing it + // So even if it is root of the inferred project and after project structure updates its now part + // of multiple project it needs to be removed from that inferred project because: + // - references in inferred project supercede the root part + // - root / reference in non - inferred project beats root in inferred project if (info.containingProjects.length > 1 && info.containingProjects[0].projectKind === ProjectKind.Inferred && info.containingProjects[0].isRoot(info)) { @@ -1728,8 +1726,8 @@ namespace ts.server { if (info.containingProjects.length === 0) { this.assignScriptInfoToInferredProject(info); } - // Or remove the root of inferred project if is referenced in more than one projects else { + // Or remove the root of inferred project if is referenced in more than one projects this.removeRootOfInferredProjectIfNowPartOfOtherProject(info); } } @@ -1848,7 +1846,7 @@ namespace ts.server { if (!this.changedFiles) { this.changedFiles = [scriptInfo]; } - else if (this.changedFiles.indexOf(scriptInfo) < 0) { + else if (!contains(this.changedFiles, scriptInfo)) { this.changedFiles.push(scriptInfo); } } @@ -2023,8 +2021,7 @@ namespace ts.server { const rootFiles: protocol.ExternalFile[] = []; for (const file of proj.rootFiles) { const normalized = toNormalizedPath(file.fileName); - const baseFileName = getBaseFileName(normalized); - if (baseFileName === "tsconfig.json" || baseFileName === "jsconfig.json") { + if (getBaseConfigFileName(normalized)) { if (this.host.fileExists(normalized)) { (tsConfigFiles || (tsConfigFiles = [])).push(normalized); } diff --git a/src/server/project.ts b/src/server/project.ts index 06e4205bc8f92..a8506982bec8e 100644 --- a/src/server/project.ts +++ b/src/server/project.ts @@ -487,13 +487,12 @@ namespace ts.server { // add a root file to project addRoot(info: ScriptInfo) { - if (!this.isRoot(info)) { - this.rootFiles.push(info); - this.rootFilesMap.set(info.path, info); - info.attachToProject(this); + Debug.assert(!this.isRoot(info)); + this.rootFiles.push(info); + this.rootFilesMap.set(info.path, info); + info.attachToProject(this); - this.markAsDirty(); - } + this.markAsDirty(); } // add a root file to project diff --git a/src/server/utilities.ts b/src/server/utilities.ts index edc3d0e1117cf..79065f48ff5d0 100644 --- a/src/server/utilities.ts +++ b/src/server/utilities.ts @@ -227,6 +227,11 @@ namespace ts.server { /* @internal */ namespace ts.server { + export function getBaseConfigFileName(configFilePath: NormalizedPath): "tsconfig.json" | "jsconfig.json" | undefined { + const base = getBaseFileName(configFilePath); + return base === "tsconfig.json" || base === "jsconfig.json" ? base : undefined; + } + export function insertSorted(array: SortedArray, insert: T, compare: Comparer): void { if (array.length === 0) { array.push(insert); From 8db05c2d854faa7e92a655084281d39798ba88fd Mon Sep 17 00:00:00 2001 From: Sheetal Nandi Date: Fri, 11 Aug 2017 16:37:47 -0700 Subject: [PATCH 057/109] More work on PR feedback update --- src/compiler/commandLineParser.ts | 7 ++-- src/compiler/utilities.ts | 4 --- .../unittests/tsserverProjectSystem.ts | 2 +- src/server/editorServices.ts | 34 +++++++++---------- src/server/project.ts | 13 ++++--- src/server/utilities.ts | 21 +++++++----- 6 files changed, 43 insertions(+), 38 deletions(-) diff --git a/src/compiler/commandLineParser.ts b/src/compiler/commandLineParser.ts index 21cf4275cca10..fb92f59d12dae 100644 --- a/src/compiler/commandLineParser.ts +++ b/src/compiler/commandLineParser.ts @@ -1482,6 +1482,7 @@ namespace ts { return error.code === Diagnostics.No_inputs_were_found_in_config_file_0_Specified_include_paths_were_1_and_exclude_paths_were_2.code; } + /*@internal*/ export function getErrorForNoInputFiles({ includeSpecs, excludeSpecs }: ConfigFileSpecs, configFileName: string | undefined) { return createCompilerDiagnostic( Diagnostics.No_inputs_were_found_in_config_file_0_Specified_include_paths_were_1_and_exclude_paths_were_2, @@ -1928,9 +1929,9 @@ namespace ts { /** * Expands an array of file specifications. * - * @param fileNames The literal file names to include. - * @param include The wildcard file specifications to include. - * @param exclude The wildcard file specifications to exclude. + * @param filesSpecs The literal file names to include. + * @param includeSpecs The wildcard file specifications to include. + * @param excludeSpecs The wildcard file specifications to exclude. * @param basePath The base path for any relative file specifications. * @param options Compiler options. * @param host The host used to resolve files and directories. diff --git a/src/compiler/utilities.ts b/src/compiler/utilities.ts index 14cab78bc84ce..76d242e8b08d8 100644 --- a/src/compiler/utilities.ts +++ b/src/compiler/utilities.ts @@ -3607,10 +3607,6 @@ namespace ts { export function getCombinedLocalAndExportSymbolFlags(symbol: Symbol): SymbolFlags { return symbol.exportSymbol ? symbol.exportSymbol.flags | symbol.flags : symbol.flags; } - - export function isRecursiveDirectoryWatch(flags: WatchDirectoryFlags) { - return (flags & WatchDirectoryFlags.Recursive) !== 0; - } } namespace ts { diff --git a/src/harness/unittests/tsserverProjectSystem.ts b/src/harness/unittests/tsserverProjectSystem.ts index deaa17c2afedb..f4513cf71132f 100644 --- a/src/harness/unittests/tsserverProjectSystem.ts +++ b/src/harness/unittests/tsserverProjectSystem.ts @@ -534,7 +534,7 @@ namespace ts.projectSystem { } /** - * This will call the directory watcher for the foldeFullPath and recursive directory watchers for this and base folders + * This will call the directory watcher for the folderFullPath and recursive directory watchers for this and base folders */ private invokeDirectoryWatcher(folderFullPath: string, fileName: string) { const relativePath = this.getRelativePathToDirectory(folderFullPath, fileName); diff --git a/src/server/editorServices.ts b/src/server/editorServices.ts index 27dbee2ebf580..bf480e355fe5e 100644 --- a/src/server/editorServices.ts +++ b/src/server/editorServices.ts @@ -246,7 +246,7 @@ namespace ts.server { export const enum WatchType { ConfigFilePath = "Config file for the program", MissingFilePath = "Missing file from program", - WildCardDirectories = "Wild card directory", + WildcardDirectories = "Wild card directory", TypeRoot = "Type root of the project", ClosedScriptInfo = "Closed Script info", ConfigFileForInferredRoot = "Config file for the inferred project root" @@ -363,7 +363,7 @@ namespace ts.server { * - Or it is present if we have configured project open with config file at that location * In this case the exists property is always true */ - private readonly mapOfConfigFileExistenceInfo = createMap(); + private readonly configFileExistenceInfoCache = createMap(); private readonly throttledOperations: ThrottledOperations; private readonly hostConfiguration: HostConfiguration; @@ -693,7 +693,7 @@ namespace ts.server { } private onConfigChangedForConfiguredProject(project: ConfiguredProject, eventKind: FileWatcherEventKind) { - const configFileExistenceInfo = this.mapOfConfigFileExistenceInfo.get(project.canonicalConfigFilePath); + const configFileExistenceInfo = this.configFileExistenceInfoCache.get(project.canonicalConfigFilePath); if (eventKind === FileWatcherEventKind.Deleted) { // Update the cached status // We arent updating or removing the cached config file presence info as that will be taken care of by @@ -725,7 +725,7 @@ namespace ts.server { private onConfigFileChangeForOpenScriptInfo(configFileName: NormalizedPath, eventKind: FileWatcherEventKind) { // This callback is called only if we dont have config file project for this config file const canonicalConfigPath = normalizedPathToPath(configFileName, this.currentDirectory, this.toCanonicalFileName); - const configFileExistenceInfo = this.mapOfConfigFileExistenceInfo.get(canonicalConfigPath); + const configFileExistenceInfo = this.configFileExistenceInfoCache.get(canonicalConfigPath); configFileExistenceInfo.exists = (eventKind !== FileWatcherEventKind.Deleted); this.logConfigFileWatchUpdate(configFileName, canonicalConfigPath, configFileExistenceInfo, ConfigFileWatcherStatus.ReloadingFiles); @@ -852,7 +852,7 @@ namespace ts.server { } private configFileExists(configFileName: NormalizedPath, canonicalConfigFilePath: string, info: ScriptInfo) { - let configFileExistenceInfo = this.mapOfConfigFileExistenceInfo.get(canonicalConfigFilePath); + let configFileExistenceInfo = this.configFileExistenceInfoCache.get(canonicalConfigFilePath); if (configFileExistenceInfo) { // By default the info would get impacted by presence of config file since its in the detection path // Only adding the info as a root to inferred project will need the existence to be watched by file watcher @@ -877,13 +877,13 @@ namespace ts.server { openFilesImpactedByConfigFile.set(info.path, false); const exists = this.host.fileExists(configFileName); configFileExistenceInfo = { exists, openFilesImpactedByConfigFile }; - this.mapOfConfigFileExistenceInfo.set(canonicalConfigFilePath, configFileExistenceInfo); + this.configFileExistenceInfoCache.set(canonicalConfigFilePath, configFileExistenceInfo); this.logConfigFileWatchUpdate(configFileName, canonicalConfigFilePath, configFileExistenceInfo, ConfigFileWatcherStatus.OpenFilesImpactedByConfigFileAdd); return exists; } private setConfigFileExistenceByNewConfiguredProject(project: ConfiguredProject) { - const configFileExistenceInfo = this.mapOfConfigFileExistenceInfo.get(project.canonicalConfigFilePath); + const configFileExistenceInfo = this.configFileExistenceInfoCache.get(project.canonicalConfigFilePath); if (configFileExistenceInfo) { Debug.assert(configFileExistenceInfo.exists); // close existing watcher @@ -900,7 +900,7 @@ namespace ts.server { else { // We could be in this scenario if project is the configured project tracked by external project // Since that route doesnt check if the config file is present or not - this.mapOfConfigFileExistenceInfo.set(project.canonicalConfigFilePath, { + this.configFileExistenceInfoCache.set(project.canonicalConfigFilePath, { exists: true, openFilesImpactedByConfigFile: createMap() }); @@ -915,7 +915,7 @@ namespace ts.server { } private setConfigFileExistenceInfoByClosedConfiguredProject(closedProject: ConfiguredProject) { - const configFileExistenceInfo = this.mapOfConfigFileExistenceInfo.get(closedProject.canonicalConfigFilePath); + const configFileExistenceInfo = this.configFileExistenceInfoCache.get(closedProject.canonicalConfigFilePath); Debug.assert(!!configFileExistenceInfo); if (configFileExistenceInfo.openFilesImpactedByConfigFile.size) { const configFileName = closedProject.getConfigFilePath(); @@ -933,7 +933,7 @@ namespace ts.server { } else { // There is not a single file open thats tracking the status of this config file. Remove from cache - this.mapOfConfigFileExistenceInfo.delete(closedProject.canonicalConfigFilePath); + this.configFileExistenceInfoCache.delete(closedProject.canonicalConfigFilePath); } } @@ -983,7 +983,7 @@ namespace ts.server { private stopWatchingConfigFilesForClosedScriptInfo(info: ScriptInfo) { Debug.assert(!info.isScriptOpen()); this.forEachConfigFileLocation(info, (configFileName, canonicalConfigFilePath) => { - const configFileExistenceInfo = this.mapOfConfigFileExistenceInfo.get(canonicalConfigFilePath); + const configFileExistenceInfo = this.configFileExistenceInfoCache.get(canonicalConfigFilePath); if (configFileExistenceInfo) { const infoIsRootOfInferredProject = configFileExistenceInfo.openFilesImpactedByConfigFile.get(info.path); @@ -1006,7 +1006,7 @@ namespace ts.server { if (!configFileExistenceInfo.openFilesImpactedByConfigFile.size && !this.getConfiguredProjectByCanonicalConfigFilePath(canonicalConfigFilePath)) { Debug.assert(!configFileExistenceInfo.configFileWatcherForRootOfInferredProject); - this.mapOfConfigFileExistenceInfo.delete(canonicalConfigFilePath); + this.configFileExistenceInfoCache.delete(canonicalConfigFilePath); } } }); @@ -1019,14 +1019,14 @@ namespace ts.server { startWatchingConfigFilesForInferredProjectRoot(info: ScriptInfo) { Debug.assert(info.isScriptOpen()); this.forEachConfigFileLocation(info, (configFileName, canonicalConfigFilePath) => { - let configFilePresenceInfo = this.mapOfConfigFileExistenceInfo.get(canonicalConfigFilePath); + let configFilePresenceInfo = this.configFileExistenceInfoCache.get(canonicalConfigFilePath); if (!configFilePresenceInfo) { // Create the cache configFilePresenceInfo = { exists: this.host.fileExists(configFileName), openFilesImpactedByConfigFile: createMap() }; - this.mapOfConfigFileExistenceInfo.set(canonicalConfigFilePath, configFilePresenceInfo); + this.configFileExistenceInfoCache.set(canonicalConfigFilePath, configFilePresenceInfo); } // Set this file as the root of inferred project @@ -1050,7 +1050,7 @@ namespace ts.server { /* @internal */ stopWatchingConfigFilesForInferredProjectRoot(info: ScriptInfo, reason: WatcherCloseReason) { this.forEachConfigFileLocation(info, (configFileName, canonicalConfigFilePath) => { - const configFileExistenceInfo = this.mapOfConfigFileExistenceInfo.get(canonicalConfigFilePath); + const configFileExistenceInfo = this.configFileExistenceInfoCache.get(canonicalConfigFilePath); if (configFileExistenceInfo && configFileExistenceInfo.openFilesImpactedByConfigFile.has(info.path)) { Debug.assert(info.isScriptOpen()); @@ -1607,14 +1607,14 @@ namespace ts.server { /* @internal */ closeDirectoryWatcher(watchType: WatchType, project: Project, directory: string, watcher: FileWatcher, flags: WatchDirectoryFlags, reason: WatcherCloseReason) { - const recursive = isRecursiveDirectoryWatch(flags); + const recursive = (flags & WatchDirectoryFlags.Recursive) !== 0; this.logger.info(`DirectoryWatcher ${recursive ? "recursive" : ""}:: Close: ${directory} Project: ${project.getProjectName()} WatchType: ${watchType} Reason: ${reason}`); watcher.close(); } /* @internal */ addDirectoryWatcher(watchType: WatchType, project: Project, directory: string, cb: ServerDirectoryWatcherCallback, flags: WatchDirectoryFlags) { - const recursive = isRecursiveDirectoryWatch(flags); + const recursive = (flags & WatchDirectoryFlags.Recursive) !== 0; this.logger.info(`DirectoryWatcher ${recursive ? "recursive" : ""}:: Added: ${directory} Project: ${project.getProjectName()} WatchType: ${watchType}`); return this.host.watchDirectory(directory, fileName => { const path = toNormalizedPath(getNormalizedAbsolutePath(fileName, directory)); diff --git a/src/server/project.ts b/src/server/project.ts index a8506982bec8e..e2b536017c525 100644 --- a/src/server/project.ts +++ b/src/server/project.ts @@ -1153,22 +1153,25 @@ namespace ts.server { createNewValue: (directory, flags) => { return { watcher: this.projectService.addDirectoryWatcher( - WatchType.WildCardDirectories, this, directory, + WatchType.WildcardDirectories, this, directory, path => this.projectService.onFileAddOrRemoveInWatchedDirectoryOfProject(this, path), flags ), flags }; }, - // Close existing watch thats not needed any more or doesnt match recursive flags - onDeleteExistingValue: (directory, wildcardDirectoryWatcher, isNotSame) => - this.closeWildcardDirectoryWatcher(directory, wildcardDirectoryWatcher, isNotSame ? WatcherCloseReason.RecursiveChanged : WatcherCloseReason.NotNeeded), + // Close existing watch thats not needed any more + onDeleteExistingValue: (directory, wildcardDirectoryWatcher) => + this.closeWildcardDirectoryWatcher(directory, wildcardDirectoryWatcher, WatcherCloseReason.NotNeeded), + // Close existing watch that doesnt match in the recursive flags + onDeleteExistingMismatchValue: (directory, wildcardDirectoryWatcher) => + this.closeWildcardDirectoryWatcher(directory, wildcardDirectoryWatcher, WatcherCloseReason.RecursiveChanged), } ); } private closeWildcardDirectoryWatcher(directory: string, { watcher, flags }: WildcardDirectoryWatcher, closeReason: WatcherCloseReason) { - this.projectService.closeDirectoryWatcher(WatchType.WildCardDirectories, this, directory, watcher, flags, closeReason); + this.projectService.closeDirectoryWatcher(WatchType.WildcardDirectories, this, directory, watcher, flags, closeReason); } stopWatchingWildCards(reason: WatcherCloseReason) { diff --git a/src/server/utilities.ts b/src/server/utilities.ts index 79065f48ff5d0..0699b4c9b6ce8 100644 --- a/src/server/utilities.ts +++ b/src/server/utilities.ts @@ -299,14 +299,14 @@ namespace ts.server { } /** - * clears already present map (!== undefined) by calling onDeleteExistingValue callback before deleting that key/value + * clears already present map by calling onDeleteExistingValue callback before deleting that key/value */ export function clearMap(map: Map, onDeleteExistingValue: (key: string, existingValue: T) => void) { // Remove all map.forEach((existingValue, key) => { - map.delete(key); onDeleteExistingValue(key, existingValue); }); + map.clear(); } export interface MutateMapOptions { @@ -314,6 +314,7 @@ namespace ts.server { onDeleteExistingValue(key: string, existingValue: T, isNotSame?: boolean): void; isSameValue?(existingValue: T, valueInNewMap: U): boolean; + onDeleteExistingMismatchValue?(key: string, existingValue: T): void; } /** @@ -322,16 +323,20 @@ namespace ts.server { export function mutateMap(map: Map, newMap: ReadonlyMap, options: MutateMapOptions) { // If there are new values update them if (newMap) { - const { isSameValue, createNewValue, onDeleteExistingValue } = options; + const { isSameValue, createNewValue, onDeleteExistingValue, onDeleteExistingMismatchValue } = options; // Needs update map.forEach((existingValue, key) => { const valueInNewMap = newMap.get(key); - if (valueInNewMap === undefined || - // different value - remove it - (isSameValue && !isSameValue(existingValue, valueInNewMap))) { - const isNotSame = valueInNewMap !== undefined; + // Not present any more in new map, remove it + if (valueInNewMap === undefined) { map.delete(key); - onDeleteExistingValue(key, existingValue, isNotSame); + onDeleteExistingValue(key, existingValue); + } + // different value - remove it + else if (isSameValue && !isSameValue(existingValue, valueInNewMap)) { + Debug.assert(!!onDeleteExistingMismatchValue); + map.delete(key); + onDeleteExistingMismatchValue(key, existingValue); } }); From 59d07dc4888da2e7ba9c1d9e8529b9141d552473 Mon Sep 17 00:00:00 2001 From: Sheetal Nandi Date: Mon, 14 Aug 2017 11:27:02 -0700 Subject: [PATCH 058/109] Simplified mutate map options --- src/compiler/builder.ts | 6 +++--- src/compiler/program.ts | 38 ++++++++++++++++++++++---------------- src/compiler/utilities.ts | 31 ++++++++++++------------------- src/server/project.ts | 2 +- 4 files changed, 38 insertions(+), 39 deletions(-) diff --git a/src/compiler/builder.ts b/src/compiler/builder.ts index 19a18f7b9c1e4..849732f0afbf7 100644 --- a/src/compiler/builder.ts +++ b/src/compiler/builder.ts @@ -104,9 +104,9 @@ namespace ts { // Add new file info createNewValue: (_path, sourceFile) => addNewFileInfo(program, sourceFile), // Remove existing file info - onDeleteExistingValue: removeExistingFileInfo, + onDeleteValue: removeExistingFileInfo, // We will update in place instead of deleting existing value and adding new one - onExistingValue: (existingInfo, sourceFile) => updateExistingFileInfo(program, existingInfo, sourceFile, hasInvalidatedResolution) + onExistingValue: (_key, existingInfo, sourceFile) => updateExistingFileInfo(program, existingInfo, sourceFile, hasInvalidatedResolution) } ); } @@ -396,7 +396,7 @@ namespace ts { createNewValue: (key): true => { referencedBy.add(key, path); return true; }, // Remove existing reference by entry: source file doesnt reference file 'key' any more // in other words source file (path) is not referenced by 'key' - onDeleteExistingValue: (key, _existingValue) => { referencedBy.remove(key, path); } + onDeleteValue: (key, _existingValue) => { referencedBy.remove(key, path); } } ); } diff --git a/src/compiler/program.ts b/src/compiler/program.ts index 142d6351a461e..c642d99ff9d41 100644 --- a/src/compiler/program.ts +++ b/src/compiler/program.ts @@ -473,7 +473,7 @@ namespace ts { createNewValue: createMissingFileWatch, // Files that are no longer missing (e.g. because they are no longer required) // should no longer be watched. - onDeleteExistingValue: closeExistingMissingFilePathFileWatcher + onDeleteValue: closeExistingMissingFilePathFileWatcher } ); } @@ -497,26 +497,32 @@ namespace ts { wildcardDirectories, { // Create new watch and recursive info - createNewValue: (directory, flags) => { - return { - watcher: watchDirectory(directory, flags), - flags - }; - }, + createNewValue: createWildcardDirectoryWatcher, // Close existing watch thats not needed any more - onDeleteExistingValue: (directory, wildcardDirectoryWatcher) => + onDeleteValue: (directory, wildcardDirectoryWatcher) => closeDirectoryWatcher(directory, wildcardDirectoryWatcher, /*flagsChanged*/ false), // Close existing watch that doesnt match in the flags - shouldDeleteExistingValue: (directory, wildcardDirectoryWatcher, flags) => { - // Watcher is same if the recursive flags match - if (wildcardDirectoryWatcher.flags === flags) { - return false; - } - closeDirectoryWatcher(directory, wildcardDirectoryWatcher, /*flagsChanged*/ true); - return true; - } + onExistingValue: updateWildcardDirectoryWatcher } ); + + function createWildcardDirectoryWatcher(directory: string, flags: WatchDirectoryFlags): WildcardDirectoryWatchers { + // Create new watch and recursive info + return { + watcher: watchDirectory(directory, flags), + flags + }; + } + + function updateWildcardDirectoryWatcher(directory: string, wildcardDirectoryWatcher: WildcardDirectoryWatchers, flags: WatchDirectoryFlags) { + // Watcher needs to be updated if the recursive flags dont match + if (wildcardDirectoryWatcher.flags === flags) { + return; + } + + closeDirectoryWatcher(directory, wildcardDirectoryWatcher, /*flagsChanged*/ true); + existingWatchedForWildcards.set(directory, createWildcardDirectoryWatcher(directory, flags)); + } } /** diff --git a/src/compiler/utilities.ts b/src/compiler/utilities.ts index 3f0fae850d691..6e26a287b8384 100644 --- a/src/compiler/utilities.ts +++ b/src/compiler/utilities.ts @@ -3491,27 +3491,25 @@ namespace ts { /** * clears already present map by calling onDeleteExistingValue callback before deleting that key/value */ - export function clearMap(map: Map, onDeleteExistingValue: (key: string, existingValue: T) => void) { + export function clearMap(map: Map, onDeleteValue: (key: string, existingValue: T) => void) { // Remove all map.forEach((existingValue, key) => { - onDeleteExistingValue(key, existingValue); + onDeleteValue(key, existingValue); }); map.clear(); } export interface MutateMapOptions { createNewValue(key: string, valueInNewMap: U): T; - onDeleteExistingValue(key: string, existingValue: T, isNotSame?: boolean): void; + onDeleteValue(key: string, existingValue: T): void; /** - * If present this is called if there is value for the key in new map and existing map + * If present this is called with the key when there is value for that key both in new map as well as existing map provided + * Caller can then decide to update or remove this key. + * If the key is removed, caller will get callback of createNewValue for that key. + * If this callback is not provided, the value of such keys is not updated. */ - onExistingValue?(existingValue: T, valueInNewMap: U): void; - /** - * If there onExistingValue is not provided, this callback if present will be called to - * detemine if the value in the map needs to be deleted - */ - shouldDeleteExistingValue?(key: string, existingValue: T, valueInNewMap: U): boolean; + onExistingValue?(key: string, existingValue: T, valueInNewMap: U): void; } /** @@ -3520,23 +3518,18 @@ namespace ts { export function mutateMap(map: Map, newMap: ReadonlyMap, options: MutateMapOptions) { // If there are new values update them if (newMap) { - const { createNewValue, onDeleteExistingValue, onExistingValue, shouldDeleteExistingValue } = options; + const { createNewValue, onDeleteValue, onExistingValue } = options; // Needs update map.forEach((existingValue, key) => { const valueInNewMap = newMap.get(key); // Not present any more in new map, remove it if (valueInNewMap === undefined) { map.delete(key); - onDeleteExistingValue(key, existingValue); + onDeleteValue(key, existingValue); } // If present notify about existing values else if (onExistingValue) { - onExistingValue(existingValue, valueInNewMap); - } - // different value, delete it here if this value cant be kept around - // Note that if the value is deleted here, new value will be created in newMap.forEach loop for this key - else if (shouldDeleteExistingValue && !shouldDeleteExistingValue(key, existingValue, valueInNewMap)) { - map.delete(key); + onExistingValue(key, existingValue, valueInNewMap); } }); @@ -3549,7 +3542,7 @@ namespace ts { }); } else { - clearMap(map, options.onDeleteExistingValue); + clearMap(map, options.onDeleteValue); } } } diff --git a/src/server/project.ts b/src/server/project.ts index 9984788403b71..2233ef1e94184 100644 --- a/src/server/project.ts +++ b/src/server/project.ts @@ -1231,7 +1231,7 @@ namespace ts.server { path => this.projectService.onTypeRootFileChanged(this, path), WatchDirectoryFlags.None ), // Close existing watch thats not needed any more - onDeleteExistingValue: (directory, watcher) => this.projectService.closeDirectoryWatcher( + onDeleteValue: (directory, watcher) => this.projectService.closeDirectoryWatcher( WatchType.TypeRoot, this, directory, watcher, WatchDirectoryFlags.None, WatcherCloseReason.NotNeeded ) } From 989508245b4dc0030f16981b038682fb60df0334 Mon Sep 17 00:00:00 2001 From: Sheetal Nandi Date: Mon, 14 Aug 2017 12:52:29 -0700 Subject: [PATCH 059/109] Updating according to feedback from PR --- src/compiler/builder.ts | 6 +++--- src/compiler/commandLineParser.ts | 10 ++++----- src/compiler/core.ts | 35 ++++++++++--------------------- src/compiler/program.ts | 2 +- src/compiler/sys.ts | 19 ++++++++++------- src/compiler/watchedProgram.ts | 2 +- src/services/services.ts | 2 +- 7 files changed, 32 insertions(+), 44 deletions(-) diff --git a/src/compiler/builder.ts b/src/compiler/builder.ts index 849732f0afbf7..04d520cb2286c 100644 --- a/src/compiler/builder.ts +++ b/src/compiler/builder.ts @@ -60,7 +60,7 @@ namespace ts { function writeFile(fileName: string, text: string, writeByteOrderMark: boolean, _onError: (message: string) => void, sourceFiles: SourceFile[]) { outputFiles.push({ name: fileName, writeByteOrderMark, text }); if (isDetailed) { - emittedSourceFiles = concatenate(emittedSourceFiles, sourceFiles); + emittedSourceFiles = addRange(emittedSourceFiles, sourceFiles); } } } @@ -139,7 +139,7 @@ namespace ts { function ensureProgramGraph(program: Program) { if (!emitHandler) { - createProgramGraph(program, noop); + createProgramGraph(program, returnFalse); } } @@ -457,7 +457,7 @@ namespace ts { } // Return array of values that needs emit - return flatMapIter(seenFileNamesMap.values()); + return arrayFrom(seenFileNamesMap.values()); } } } diff --git a/src/compiler/commandLineParser.ts b/src/compiler/commandLineParser.ts index 3748dd9b9a440..ac8f1ee8f9796 100644 --- a/src/compiler/commandLineParser.ts +++ b/src/compiler/commandLineParser.ts @@ -1975,15 +1975,13 @@ namespace ts { } /** - * Expands an array of file specifications. - * - * @param fileNames The literal file names to include. - * @param include The wildcard file specifications to include. - * @param exclude The wildcard file specifications to exclude. + * Gets the file names from the provided config file specs that contain, files, include, exclude and + * other properties needed to resolve the file names + * @param spec The config file specs extracted with file names to include, wildcards to include/exclude and other details * @param basePath The base path for any relative file specifications. * @param options Compiler options. * @param host The host used to resolve files and directories. - * @param errors An array for diagnostic reporting. + * @param extraFileExtensions optionaly file extra file extension information from host */ export function getFileNamesFromConfigSpecs(spec: ConfigFileSpecs, basePath: string, options: CompilerOptions, host: ParseConfigHost, extraFileExtensions: ReadonlyArray = []): ExpandResult { basePath = normalizePath(basePath); diff --git a/src/compiler/core.ts b/src/compiler/core.ts index c163de2446615..751177c4ef85b 100644 --- a/src/compiler/core.ts +++ b/src/compiler/core.ts @@ -459,14 +459,12 @@ namespace ts { return result; } - export function flatMapIter(iter: Iterator): T[]; - export function flatMapIter(iter: Iterator, mapfn: (x: T) => U | U[] | undefined): U[]; - export function flatMapIter(iter: Iterator, mapfn?: (x: any) => any): any[] { - const result = []; + export function flatMapIter(iter: Iterator, mapfn: (x: T) => U | U[] | undefined): U[] { + const result: U[] = []; while (true) { const { value, done } = iter.next(); if (done) break; - const res = mapfn ? mapfn(value) : value; + const res = mapfn(value); if (res) { if (isArray(res)) { result.push(...res); @@ -1221,7 +1219,10 @@ namespace ts { } /** Does nothing. */ - export function noop(): any {} + export function noop(): void { } + + /** Do nothing and return false */ + export function returnFalse(): false { return false; } /** Throws an error because a function is not implemented. */ export function notImplemented(): never { @@ -2626,25 +2627,11 @@ namespace ts { return sourceFile.checkJsDirective ? sourceFile.checkJsDirective.enabled : compilerOptions.checkJs; } - export interface HostForCaching { + export interface HostForCaching extends PartialSystem { useCaseSensitiveFileNames: boolean; - writeFile(path: string, data: string, writeByteOrderMark?: boolean): void; - fileExists(path: string): boolean; - directoryExists(path: string): boolean; - createDirectory(path: string): void; - getCurrentDirectory(): string; - getDirectories(path: string): string[]; - readDirectory(path: string, extensions?: ReadonlyArray, exclude?: ReadonlyArray, include?: ReadonlyArray, depth?: number): string[]; - } - - export interface CachedHost { - writeFile(path: string, data: string, writeByteOrderMark?: boolean): void; - fileExists(path: string): boolean; - directoryExists(path: string): boolean; - createDirectory(path: string): void; - getCurrentDirectory(): string; - getDirectories(path: string): string[]; - readDirectory(path: string, extensions?: ReadonlyArray, exclude?: ReadonlyArray, include?: ReadonlyArray, depth?: number): string[]; + } + + export interface CachedHost extends PartialSystem { addOrDeleteFileOrFolder(fileOrFolder: string): void; clearCache(): void; } diff --git a/src/compiler/program.ts b/src/compiler/program.ts index c642d99ff9d41..2dd3e465268e4 100644 --- a/src/compiler/program.ts +++ b/src/compiler/program.ts @@ -585,7 +585,7 @@ namespace ts { let moduleResolutionCache: ModuleResolutionCache; let resolveModuleNamesWorker: (moduleNames: string[], containingFile: string) => ResolvedModuleFull[]; - const hasInvalidatedResolution = host.hasInvalidatedResolution || noop; + const hasInvalidatedResolution = host.hasInvalidatedResolution || returnFalse; 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. diff --git a/src/compiler/sys.ts b/src/compiler/sys.ts index 193997a8a86a9..aafb97f4131f0 100644 --- a/src/compiler/sys.ts +++ b/src/compiler/sys.ts @@ -30,14 +30,23 @@ namespace ts { mtime?: Date; } - export interface System { + export interface PartialSystem { + writeFile(path: string, data: string, writeByteOrderMark?: boolean): void; + fileExists(path: string): boolean; + directoryExists(path: string): boolean; + createDirectory(path: string): void; + getCurrentDirectory(): string; + getDirectories(path: string): string[]; + readDirectory(path: string, extensions?: ReadonlyArray, exclude?: ReadonlyArray, include?: ReadonlyArray, depth?: number): string[]; + } + + export interface System extends PartialSystem { args: string[]; newLine: string; useCaseSensitiveFileNames: boolean; write(s: string): void; readFile(path: string, encoding?: string): string | undefined; getFileSize?(path: string): number; - writeFile(path: string, data: string, writeByteOrderMark?: boolean): void; /** * @pollingInterval - this parameter is used in polling-based watchers and ignored in watchers that * use native OS file watching @@ -45,13 +54,7 @@ namespace ts { watchFile?(path: string, callback: FileWatcherCallback, pollingInterval?: number): FileWatcher; watchDirectory?(path: string, callback: DirectoryWatcherCallback, recursive?: boolean): FileWatcher; resolvePath(path: string): string; - fileExists(path: string): boolean; - directoryExists(path: string): boolean; - createDirectory(path: string): void; getExecutingFilePath(): string; - getCurrentDirectory(): string; - getDirectories(path: string): string[]; - readDirectory(path: string, extensions?: ReadonlyArray, exclude?: ReadonlyArray, include?: ReadonlyArray, depth?: number): string[]; getModifiedTime?(path: string): Date; /** * This should be cryptographically secure. diff --git a/src/compiler/watchedProgram.ts b/src/compiler/watchedProgram.ts index ca0292fa53321..5229e11ea9757 100644 --- a/src/compiler/watchedProgram.ts +++ b/src/compiler/watchedProgram.ts @@ -630,7 +630,7 @@ namespace ts { // Reload is pending, do the reload if (!needsReload) { - const result = getFileNamesFromConfigSpecs(configFileSpecs, getDirectoryPath(configFileName), compilerOptions, host, /*extraFileExtension*/ []); + const result = getFileNamesFromConfigSpecs(configFileSpecs, getDirectoryPath(configFileName), compilerOptions, host); if (!configFileSpecs.filesSpecs) { reportDiagnostic(getErrorForNoInputFiles(configFileSpecs, configFileName)); } diff --git a/src/services/services.ts b/src/services/services.ts index ba1c48b903786..b06f92344d3f1 100644 --- a/src/services/services.ts +++ b/src/services/services.ts @@ -1116,7 +1116,7 @@ namespace ts { let hostCache = new HostCache(host, getCanonicalFileName); const rootFileNames = hostCache.getRootFileNames(); - const hasInvalidatedResolution: HasInvalidatedResolution = host.hasInvalidatedResolution || noop; + const hasInvalidatedResolution: HasInvalidatedResolution = host.hasInvalidatedResolution || returnFalse; // If the program is already up-to-date, we can reuse it if (isProgramUptoDate(program, rootFileNames, hostCache.compilationSettings(), path => hostCache.getVersion(path), fileExists, hasInvalidatedResolution)) { From f1b1b12604dad4af982a1d76181dcb20bef1cb73 Mon Sep 17 00:00:00 2001 From: Sheetal Nandi Date: Mon, 14 Aug 2017 14:59:51 -0700 Subject: [PATCH 060/109] More work based on feedback --- src/compiler/core.ts | 118 ++++++++++++++++++++++---------- src/compiler/resolutionCache.ts | 4 +- src/compiler/watchedProgram.ts | 67 +++++++++--------- src/server/editorServices.ts | 12 ++-- src/server/lsHost.ts | 26 ++++--- src/server/project.ts | 20 +++--- src/server/scriptInfo.ts | 2 +- 7 files changed, 147 insertions(+), 102 deletions(-) diff --git a/src/compiler/core.ts b/src/compiler/core.ts index 751177c4ef85b..1eab3582cb974 100644 --- a/src/compiler/core.ts +++ b/src/compiler/core.ts @@ -2631,12 +2631,16 @@ namespace ts { useCaseSensitiveFileNames: boolean; } - export interface CachedHost extends PartialSystem { - addOrDeleteFileOrFolder(fileOrFolder: string): void; + export interface CachedHost { + addOrDeleteFileOrFolder(fileOrFolder: string, fileOrFolderPath: Path): void; + addOrDeleteFile(fileName: string, filePath: Path, eventKind: FileWatcherEventKind): void; clearCache(): void; } - export function createCachedHost(host: HostForCaching): CachedHost { + export interface CachedPartialSystem extends PartialSystem, CachedHost { + } + + export function createCachedPartialSystem(host: HostForCaching): CachedPartialSystem { const cachedReadDirectoryResult = createMap(); const getCurrentDirectory = memoize(() => host.getCurrentDirectory()); const getCanonicalFileName = createGetCanonicalFileName(host.useCaseSensitiveFileNames); @@ -2649,6 +2653,7 @@ namespace ts { getDirectories, readDirectory, addOrDeleteFileOrFolder, + addOrDeleteFile, clearCache }; @@ -2656,33 +2661,46 @@ namespace ts { return ts.toPath(fileName, getCurrentDirectory(), getCanonicalFileName); } - function getFileSystemEntries(rootDir: string) { - const path = toPath(rootDir); - const cachedResult = cachedReadDirectoryResult.get(path); - if (cachedResult) { - return cachedResult; - } + function getCachedFileSystemEntries(rootDirPath: Path): FileSystemEntries | undefined { + return cachedReadDirectoryResult.get(rootDirPath); + } + + function getCachedFileSystemEntriesForBaseDir(path: Path): FileSystemEntries | undefined { + return getCachedFileSystemEntries(getDirectoryPath(path)); + } + function getBaseNameOfFileName(fileName: string) { + return getBaseFileName(normalizePath(fileName)); + } + + function createCachedFileSystemEntries(rootDir: string, rootDirPath: Path) { const resultFromHost: FileSystemEntries = { files: host.readDirectory(rootDir, /*extensions*/ undefined, /*exclude*/ undefined, /*include*/["*.*"]) || [], directories: host.getDirectories(rootDir) || [] }; - cachedReadDirectoryResult.set(path, resultFromHost); + cachedReadDirectoryResult.set(rootDirPath, resultFromHost); return resultFromHost; } - function canWorkWithCacheForDir(rootDir: string) { - // Some of the hosts might not be able to handle read directory or getDirectories - const path = toPath(rootDir); - if (cachedReadDirectoryResult.get(path)) { - return true; + /** + * If the readDirectory result was already cached, it returns that + * Otherwise gets result from host and caches it. + * The host request is done under try catch block to avoid caching incorrect result + */ + function tryReadDirectory(rootDir: string, rootDirPath: Path): FileSystemEntries | undefined { + const cachedResult = getCachedFileSystemEntries(rootDirPath); + if (cachedResult) { + return cachedResult; } + try { - return getFileSystemEntries(rootDir); + return createCachedFileSystemEntries(rootDir, rootDirPath); } catch (_e) { - return false; + // If there is exception to read directories, dont cache the result and direct the calls to host + Debug.assert(!cachedReadDirectoryResult.has(rootDirPath)); + return undefined; } } @@ -2708,19 +2726,18 @@ namespace ts { function writeFile(fileName: string, data: string, writeByteOrderMark?: boolean): void { const path = toPath(fileName); - const result = cachedReadDirectoryResult.get(getDirectoryPath(path)); - const baseFileName = getBaseFileName(normalizePath(fileName)); + const result = getCachedFileSystemEntriesForBaseDir(path); if (result) { - result.files = updateFileSystemEntry(result.files, baseFileName, /*isValid*/ true); + updateFilesOfFileSystemEntry(result, getBaseNameOfFileName(fileName), /*fileExists*/ true); } return host.writeFile(fileName, data, writeByteOrderMark); } function fileExists(fileName: string): boolean { const path = toPath(fileName); - const result = cachedReadDirectoryResult.get(getDirectoryPath(path)); - const baseName = getBaseFileName(normalizePath(fileName)); - return (result && hasEntry(result.files, baseName)) || host.fileExists(fileName); + const result = getCachedFileSystemEntriesForBaseDir(path); + return (result && hasEntry(result.files, getBaseNameOfFileName(fileName))) || + host.fileExists(fileName); } function directoryExists(dirPath: string): boolean { @@ -2730,8 +2747,8 @@ namespace ts { function createDirectory(dirPath: string) { const path = toPath(dirPath); - const result = cachedReadDirectoryResult.get(getDirectoryPath(path)); - const baseFileName = getBaseFileName(path); + const result = getCachedFileSystemEntriesForBaseDir(path); + const baseFileName = getBaseNameOfFileName(dirPath); if (result) { result.directories = updateFileSystemEntry(result.directories, baseFileName, /*isValid*/ true); } @@ -2739,41 +2756,68 @@ namespace ts { } function getDirectories(rootDir: string): string[] { - if (canWorkWithCacheForDir(rootDir)) { - return getFileSystemEntries(rootDir).directories.slice(); + const rootDirPath = toPath(rootDir); + const result = tryReadDirectory(rootDir, rootDirPath); + if (result) { + return result.directories.slice(); } return host.getDirectories(rootDir); } + function readDirectory(rootDir: string, extensions?: ReadonlyArray, excludes?: ReadonlyArray, includes?: ReadonlyArray, depth?: number): string[] { - if (canWorkWithCacheForDir(rootDir)) { - return matchFiles(rootDir, extensions, excludes, includes, host.useCaseSensitiveFileNames, getCurrentDirectory(), depth, path => getFileSystemEntries(path)); + const rootDirPath = toPath(rootDir); + const result = tryReadDirectory(rootDir, rootDirPath); + if (result) { + return matchFiles(rootDir, extensions, excludes, includes, host.useCaseSensitiveFileNames, getCurrentDirectory(), depth, getFileSystemEntries); } return host.readDirectory(rootDir, extensions, excludes, includes, depth); + + function getFileSystemEntries(dir: string) { + const path = toPath(dir); + if (path === rootDirPath) { + return result; + } + return getCachedFileSystemEntries(path) || createCachedFileSystemEntries(dir, path); + } } - function addOrDeleteFileOrFolder(fileOrFolder: string) { - const path = toPath(fileOrFolder); - const existingResult = cachedReadDirectoryResult.get(path); + function addOrDeleteFileOrFolder(fileOrFolder: string, fileOrFolderPath: Path) { + const existingResult = getCachedFileSystemEntries(fileOrFolderPath); if (existingResult) { // This was a folder already present, remove it if this doesnt exist any more if (!host.directoryExists(fileOrFolder)) { - cachedReadDirectoryResult.delete(path); + cachedReadDirectoryResult.delete(fileOrFolderPath); } } else { // This was earlier a file (hence not in cached directory contents) // or we never cached the directory containing it - const parentResult = cachedReadDirectoryResult.get(getDirectoryPath(path)); + const parentResult = getCachedFileSystemEntriesForBaseDir(fileOrFolderPath); if (parentResult) { - const baseName = getBaseFileName(fileOrFolder); + const baseName = getBaseNameOfFileName(fileOrFolder); if (parentResult) { - parentResult.files = updateFileSystemEntry(parentResult.files, baseName, host.fileExists(path)); - parentResult.directories = updateFileSystemEntry(parentResult.directories, baseName, host.directoryExists(path)); + updateFilesOfFileSystemEntry(parentResult, baseName, host.fileExists(fileOrFolderPath)); + parentResult.directories = updateFileSystemEntry(parentResult.directories, baseName, host.directoryExists(fileOrFolderPath)); } } } } + function addOrDeleteFile(fileName: string, filePath: Path, eventKind: FileWatcherEventKind) { + if (eventKind === FileWatcherEventKind.Changed) { + return; + } + + const parentResult = getCachedFileSystemEntriesForBaseDir(filePath); + if (parentResult) { + updateFilesOfFileSystemEntry(parentResult, getBaseNameOfFileName(fileName), eventKind === FileWatcherEventKind.Created); + } + } + + function updateFilesOfFileSystemEntry(parentResult: FileSystemEntries, baseName: string, fileExists: boolean) { + parentResult.files = updateFileSystemEntry(parentResult.files, baseName, fileExists); + } + function clearCache() { cachedReadDirectoryResult.clear(); } diff --git a/src/compiler/resolutionCache.ts b/src/compiler/resolutionCache.ts index 615340e487208..0379289cc8ee1 100644 --- a/src/compiler/resolutionCache.ts +++ b/src/compiler/resolutionCache.ts @@ -40,7 +40,7 @@ namespace ts { export function createResolutionCache( toPath: (fileName: string) => Path, getCompilerOptions: () => CompilerOptions, - watchForFailedLookupLocation: (failedLookupLocation: string, containingFile: string, name: string) => FileWatcher, + watchForFailedLookupLocation: (failedLookupLocation: string, failedLookupLocationPath: Path, containingFile: string, name: string) => FileWatcher, log: (s: string) => void, resolveWithGlobalCache?: ResolverWithGlobalCache): ResolutionCache { @@ -201,7 +201,7 @@ namespace ts { } else { log(`Watcher: FailedLookupLocations: Status: new watch: Location: ${failedLookupLocation}, containingFile: ${containingFile}, name: ${name}`); - const fileWatcher = watchForFailedLookupLocation(failedLookupLocation, containingFile, name); + const fileWatcher = watchForFailedLookupLocation(failedLookupLocation, failedLookupLocationPath, containingFile, name); failedLookupLocationsWatches.set(failedLookupLocationPath, { fileWatcher, refCount: 1 }); } } diff --git a/src/compiler/watchedProgram.ts b/src/compiler/watchedProgram.ts index 5229e11ea9757..e79049b983c06 100644 --- a/src/compiler/watchedProgram.ts +++ b/src/compiler/watchedProgram.ts @@ -520,7 +520,7 @@ namespace ts { function onSourceFileChange(fileName: string, path: Path, eventKind: FileWatcherEventKind) { writeLog(`Source file path : ${path} changed: ${FileWatcherEventKind[eventKind]}, fileName: ${fileName}`); - updateCachedSystem(fileName, path); + updateCachedSystemWithFile(fileName, path, eventKind); const hostSourceFile = sourceFilesCache.get(path); if (hostSourceFile) { // Update the cache @@ -547,21 +547,19 @@ namespace ts { scheduleProgramUpdate(); } - function updateCachedSystem(fileName: string, path: Path) { + function updateCachedSystemWithFile(fileName: string, path: Path, eventKind: FileWatcherEventKind) { if (configFileName) { - const absoluteNormalizedPath = getNormalizedAbsolutePath(fileName, getDirectoryPath(path)); - (host as CachedSystem).addOrDeleteFileOrFolder(normalizePath(absoluteNormalizedPath)); + (host as CachedSystem).addOrDeleteFile(fileName, path, eventKind); } } - function watchFailedLookupLocation(failedLookupLocation: string, containingFile: string, name: string) { - return host.watchFile(failedLookupLocation, (fileName, eventKind) => onFailedLookupLocationChange(fileName, eventKind, failedLookupLocation, containingFile, name)); + function watchFailedLookupLocation(failedLookupLocation: string, failedLookupLocationPath: Path, containingFile: string, name: string) { + return host.watchFile(failedLookupLocation, (fileName, eventKind) => onFailedLookupLocationChange(fileName, eventKind, failedLookupLocation, failedLookupLocationPath, containingFile, name)); } - function onFailedLookupLocationChange(fileName: string, eventKind: FileWatcherEventKind, failedLookupLocation: string, containingFile: string, name: string) { + function onFailedLookupLocationChange(fileName: string, eventKind: FileWatcherEventKind, failedLookupLocation: string, failedLookupLocationPath: Path, containingFile: string, name: string) { writeLog(`Failed lookup location : ${failedLookupLocation} changed: ${FileWatcherEventKind[eventKind]}, fileName: ${fileName} containingFile: ${containingFile}, name: ${name}`); - const path = toPath(failedLookupLocation); - updateCachedSystem(failedLookupLocation, path); + updateCachedSystemWithFile(fileName, failedLookupLocationPath, eventKind); resolutionCache.invalidateResolutionOfChangedFailedLookupLocation(failedLookupLocation); scheduleProgramUpdate(); } @@ -574,14 +572,14 @@ namespace ts { fileWatcher.close(); } - function onMissingFileChange(filename: string, missingFilePath: Path, eventKind: FileWatcherEventKind) { - writeLog(`Missing file path : ${missingFilePath} changed: ${FileWatcherEventKind[eventKind]}, fileName: ${filename}`); + function onMissingFileChange(fileName: string, missingFilePath: Path, eventKind: FileWatcherEventKind) { + writeLog(`Missing file path : ${missingFilePath} changed: ${FileWatcherEventKind[eventKind]}, fileName: ${fileName}`); + updateCachedSystemWithFile(fileName, missingFilePath, eventKind); + if (eventKind === FileWatcherEventKind.Created && missingFilesMap.has(missingFilePath)) { closeMissingFilePathWatcher(missingFilePath, missingFilesMap.get(missingFilePath)); missingFilesMap.delete(missingFilePath); - updateCachedSystem(filename, missingFilePath); - // Delete the entry in the source files cache so that new source file is created removeSourceFile(missingFilePath); @@ -600,8 +598,8 @@ namespace ts { } function watchWildCardDirectory(directory: string, flags: WatchDirectoryFlags) { - return host.watchDirectory(directory, fileName => - onFileAddOrRemoveInWatchedDirectory(getNormalizedAbsolutePath(fileName, directory)), + return host.watchDirectory(directory, fileOrFolder => + onFileAddOrRemoveInWatchedDirectory(getNormalizedAbsolutePath(fileOrFolder, directory)), (flags & WatchDirectoryFlags.Recursive) !== 0); } @@ -609,24 +607,24 @@ namespace ts { watcher.close(); } - function onFileAddOrRemoveInWatchedDirectory(fileName: string) { + function onFileAddOrRemoveInWatchedDirectory(fileOrFolder: string) { Debug.assert(!!configFileName); - const path = toPath(fileName); + const fileOrFolderPath = toPath(fileOrFolder); // Since the file existance changed, update the sourceFiles cache - updateCachedSystem(fileName, path); - removeSourceFile(path); + (host as CachedSystem).addOrDeleteFileOrFolder(fileOrFolder, fileOrFolderPath); + removeSourceFile(fileOrFolderPath); // If a change was made inside "folder/file", node will trigger the callback twice: // one with the fileName being "folder/file", and the other one with "folder". // We don't respond to the second one. - if (fileName && !isSupportedSourceFileName(fileName, compilerOptions)) { - writeLog(`Project: ${configFileName} Detected file add/remove of non supported extension: ${fileName}`); + if (fileOrFolder && !isSupportedSourceFileName(fileOrFolder, compilerOptions)) { + writeLog(`Project: ${configFileName} Detected file add/remove of non supported extension: ${fileOrFolder}`); return; } - writeLog(`Project: ${configFileName} Detected file add/remove of supported extension: ${fileName}`); + writeLog(`Project: ${configFileName} Detected file add/remove of supported extension: ${fileOrFolder}`); // Reload is pending, do the reload if (!needsReload) { @@ -658,9 +656,7 @@ namespace ts { } } - interface CachedSystem extends System { - addOrDeleteFileOrFolder(fileOrFolder: string): void; - clearCache(): void; + interface CachedSystem extends System, CachedHost { } function createCachedSystem(host: System): CachedSystem { @@ -675,7 +671,7 @@ namespace ts { const setTimeout = host.setTimeout ? (callback: (...args: any[]) => void, ms: number, ...args: any[]) => host.setTimeout(callback, ms, ...args) : undefined; const clearTimeout = host.clearTimeout ? (timeoutId: any) => host.clearTimeout(timeoutId) : undefined; - const cachedHost = createCachedHost(host); + const cachedPartialSystem = createCachedPartialSystem(host); return { args: host.args, newLine: host.newLine, @@ -683,17 +679,17 @@ namespace ts { write: s => host.write(s), readFile: (path, encoding?) => host.readFile(path, encoding), getFileSize, - writeFile: (fileName, data, writeByteOrderMark?) => cachedHost.writeFile(fileName, data, writeByteOrderMark), + writeFile: (fileName, data, writeByteOrderMark?) => cachedPartialSystem.writeFile(fileName, data, writeByteOrderMark), watchFile, watchDirectory, resolvePath: path => host.resolvePath(path), - fileExists: fileName => cachedHost.fileExists(fileName), - directoryExists: dir => cachedHost.directoryExists(dir), - createDirectory: dir => cachedHost.createDirectory(dir), + fileExists: fileName => cachedPartialSystem.fileExists(fileName), + directoryExists: dir => cachedPartialSystem.directoryExists(dir), + createDirectory: dir => cachedPartialSystem.createDirectory(dir), getExecutingFilePath: () => host.getExecutingFilePath(), - getCurrentDirectory: () => cachedHost.getCurrentDirectory(), - getDirectories: dir => cachedHost.getDirectories(dir), - readDirectory: (path, extensions, excludes, includes, depth) => cachedHost.readDirectory(path, extensions, excludes, includes, depth), + getCurrentDirectory: () => cachedPartialSystem.getCurrentDirectory(), + getDirectories: dir => cachedPartialSystem.getDirectories(dir), + readDirectory: (path, extensions, excludes, includes, depth) => cachedPartialSystem.readDirectory(path, extensions, excludes, includes, depth), getModifiedTime, createHash, getMemoryUsage, @@ -704,8 +700,9 @@ namespace ts { debugMode: host.debugMode, setTimeout, clearTimeout, - addOrDeleteFileOrFolder: fileOrFolder => cachedHost.addOrDeleteFileOrFolder(fileOrFolder), - clearCache: () => cachedHost.clearCache() + addOrDeleteFileOrFolder: (fileOrFolder, fileOrFolderPath) => cachedPartialSystem.addOrDeleteFileOrFolder(fileOrFolder, fileOrFolderPath), + addOrDeleteFile: (file, filePath, eventKind) => cachedPartialSystem.addOrDeleteFile(file, filePath, eventKind), + clearCache: () => cachedPartialSystem.clearCache() }; } } diff --git a/src/server/editorServices.ts b/src/server/editorServices.ts index 8507b6f5cd27e..0b7ba9e744109 100644 --- a/src/server/editorServices.ts +++ b/src/server/editorServices.ts @@ -695,8 +695,8 @@ namespace ts.server { } /* @internal */ - onTypeRootFileChanged(project: ConfiguredProject, fileName: NormalizedPath) { - project.getCachedServerHost().addOrDeleteFileOrFolder(fileName); + onTypeRootFileChanged(project: ConfiguredProject, fileOrFolder: NormalizedPath) { + project.getCachedServerHost().addOrDeleteFileOrFolder(fileOrFolder, this.toPath(fileOrFolder)); project.updateTypes(); this.delayUpdateProjectGraphAndInferredProjectsRefresh(project); } @@ -707,15 +707,15 @@ namespace ts.server { * @param fileName the absolute file name that changed in watched directory */ /* @internal */ - onFileAddOrRemoveInWatchedDirectoryOfProject(project: ConfiguredProject, fileName: NormalizedPath) { - project.getCachedServerHost().addOrDeleteFileOrFolder(fileName); + onFileAddOrRemoveInWatchedDirectoryOfProject(project: ConfiguredProject, fileOrFolder: NormalizedPath) { + project.getCachedServerHost().addOrDeleteFileOrFolder(fileOrFolder, this.toPath(fileOrFolder)); const configFilename = project.getConfigFilePath(); // If a change was made inside "folder/file", node will trigger the callback twice: // one with the fileName being "folder/file", and the other one with "folder". // We don't respond to the second one. - if (fileName && !isSupportedSourceFileName(fileName, project.getCompilerOptions(), this.hostConfiguration.extraFileExtensions)) { - this.logger.info(`Project: ${configFilename} Detected file add/remove of non supported extension: ${fileName}`); + if (fileOrFolder && !isSupportedSourceFileName(fileOrFolder, project.getCompilerOptions(), this.hostConfiguration.extraFileExtensions)) { + this.logger.info(`Project: ${configFilename} Detected file add/remove of non supported extension: ${fileOrFolder}`); return; } diff --git a/src/server/lsHost.ts b/src/server/lsHost.ts index f4cbd402604ca..44b9893bc3d42 100644 --- a/src/server/lsHost.ts +++ b/src/server/lsHost.ts @@ -4,12 +4,13 @@ /// namespace ts.server { + /*@internal*/ export class CachedServerHost implements ServerHost { args: string[]; newLine: string; useCaseSensitiveFileNames: boolean; - private readonly cachedHost: CachedHost; + private readonly cachedPartialSystem: CachedPartialSystem; readonly trace: (s: string) => void; readonly realpath?: (path: string) => string; @@ -24,7 +25,7 @@ namespace ts.server { if (this.host.realpath) { this.realpath = path => this.host.realpath(path); } - this.cachedHost = createCachedHost(host); + this.cachedPartialSystem = createCachedPartialSystem(host); } write(s: string) { @@ -32,7 +33,7 @@ namespace ts.server { } writeFile(fileName: string, data: string, writeByteOrderMark?: boolean) { - this.cachedHost.writeFile(fileName, data, writeByteOrderMark); + this.cachedPartialSystem.writeFile(fileName, data, writeByteOrderMark); } resolvePath(path: string) { @@ -48,7 +49,7 @@ namespace ts.server { } getCurrentDirectory() { - return this.cachedHost.getCurrentDirectory(); + return this.cachedPartialSystem.getCurrentDirectory(); } exit(exitCode?: number) { @@ -61,32 +62,35 @@ namespace ts.server { } getDirectories(rootDir: string) { - return this.cachedHost.getDirectories(rootDir); + return this.cachedPartialSystem.getDirectories(rootDir); } readDirectory(rootDir: string, extensions: string[], excludes: string[], includes: string[], depth: number): string[] { - return this.cachedHost.readDirectory(rootDir, extensions, excludes, includes, depth); + return this.cachedPartialSystem.readDirectory(rootDir, extensions, excludes, includes, depth); } fileExists(fileName: string): boolean { - return this.cachedHost.fileExists(fileName); + return this.cachedPartialSystem.fileExists(fileName); } directoryExists(dirPath: string) { - return this.cachedHost.directoryExists(dirPath); + return this.cachedPartialSystem.directoryExists(dirPath); } readFile(path: string, encoding?: string): string { return this.host.readFile(path, encoding); } + addOrDeleteFileOrFolder(fileOrFolder: NormalizedPath, fileOrFolderPath: Path) { + return this.cachedPartialSystem.addOrDeleteFileOrFolder(fileOrFolder, fileOrFolderPath); + } - addOrDeleteFileOrFolder(fileOrFolder: NormalizedPath) { - return this.cachedHost.addOrDeleteFileOrFolder(fileOrFolder); + addOrDeleteFile(file: string, path: Path, eventKind: FileWatcherEventKind) { + return this.cachedPartialSystem.addOrDeleteFile(file, path, eventKind); } clearCache() { - return this.cachedHost.clearCache(); + return this.cachedPartialSystem.clearCache(); } setTimeout(callback: (...args: any[]) => void, ms: number, ...args: any[]) { diff --git a/src/server/project.ts b/src/server/project.ts index 2233ef1e94184..552778e86c0ea 100644 --- a/src/server/project.ts +++ b/src/server/project.ts @@ -218,7 +218,7 @@ namespace ts.server { this.resolutionCache = createResolutionCache( fileName => this.projectService.toPath(fileName), () => this.compilerOptions, - (failedLookupLocation, containingFile, name) => this.watchFailedLookupLocation(failedLookupLocation, containingFile, name), + (failedLookupLocation, failedLookupLocationPath, containingFile, name) => this.watchFailedLookupLocation(failedLookupLocation, failedLookupLocationPath, containingFile, name), s => this.projectService.logger.info(s), (primaryResult, moduleName, compilerOptions, host) => resolveWithGlobalCache(primaryResult, moduleName, compilerOptions, host, this.getTypeAcquisition().enable ? this.projectService.typingsInstaller.globalTypingsCacheLocation : undefined, this.getProjectName()) @@ -235,12 +235,12 @@ namespace ts.server { this.markAsDirty(); } - private watchFailedLookupLocation(failedLookupLocation: string, containingFile: string, name: string) { + private watchFailedLookupLocation(failedLookupLocation: string, failedLookupLocationPath: Path, containingFile: string, name: string) { // There is some kind of change in the failed lookup location, update the program - return this.projectService.addFileWatcher(WatchType.FailedLookupLocation, this, failedLookupLocation, (__fileName, eventKind) => { + return this.projectService.addFileWatcher(WatchType.FailedLookupLocation, this, failedLookupLocation, (fileName, eventKind) => { this.projectService.logger.info(`Watcher: FailedLookupLocations: Status: ${FileWatcherEventKind[eventKind]}: Location: ${failedLookupLocation}, containingFile: ${containingFile}, name: ${name}`); if (this.projectKind === ProjectKind.Configured) { - (this.lsHost.host as CachedServerHost).addOrDeleteFileOrFolder(toNormalizedPath(failedLookupLocation)); + (this.lsHost.host as CachedServerHost).addOrDeleteFile(fileName, failedLookupLocationPath, eventKind); } this.resolutionCache.invalidateResolutionOfChangedFailedLookupLocation(failedLookupLocation); this.markAsDirty(); @@ -719,16 +719,15 @@ namespace ts.server { private addMissingFileWatcher(missingFilePath: Path) { const fileWatcher = this.projectService.addFileWatcher( WatchType.MissingFilePath, this, missingFilePath, - (filename, eventKind) => { + (fileName, eventKind) => { + if (this.projectKind === ProjectKind.Configured) { + (this.lsHost.host as CachedServerHost).addOrDeleteFile(fileName, missingFilePath, eventKind); + } + if (eventKind === FileWatcherEventKind.Created && this.missingFilesMap.has(missingFilePath)) { this.missingFilesMap.delete(missingFilePath); this.closeMissingFileWatcher(missingFilePath, fileWatcher, WatcherCloseReason.FileCreated); - if (this.projectKind === ProjectKind.Configured) { - const absoluteNormalizedPath = getNormalizedAbsolutePath(filename, getDirectoryPath(missingFilePath)); - (this.lsHost.host as CachedServerHost).addOrDeleteFileOrFolder(toNormalizedPath(absoluteNormalizedPath)); - } - // When a missing file is created, we should update the graph. this.markAsDirty(); this.projectService.delayUpdateProjectGraphAndInferredProjectsRefresh(this); @@ -1076,6 +1075,7 @@ namespace ts.server { return super.updateGraph(); } + /*@internal*/ getCachedServerHost() { return this.lsHost.host as CachedServerHost; } diff --git a/src/server/scriptInfo.ts b/src/server/scriptInfo.ts index 6d439e9238543..2dcbc52d87dde 100644 --- a/src/server/scriptInfo.ts +++ b/src/server/scriptInfo.ts @@ -237,7 +237,7 @@ namespace ts.server { detachAllProjects() { for (const p of this.containingProjects) { if (p.projectKind === ProjectKind.Configured) { - (p.lsHost.host as CachedServerHost).addOrDeleteFileOrFolder(this.fileName); + (p.lsHost.host as CachedServerHost).addOrDeleteFile(this.fileName, this.path, FileWatcherEventKind.Deleted); } const isInfoRoot = p.isRoot(this); // detach is unnecessary since we'll clean the list of containing projects anyways From 136b091a4a71065db75c972adeb810bb1a2b812f Mon Sep 17 00:00:00 2001 From: Sheetal Nandi Date: Mon, 14 Aug 2017 15:52:20 -0700 Subject: [PATCH 061/109] Update based on feedback --- src/compiler/program.ts | 35 ++++++++++--------- src/compiler/resolutionCache.ts | 23 +++++++++---- src/compiler/types.ts | 3 +- src/compiler/utilities.ts | 9 +++-- src/compiler/watchedProgram.ts | 9 +++-- src/harness/unittests/programMissingFiles.ts | 36 ++++++++++++-------- src/server/project.ts | 5 --- 7 files changed, 69 insertions(+), 51 deletions(-) diff --git a/src/compiler/program.ts b/src/compiler/program.ts index 2dd3e465268e4..04af5c4d9726a 100644 --- a/src/compiler/program.ts +++ b/src/compiler/program.ts @@ -393,7 +393,7 @@ namespace ts { allDiagnostics?: Diagnostic[]; } - export function isProgramUptoDate(program: Program, rootFileNames: string[], newOptions: CompilerOptions, + export function isProgramUptoDate(program: Program | undefined, rootFileNames: string[], newOptions: CompilerOptions, 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) { @@ -406,14 +406,12 @@ namespace ts { } // If any file is not up-to-date, then the whole program is not up-to-date - for (const file of program.getSourceFiles()) { - if (!sourceFileUpToDate(program.getSourceFile(file.fileName))) { + if (program.getSourceFiles().some(sourceFileNotUptoDate)) { return false; - } } // If any of the missing file paths are now created - if (program.getMissingFilePaths().some(missingFilePath => fileExists(missingFilePath))) { + if (program.getMissingFilePaths().some(fileExists)) { return false; } @@ -431,15 +429,18 @@ namespace ts { return true; - function sourceFileUpToDate(sourceFile: SourceFile): boolean { - return sourceFile && - sourceFile.version === getSourceVersion(sourceFile.path) && - !hasInvalidatedResolution(sourceFile.path); + function sourceFileNotUptoDate(sourceFile: SourceFile): boolean { + return sourceFile.version !== getSourceVersion(sourceFile.path) || + hasInvalidatedResolution(sourceFile.path); } } + /** + * Determined if source file needs to be re-created even if its text hasnt changed + */ function shouldProgramCreateNewSourceFiles(program: Program, newOptions: CompilerOptions) { // If any of these options change, we cant reuse old source file even if version match + // The change in options like these could result in change in syntax tree change const oldOptions = program && program.getCompilerOptions(); return oldOptions && (oldOptions.target !== newOptions.target || @@ -478,19 +479,19 @@ namespace ts { ); } - export interface WildcardDirectoryWatchers { + export interface WildcardDirectoryWatcher { watcher: FileWatcher; flags: WatchDirectoryFlags; } /** - * Updates the existing wild card directory watcyhes with the new set of wild card directories from the config file after new program is created + * Updates the existing wild card directory watches with the new set of wild card directories from the config file after new program is created */ export function updateWatchingWildcardDirectories( - existingWatchedForWildcards: Map, + existingWatchedForWildcards: Map, wildcardDirectories: Map, watchDirectory: (directory: string, flags: WatchDirectoryFlags) => FileWatcher, - closeDirectoryWatcher: (directory: string, wildcardDirectoryWatcher: WildcardDirectoryWatchers, flagsChanged: boolean) => void + closeDirectoryWatcher: (directory: string, wildcardDirectoryWatcher: WildcardDirectoryWatcher, flagsChanged: boolean) => void ) { mutateMap( existingWatchedForWildcards, @@ -506,7 +507,7 @@ namespace ts { } ); - function createWildcardDirectoryWatcher(directory: string, flags: WatchDirectoryFlags): WildcardDirectoryWatchers { + function createWildcardDirectoryWatcher(directory: string, flags: WatchDirectoryFlags): WildcardDirectoryWatcher { // Create new watch and recursive info return { watcher: watchDirectory(directory, flags), @@ -514,7 +515,7 @@ namespace ts { }; } - function updateWildcardDirectoryWatcher(directory: string, wildcardDirectoryWatcher: WildcardDirectoryWatchers, flags: WatchDirectoryFlags) { + function updateWildcardDirectoryWatcher(directory: string, wildcardDirectoryWatcher: WildcardDirectoryWatcher, flags: WatchDirectoryFlags) { // Watcher needs to be updated if the recursive flags dont match if (wildcardDirectoryWatcher.flags === flags) { return; @@ -622,7 +623,7 @@ namespace ts { let redirectTargetsSet = createMap(); const filesByName = createMap(); - let missingFilePaths: Path[]; + let missingFilePaths: ReadonlyArray; // stores 'filename -> file association' ignoring case // used to track cases when two file names differ only in casing const filesByNameIgnoreCase = host.useCaseSensitiveFileNames() ? createMap() : undefined; @@ -666,6 +667,8 @@ namespace ts { missingFilePaths = arrayFrom(filesByName.keys(), p => p).filter(p => !filesByName.get(p)); } + Debug.assert(!!missingFilePaths); + // unconditionally set moduleResolutionCache to undefined to avoid unnecessary leaks moduleResolutionCache = undefined; diff --git a/src/compiler/resolutionCache.ts b/src/compiler/resolutionCache.ts index 0379289cc8ee1..dc2e61cd57842 100644 --- a/src/compiler/resolutionCache.ts +++ b/src/compiler/resolutionCache.ts @@ -1,7 +1,9 @@ /// /// +/*@internal*/ namespace ts { + /** This is the cache of module/typedirectives resolution that can be retained across program */ export interface ResolutionCache { setModuleResolutionHost(host: ModuleResolutionHost): void; @@ -19,10 +21,15 @@ namespace ts { clear(): void; } - type NameResolutionWithFailedLookupLocations = { failedLookupLocations: string[], isInvalidated?: boolean }; - type ResolverWithGlobalCache = (primaryResult: ResolvedModuleWithFailedLookupLocations, moduleName: string, compilerOptions: CompilerOptions, host: ModuleResolutionHost) => ResolvedModuleWithFailedLookupLocations | undefined; + interface NameResolutionWithFailedLookupLocations { + readonly failedLookupLocations: string[]; + isInvalidated?: boolean; + } + + interface ResolverWithGlobalCache { + (primaryResult: ResolvedModuleWithFailedLookupLocations, moduleName: string, compilerOptions: CompilerOptions, host: ModuleResolutionHost): ResolvedModuleWithFailedLookupLocations | undefined; + } - /*@internal*/ export function resolveWithGlobalCache(primaryResult: ResolvedModuleWithFailedLookupLocations, moduleName: string, compilerOptions: CompilerOptions, host: ModuleResolutionHost, globalCache: string | undefined, projectName: string): ResolvedModuleWithFailedLookupLocations | undefined { if (!isExternalModuleNameRelative(moduleName) && !(primaryResult.resolvedModule && extensionIsTypeScript(primaryResult.resolvedModule.extension)) && globalCache !== undefined) { // otherwise try to load typings from @types @@ -36,7 +43,11 @@ namespace ts { } } - /*@internal*/ + interface FailedLookupLocationsWatcher { + fileWatcher: FileWatcher; + refCount: number; + } + export function createResolutionCache( toPath: (fileName: string) => Path, getCompilerOptions: () => CompilerOptions, @@ -51,7 +62,6 @@ namespace ts { const resolvedModuleNames = createMap>(); const resolvedTypeReferenceDirectives = createMap>(); - type FailedLookupLocationsWatcher = { fileWatcher: FileWatcher; refCount: number }; const failedLookupLocationsWatches = createMap(); return { @@ -71,11 +81,10 @@ namespace ts { } function clear() { - failedLookupLocationsWatches.forEach((failedLookupLocationWatcher, failedLookupLocationPath: Path) => { + clearMap(failedLookupLocationsWatches, (failedLookupLocationPath, failedLookupLocationWatcher) => { log(`Watcher: FailedLookupLocations: Status: ForceClose: LocationPath: ${failedLookupLocationPath}, refCount: ${failedLookupLocationWatcher.refCount}`); failedLookupLocationWatcher.fileWatcher.close(); }); - failedLookupLocationsWatches.clear(); resolvedModuleNames.clear(); resolvedTypeReferenceDirectives.clear(); } diff --git a/src/compiler/types.ts b/src/compiler/types.ts index e4ea722df3dcf..9fe93f55cb3c4 100644 --- a/src/compiler/types.ts +++ b/src/compiler/types.ts @@ -2411,7 +2411,7 @@ namespace ts { * program source file but could not be located. */ /* @internal */ - getMissingFilePaths(): Path[]; + getMissingFilePaths(): ReadonlyArray; /** * Emits the JavaScript and declaration files. If targetSourceFile is not specified, then @@ -3561,6 +3561,7 @@ namespace ts { charset?: string; checkJs?: boolean; /* @internal */ configFilePath?: string; + /** configFile is set as non enumerable property so as to avoid checking of json source files */ /* @internal */ readonly configFile?: JsonSourceFile; declaration?: boolean; declarationDir?: string; diff --git a/src/compiler/utilities.ts b/src/compiler/utilities.ts index 6e26a287b8384..ebb6c7855885e 100644 --- a/src/compiler/utilities.ts +++ b/src/compiler/utilities.ts @@ -403,7 +403,10 @@ namespace ts { ((node).name.kind === SyntaxKind.StringLiteral || isGlobalScopeAugmentation(node)); } - /* @internal */ + export function isModuleWithStringLiteralName(node: Node): node is ModuleDeclaration { + return isModuleDeclaration(node) && node.name.kind === SyntaxKind.StringLiteral; + } + export function isNonGlobalAmbientModule(node: Node): node is ModuleDeclaration & { name: StringLiteral } { return isModuleDeclaration(node) && isStringLiteral(node.name); } @@ -1403,10 +1406,6 @@ namespace ts { return SpecialPropertyAssignmentKind.None; } - export function isModuleWithStringLiteralName(node: Node): node is ModuleDeclaration { - return isModuleDeclaration(node) && node.name.kind === SyntaxKind.StringLiteral; - } - export function getExternalModuleName(node: Node): Expression { if (node.kind === SyntaxKind.ImportDeclaration) { return (node).moduleSpecifier; diff --git a/src/compiler/watchedProgram.ts b/src/compiler/watchedProgram.ts index e79049b983c06..6423b9818969d 100644 --- a/src/compiler/watchedProgram.ts +++ b/src/compiler/watchedProgram.ts @@ -241,12 +241,13 @@ namespace ts { let needsReload: boolean; // true if the config file changed and needs to reload it from the disk let missingFilesMap: Map; // Map of file watchers for the missing files let configFileWatcher: FileWatcher; // watcher for the config file - let watchedWildcardDirectories: Map; // map of watchers for the wild card directories in the config file + let watchedWildcardDirectories: Map; // map of watchers for the wild card directories in the config file let timerToUpdateProgram: any; // timer callback to recompile the program const sourceFilesCache = createMap(); // 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 + let hasChangedCompilerOptions = false; // True if the compiler options have changed between compilations watchingHost = watchingHost || createWatchingSystemHost(compilerOptions.pretty); const { system, parseConfigFile, reportDiagnostic, reportWatchDiagnostic, beforeCompile, afterCompile } = watchingHost; @@ -291,9 +292,10 @@ namespace ts { // Create the compiler host const compilerHost = createWatchedCompilerHost(compilerOptions); resolutionCache.setModuleResolutionHost(compilerHost); - if (changesAffectModuleResolution(program && program.getCompilerOptions(), compilerOptions)) { + if (hasChangedCompilerOptions && changesAffectModuleResolution(program && program.getCompilerOptions(), compilerOptions)) { resolutionCache.clear(); } + hasChangedCompilerOptions = false; beforeCompile(compilerOptions); // Compile the program @@ -504,6 +506,7 @@ namespace ts { const configParseResult = parseConfigFile(configFileName, optionsToExtendForConfigFile, cachedHost, reportDiagnostic, reportWatchDiagnostic); rootFileNames = configParseResult.fileNames; compilerOptions = configParseResult.options; + hasChangedCompilerOptions = true; configFileSpecs = configParseResult.configFileSpecs; configFileWildCardDirectories = configParseResult.wildcardDirectories; @@ -603,7 +606,7 @@ namespace ts { (flags & WatchDirectoryFlags.Recursive) !== 0); } - function stopWatchingWildCardDirectory(_directory: string, { watcher }: WildcardDirectoryWatchers, _recursiveChanged: boolean) { + function stopWatchingWildCardDirectory(_directory: string, { watcher }: WildcardDirectoryWatcher, _recursiveChanged: boolean) { watcher.close(); } diff --git a/src/harness/unittests/programMissingFiles.ts b/src/harness/unittests/programMissingFiles.ts index 668b684a2501b..20323b3738085 100644 --- a/src/harness/unittests/programMissingFiles.ts +++ b/src/harness/unittests/programMissingFiles.ts @@ -1,6 +1,18 @@ /// namespace ts { + function verifyMissingFilePaths(missingPaths: ReadonlyArray, expected: ReadonlyArray) { + assert.isDefined(missingPaths); + const map = arrayToMap(expected, k => k, _v => true); + for (const missing of missingPaths) { + const value = map.get(missing); + assert.isTrue(value, `${missing} to be ${value === undefined ? "not present" : "present only once"}, in actual: ${missingPaths} expected: ${expected}`); + map.set(missing, false); + } + const notFound = mapDefinedIter(map.keys(), k => map.get(k) === true ? k : undefined); + assert.equal(notFound.length, 0, `Not found ${notFound} in actual: ${missingPaths} expected: ${expected}`); + } + describe("Program.getMissingFilePaths", () => { const options: CompilerOptions = { @@ -40,34 +52,31 @@ namespace ts { it("handles no missing root files", () => { const program = createProgram([emptyFileRelativePath], options, testCompilerHost); const missing = program.getMissingFilePaths(); - assert.isDefined(missing); - assert.deepEqual(missing, []); + verifyMissingFilePaths(missing, []); }); it("handles missing root file", () => { const program = createProgram(["./nonexistent.ts"], options, testCompilerHost); const missing = program.getMissingFilePaths(); - assert.isDefined(missing); - assert.deepEqual(missing, ["d:/pretend/nonexistent.ts"]); // Absolute path + verifyMissingFilePaths(missing, ["d:/pretend/nonexistent.ts"]); // Absolute path }); it("handles multiple missing root files", () => { const program = createProgram(["./nonexistent0.ts", "./nonexistent1.ts"], options, testCompilerHost); - const missing = program.getMissingFilePaths().sort(); - assert.deepEqual(missing, ["d:/pretend/nonexistent0.ts", "d:/pretend/nonexistent1.ts"]); + const missing = program.getMissingFilePaths(); + verifyMissingFilePaths(missing, ["d:/pretend/nonexistent0.ts", "d:/pretend/nonexistent1.ts"]); }); it("handles a mix of present and missing root files", () => { const program = createProgram(["./nonexistent0.ts", emptyFileRelativePath, "./nonexistent1.ts"], options, testCompilerHost); - const missing = program.getMissingFilePaths().sort(); - assert.deepEqual(missing, ["d:/pretend/nonexistent0.ts", "d:/pretend/nonexistent1.ts"]); + const missing = program.getMissingFilePaths(); + verifyMissingFilePaths(missing, ["d:/pretend/nonexistent0.ts", "d:/pretend/nonexistent1.ts"]); }); it("handles repeatedly specified root files", () => { const program = createProgram(["./nonexistent.ts", "./nonexistent.ts"], options, testCompilerHost); const missing = program.getMissingFilePaths(); - assert.isDefined(missing); - assert.deepEqual(missing, ["d:/pretend/nonexistent.ts"]); + verifyMissingFilePaths(missing, ["d:/pretend/nonexistent.ts"]); }); it("normalizes file paths", () => { @@ -81,9 +90,8 @@ namespace ts { it("handles missing triple slash references", () => { const program = createProgram([referenceFileRelativePath], options, testCompilerHost); - const missing = program.getMissingFilePaths().sort(); - assert.isDefined(missing); - assert.deepEqual(missing, [ + const missing = program.getMissingFilePaths(); + verifyMissingFilePaths(missing, [ // From absolute reference "d:/imaginary/nonexistent1.ts", @@ -100,4 +108,4 @@ namespace ts { ]); }); }); -} \ No newline at end of file +} diff --git a/src/server/project.ts b/src/server/project.ts index 552778e86c0ea..d50621c50bb25 100644 --- a/src/server/project.ts +++ b/src/server/project.ts @@ -1020,11 +1020,6 @@ namespace ts.server { } } - interface WildcardDirectoryWatcher { - watcher: FileWatcher; - flags: WatchDirectoryFlags; - } - /** * If a file is opened, the server will look for a tsconfig (or jsconfig) * and if successfull create a ConfiguredProject for it. From 6bf9133461fd1f3f2cae1fbc02ee4a6319a000a6 Mon Sep 17 00:00:00 2001 From: Sheetal Nandi Date: Mon, 14 Aug 2017 15:52:20 -0700 Subject: [PATCH 062/109] Update to PR feedback --- src/compiler/resolutionCache.ts | 39 ++++++++++++++++----------------- src/server/project.ts | 4 ++-- 2 files changed, 21 insertions(+), 22 deletions(-) diff --git a/src/compiler/resolutionCache.ts b/src/compiler/resolutionCache.ts index dc2e61cd57842..e50e32dfcef22 100644 --- a/src/compiler/resolutionCache.ts +++ b/src/compiler/resolutionCache.ts @@ -26,23 +26,6 @@ namespace ts { isInvalidated?: boolean; } - interface ResolverWithGlobalCache { - (primaryResult: ResolvedModuleWithFailedLookupLocations, moduleName: string, compilerOptions: CompilerOptions, host: ModuleResolutionHost): ResolvedModuleWithFailedLookupLocations | undefined; - } - - export function resolveWithGlobalCache(primaryResult: ResolvedModuleWithFailedLookupLocations, moduleName: string, compilerOptions: CompilerOptions, host: ModuleResolutionHost, globalCache: string | undefined, projectName: string): ResolvedModuleWithFailedLookupLocations | undefined { - if (!isExternalModuleNameRelative(moduleName) && !(primaryResult.resolvedModule && extensionIsTypeScript(primaryResult.resolvedModule.extension)) && globalCache !== undefined) { - // otherwise try to load typings from @types - - // create different collection of failed lookup locations for second pass - // if it will fail and we've already found something during the first pass - we don't want to pollute its results - const { resolvedModule, failedLookupLocations } = loadModuleFromGlobalCache(moduleName, projectName, compilerOptions, host, globalCache); - if (resolvedModule) { - return { resolvedModule, failedLookupLocations: primaryResult.failedLookupLocations.concat(failedLookupLocations) }; - } - } - } - interface FailedLookupLocationsWatcher { fileWatcher: FileWatcher; refCount: number; @@ -53,7 +36,8 @@ namespace ts { getCompilerOptions: () => CompilerOptions, watchForFailedLookupLocation: (failedLookupLocation: string, failedLookupLocationPath: Path, containingFile: string, name: string) => FileWatcher, log: (s: string) => void, - resolveWithGlobalCache?: ResolverWithGlobalCache): ResolutionCache { + projectName?: string, + getGlobalCache?: () => string | undefined): ResolutionCache { let host: ModuleResolutionHost; let filesWithChangedSetOfUnresolvedImports: Path[]; @@ -107,9 +91,24 @@ namespace ts { 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 + // return result immediately only if global cache support is not enabled or if it is .ts, .tsx or .d.ts + if (!getGlobalCache) { + return primaryResult; + } + // otherwise try to load typings from @types - return (resolveWithGlobalCache && resolveWithGlobalCache(primaryResult, moduleName, compilerOptions, host)) || primaryResult; + const globalCache = getGlobalCache(); + if (globalCache !== undefined && !isExternalModuleNameRelative(moduleName) && !(primaryResult.resolvedModule && extensionIsTypeScript(primaryResult.resolvedModule.extension))) { + // create different collection of failed lookup locations for second pass + // if it will fail and we've already found something during the first pass - we don't want to pollute its results + const { resolvedModule, failedLookupLocations } = loadModuleFromGlobalCache(moduleName, projectName, compilerOptions, host, globalCache); + if (resolvedModule) { + return { resolvedModule, failedLookupLocations: primaryResult.failedLookupLocations.concat(failedLookupLocations) }; + } + } + + // Default return the result from the first pass + return primaryResult; } function resolveNamesWithLocalCache( diff --git a/src/server/project.ts b/src/server/project.ts index d50621c50bb25..fc6dca31ecdac 100644 --- a/src/server/project.ts +++ b/src/server/project.ts @@ -220,8 +220,8 @@ namespace ts.server { () => this.compilerOptions, (failedLookupLocation, failedLookupLocationPath, containingFile, name) => this.watchFailedLookupLocation(failedLookupLocation, failedLookupLocationPath, containingFile, name), s => this.projectService.logger.info(s), - (primaryResult, moduleName, compilerOptions, host) => resolveWithGlobalCache(primaryResult, moduleName, compilerOptions, host, - this.getTypeAcquisition().enable ? this.projectService.typingsInstaller.globalTypingsCacheLocation : undefined, this.getProjectName()) + this.getProjectName(), + () => this.getTypeAcquisition().enable ? this.projectService.typingsInstaller.globalTypingsCacheLocation : undefined ); this.lsHost.compilationSettings = this.compilerOptions; this.resolutionCache.setModuleResolutionHost(this.lsHost); From a99c04e8f9893f9f193fad81df6a7f4430a5a077 Mon Sep 17 00:00:00 2001 From: Sheetal Nandi Date: Mon, 14 Aug 2017 15:52:20 -0700 Subject: [PATCH 063/109] Make the failedLookuplocations to be readonly array --- src/compiler/resolutionCache.ts | 36 ++++++++++++++++++--------------- src/compiler/types.ts | 4 ++-- src/compiler/watchedProgram.ts | 5 +++-- src/server/project.ts | 2 +- 4 files changed, 26 insertions(+), 21 deletions(-) diff --git a/src/compiler/resolutionCache.ts b/src/compiler/resolutionCache.ts index e50e32dfcef22..4fefd87a834f7 100644 --- a/src/compiler/resolutionCache.ts +++ b/src/compiler/resolutionCache.ts @@ -13,7 +13,7 @@ namespace ts { resolveModuleNames(moduleNames: string[], containingFile: string, logChanges: boolean): ResolvedModuleFull[]; resolveTypeReferenceDirectives(typeDirectiveNames: string[], containingFile: string): ResolvedTypeReferenceDirective[]; - invalidateResolutionOfDeletedFile(filePath: Path): void; + invalidateResolutionOfFile(filePath: Path): void; invalidateResolutionOfChangedFailedLookupLocation(failedLookupLocation: string): void; createHasInvalidatedResolution(): HasInvalidatedResolution; @@ -22,7 +22,7 @@ namespace ts { } interface NameResolutionWithFailedLookupLocations { - readonly failedLookupLocations: string[]; + readonly failedLookupLocations: ReadonlyArray; isInvalidated?: boolean; } @@ -40,9 +40,12 @@ namespace ts { getGlobalCache?: () => string | undefined): ResolutionCache { let host: ModuleResolutionHost; - let filesWithChangedSetOfUnresolvedImports: Path[]; - let filesWithInvalidatedResolutions: Map; + let filesWithChangedSetOfUnresolvedImports: Path[] | undefined; + let filesWithInvalidatedResolutions: Map | undefined; + // The resolvedModuleNames and resolvedTypeReferenceDirectives are the cache of resolutions per file. + // The key in the map is source file's path. + // The values are Map of resolutions with key being name lookedup. const resolvedModuleNames = createMap>(); const resolvedTypeReferenceDirectives = createMap>(); @@ -54,7 +57,7 @@ namespace ts { finishRecordingFilesWithChangedResolutions, resolveModuleNames, resolveTypeReferenceDirectives, - invalidateResolutionOfDeletedFile, + invalidateResolutionOfFile, invalidateResolutionOfChangedFailedLookupLocation, createHasInvalidatedResolution, clear @@ -65,6 +68,7 @@ namespace ts { } function clear() { + // Close all the watches for failed lookup locations, irrespective of refcounts for them since this is to clear the cache clearMap(failedLookupLocationsWatches, (failedLookupLocationPath, failedLookupLocationWatcher) => { log(`Watcher: FailedLookupLocations: Status: ForceClose: LocationPath: ${failedLookupLocationPath}, refCount: ${failedLookupLocationWatcher.refCount}`); failedLookupLocationWatcher.fileWatcher.close(); @@ -103,7 +107,7 @@ namespace ts { // if it will fail and we've already found something during the first pass - we don't want to pollute its results const { resolvedModule, failedLookupLocations } = loadModuleFromGlobalCache(moduleName, projectName, compilerOptions, host, globalCache); if (resolvedModule) { - return { resolvedModule, failedLookupLocations: primaryResult.failedLookupLocations.concat(failedLookupLocations) }; + return { resolvedModule, failedLookupLocations: addRange(primaryResult.failedLookupLocations as Array, failedLookupLocations) }; } } @@ -229,20 +233,20 @@ namespace ts { } type FailedLookupLocationAction = (failedLookupLocation: string, failedLookupLocationPath: Path, containingFile: string, name: string) => void; - function withFailedLookupLocations(failedLookupLocations: string[], containingFile: string, name: string, fn: FailedLookupLocationAction) { + function withFailedLookupLocations(failedLookupLocations: ReadonlyArray, containingFile: string, name: string, fn: FailedLookupLocationAction) { forEach(failedLookupLocations, failedLookupLocation => { fn(failedLookupLocation, toPath(failedLookupLocation), containingFile, name); }); } - function updateFailedLookupLocationWatches(containingFile: string, name: string, existingFailedLookupLocations: string[], failedLookupLocations: string[]) { + function updateFailedLookupLocationWatches(containingFile: string, name: string, existingFailedLookupLocations: ReadonlyArray | undefined, failedLookupLocations: ReadonlyArray) { if (failedLookupLocations) { if (existingFailedLookupLocations) { - const existingWatches = arrayToMap(existingFailedLookupLocations, failedLookupLocation => toPath(failedLookupLocation)); + const existingWatches = arrayToMap(existingFailedLookupLocations, toPath); for (const failedLookupLocation of failedLookupLocations) { const failedLookupLocationPath = toPath(failedLookupLocation); if (existingWatches && existingWatches.has(failedLookupLocationPath)) { - // still has same failed lookup location, keep the was + // still has same failed lookup location, keep the watch existingWatches.delete(failedLookupLocationPath); } else { @@ -272,7 +276,7 @@ namespace ts { cache: Map>, getResult: (s: T) => R, getResultFileName: (result: R) => string | undefined) { - cache.forEach((value, path: Path) => { + cache.forEach((value, path) => { if (path === deletedFilePath) { cache.delete(path); value.forEach((resolution, name) => { @@ -280,7 +284,7 @@ namespace ts { }); } else if (value) { - value.forEach((resolution, __name) => { + value.forEach(resolution => { if (resolution && !resolution.isInvalidated) { const result = getResult(resolution); if (result) { @@ -298,10 +302,10 @@ namespace ts { function invalidateResolutionCacheOfChangedFailedLookupLocation( failedLookupLocation: string, cache: Map>) { - cache.forEach((value, containingFile: Path) => { + cache.forEach((value, containingFile) => { if (value) { - value.forEach((resolution, __name) => { - if (resolution && !resolution.isInvalidated && contains(resolution.failedLookupLocations, failedLookupLocation)) { + value.forEach(resolution => { + if (resolution && !resolution.isInvalidated && some(resolution.failedLookupLocations, location => toPath(location) === failedLookupLocation)) { // Mark the file as needing re-evaluation of module resolution instead of using it blindly. resolution.isInvalidated = true; (filesWithInvalidatedResolutions || (filesWithInvalidatedResolutions = createMap())).set(containingFile, true); @@ -311,7 +315,7 @@ namespace ts { }); } - function invalidateResolutionOfDeletedFile(filePath: Path) { + function invalidateResolutionOfFile(filePath: Path) { invalidateResolutionCacheOfDeletedFile(filePath, resolvedModuleNames, m => m.resolvedModule, r => r.resolvedFileName); invalidateResolutionCacheOfDeletedFile(filePath, resolvedTypeReferenceDirectives, m => m.resolvedTypeReferenceDirective, r => r.resolvedFileName); } diff --git a/src/compiler/types.ts b/src/compiler/types.ts index 9fe93f55cb3c4..87d0ad153c310 100644 --- a/src/compiler/types.ts +++ b/src/compiler/types.ts @@ -4011,7 +4011,7 @@ namespace ts { export interface ResolvedModuleWithFailedLookupLocations { readonly resolvedModule: ResolvedModuleFull | undefined; /* @internal */ - readonly failedLookupLocations: string[]; + readonly failedLookupLocations: ReadonlyArray; /*@internal*/ isInvalidated?: boolean; } @@ -4025,7 +4025,7 @@ namespace ts { export interface ResolvedTypeReferenceDirectiveWithFailedLookupLocations { readonly resolvedTypeReferenceDirective: ResolvedTypeReferenceDirective; - readonly failedLookupLocations: string[]; + readonly failedLookupLocations: ReadonlyArray; /*@internal*/ isInvalidated?: boolean; } diff --git a/src/compiler/watchedProgram.ts b/src/compiler/watchedProgram.ts index 6423b9818969d..bd2473a699d6e 100644 --- a/src/compiler/watchedProgram.ts +++ b/src/compiler/watchedProgram.ts @@ -437,7 +437,7 @@ namespace ts { if (hostSourceFile !== undefined) { if (!isString(hostSourceFile)) { hostSourceFile.fileWatcher.close(); - resolutionCache.invalidateResolutionOfDeletedFile(path); + resolutionCache.invalidateResolutionOfFile(path); } sourceFilesCache.delete(path); } @@ -461,6 +461,7 @@ namespace ts { } else if (hostSourceFileInfo.sourceFile === oldSourceFile) { sourceFilesCache.delete(oldSourceFile.path); + resolutionCache.invalidateResolutionOfFile(oldSourceFile.path); } } } @@ -528,7 +529,7 @@ namespace ts { if (hostSourceFile) { // Update the cache if (eventKind === FileWatcherEventKind.Deleted) { - resolutionCache.invalidateResolutionOfDeletedFile(path); + resolutionCache.invalidateResolutionOfFile(path); if (!isString(hostSourceFile)) { hostSourceFile.fileWatcher.close(); sourceFilesCache.set(path, (hostSourceFile.version++).toString()); diff --git a/src/server/project.ts b/src/server/project.ts index fc6dca31ecdac..c5ee3aa42bb39 100644 --- a/src/server/project.ts +++ b/src/server/project.ts @@ -547,7 +547,7 @@ namespace ts.server { if (this.isRoot(info)) { this.removeRoot(info); } - this.resolutionCache.invalidateResolutionOfDeletedFile(info.path); + this.resolutionCache.invalidateResolutionOfFile(info.path); this.cachedUnresolvedImportsPerFile.remove(info.path); if (detachFromProject) { From b66b7525617ecb1bd5b70944224e6b80db7bf772 Mon Sep 17 00:00:00 2001 From: Sheetal Nandi Date: Fri, 18 Aug 2017 11:15:42 -0700 Subject: [PATCH 064/109] Update based on feedback --- src/compiler/core.ts | 2 +- src/compiler/sys.ts | 3 +++ src/harness/unittests/programMissingFiles.ts | 2 +- 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/src/compiler/core.ts b/src/compiler/core.ts index 1eab3582cb974..e1193d7f71bf2 100644 --- a/src/compiler/core.ts +++ b/src/compiler/core.ts @@ -2736,7 +2736,7 @@ namespace ts { function fileExists(fileName: string): boolean { const path = toPath(fileName); const result = getCachedFileSystemEntriesForBaseDir(path); - return (result && hasEntry(result.files, getBaseNameOfFileName(fileName))) || + return result && hasEntry(result.files, getBaseNameOfFileName(fileName)) || host.fileExists(fileName); } diff --git a/src/compiler/sys.ts b/src/compiler/sys.ts index aafb97f4131f0..982d679381924 100644 --- a/src/compiler/sys.ts +++ b/src/compiler/sys.ts @@ -30,6 +30,9 @@ namespace ts { mtime?: Date; } + /** + * Partial interface of the System thats needed to support the caching of directory structure + */ export interface PartialSystem { writeFile(path: string, data: string, writeByteOrderMark?: boolean): void; fileExists(path: string): boolean; diff --git a/src/harness/unittests/programMissingFiles.ts b/src/harness/unittests/programMissingFiles.ts index 20323b3738085..ce8abc232e746 100644 --- a/src/harness/unittests/programMissingFiles.ts +++ b/src/harness/unittests/programMissingFiles.ts @@ -3,7 +3,7 @@ namespace ts { function verifyMissingFilePaths(missingPaths: ReadonlyArray, expected: ReadonlyArray) { assert.isDefined(missingPaths); - const map = arrayToMap(expected, k => k, _v => true); + const map = arrayToSet(expected) as Map; for (const missing of missingPaths) { const value = map.get(missing); assert.isTrue(value, `${missing} to be ${value === undefined ? "not present" : "present only once"}, in actual: ${missingPaths} expected: ${expected}`); From da0d374b303d1dc517767508922b765a46700c20 Mon Sep 17 00:00:00 2001 From: Sheetal Nandi Date: Fri, 18 Aug 2017 11:55:47 -0700 Subject: [PATCH 065/109] Made updates to not expose methods/types that arent needed. --- src/compiler/commandLineParser.ts | 11 +++++---- src/server/project.ts | 37 +++++++++++++++++++++++-------- 2 files changed, 33 insertions(+), 15 deletions(-) diff --git a/src/compiler/commandLineParser.ts b/src/compiler/commandLineParser.ts index d09e4cab6d2a4..cc3047b6d3497 100644 --- a/src/compiler/commandLineParser.ts +++ b/src/compiler/commandLineParser.ts @@ -1479,6 +1479,7 @@ namespace ts { } } + /*@internal*/ export function isErrorNoInputFiles(error: Diagnostic) { return error.code === Diagnostics.No_inputs_were_found_in_config_file_0_Specified_include_paths_were_1_and_exclude_paths_were_2.code; } @@ -1975,15 +1976,13 @@ namespace ts { } /** - * Expands an array of file specifications. - * - * @param fileNames The literal file names to include. - * @param include The wildcard file specifications to include. - * @param exclude The wildcard file specifications to exclude. + * Gets the file names from the provided config file specs that contain, files, include, exclude and + * other properties needed to resolve the file names + * @param spec The config file specs extracted with file names to include, wildcards to include/exclude and other details * @param basePath The base path for any relative file specifications. * @param options Compiler options. * @param host The host used to resolve files and directories. - * @param errors An array for diagnostic reporting. + * @param extraFileExtensions optionaly file extra file extension information from host */ export function getFileNamesFromConfigSpecs(spec: ConfigFileSpecs, basePath: string, options: CompilerOptions, host: ParseConfigHost, extraFileExtensions: ReadonlyArray): ExpandResult { basePath = normalizePath(basePath); diff --git a/src/server/project.ts b/src/server/project.ts index dde5d8070e4aa..88a08bfc8638c 100644 --- a/src/server/project.ts +++ b/src/server/project.ts @@ -158,8 +158,6 @@ namespace ts.server { private typingFiles: SortedReadonlyArray; - protected projectErrors: Diagnostic[]; - public typesVersion = 0; public isNonTsProject() { @@ -233,12 +231,12 @@ namespace ts.server { /** * Get the errors that dont have any file name associated */ - getGlobalProjectErrors() { - return filter(this.projectErrors, diagnostic => !diagnostic.file); + getGlobalProjectErrors(): ReadonlyArray { + return emptyArray; } - getAllProjectErrors() { - return this.projectErrors; + getAllProjectErrors(): ReadonlyArray { + return emptyArray; } getLanguageService(ensureSynchronized = true): LanguageService { @@ -323,7 +321,6 @@ namespace ts.server { this.program = undefined; this.builder = undefined; this.cachedUnresolvedImportsPerFile = undefined; - this.projectErrors = undefined; this.lsHost.dispose(); this.lsHost = undefined; @@ -371,6 +368,7 @@ namespace ts.server { return result; } + /*@internal*/ getRootFilesMap() { return this.rootFilesMap; } @@ -496,7 +494,7 @@ namespace ts.server { this.markAsDirty(); } - // add a root file to project + // add a root file that doesnt exist on host addMissingFileRoot(fileName: NormalizedPath) { const path = this.projectService.toPath(fileName); this.rootFilesMap.set(path, fileName); @@ -1004,6 +1002,8 @@ namespace ts.server { /** Used for configured projects which may have multiple open roots */ openRefCount = 0; + private projectErrors: Diagnostic[]; + constructor(configFileName: NormalizedPath, projectService: ProjectService, documentRegistry: DocumentRegistry, @@ -1018,7 +1018,7 @@ namespace ts.server { } /** - * Checks if the project has reload from disk pending, if thats pending, it reloads (and then updates graph as part of that) instead of just updating the graph + * If the project has reload from disk pending, it reloads (and then updates graph as part of that) instead of just updating the graph * @returns: true if set of files in the project stays the same and false - otherwise. */ updateGraph(): boolean { @@ -1030,6 +1030,7 @@ namespace ts.server { return super.updateGraph(); } + /*@internal*/ getCachedServerHost() { return this.lsHost.host as CachedServerHost; } @@ -1119,6 +1120,20 @@ namespace ts.server { return getDirectoryPath(this.getConfigFilePath()); } + /** + * Get the errors that dont have any file name associated + */ + getGlobalProjectErrors(): ReadonlyArray { + return filter(this.projectErrors, diagnostic => !diagnostic.file); + } + + /** + * Get all the project errors + */ + getAllProjectErrors(): ReadonlyArray { + return this.projectErrors; + } + setProjectErrors(projectErrors: Diagnostic[]) { this.projectErrors = projectErrors; } @@ -1143,6 +1158,7 @@ namespace ts.server { })); } + /*@internal*/ watchWildcards(wildcardDirectories: Map) { mutateMap( this.directoriesWatchedForWildcards || (this.directoriesWatchedForWildcards = createMap()), @@ -1175,6 +1191,7 @@ namespace ts.server { this.projectService.closeDirectoryWatcher(WatchType.WildcardDirectories, this, directory, watcher, flags, closeReason); } + /*@internal*/ stopWatchingWildCards(reason: WatcherCloseReason) { if (this.directoriesWatchedForWildcards) { clearMap( @@ -1185,6 +1202,7 @@ namespace ts.server { } } + /*@internal*/ watchTypeRoots() { const newTypeRoots = arrayToSet(this.getEffectiveTypeRoots(), dir => this.projectService.toCanonicalFileName(dir)); mutateMap( @@ -1203,6 +1221,7 @@ namespace ts.server { ); } + /*@internal*/ stopWatchingTypeRoots(reason: WatcherCloseReason) { if (this.typeRootsWatchers) { clearMap( From 8deef58fd67877d6a5deccbf0f5530d7402c2856 Mon Sep 17 00:00:00 2001 From: Sheetal Nandi Date: Fri, 18 Aug 2017 12:15:03 -0700 Subject: [PATCH 066/109] Remove the unused function from the Project since builder has this logic now. --- src/compiler/program.ts | 5 +++- src/server/project.ts | 51 ----------------------------------------- 2 files changed, 4 insertions(+), 52 deletions(-) diff --git a/src/compiler/program.ts b/src/compiler/program.ts index 04af5c4d9726a..c3b83e1ba7c61 100644 --- a/src/compiler/program.ts +++ b/src/compiler/program.ts @@ -485,7 +485,10 @@ namespace ts { } /** - * Updates the existing wild card directory watches with the new set of wild card directories from the config file after new program is created + * Updates the existing wild card directory watches with the new set of wild card directories from the config file + * after new program is created because the config file was reloaded or program was created first time from the config file + * Note that there is no need to call this function when the program is updated with additional files without reloading config files, + * as wildcard directories wont change unless reloading config file */ export function updateWatchingWildcardDirectories( existingWatchedForWildcards: Map, diff --git a/src/server/project.ts b/src/server/project.ts index 2243dfc2b1d68..a2141d640350d 100644 --- a/src/server/project.ts +++ b/src/server/project.ts @@ -860,57 +860,6 @@ namespace ts.server { } } - getReferencedFiles(path: Path): Map { - const referencedFiles = createMap(); - if (!this.languageServiceEnabled) { - return referencedFiles; - } - - const sourceFile = this.getSourceFile(path); - if (!sourceFile) { - return referencedFiles; - } - // We need to use a set here since the code can contain the same import twice, - // but that will only be one dependency. - // To avoid invernal conversion, the key of the referencedFiles map must be of type Path - if (sourceFile.imports && sourceFile.imports.length > 0) { - const checker: TypeChecker = this.program.getTypeChecker(); - for (const importName of sourceFile.imports) { - const symbol = checker.getSymbolAtLocation(importName); - if (symbol && symbol.declarations && symbol.declarations[0]) { - const declarationSourceFile = symbol.declarations[0].getSourceFile(); - if (declarationSourceFile) { - referencedFiles.set(declarationSourceFile.path, true); - } - } - } - } - - const currentDirectory = getDirectoryPath(path); - // Handle triple slash references - if (sourceFile.referencedFiles && sourceFile.referencedFiles.length > 0) { - for (const referencedFile of sourceFile.referencedFiles) { - const referencedPath = this.projectService.toPath(referencedFile.fileName, currentDirectory); - referencedFiles.set(referencedPath, true); - } - } - - // Handle type reference directives - if (sourceFile.resolvedTypeReferenceDirectiveNames) { - sourceFile.resolvedTypeReferenceDirectiveNames.forEach((resolvedTypeReferenceDirective) => { - if (!resolvedTypeReferenceDirective) { - return; - } - - const fileName = resolvedTypeReferenceDirective.resolvedFileName; - const typeFilePath = this.projectService.toPath(fileName, currentDirectory); - referencedFiles.set(typeFilePath, true); - }); - } - - return referencedFiles; - } - // remove a root file from project protected removeRoot(info: ScriptInfo): void { orderedRemoveItem(this.rootFiles, info); From c4251280754dea435b60fd32d6928f446bea3e42 Mon Sep 17 00:00:00 2001 From: Sheetal Nandi Date: Fri, 18 Aug 2017 13:13:12 -0700 Subject: [PATCH 067/109] When getting default project from session, get it only if the script info is not orphan Since the closed script info's that are orphan arent removed immediately but on next open request, treat the orphan script infos as if they are not present in the session --- .../unittests/cachingInServerLSHost.ts | 2 +- .../unittests/tsserverProjectSystem.ts | 43 ++++++++++++++++++- src/server/editorServices.ts | 24 +++++------ src/server/scriptInfo.ts | 4 ++ 4 files changed, 59 insertions(+), 14 deletions(-) diff --git a/src/harness/unittests/cachingInServerLSHost.ts b/src/harness/unittests/cachingInServerLSHost.ts index 19ec12d7c5b00..74972d23d96ab 100644 --- a/src/harness/unittests/cachingInServerLSHost.ts +++ b/src/harness/unittests/cachingInServerLSHost.ts @@ -63,7 +63,7 @@ namespace ts { const projectService = new server.ProjectService(svcOpts); const rootScriptInfo = projectService.getOrCreateScriptInfo(rootFile, /* openedByClient */ true, /*containingProject*/ undefined); - const project = projectService.assignScriptInfoToInferredProject(rootScriptInfo); + const project = projectService.assignOrphanScriptInfoToInferredProject(rootScriptInfo); project.setCompilerOptions({ module: ts.ModuleKind.AMD, noLib: true } ); return { project, diff --git a/src/harness/unittests/tsserverProjectSystem.ts b/src/harness/unittests/tsserverProjectSystem.ts index 8abec41de4127..20fc4cb8a5888 100644 --- a/src/harness/unittests/tsserverProjectSystem.ts +++ b/src/harness/unittests/tsserverProjectSystem.ts @@ -202,7 +202,7 @@ namespace ts.projectSystem { typingsInstaller: undefined, byteLength: Utils.byteLength, hrtime: process.hrtime, - logger: nullLogger, + logger: opts.logger || nullLogger, canUseEvents: false }; @@ -2634,6 +2634,47 @@ namespace ts.projectSystem { checkNumberOfProjects(projectService, { configuredProjects: 0, externalProjects: 1, inferredProjects: 0 }); checkProjectActualFiles(projectService.externalProjects[0], [site.path, libFile.path]); }); + + it("Getting errors from closed script info does not throw exception (because of getting project from orphan script info)", () => { + let hasErrorMsg = false; + const { close, hasLevel, loggingEnabled, info, group, perftrc, getLogFileName } = nullLogger; + const logger: server.Logger = { + close, hasLevel, loggingEnabled, info, group, perftrc, getLogFileName, + err: () => { + hasErrorMsg = true; + } + }; + const f1 = { + path: "/a/b/app.ts", + content: "let x = 1;" + }; + const config = { + path: "/a/b/tsconfig.json", + content: JSON.stringify({ compilerOptions: {} }) + }; + const host = createServerHost([f1, libFile, config]); + const session = createSession(host, { logger }); + session.executeCommandSeq({ + command: server.CommandNames.Open, + arguments: { + file: f1.path + } + }); + session.executeCommandSeq({ + command: server.CommandNames.Close, + arguments: { + file: f1.path + } + }); + session.executeCommandSeq({ + command: server.CommandNames.Geterr, + arguments: { + delay: 0, + files: [f1.path] + } + }); + assert.isFalse(hasErrorMsg); + }); }); describe("Proper errors", () => { diff --git a/src/server/editorServices.ts b/src/server/editorServices.ts index 02e039ae88d87..9f24f1ffa3f82 100644 --- a/src/server/editorServices.ts +++ b/src/server/editorServices.ts @@ -564,7 +564,7 @@ namespace ts.server { this.ensureProjectStructuresUptoDate(); } const scriptInfo = this.getScriptInfoForNormalizedPath(fileName); - return scriptInfo && scriptInfo.getDefaultProject(); + return scriptInfo && !scriptInfo.isOrphan() && scriptInfo.getDefaultProject(); } /** @@ -588,7 +588,7 @@ namespace ts.server { else { projectsToUpdate = []; for (const f of this.changedFiles) { - projectsToUpdate = projectsToUpdate.concat(f.containingProjects); + addRange(projectsToUpdate, f.containingProjects); } } this.changedFiles = undefined; @@ -789,8 +789,8 @@ namespace ts.server { } /*@internal*/ - assignScriptInfoToInferredProject(info: ScriptInfo, projectRootPath?: string) { - Debug.assert(info.containingProjects.length === 0); + assignOrphanScriptInfoToInferredProject(info: ScriptInfo, projectRootPath?: string) { + Debug.assert(info.isOrphan()); const project = this.getOrCreateInferredProjectForProjectRootPathIfEnabled(info, projectRootPath) || this.getOrCreateSingleInferredProjectIfEnabled() || @@ -823,7 +823,7 @@ namespace ts.server { } private addToListOfOpenFiles(info: ScriptInfo) { - Debug.assert(info.containingProjects.length !== 0); + Debug.assert(!info.isOrphan()); for (const p of info.containingProjects) { // file is the part of configured project, addref the project if (p.projectKind === ProjectKind.Configured) { @@ -885,8 +885,8 @@ namespace ts.server { // collect orphaned files and assign them to inferred project just like we treat open of a file for (const f of this.openFiles) { - if (f.containingProjects.length === 0) { - this.assignScriptInfoToInferredProject(f); + if (f.isOrphan()) { + this.assignOrphanScriptInfoToInferredProject(f); } } @@ -907,7 +907,7 @@ namespace ts.server { private deleteOrphanScriptInfoNotInAnyProject() { this.filenameToScriptInfo.forEach(info => { - if (!info.isScriptOpen() && info.containingProjects.length === 0) { + if (!info.isScriptOpen() && info.isOrphan()) { // if there are not projects that include this script info - delete it this.stopWatchingScriptInfo(info, WatcherCloseReason.OrphanScriptInfo); this.filenameToScriptInfo.delete(info.path); @@ -1809,8 +1809,8 @@ namespace ts.server { for (const info of this.openFiles) { // collect all orphaned script infos from open files - if (info.containingProjects.length === 0) { - this.assignScriptInfoToInferredProject(info); + if (info.isOrphan()) { + this.assignOrphanScriptInfoToInferredProject(info); } else { // Or remove the root of inferred project if is referenced in more than one projects @@ -1867,8 +1867,8 @@ namespace ts.server { // At this point if file is part of any any configured or external project, then it would be present in the containing projects // So if it still doesnt have any containing projects, it needs to be part of inferred project - if (info.containingProjects.length === 0) { - this.assignScriptInfoToInferredProject(info, projectRootPath); + if (info.isOrphan()) { + this.assignOrphanScriptInfoToInferredProject(info, projectRootPath); } this.addToListOfOpenFiles(info); diff --git a/src/server/scriptInfo.ts b/src/server/scriptInfo.ts index 6d439e9238543..133879a2dcee2 100644 --- a/src/server/scriptInfo.ts +++ b/src/server/scriptInfo.ts @@ -328,6 +328,10 @@ namespace ts.server { } } + isOrphan() { + return this.containingProjects.length === 0; + } + /** * @param line 1 based index */ From 84b2e2303335d0583077b11f845da5b95046c157 Mon Sep 17 00:00:00 2001 From: Sheetal Nandi Date: Mon, 21 Aug 2017 11:39:04 -0700 Subject: [PATCH 068/109] More PR feedback work --- src/server/editorServices.ts | 18 +++++++++++++----- src/server/project.ts | 2 +- 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/src/server/editorServices.ts b/src/server/editorServices.ts index f1477fceac8b7..817f9eee60635 100644 --- a/src/server/editorServices.ts +++ b/src/server/editorServices.ts @@ -1718,7 +1718,7 @@ namespace ts.server { */ reloadProjects() { this.logger.info("reload projects."); - this.reloadConfiguredsProjectForFiles(this.openFiles, /*delayReload*/ false); + this.reloadConfiguredProjectForFiles(this.openFiles, /*delayReload*/ false); this.refreshInferredProjects(); } @@ -1734,7 +1734,7 @@ namespace ts.server { } } ); - this.reloadConfiguredsProjectForFiles(openFiles, /*delayReload*/ true); + this.reloadConfiguredProjectForFiles(openFiles, /*delayReload*/ true); this.delayInferredProjectsRefresh(); } @@ -1744,7 +1744,7 @@ namespace ts.server { * or schedules it for reload depending on delayReload option * If the there is no existing project it just opens the configured project for the config file */ - private reloadConfiguredsProjectForFiles(openFiles: ScriptInfo[], delayReload: boolean) { + private reloadConfiguredProjectForFiles(openFiles: ReadonlyArray, delayReload: boolean) { const updatedProjects = createMap(); // try to reload config file for all open files for (const info of openFiles) { @@ -1778,11 +1778,19 @@ namespace ts.server { */ private removeRootOfInferredProjectIfNowPartOfOtherProject(info: ScriptInfo) { // If the script info is root of inferred project, it could only be first containing project - // since info is added to inferred project and made root only when there are no other projects containing it - // So even if it is root of the inferred project and after project structure updates its now part + // since info is added as root to the inferred project only when there are no other projects containing it + // So when it is root of the inferred project and after project structure updates its now part // of multiple project it needs to be removed from that inferred project because: // - references in inferred project supercede the root part // - root / reference in non - inferred project beats root in inferred project + + // eg. say this is structure /a/b/a.ts /a/b/c.ts where c.ts references a.ts + // When a.ts is opened, since there is no configured project/external project a.ts can be part of + // a.ts is added as root to inferred project. + // Now at time of opening c.ts, c.ts is also not aprt of any existing project, + // so it will be added to inferred project as a root. (for sake of this example assume single inferred project is false) + // So at this poing a.ts is part of first inferred project and second inferred project (of which c.ts is root) + // And hence it needs to be removed from the first inferred project. if (info.containingProjects.length > 1 && info.containingProjects[0].projectKind === ProjectKind.Inferred && info.containingProjects[0].isRoot(info)) { diff --git a/src/server/project.ts b/src/server/project.ts index 88a08bfc8638c..5c199c2d81275 100644 --- a/src/server/project.ts +++ b/src/server/project.ts @@ -937,7 +937,7 @@ namespace ts.server { this.projectService.stopWatchingConfigFilesForInferredProjectRoot(info, WatcherCloseReason.NotNeeded); super.removeRoot(info); if (this._isJsInferredProject && info.isJavaScript()) { - if (!some(this.getRootScriptInfos(), info => info.isJavaScript())) { + if (every(this.getRootScriptInfos(), rootInfo => !rootInfo.isJavaScript())) { this.toggleJsInferredProject(/*isJsInferredProject*/ false); } } From 7173da2134d82bbd17c1aa2d97bd083fe63f9d88 Mon Sep 17 00:00:00 2001 From: Sheetal Nandi Date: Fri, 18 Aug 2017 15:38:47 -0700 Subject: [PATCH 069/109] Adding test for #16329 to verify the caching of file system when opening file --- .../unittests/tsserverProjectSystem.ts | 126 ++++++++++++++++++ 1 file changed, 126 insertions(+) diff --git a/src/harness/unittests/tsserverProjectSystem.ts b/src/harness/unittests/tsserverProjectSystem.ts index cccbcfa32c0c0..86fe129a1c7b5 100644 --- a/src/harness/unittests/tsserverProjectSystem.ts +++ b/src/harness/unittests/tsserverProjectSystem.ts @@ -3996,4 +3996,130 @@ namespace ts.projectSystem { } }); }); + + describe("CachingFileSystemInformation", () => { + function getFunctionWithCalledMapForSingleArgumentCb(cb: (f: string) => T) { + const calledMap = createMultiMap(); + return { + cb: (f: string) => { + calledMap.add(f, /*value*/ true); + return cb(f); + }, + calledMap + }; + } + + function getFunctionWithCalledMapForFiveArgumentCb(cb: (f: string, arg1?: U, arg2?: V, arg3?: W, arg4?: X) => T) { + const calledMap = createMultiMap<[U, V, W, X]>(); + return { + cb: (f: string, arg1?: U, arg2?: V, arg3?: W, arg4?: X) => { + calledMap.add(f, [arg1, arg2, arg3, arg4]); + return cb(f, arg1, arg2, arg3, arg4); + }, + calledMap + }; + } + + function checkMultiMapKeysForSingleEntry(caption: string, multiMap: MultiMap, expectedKeys: string[]) { + assert.equal(multiMap.size, expectedKeys.length, `${caption}: incorrect size of map: Actual keys: ${arrayFrom(multiMap.keys())} Expected: ${expectedKeys}`); + for (const name of expectedKeys) { + assert.isTrue(multiMap.has(name), `${caption} is expected to contain ${name}, actual keys: ${arrayFrom(multiMap.keys())}`); + assert.equal(multiMap.get(name).length, 1, `${caption} is expected to have just one entry for key ${name}, actual entry: ${multiMap.get(name)}`); + } + } + + it("when calling goto definition of module", () => { + const clientFile: FileOrFolder = { + path: "/a/b/controllers/vessels/client.ts", + content: ` + import { Vessel } from '~/models/vessel'; + const v = new Vessel(); + ` + }; + const anotherModuleFile: FileOrFolder = { + path: "/a/b/utils/db.ts", + content: "export class Bookshelf { }" + }; + const moduleFile: FileOrFolder = { + path: "/a/b/models/vessel.ts", + content: ` + import { Bookshelf } from '~/utils/db'; + export class Vessel extends Bookshelf {} + ` + }; + const tsconfigFile: FileOrFolder = { + path: "/a/b/tsconfig.json", + content: JSON.stringify({ + compilerOptions: { + target: "es6", + module: "es6", + baseUrl: "./", // all paths are relative to the baseUrl + paths: { + "~/*": ["*"] // resolve any `~/foo/bar` to `/foo/bar` + } + }, + exclude: [ + "api", + "build", + "node_modules", + "public", + "seeds", + "sql_updates", + "tests.build" + ] + }) + }; + + const projectFiles = [clientFile, anotherModuleFile, moduleFile, tsconfigFile]; + const host = createServerHost(projectFiles); + const session = createSession(host); + const projectService = session.getProjectService(); + const { configFileName } = projectService.openClientFile(clientFile.path); + + assert.isDefined(configFileName, `should find config`); + checkNumberOfConfiguredProjects(projectService, 1); + + const project = projectService.configuredProjects.get(tsconfigFile.path); + checkProjectActualFiles(project, map(projectFiles, f => f.path)); + + const fileExistsCalledOn = getFunctionWithCalledMapForSingleArgumentCb(host.fileExists.bind(host)); + host.fileExists = fileExistsCalledOn.cb; + const directoryExistsCalledOn = getFunctionWithCalledMapForSingleArgumentCb(host.directoryExists.bind(host)); + host.directoryExists = directoryExistsCalledOn.cb; + const getDirectoriesCalledOn = getFunctionWithCalledMapForSingleArgumentCb(host.getDirectories.bind(host)); + host.getDirectories = getDirectoriesCalledOn.cb; + const readFileCalledOn = getFunctionWithCalledMapForSingleArgumentCb(host.readFile.bind(host)); + host.readFile = readFileCalledOn.cb; + const readDirectoryCalledOn = getFunctionWithCalledMapForFiveArgumentCb, ReadonlyArray, ReadonlyArray, number>(host.readDirectory.bind(host)); + host.readDirectory = readDirectoryCalledOn.cb; + + + // Get definitions shouldnt make host requests + const getDefinitionRequest = makeSessionRequest(protocol.CommandTypes.Definition, { + file: clientFile.path, + position: clientFile.content.indexOf("/vessel") + 1, + line: undefined, + offset: undefined + }); + const { response } = session.executeCommand(getDefinitionRequest); + assert.equal(response[0].file, moduleFile.path, "Should go to definition of vessel: response: " + JSON.stringify(response)); + assert.equal(fileExistsCalledOn.calledMap.size, 0, `fileExists shouldnt be called`); + assert.equal(directoryExistsCalledOn.calledMap.size, 0, `directoryExists shouldnt be called`); + assert.equal(getDirectoriesCalledOn.calledMap.size, 0, `getDirectories shouldnt be called`); + assert.equal(readFileCalledOn.calledMap.size, 0, `readFile shouldnt be called`); + assert.equal(readDirectoryCalledOn.calledMap.size, 0, `readDirectory shouldnt be called`); + + // Open the file should call only file exists on module directory and use cached value for parental directory + const { configFileName: config2 } = projectService.openClientFile(moduleFile.path); + assert.equal(config2, configFileName); + checkMultiMapKeysForSingleEntry("fileExists", fileExistsCalledOn.calledMap, ["/a/b/models/tsconfig.json", "/a/b/models/jsconfig.json"]); + assert.equal(directoryExistsCalledOn.calledMap.size, 0, `directoryExists shouldnt be called`); + assert.equal(getDirectoriesCalledOn.calledMap.size, 0, `getDirectories shouldnt be called`); + assert.equal(readFileCalledOn.calledMap.size, 0, `readFile shouldnt be called`); + assert.equal(readDirectoryCalledOn.calledMap.size, 0, `readDirectory shouldnt be called`); + + checkNumberOfConfiguredProjects(projectService, 1); + assert.strictEqual(projectService.configuredProjects.get(tsconfigFile.path), project); + }); + }); } From e500be28cdb54c0644db1b1efb1e817263771a07 Mon Sep 17 00:00:00 2001 From: Sheetal Nandi Date: Fri, 18 Aug 2017 17:28:38 -0700 Subject: [PATCH 070/109] Adding test for #16456 to verify watched directories in case-sensitive and non sensitive file system and fixing caching --- src/compiler/core.ts | 36 +-- src/compiler/resolutionCache.ts | 12 +- src/compiler/watchedProgram.ts | 4 +- .../unittests/cachingInServerLSHost.ts | 2 +- src/harness/unittests/tscWatchMode.ts | 5 +- .../unittests/tsserverProjectSystem.ts | 250 +++++++++++++++--- src/harness/virtualFileSystemWithWatch.ts | 9 +- src/server/editorServices.ts | 15 +- src/server/project.ts | 11 +- 9 files changed, 260 insertions(+), 84 deletions(-) diff --git a/src/compiler/core.ts b/src/compiler/core.ts index c93c9639ab268..00e8065a9f903 100644 --- a/src/compiler/core.ts +++ b/src/compiler/core.ts @@ -2076,8 +2076,8 @@ namespace ts { } export interface FileSystemEntries { - files: ReadonlyArray; - directories: ReadonlyArray; + readonly files: ReadonlyArray; + readonly directories: ReadonlyArray; } export interface FileMatcherPatterns { @@ -2644,8 +2644,13 @@ namespace ts { export interface CachedPartialSystem extends PartialSystem, CachedHost { } + interface MutableFileSystemEntries { + readonly files: string[]; + readonly directories: string[]; + } + export function createCachedPartialSystem(host: HostForCaching): CachedPartialSystem { - const cachedReadDirectoryResult = createMap(); + const cachedReadDirectoryResult = createMap(); const getCurrentDirectory = memoize(() => host.getCurrentDirectory()); const getCanonicalFileName = createGetCanonicalFileName(host.useCaseSensitiveFileNames); return { @@ -2665,11 +2670,11 @@ namespace ts { return ts.toPath(fileName, getCurrentDirectory(), getCanonicalFileName); } - function getCachedFileSystemEntries(rootDirPath: Path): FileSystemEntries | undefined { + function getCachedFileSystemEntries(rootDirPath: Path): MutableFileSystemEntries | undefined { return cachedReadDirectoryResult.get(rootDirPath); } - function getCachedFileSystemEntriesForBaseDir(path: Path): FileSystemEntries | undefined { + function getCachedFileSystemEntriesForBaseDir(path: Path): MutableFileSystemEntries | undefined { return getCachedFileSystemEntries(getDirectoryPath(path)); } @@ -2678,8 +2683,8 @@ namespace ts { } function createCachedFileSystemEntries(rootDir: string, rootDirPath: Path) { - const resultFromHost: FileSystemEntries = { - files: host.readDirectory(rootDir, /*extensions*/ undefined, /*exclude*/ undefined, /*include*/["*.*"]) || [], + const resultFromHost: MutableFileSystemEntries = { + files: map(host.readDirectory(rootDir, /*extensions*/ undefined, /*exclude*/ undefined, /*include*/["*.*"]), getBaseNameOfFileName) || [], directories: host.getDirectories(rootDir) || [] }; @@ -2692,7 +2697,7 @@ namespace ts { * Otherwise gets result from host and caches it. * The host request is done under try catch block to avoid caching incorrect result */ - function tryReadDirectory(rootDir: string, rootDirPath: Path): FileSystemEntries | undefined { + function tryReadDirectory(rootDir: string, rootDirPath: Path): MutableFileSystemEntries | undefined { const cachedResult = getCachedFileSystemEntries(rootDirPath); if (cachedResult) { return cachedResult; @@ -2716,16 +2721,15 @@ namespace ts { return some(entries, file => fileNameEqual(file, name)); } - function updateFileSystemEntry(entries: ReadonlyArray, baseName: string, isValid: boolean) { + function updateFileSystemEntry(entries: string[], baseName: string, isValid: boolean) { if (hasEntry(entries, baseName)) { if (!isValid) { - return filter(entries, entry => !fileNameEqual(entry, baseName)); + return filterMutate(entries, entry => !fileNameEqual(entry, baseName)); } } else if (isValid) { - return entries.concat(baseName); + return entries.push(baseName); } - return entries; } function writeFile(fileName: string, data: string, writeByteOrderMark?: boolean): void { @@ -2754,7 +2758,7 @@ namespace ts { const result = getCachedFileSystemEntriesForBaseDir(path); const baseFileName = getBaseNameOfFileName(dirPath); if (result) { - result.directories = updateFileSystemEntry(result.directories, baseFileName, /*isValid*/ true); + updateFileSystemEntry(result.directories, baseFileName, /*isValid*/ true); } host.createDirectory(dirPath); } @@ -2801,7 +2805,7 @@ namespace ts { const baseName = getBaseNameOfFileName(fileOrFolder); if (parentResult) { updateFilesOfFileSystemEntry(parentResult, baseName, host.fileExists(fileOrFolderPath)); - parentResult.directories = updateFileSystemEntry(parentResult.directories, baseName, host.directoryExists(fileOrFolderPath)); + updateFileSystemEntry(parentResult.directories, baseName, host.directoryExists(fileOrFolderPath)); } } } @@ -2818,8 +2822,8 @@ namespace ts { } } - function updateFilesOfFileSystemEntry(parentResult: FileSystemEntries, baseName: string, fileExists: boolean) { - parentResult.files = updateFileSystemEntry(parentResult.files, baseName, fileExists); + function updateFilesOfFileSystemEntry(parentResult: MutableFileSystemEntries, baseName: string, fileExists: boolean) { + updateFileSystemEntry(parentResult.files, baseName, fileExists); } function clearCache() { diff --git a/src/compiler/resolutionCache.ts b/src/compiler/resolutionCache.ts index 4fefd87a834f7..e331133fe7449 100644 --- a/src/compiler/resolutionCache.ts +++ b/src/compiler/resolutionCache.ts @@ -14,7 +14,7 @@ namespace ts { resolveTypeReferenceDirectives(typeDirectiveNames: string[], containingFile: string): ResolvedTypeReferenceDirective[]; invalidateResolutionOfFile(filePath: Path): void; - invalidateResolutionOfChangedFailedLookupLocation(failedLookupLocation: string): void; + invalidateResolutionOfChangedFailedLookupLocation(failedLookupLocationPath: Path): void; createHasInvalidatedResolution(): HasInvalidatedResolution; @@ -300,12 +300,12 @@ namespace ts { } function invalidateResolutionCacheOfChangedFailedLookupLocation( - failedLookupLocation: string, + failedLookupLocationPath: Path, cache: Map>) { cache.forEach((value, containingFile) => { if (value) { value.forEach(resolution => { - if (resolution && !resolution.isInvalidated && some(resolution.failedLookupLocations, location => toPath(location) === failedLookupLocation)) { + if (resolution && !resolution.isInvalidated && some(resolution.failedLookupLocations, location => toPath(location) === failedLookupLocationPath)) { // Mark the file as needing re-evaluation of module resolution instead of using it blindly. resolution.isInvalidated = true; (filesWithInvalidatedResolutions || (filesWithInvalidatedResolutions = createMap())).set(containingFile, true); @@ -320,9 +320,9 @@ namespace ts { invalidateResolutionCacheOfDeletedFile(filePath, resolvedTypeReferenceDirectives, m => m.resolvedTypeReferenceDirective, r => r.resolvedFileName); } - function invalidateResolutionOfChangedFailedLookupLocation(failedLookupLocation: string) { - invalidateResolutionCacheOfChangedFailedLookupLocation(failedLookupLocation, resolvedModuleNames); - invalidateResolutionCacheOfChangedFailedLookupLocation(failedLookupLocation, resolvedTypeReferenceDirectives); + function invalidateResolutionOfChangedFailedLookupLocation(failedLookupLocationPath: Path) { + invalidateResolutionCacheOfChangedFailedLookupLocation(failedLookupLocationPath, resolvedModuleNames); + invalidateResolutionCacheOfChangedFailedLookupLocation(failedLookupLocationPath, resolvedTypeReferenceDirectives); } } } diff --git a/src/compiler/watchedProgram.ts b/src/compiler/watchedProgram.ts index bd2473a699d6e..084c707cea8fa 100644 --- a/src/compiler/watchedProgram.ts +++ b/src/compiler/watchedProgram.ts @@ -564,7 +564,7 @@ namespace ts { function onFailedLookupLocationChange(fileName: string, eventKind: FileWatcherEventKind, failedLookupLocation: string, failedLookupLocationPath: Path, containingFile: string, name: string) { writeLog(`Failed lookup location : ${failedLookupLocation} changed: ${FileWatcherEventKind[eventKind]}, fileName: ${fileName} containingFile: ${containingFile}, name: ${name}`); updateCachedSystemWithFile(fileName, failedLookupLocationPath, eventKind); - resolutionCache.invalidateResolutionOfChangedFailedLookupLocation(failedLookupLocation); + resolutionCache.invalidateResolutionOfChangedFailedLookupLocation(failedLookupLocationPath); scheduleProgramUpdate(); } @@ -633,7 +633,7 @@ namespace ts { // Reload is pending, do the reload if (!needsReload) { const result = getFileNamesFromConfigSpecs(configFileSpecs, getDirectoryPath(configFileName), compilerOptions, host); - if (!configFileSpecs.filesSpecs) { + if (!configFileSpecs.filesSpecs && result.fileNames.length === 0) { reportDiagnostic(getErrorForNoInputFiles(configFileSpecs, configFileName)); } rootFileNames = result.fileNames; diff --git a/src/harness/unittests/cachingInServerLSHost.ts b/src/harness/unittests/cachingInServerLSHost.ts index 079eabebc1e63..b7b382e5a1d0e 100644 --- a/src/harness/unittests/cachingInServerLSHost.ts +++ b/src/harness/unittests/cachingInServerLSHost.ts @@ -61,7 +61,7 @@ namespace ts { typingsInstaller: undefined }; const projectService = new server.ProjectService(svcOpts); - const rootScriptInfo = projectService.getOrCreateScriptInfo(rootFile, /* openedByClient */ true, /*containingProject*/ undefined); + const rootScriptInfo = projectService.getOrCreateScriptInfo(rootFile, /* openedByClient */ true, serverHost); const project = projectService.assignOrphanScriptInfoToInferredProject(rootScriptInfo); project.setCompilerOptions({ module: ts.ModuleKind.AMD, noLib: true } ); diff --git a/src/harness/unittests/tscWatchMode.ts b/src/harness/unittests/tscWatchMode.ts index e8cba10f0880c..f5ed08b9b872c 100644 --- a/src/harness/unittests/tscWatchMode.ts +++ b/src/harness/unittests/tscWatchMode.ts @@ -130,7 +130,7 @@ namespace ts.tscWatch { const host = createWatchedSystem([f1, config], { useCaseSensitiveFileNames: false }); const upperCaseConfigFilePath = combinePaths(getDirectoryPath(config.path).toUpperCase(), getBaseFileName(config.path)); const watch = createWatchModeWithConfigFile(upperCaseConfigFilePath, host); - checkProgramActualFiles(watch(), [f1.path]); + checkProgramActualFiles(watch(), [combinePaths(getDirectoryPath(upperCaseConfigFilePath), getBaseFileName(f1.path))]); }); it("create configured project without file list", () => { @@ -722,12 +722,13 @@ namespace ts.tscWatch { const host = createWatchedSystem([moduleFile, file1, configFile, libFile]); createWatchModeWithConfigFile(configFile.path, host); - const error = `error TS6053: File '${moduleFile.path}' not found.${host.newLine}`; + const error = "a/b/file1.ts(1,20): error TS2307: Cannot find module \'./moduleFile\'.\n"; checkOutputDoesNotContain(host, [error]); const moduleFileOldPath = moduleFile.path; const moduleFileNewPath = "/a/b/moduleFile1.ts"; moduleFile.path = moduleFileNewPath; + host.clearOutput(); host.reloadFS([moduleFile, file1, configFile, libFile]); host.runQueuedTimeoutCallbacks(); checkOutputContains(host, [error]); diff --git a/src/harness/unittests/tsserverProjectSystem.ts b/src/harness/unittests/tsserverProjectSystem.ts index 86fe129a1c7b5..37cbd20521d1f 100644 --- a/src/harness/unittests/tsserverProjectSystem.ts +++ b/src/harness/unittests/tsserverProjectSystem.ts @@ -3998,33 +3998,206 @@ namespace ts.projectSystem { }); describe("CachingFileSystemInformation", () => { - function getFunctionWithCalledMapForSingleArgumentCb(cb: (f: string) => T) { - const calledMap = createMultiMap(); + type CalledMaps = { + fileExists: MultiMap; + directoryExists: MultiMap; + getDirectories: MultiMap; + readFile: MultiMap; + readDirectory: MultiMap<[ReadonlyArray, ReadonlyArray, ReadonlyArray, number]>; + }; + + function createCallsTrackingHost(host: TestServerHost) { + const keys: Array = ["fileExists", "directoryExists", "getDirectories", "readFile", "readDirectory"]; + const calledMaps = getCallsTrackingMap(); return { - cb: (f: string) => { - calledMap.add(f, /*value*/ true); - return cb(f); - }, - calledMap - }; + verifyNoCall, + verifyCalledOnEachEntryOnce, + verifyCalledOnEachEntry, + verifyNoHostCalls, + verifyNoHostCallsExceptFileExistsOnce, + clear + }; + + function getCallsTrackingMap() { + const calledMaps: { [s: string]: Map } = {}; + for (let i = 0; i < keys.length - 1; i++) { + setCallsTrackingWithSingleArgFn(keys[i]); + } + setCallsTrackingWithFiveArgFn(keys[keys.length - 1]); + return calledMaps as CalledMaps; + + function setCallsTrackingWithSingleArgFn(prop: keyof CalledMaps) { + const calledMap = createMultiMap(); + const cb = (host)[prop].bind(host); + (host)[prop] = (f: string) => { + calledMap.add(f, /*value*/ true); + return cb(f); + }; + calledMaps[prop] = calledMap; + } + + function setCallsTrackingWithFiveArgFn(prop: keyof CalledMaps) { + const calledMap = createMultiMap<[U, V, W, X]>(); + const cb = (host)[prop].bind(host); + (host)[prop] = (f: string, arg1?: U, arg2?: V, arg3?: W, arg4?: X) => { + calledMap.add(f, [arg1, arg2, arg3, arg4]); + return cb(f, arg1, arg2, arg3, arg4); + }; + calledMaps[prop] = calledMap; + } + } + + function verifyNoCall(callback: keyof CalledMaps) { + const calledMap = calledMaps[callback]; + assert.equal(calledMap.size, 0, `${callback} shouldnt be called: ${arrayFrom(calledMap.keys())}`); + } + + function verifyCalledOnEachEntry(callback: keyof CalledMaps, expectedKeys: Map) { + const calledMap = calledMaps[callback]; + assert.equal(calledMap.size, expectedKeys.size, `${callback}: incorrect size of map: Actual keys: ${arrayFrom(calledMap.keys())} Expected: ${arrayFrom(expectedKeys.keys())}`); + expectedKeys.forEach((called, name) => { + assert.isTrue(calledMap.has(name), `${callback} is expected to contain ${name}, actual keys: ${arrayFrom(calledMap.keys())}`); + assert.equal(calledMap.get(name).length, called, `${callback} is expected to be called ${called} times with ${name}. Actual entry: ${calledMap.get(name)}`); + }); + } + + function verifyCalledOnEachEntryOnce(callback: keyof CalledMaps, expectedKeys: string[]) { + return verifyCalledOnEachEntry(callback, zipToMap(expectedKeys, expectedKeys.map(() => 1))); + } + + function verifyNoHostCalls() { + for (const key of keys) { + verifyNoCall(key); + } + } + + function verifyNoHostCallsExceptFileExistsOnce(expectedKeys: string[]) { + verifyCalledOnEachEntryOnce("fileExists", expectedKeys); + verifyNoCall("directoryExists"); + verifyNoCall("getDirectories"); + verifyNoCall("readFile"); + verifyNoCall("readDirectory"); + } + + function clear() { + for (const key of keys) { + calledMaps[key].clear(); + } + } } - function getFunctionWithCalledMapForFiveArgumentCb(cb: (f: string, arg1?: U, arg2?: V, arg3?: W, arg4?: X) => T) { - const calledMap = createMultiMap<[U, V, W, X]>(); - return { - cb: (f: string, arg1?: U, arg2?: V, arg3?: W, arg4?: X) => { - calledMap.add(f, [arg1, arg2, arg3, arg4]); - return cb(f, arg1, arg2, arg3, arg4); - }, - calledMap + function verifyWatchDirectoriesCaseSensitivity(useCaseSensitiveFileNames: boolean) { + const frontendDir = "/Users/someuser/work/applications/frontend"; + const canonicalFrontendDir = useCaseSensitiveFileNames ? frontendDir : frontendDir.toLowerCase(); + const file1: FileOrFolder = { + path: `${frontendDir}/src/app/utils/Analytic.ts`, + content: "export class SomeClass { };" }; - } + const file2: FileOrFolder = { + path: `${frontendDir}/src/app/redux/configureStore.ts`, + content: "export class configureStore { }" + }; + const file3: FileOrFolder = { + path: `${frontendDir}/src/app/utils/Cookie.ts`, + content: "export class Cookie { }" + }; + const es2016LibFile: FileOrFolder = { + path: "/a/lib/lib.es2016.full.d.ts", + content: libFile.content + }; + const tsconfigFile: FileOrFolder = { + path: `${frontendDir}/tsconfig.json`, + content: JSON.stringify({ + "compilerOptions": { + "strict": true, + "strictNullChecks": true, + "target": "es2016", + "module": "commonjs", + "moduleResolution": "node", + "sourceMap": true, + "noEmitOnError": true, + "experimentalDecorators": true, + "emitDecoratorMetadata": true, + "types": [ + "node", + "jest" + ], + "noUnusedLocals": true, + "outDir": "./compiled", + "typeRoots": [ + "types", + "node_modules/@types" + ], + "baseUrl": ".", + "paths": { + "*": [ + "types/*" + ] + } + }, + "include": [ + "src/**/*" + ], + "exclude": [ + "node_modules", + "compiled" + ] + }) + }; + const projectFiles = [file1, file2, es2016LibFile, tsconfigFile]; + const host = createServerHost(projectFiles, { useCaseSensitiveFileNames }); + const projectService = createProjectService(host); + const canonicalConfigPath = useCaseSensitiveFileNames ? tsconfigFile.path : tsconfigFile.path.toLowerCase(); + const { configFileName } = projectService.openClientFile(file1.path); + assert.equal(configFileName, tsconfigFile.path, `should find config`); + checkNumberOfConfiguredProjects(projectService, 1); + + const project = projectService.configuredProjects.get(canonicalConfigPath); + verifyProjectAndWatchedDirectories(); + + const callsTrackingHost = createCallsTrackingHost(host); + + // Create file cookie.ts + projectFiles.push(file3); + host.reloadFS(projectFiles); + host.runQueuedTimeoutCallbacks(); + + const canonicalFile3Path = useCaseSensitiveFileNames ? file3.path : file3.path.toLocaleLowerCase(); + callsTrackingHost.verifyCalledOnEachEntryOnce("fileExists", [canonicalFile3Path]); + + // Called for type root resolution + const directoryExistsCalled = createMap(); + for (let dir = frontendDir; dir !== "/"; dir = getDirectoryPath(dir)) { + directoryExistsCalled.set(`${dir}/node_modules`, 2); + } + directoryExistsCalled.set(`/node_modules`, 2); + directoryExistsCalled.set(`${frontendDir}/types`, 2); + directoryExistsCalled.set(`${frontendDir}/node_modules/@types`, 2); + directoryExistsCalled.set(canonicalFile3Path, 1); + callsTrackingHost.verifyCalledOnEachEntry("directoryExists", directoryExistsCalled); + + callsTrackingHost.verifyNoCall("getDirectories"); + callsTrackingHost.verifyCalledOnEachEntryOnce("readFile", [file3.path]); + callsTrackingHost.verifyNoCall("readDirectory"); + + checkNumberOfConfiguredProjects(projectService, 1); + assert.strictEqual(projectService.configuredProjects.get(canonicalConfigPath), project); + verifyProjectAndWatchedDirectories(); + + callsTrackingHost.clear(); - function checkMultiMapKeysForSingleEntry(caption: string, multiMap: MultiMap, expectedKeys: string[]) { - assert.equal(multiMap.size, expectedKeys.length, `${caption}: incorrect size of map: Actual keys: ${arrayFrom(multiMap.keys())} Expected: ${expectedKeys}`); - for (const name of expectedKeys) { - assert.isTrue(multiMap.has(name), `${caption} is expected to contain ${name}, actual keys: ${arrayFrom(multiMap.keys())}`); - assert.equal(multiMap.get(name).length, 1, `${caption} is expected to have just one entry for key ${name}, actual entry: ${multiMap.get(name)}`); + const { configFileName: configFile2 } = projectService.openClientFile(file3.path); + assert.equal(configFile2, configFileName); + + checkNumberOfConfiguredProjects(projectService, 1); + assert.strictEqual(projectService.configuredProjects.get(canonicalConfigPath), project); + verifyProjectAndWatchedDirectories(); + callsTrackingHost.verifyNoHostCalls(); + + function verifyProjectAndWatchedDirectories() { + checkProjectActualFiles(project, map(projectFiles, f => f.path)); + checkWatchedDirectories(host, [`${canonicalFrontendDir}/src`], /*recursive*/ true); + checkWatchedDirectories(host, [`${canonicalFrontendDir}/types`, `${canonicalFrontendDir}/node_modules/@types`], /*recursive*/ false); } } @@ -4069,7 +4242,6 @@ namespace ts.projectSystem { ] }) }; - const projectFiles = [clientFile, anotherModuleFile, moduleFile, tsconfigFile]; const host = createServerHost(projectFiles); const session = createSession(host); @@ -4082,17 +4254,7 @@ namespace ts.projectSystem { const project = projectService.configuredProjects.get(tsconfigFile.path); checkProjectActualFiles(project, map(projectFiles, f => f.path)); - const fileExistsCalledOn = getFunctionWithCalledMapForSingleArgumentCb(host.fileExists.bind(host)); - host.fileExists = fileExistsCalledOn.cb; - const directoryExistsCalledOn = getFunctionWithCalledMapForSingleArgumentCb(host.directoryExists.bind(host)); - host.directoryExists = directoryExistsCalledOn.cb; - const getDirectoriesCalledOn = getFunctionWithCalledMapForSingleArgumentCb(host.getDirectories.bind(host)); - host.getDirectories = getDirectoriesCalledOn.cb; - const readFileCalledOn = getFunctionWithCalledMapForSingleArgumentCb(host.readFile.bind(host)); - host.readFile = readFileCalledOn.cb; - const readDirectoryCalledOn = getFunctionWithCalledMapForFiveArgumentCb, ReadonlyArray, ReadonlyArray, number>(host.readDirectory.bind(host)); - host.readDirectory = readDirectoryCalledOn.cb; - + const callsTrackingHost = createCallsTrackingHost(host); // Get definitions shouldnt make host requests const getDefinitionRequest = makeSessionRequest(protocol.CommandTypes.Definition, { @@ -4103,23 +4265,23 @@ namespace ts.projectSystem { }); const { response } = session.executeCommand(getDefinitionRequest); assert.equal(response[0].file, moduleFile.path, "Should go to definition of vessel: response: " + JSON.stringify(response)); - assert.equal(fileExistsCalledOn.calledMap.size, 0, `fileExists shouldnt be called`); - assert.equal(directoryExistsCalledOn.calledMap.size, 0, `directoryExists shouldnt be called`); - assert.equal(getDirectoriesCalledOn.calledMap.size, 0, `getDirectories shouldnt be called`); - assert.equal(readFileCalledOn.calledMap.size, 0, `readFile shouldnt be called`); - assert.equal(readDirectoryCalledOn.calledMap.size, 0, `readDirectory shouldnt be called`); + callsTrackingHost.verifyNoHostCalls(); // Open the file should call only file exists on module directory and use cached value for parental directory const { configFileName: config2 } = projectService.openClientFile(moduleFile.path); assert.equal(config2, configFileName); - checkMultiMapKeysForSingleEntry("fileExists", fileExistsCalledOn.calledMap, ["/a/b/models/tsconfig.json", "/a/b/models/jsconfig.json"]); - assert.equal(directoryExistsCalledOn.calledMap.size, 0, `directoryExists shouldnt be called`); - assert.equal(getDirectoriesCalledOn.calledMap.size, 0, `getDirectories shouldnt be called`); - assert.equal(readFileCalledOn.calledMap.size, 0, `readFile shouldnt be called`); - assert.equal(readDirectoryCalledOn.calledMap.size, 0, `readDirectory shouldnt be called`); + callsTrackingHost.verifyNoHostCallsExceptFileExistsOnce(["/a/b/models/tsconfig.json", "/a/b/models/jsconfig.json"]); checkNumberOfConfiguredProjects(projectService, 1); assert.strictEqual(projectService.configuredProjects.get(tsconfigFile.path), project); }); + + it("WatchDirectories for config file With non case sensitive file system", () => { + verifyWatchDirectoriesCaseSensitivity(/*useCaseSensitiveFileNames*/ false); + }); + + it("WatchDirectories for config file With case sensitive file system", () => { + verifyWatchDirectoriesCaseSensitivity(/*useCaseSensitiveFileNames*/ true); + }); }); } diff --git a/src/harness/virtualFileSystemWithWatch.ts b/src/harness/virtualFileSystemWithWatch.ts index b77618df36061..44beb83922f3d 100644 --- a/src/harness/virtualFileSystemWithWatch.ts +++ b/src/harness/virtualFileSystemWithWatch.ts @@ -197,9 +197,12 @@ namespace ts.TestFSWithWatch { this.reloadFS(fileOrFolderList); } + toNormalizedAbsolutePath(s: string) { + return getNormalizedAbsolutePath(s, this.currentDirectory); + } + toFullPath(s: string) { - const fullPath = getNormalizedAbsolutePath(s, this.currentDirectory); - return this.toPath(fullPath); + return this.toPath(this.toNormalizedAbsolutePath(s)); } reloadFS(fileOrFolderList: FileOrFolder[]) { @@ -413,7 +416,7 @@ namespace ts.TestFSWithWatch { } readDirectory(path: string, extensions?: ReadonlyArray, exclude?: ReadonlyArray, include?: ReadonlyArray, depth?: number): string[] { - return ts.matchFiles(this.toFullPath(path), extensions, exclude, include, this.useCaseSensitiveFileNames, this.getCurrentDirectory(), depth, (dir) => { + return ts.matchFiles(this.toNormalizedAbsolutePath(path), extensions, exclude, include, this.useCaseSensitiveFileNames, this.getCurrentDirectory(), depth, (dir) => { const directories: string[] = []; const files: string[] = []; const dirEntry = this.fs.get(this.toPath(dir)); diff --git a/src/server/editorServices.ts b/src/server/editorServices.ts index fb4419df79d93..6cbc5263249a0 100644 --- a/src/server/editorServices.ts +++ b/src/server/editorServices.ts @@ -1429,7 +1429,7 @@ namespace ts.server { else { const scriptKind = propertyReader.getScriptKind(f); const hasMixedContent = propertyReader.hasMixedContent(f, this.hostConfiguration.extraFileExtensions); - scriptInfo = this.getOrCreateScriptInfoForNormalizedPath(normalizedPath, /*openedByClient*/ clientFileName === newRootFile, /*fileContent*/ undefined, scriptKind, hasMixedContent); + scriptInfo = this.getOrCreateScriptInfoForNormalizedPath(normalizedPath, /*openedByClient*/ clientFileName === newRootFile, /*fileContent*/ undefined, scriptKind, hasMixedContent, project.lsHost.host); path = scriptInfo.path; // If this script info is not already a root add it if (!project.isRoot(scriptInfo)) { @@ -1583,9 +1583,12 @@ namespace ts.server { * @param uncheckedFileName is absolute pathname * @param fileContent is a known version of the file content that is more up to date than the one on disk */ - - getOrCreateScriptInfo(uncheckedFileName: string, openedByClient: boolean, fileContent?: string, scriptKind?: ScriptKind) { - return this.getOrCreateScriptInfoForNormalizedPath(toNormalizedPath(uncheckedFileName), openedByClient, fileContent, scriptKind); + /*@internal*/ + getOrCreateScriptInfo(uncheckedFileName: string, openedByClient: boolean, hostToQueryFileExistsOn: ServerHost) { + return this.getOrCreateScriptInfoForNormalizedPath( + toNormalizedPath(uncheckedFileName), openedByClient, /*fileContent*/ undefined, + /*scriptKind*/ undefined, /*hasMixedContent*/ undefined, hostToQueryFileExistsOn + ); } getScriptInfo(uncheckedFileName: string) { @@ -1610,11 +1613,11 @@ namespace ts.server { } } - getOrCreateScriptInfoForNormalizedPath(fileName: NormalizedPath, openedByClient: boolean, fileContent?: string, scriptKind?: ScriptKind, hasMixedContent?: boolean) { + getOrCreateScriptInfoForNormalizedPath(fileName: NormalizedPath, openedByClient: boolean, fileContent?: string, scriptKind?: ScriptKind, hasMixedContent?: boolean, hostToQueryFileExistsOn?: ServerHost) { const path = normalizedPathToPath(fileName, this.currentDirectory, this.toCanonicalFileName); let info = this.getScriptInfoForPath(path); if (!info) { - if (openedByClient || this.host.fileExists(fileName)) { + if (openedByClient || (hostToQueryFileExistsOn || this.host).fileExists(fileName)) { info = new ScriptInfo(this.host, fileName, scriptKind, hasMixedContent, path); this.filenameToScriptInfo.set(info.path, info); diff --git a/src/server/project.ts b/src/server/project.ts index d8344bd6a2ca5..b35298b293867 100644 --- a/src/server/project.ts +++ b/src/server/project.ts @@ -240,7 +240,7 @@ namespace ts.server { if (this.projectKind === ProjectKind.Configured) { (this.lsHost.host as CachedServerHost).addOrDeleteFile(fileName, failedLookupLocationPath, eventKind); } - this.resolutionCache.invalidateResolutionOfChangedFailedLookupLocation(failedLookupLocation); + this.resolutionCache.invalidateResolutionOfChangedFailedLookupLocation(failedLookupLocationPath); this.markAsDirty(); this.projectService.delayUpdateProjectGraphAndInferredProjectsRefresh(this); }); @@ -701,7 +701,7 @@ namespace ts.server { // by the LSHost for files in the program when the program is retrieved above but // the program doesn't contain external files so this must be done explicitly. inserted => { - const scriptInfo = this.projectService.getOrCreateScriptInfo(inserted, /*openedByClient*/ false); + const scriptInfo = this.projectService.getOrCreateScriptInfo(inserted, /*openedByClient*/ false, this.lsHost.host); scriptInfo.attachToProject(this); }, removed => { @@ -744,7 +744,7 @@ namespace ts.server { } getScriptInfoLSHost(fileName: string) { - const scriptInfo = this.projectService.getOrCreateScriptInfo(fileName, /*openedByClient*/ false); + const scriptInfo = this.projectService.getOrCreateScriptInfo(fileName, /*openedByClient*/ false, this.lsHost.host); if (scriptInfo) { const existingValue = this.rootFilesMap.get(scriptInfo.path); if (existingValue !== undefined && existingValue !== scriptInfo) { @@ -758,7 +758,10 @@ namespace ts.server { } getScriptInfoForNormalizedPath(fileName: NormalizedPath) { - const scriptInfo = this.projectService.getOrCreateScriptInfoForNormalizedPath(fileName, /*openedByClient*/ false); + const scriptInfo = this.projectService.getOrCreateScriptInfoForNormalizedPath( + fileName, /*openedByClient*/ false, /*fileContent*/ undefined, + /*scriptKind*/ undefined, /*hasMixedContent*/ undefined, this.lsHost.host + ); if (scriptInfo && !scriptInfo.isAttached(this)) { return Errors.ThrowProjectDoesNotContainDocument(fileName, this); } From 6227a36ff0f9879306e47c014de3471a0fc0fc28 Mon Sep 17 00:00:00 2001 From: Sheetal Nandi Date: Mon, 21 Aug 2017 14:55:33 -0700 Subject: [PATCH 071/109] In Server when polling the file stat's do not send changed event in case the file doesnt exist. --- src/server/server.ts | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/server/server.ts b/src/server/server.ts index 0fe37dd8ba0d5..0ccff7c4d2521 100644 --- a/src/server/server.ts +++ b/src/server/server.ts @@ -538,7 +538,15 @@ namespace ts.server { fs.stat(watchedFile.fileName, (err: any, stats: any) => { if (err) { - watchedFile.callback(watchedFile.fileName, FileWatcherEventKind.Changed); + if (err.code === "ENOENT") { + if (watchedFile.mtime.getTime() !== 0) { + watchedFile.mtime = new Date(0); + watchedFile.callback(watchedFile.fileName, FileWatcherEventKind.Deleted); + } + } + else { + watchedFile.callback(watchedFile.fileName, FileWatcherEventKind.Changed); + } } else { const oldTime = watchedFile.mtime.getTime(); From 55931c46bb690ba7187767006219f9ca186ac794 Mon Sep 17 00:00:00 2001 From: Sheetal Nandi Date: Mon, 21 Aug 2017 15:09:03 -0700 Subject: [PATCH 072/109] Update the failed lookup watches without doing lookups. This helps in not having to deal with duplicate locations and checking if there exists watch Anyways the watches are refCount based so we would just addref and remove ref on the same watches --- src/compiler/resolutionCache.ts | 32 ++++---------------------------- src/server/editorServices.ts | 2 +- 2 files changed, 5 insertions(+), 29 deletions(-) diff --git a/src/compiler/resolutionCache.ts b/src/compiler/resolutionCache.ts index e331133fe7449..6d7c1f6c0e4f6 100644 --- a/src/compiler/resolutionCache.ts +++ b/src/compiler/resolutionCache.ts @@ -240,35 +240,11 @@ namespace ts { } function updateFailedLookupLocationWatches(containingFile: string, name: string, existingFailedLookupLocations: ReadonlyArray | undefined, failedLookupLocations: ReadonlyArray) { - if (failedLookupLocations) { - if (existingFailedLookupLocations) { - const existingWatches = arrayToMap(existingFailedLookupLocations, toPath); - for (const failedLookupLocation of failedLookupLocations) { - const failedLookupLocationPath = toPath(failedLookupLocation); - if (existingWatches && existingWatches.has(failedLookupLocationPath)) { - // still has same failed lookup location, keep the watch - existingWatches.delete(failedLookupLocationPath); - } - else { - // Create new watch - watchFailedLookupLocation(failedLookupLocation, failedLookupLocationPath, containingFile, name); - } - } + // Watch all the failed lookup locations + withFailedLookupLocations(failedLookupLocations, containingFile, name, watchFailedLookupLocation); - // Close all the watches that are still present in the existingWatches since those are not the locations looked up for buy new resolution - existingWatches.forEach((failedLookupLocation, failedLookupLocationPath: Path) => - closeFailedLookupLocationWatcher(failedLookupLocation, failedLookupLocationPath, containingFile, name) - ); - } - else { - // Watch all the failed lookup locations - withFailedLookupLocations(failedLookupLocations, containingFile, name, watchFailedLookupLocation); - } - } - else { - // Close existing watches for the failed locations - withFailedLookupLocations(existingFailedLookupLocations, containingFile, name, closeFailedLookupLocationWatcher); - } + // Close existing watches for the failed locations + withFailedLookupLocations(existingFailedLookupLocations, containingFile, name, closeFailedLookupLocationWatcher); } function invalidateResolutionCacheOfDeletedFile( diff --git a/src/server/editorServices.ts b/src/server/editorServices.ts index 6cbc5263249a0..884ee518a30b9 100644 --- a/src/server/editorServices.ts +++ b/src/server/editorServices.ts @@ -1023,7 +1023,7 @@ namespace ts.server { if (this.configuredProjects.has(canonicalConfigFilePath)) { watches.push(WatchType.ConfigFilePath); } - this.logger.info(`ConfigFilePresence:: Current Watches: ['${watches.join("','")}']:: File: ${configFileName} Currently impacted open files: RootsOfInferredProjects: ${inferredRoots} OtherOpenFiles: ${otherFiles} Status: ${status}`); + this.logger.info(`ConfigFilePresence:: Current Watches: ${watches}:: File: ${configFileName} Currently impacted open files: RootsOfInferredProjects: ${inferredRoots} OtherOpenFiles: ${otherFiles} Status: ${status}`); } /** From e65df125f8b82f345f9e0eca5ed392ed62a1822b Mon Sep 17 00:00:00 2001 From: Sheetal Nandi Date: Tue, 22 Aug 2017 10:28:02 -0700 Subject: [PATCH 073/109] Add test for #16955 which simulates npm install --- .../unittests/tsserverProjectSystem.ts | 421 +++++++++++++----- 1 file changed, 302 insertions(+), 119 deletions(-) diff --git a/src/harness/unittests/tsserverProjectSystem.ts b/src/harness/unittests/tsserverProjectSystem.ts index 37cbd20521d1f..58c2b3a614d7f 100644 --- a/src/harness/unittests/tsserverProjectSystem.ts +++ b/src/harness/unittests/tsserverProjectSystem.ts @@ -4086,121 +4086,6 @@ namespace ts.projectSystem { } } - function verifyWatchDirectoriesCaseSensitivity(useCaseSensitiveFileNames: boolean) { - const frontendDir = "/Users/someuser/work/applications/frontend"; - const canonicalFrontendDir = useCaseSensitiveFileNames ? frontendDir : frontendDir.toLowerCase(); - const file1: FileOrFolder = { - path: `${frontendDir}/src/app/utils/Analytic.ts`, - content: "export class SomeClass { };" - }; - const file2: FileOrFolder = { - path: `${frontendDir}/src/app/redux/configureStore.ts`, - content: "export class configureStore { }" - }; - const file3: FileOrFolder = { - path: `${frontendDir}/src/app/utils/Cookie.ts`, - content: "export class Cookie { }" - }; - const es2016LibFile: FileOrFolder = { - path: "/a/lib/lib.es2016.full.d.ts", - content: libFile.content - }; - const tsconfigFile: FileOrFolder = { - path: `${frontendDir}/tsconfig.json`, - content: JSON.stringify({ - "compilerOptions": { - "strict": true, - "strictNullChecks": true, - "target": "es2016", - "module": "commonjs", - "moduleResolution": "node", - "sourceMap": true, - "noEmitOnError": true, - "experimentalDecorators": true, - "emitDecoratorMetadata": true, - "types": [ - "node", - "jest" - ], - "noUnusedLocals": true, - "outDir": "./compiled", - "typeRoots": [ - "types", - "node_modules/@types" - ], - "baseUrl": ".", - "paths": { - "*": [ - "types/*" - ] - } - }, - "include": [ - "src/**/*" - ], - "exclude": [ - "node_modules", - "compiled" - ] - }) - }; - const projectFiles = [file1, file2, es2016LibFile, tsconfigFile]; - const host = createServerHost(projectFiles, { useCaseSensitiveFileNames }); - const projectService = createProjectService(host); - const canonicalConfigPath = useCaseSensitiveFileNames ? tsconfigFile.path : tsconfigFile.path.toLowerCase(); - const { configFileName } = projectService.openClientFile(file1.path); - assert.equal(configFileName, tsconfigFile.path, `should find config`); - checkNumberOfConfiguredProjects(projectService, 1); - - const project = projectService.configuredProjects.get(canonicalConfigPath); - verifyProjectAndWatchedDirectories(); - - const callsTrackingHost = createCallsTrackingHost(host); - - // Create file cookie.ts - projectFiles.push(file3); - host.reloadFS(projectFiles); - host.runQueuedTimeoutCallbacks(); - - const canonicalFile3Path = useCaseSensitiveFileNames ? file3.path : file3.path.toLocaleLowerCase(); - callsTrackingHost.verifyCalledOnEachEntryOnce("fileExists", [canonicalFile3Path]); - - // Called for type root resolution - const directoryExistsCalled = createMap(); - for (let dir = frontendDir; dir !== "/"; dir = getDirectoryPath(dir)) { - directoryExistsCalled.set(`${dir}/node_modules`, 2); - } - directoryExistsCalled.set(`/node_modules`, 2); - directoryExistsCalled.set(`${frontendDir}/types`, 2); - directoryExistsCalled.set(`${frontendDir}/node_modules/@types`, 2); - directoryExistsCalled.set(canonicalFile3Path, 1); - callsTrackingHost.verifyCalledOnEachEntry("directoryExists", directoryExistsCalled); - - callsTrackingHost.verifyNoCall("getDirectories"); - callsTrackingHost.verifyCalledOnEachEntryOnce("readFile", [file3.path]); - callsTrackingHost.verifyNoCall("readDirectory"); - - checkNumberOfConfiguredProjects(projectService, 1); - assert.strictEqual(projectService.configuredProjects.get(canonicalConfigPath), project); - verifyProjectAndWatchedDirectories(); - - callsTrackingHost.clear(); - - const { configFileName: configFile2 } = projectService.openClientFile(file3.path); - assert.equal(configFile2, configFileName); - - checkNumberOfConfiguredProjects(projectService, 1); - assert.strictEqual(projectService.configuredProjects.get(canonicalConfigPath), project); - verifyProjectAndWatchedDirectories(); - callsTrackingHost.verifyNoHostCalls(); - - function verifyProjectAndWatchedDirectories() { - checkProjectActualFiles(project, map(projectFiles, f => f.path)); - checkWatchedDirectories(host, [`${canonicalFrontendDir}/src`], /*recursive*/ true); - checkWatchedDirectories(host, [`${canonicalFrontendDir}/types`, `${canonicalFrontendDir}/node_modules/@types`], /*recursive*/ false); - } - } - it("when calling goto definition of module", () => { const clientFile: FileOrFolder = { path: "/a/b/controllers/vessels/client.ts", @@ -4276,12 +4161,310 @@ namespace ts.projectSystem { assert.strictEqual(projectService.configuredProjects.get(tsconfigFile.path), project); }); - it("WatchDirectories for config file With non case sensitive file system", () => { - verifyWatchDirectoriesCaseSensitivity(/*useCaseSensitiveFileNames*/ false); + describe("WatchDirectories for config file with", () => { + function verifyWatchDirectoriesCaseSensitivity(useCaseSensitiveFileNames: boolean) { + const frontendDir = "/Users/someuser/work/applications/frontend"; + const canonicalFrontendDir = useCaseSensitiveFileNames ? frontendDir : frontendDir.toLowerCase(); + const file1: FileOrFolder = { + path: `${frontendDir}/src/app/utils/Analytic.ts`, + content: "export class SomeClass { };" + }; + const file2: FileOrFolder = { + path: `${frontendDir}/src/app/redux/configureStore.ts`, + content: "export class configureStore { }" + }; + const file3: FileOrFolder = { + path: `${frontendDir}/src/app/utils/Cookie.ts`, + content: "export class Cookie { }" + }; + const es2016LibFile: FileOrFolder = { + path: "/a/lib/lib.es2016.full.d.ts", + content: libFile.content + }; + const tsconfigFile: FileOrFolder = { + path: `${frontendDir}/tsconfig.json`, + content: JSON.stringify({ + "compilerOptions": { + "strict": true, + "strictNullChecks": true, + "target": "es2016", + "module": "commonjs", + "moduleResolution": "node", + "sourceMap": true, + "noEmitOnError": true, + "experimentalDecorators": true, + "emitDecoratorMetadata": true, + "types": [ + "node", + "jest" + ], + "noUnusedLocals": true, + "outDir": "./compiled", + "typeRoots": [ + "types", + "node_modules/@types" + ], + "baseUrl": ".", + "paths": { + "*": [ + "types/*" + ] + } + }, + "include": [ + "src/**/*" + ], + "exclude": [ + "node_modules", + "compiled" + ] + }) + }; + const projectFiles = [file1, file2, es2016LibFile, tsconfigFile]; + const host = createServerHost(projectFiles, { useCaseSensitiveFileNames }); + const projectService = createProjectService(host); + const canonicalConfigPath = useCaseSensitiveFileNames ? tsconfigFile.path : tsconfigFile.path.toLowerCase(); + const { configFileName } = projectService.openClientFile(file1.path); + assert.equal(configFileName, tsconfigFile.path, `should find config`); + checkNumberOfConfiguredProjects(projectService, 1); + + const project = projectService.configuredProjects.get(canonicalConfigPath); + verifyProjectAndWatchedDirectories(); + + const callsTrackingHost = createCallsTrackingHost(host); + + // Create file cookie.ts + projectFiles.push(file3); + host.reloadFS(projectFiles); + host.runQueuedTimeoutCallbacks(); + + const canonicalFile3Path = useCaseSensitiveFileNames ? file3.path : file3.path.toLocaleLowerCase(); + callsTrackingHost.verifyCalledOnEachEntryOnce("fileExists", [canonicalFile3Path]); + + // Called for type root resolution + const directoryExistsCalled = createMap(); + for (let dir = frontendDir; dir !== "/"; dir = getDirectoryPath(dir)) { + directoryExistsCalled.set(`${dir}/node_modules`, 2); + } + directoryExistsCalled.set(`/node_modules`, 2); + directoryExistsCalled.set(`${frontendDir}/types`, 2); + directoryExistsCalled.set(`${frontendDir}/node_modules/@types`, 2); + directoryExistsCalled.set(canonicalFile3Path, 1); + callsTrackingHost.verifyCalledOnEachEntry("directoryExists", directoryExistsCalled); + + callsTrackingHost.verifyNoCall("getDirectories"); + callsTrackingHost.verifyCalledOnEachEntryOnce("readFile", [file3.path]); + callsTrackingHost.verifyNoCall("readDirectory"); + + checkNumberOfConfiguredProjects(projectService, 1); + assert.strictEqual(projectService.configuredProjects.get(canonicalConfigPath), project); + verifyProjectAndWatchedDirectories(); + + callsTrackingHost.clear(); + + const { configFileName: configFile2 } = projectService.openClientFile(file3.path); + assert.equal(configFile2, configFileName); + + checkNumberOfConfiguredProjects(projectService, 1); + assert.strictEqual(projectService.configuredProjects.get(canonicalConfigPath), project); + verifyProjectAndWatchedDirectories(); + callsTrackingHost.verifyNoHostCalls(); + + function verifyProjectAndWatchedDirectories() { + checkProjectActualFiles(project, map(projectFiles, f => f.path)); + checkWatchedDirectories(host, [`${canonicalFrontendDir}/src`], /*recursive*/ true); + checkWatchedDirectories(host, [`${canonicalFrontendDir}/types`, `${canonicalFrontendDir}/node_modules/@types`], /*recursive*/ false); + } + } + + it("case insensitive file system", () => { + verifyWatchDirectoriesCaseSensitivity(/*useCaseSensitiveFileNames*/ false); + }); + + it("case sensitive file system", () => { + verifyWatchDirectoriesCaseSensitivity(/*useCaseSensitiveFileNames*/ true); + }); }); - it("WatchDirectories for config file With case sensitive file system", () => { - verifyWatchDirectoriesCaseSensitivity(/*useCaseSensitiveFileNames*/ true); + describe("Verify npm install in directory with tsconfig file works when", () => { + function verifyNpmInstall(timeoutDuringPartialInstallation: boolean) { + const app: FileOrFolder = { + path: "/a/b/app.ts", + content: "import _ from 'lodash';" + }; + const tsconfigJson: FileOrFolder = { + path: "/a/b/tsconfig.json", + content: '{ "compilerOptions": { } }' + }; + const packageJson: FileOrFolder = { + path: "/a/b/package.json", + content: ` +{ + "name": "test", + "version": "1.0.0", + "description": "", + "main": "index.js", + "dependencies": { + "lodash", + "rxjs" + }, + "devDependencies": { + "@types/lodash", + "typescript" + }, + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "keywords": [], + "author": "", + "license": "ISC" +} +` + }; + const appFolder = getDirectoryPath(app.path); + const projectFiles = [app, libFile, tsconfigJson]; + const otherFiles = [packageJson]; + const host = createServerHost(projectFiles.concat(otherFiles)); + const projectService = createProjectService(host); + const { configFileName } = projectService.openClientFile(app.path); + assert.equal(configFileName, tsconfigJson.path, `should find config`); + const watchedModuleLocations = getNodeModulesWatchedDirectories(appFolder, "lodash"); + verifyProject(); + + let timeoutAfterReloadFs = timeoutDuringPartialInstallation; + + // Simulate npm install + const filesAndFoldersToAdd: FileOrFolder[] = [ + { "path": "/a/b/node_modules" }, + { "path": "/a/b/node_modules/.staging/@types" }, + { "path": "/a/b/node_modules/.staging/lodash-b0733faa" }, + { "path": "/a/b/node_modules/.staging/@types/lodash-e56c4fe7" }, + { "path": "/a/b/node_modules/.staging/symbol-observable-24bcbbff" }, + { "path": "/a/b/node_modules/.staging/rxjs-22375c61" }, + { "path": "/a/b/node_modules/.staging/typescript-8493ea5d" }, + { "path": "/a/b/node_modules/.staging/symbol-observable-24bcbbff/package.json", "content": "{\n \"name\": \"symbol-observable\",\n \"version\": \"1.0.4\",\n \"description\": \"Symbol.observable ponyfill\",\n \"license\": \"MIT\",\n \"repository\": \"blesh/symbol-observable\",\n \"author\": {\n \"name\": \"Ben Lesh\",\n \"email\": \"ben@benlesh.com\"\n },\n \"engines\": {\n \"node\": \">=0.10.0\"\n },\n \"scripts\": {\n \"test\": \"npm run build && mocha && tsc ./ts-test/test.ts && node ./ts-test/test.js && check-es3-syntax -p lib/ --kill\",\n \"build\": \"babel es --out-dir lib\",\n \"prepublish\": \"npm test\"\n },\n \"files\": [\n \"" }, + { "path": "/a/b/node_modules/.staging/lodash-b0733faa/package.json", "content": "{\n \"name\": \"lodash\",\n \"version\": \"4.17.4\",\n \"description\": \"Lodash modular utilities.\",\n \"keywords\": \"modules, stdlib, util\",\n \"homepage\": \"https://lodash.com/\",\n \"repository\": \"lodash/lodash\",\n \"icon\": \"https://lodash.com/icon.svg\",\n \"license\": \"MIT\",\n \"main\": \"lodash.js\",\n \"author\": \"John-David Dalton (http://allyoucanleet.com/)\",\n \"contributors\": [\n \"John-David Dalton (http://allyoucanleet.com/)\",\n \"Mathias Bynens \",\n \"contributors\": [\n {\n \"name\": \"Ben Lesh\",\n \"email\": \"ben@benlesh.com\"\n },\n {\n \"name\": \"Paul Taylor\",\n \"email\": \"paul.e.taylor@me.com\"\n },\n {\n \"name\": \"Jeff Cross\",\n \"email\": \"crossj@google.com\"\n },\n {\n \"name\": \"Matthew Podwysocki\",\n \"email\": \"matthewp@microsoft.com\"\n },\n {\n \"name\": \"OJ Kwon\",\n \"email\": \"kwon.ohjoong@gmail.com\"\n },\n {\n \"name\": \"Andre Staltz\",\n \"email\": \"andre@staltz.com\"\n }\n ],\n \"license\": \"Apache-2.0\",\n \"bugs\": {\n \"url\": \"https://github.com/ReactiveX/RxJS/issues\"\n },\n \"homepage\": \"https://github.com/ReactiveX/RxJS\",\n \"devDependencies\": {\n \"babel-polyfill\": \"^6.23.0\",\n \"benchmark\": \"^2.1.0\",\n \"benchpress\": \"2.0.0-beta.1\",\n \"chai\": \"^3.5.0\",\n \"color\": \"^0.11.1\",\n \"colors\": \"1.1.2\",\n \"commitizen\": \"^2.8.6\",\n \"coveralls\": \"^2.11.13\",\n \"cz-conventional-changelog\": \"^1.2.0\",\n \"danger\": \"^1.1.0\",\n \"doctoc\": \"^1.0.0\",\n \"escape-string-regexp\": \"^1.0.5 \",\n \"esdoc\": \"^0.4.7\",\n \"eslint\": \"^3.8.0\",\n \"fs-extra\": \"^2.1.2\",\n \"get-folder-size\": \"^1.0.0\",\n \"glob\": \"^7.0.3\",\n \"gm\": \"^1.22.0\",\n \"google-closure-compiler-js\": \"^20170218.0.0\",\n \"gzip-size\": \"^3.0.0\",\n \"http-server\": \"^0.9.0\",\n \"husky\": \"^0.13.3\",\n \"lint-staged\": \"3.2.5\",\n \"lodash\": \"^4.15.0\",\n \"madge\": \"^1.4.3\",\n \"markdown-doctest\": \"^0.9.1\",\n \"minimist\": \"^1.2.0\",\n \"mkdirp\": \"^0.5.1\",\n \"mocha\": \"^3.0.2\",\n \"mocha-in-sauce\": \"0.0.1\",\n \"npm-run-all\": \"^4.0.2\",\n \"npm-scripts-info\": \"^0.3.4\",\n \"nyc\": \"^10.2.0\",\n \"opn-cli\": \"^3.1.0\",\n \"platform\": \"^1.3.1\",\n \"promise\": \"^7.1.1\",\n \"protractor\": \"^3.1.1\",\n \"rollup\": \"0.36.3\",\n \"rollup-plugin-inject\": \"^2.0.0\",\n \"rollup-plugin-node-resolve\": \"^2.0.0\",\n \"rx\": \"latest\",\n \"rxjs\": \"latest\",\n \"shx\": \"^0.2.2\",\n \"sinon\": \"^2.1.0\",\n \"sinon-chai\": \"^2.9.0\",\n \"source-map-support\": \"^0.4.0\",\n \"tslib\": \"^1.5.0\",\n \"tslint\": \"^4.4.2\",\n \"typescript\": \"~2.0.6\",\n \"typings\": \"^2.0.0\",\n \"validate-commit-msg\": \"^2.14.0\",\n \"watch\": \"^1.0.1\",\n \"webpack\": \"^1.13.1\",\n \"xmlhttprequest\": \"1.8.0\"\n },\n \"engines\": {\n \"npm\": \">=2.0.0\"\n },\n \"typings\": \"Rx.d.ts\",\n \"dependencies\": {\n \"symbol-observable\": \"^1.0.1\"\n }\n}" }, + { "path": "/a/b/node_modules/.staging/typescript-8493ea5d/package.json", "content": "{\n \"name\": \"typescript\",\n \"author\": \"Microsoft Corp.\",\n \"homepage\": \"http://typescriptlang.org/\",\n \"version\": \"2.4.2\",\n \"license\": \"Apache-2.0\",\n \"description\": \"TypeScript is a language for application scale JavaScript development\",\n \"keywords\": [\n \"TypeScript\",\n \"Microsoft\",\n \"compiler\",\n \"language\",\n \"javascript\"\n ],\n \"bugs\": {\n \"url\": \"https://github.com/Microsoft/TypeScript/issues\"\n },\n \"repository\": {\n \"type\": \"git\",\n \"url\": \"https://github.com/Microsoft/TypeScript.git\"\n },\n \"main\": \"./lib/typescript.js\",\n \"typings\": \"./lib/typescript.d.ts\",\n \"bin\": {\n \"tsc\": \"./bin/tsc\",\n \"tsserver\": \"./bin/tsserver\"\n },\n \"engines\": {\n \"node\": \">=4.2.0\"\n },\n \"devDependencies\": {\n \"@types/browserify\": \"latest\",\n \"@types/chai\": \"latest\",\n \"@types/convert-source-map\": \"latest\",\n \"@types/del\": \"latest\",\n \"@types/glob\": \"latest\",\n \"@types/gulp\": \"latest\",\n \"@types/gulp-concat\": \"latest\",\n \"@types/gulp-help\": \"latest\",\n \"@types/gulp-newer\": \"latest\",\n \"@types/gulp-sourcemaps\": \"latest\",\n \"@types/merge2\": \"latest\",\n \"@types/minimatch\": \"latest\",\n \"@types/minimist\": \"latest\",\n \"@types/mkdirp\": \"latest\",\n \"@types/mocha\": \"latest\",\n \"@types/node\": \"latest\",\n \"@types/q\": \"latest\",\n \"@types/run-sequence\": \"latest\",\n \"@types/through2\": \"latest\",\n \"browserify\": \"latest\",\n \"chai\": \"latest\",\n \"convert-source-map\": \"latest\",\n \"del\": \"latest\",\n \"gulp\": \"latest\",\n \"gulp-clone\": \"latest\",\n \"gulp-concat\": \"latest\",\n \"gulp-help\": \"latest\",\n \"gulp-insert\": \"latest\",\n \"gulp-newer\": \"latest\",\n \"gulp-sourcemaps\": \"latest\",\n \"gulp-typescript\": \"latest\",\n \"into-stream\": \"latest\",\n \"istanbul\": \"latest\",\n \"jake\": \"latest\",\n \"merge2\": \"latest\",\n \"minimist\": \"latest\",\n \"mkdirp\": \"latest\",\n \"mocha\": \"latest\",\n \"mocha-fivemat-progress-reporter\": \"latest\",\n \"q\": \"latest\",\n \"run-sequence\": \"latest\",\n \"sorcery\": \"latest\",\n \"through2\": \"latest\",\n \"travis-fold\": \"latest\",\n \"ts-node\": \"latest\",\n \"tslint\": \"latest\",\n \"typescript\": \"^2.4\"\n },\n \"scripts\": {\n \"pretest\": \"jake tests\",\n \"test\": \"jake runtests-parallel\",\n \"build\": \"npm run build:compiler && npm run build:tests\",\n \"build:compiler\": \"jake local\",\n \"build:tests\": \"jake tests\",\n \"start\": \"node lib/tsc\",\n \"clean\": \"jake clean\",\n \"gulp\": \"gulp\",\n \"jake\": \"jake\",\n \"lint\": \"jake lint\",\n \"setup-hooks\": \"node scripts/link-hooks.js\"\n },\n \"browser\": {\n \"buffer\": false,\n \"fs\": false,\n \"os\": false,\n \"path\": false\n }\n}" }, + { "path": "/a/b/node_modules/.staging/symbol-observable-24bcbbff/index.js", "content": "module.exports = require('./lib/index');\n" }, + { "path": "/a/b/node_modules/.staging/symbol-observable-24bcbbff/index.d.ts", "content": "declare const observableSymbol: symbol;\nexport default observableSymbol;\n" }, + { "path": "/a/b/node_modules/.staging/symbol-observable-24bcbbff/lib" }, + { "path": "/a/b/node_modules/.staging/symbol-observable-24bcbbff/lib/index.js", "content": "'use strict';\n\nObject.defineProperty(exports, \"__esModule\", {\n value: true\n});\n\nvar _ponyfill = require('./ponyfill');\n\nvar _ponyfill2 = _interopRequireDefault(_ponyfill);\n\nfunction _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; }\n\nvar root; /* global window */\n\n\nif (typeof self !== 'undefined') {\n root = self;\n} else if (typeof window !== 'undefined') {\n root = window;\n} else if (typeof global !== 'undefined') {\n root = global;\n} else if (typeof module !== 'undefined') {\n root = module;\n} else {\n root = Function('return this')();\n}\n\nvar result = (0, _ponyfill2['default'])(root);\nexports['default'] = result;" }, + ]; + verifyAfterPartialOrCompleteNpmInstall(2); + + filesAndFoldersToAdd.push( + { "path": "/a/b/node_modules/.staging/typescript-8493ea5d/lib" }, + { "path": "/a/b/node_modules/.staging/rxjs-22375c61/add/operator" }, + { "path": "/a/b/node_modules/.staging/@types/lodash-e56c4fe7/package.json", "content": "{\n \"name\": \"@types/lodash\",\n \"version\": \"4.14.74\",\n \"description\": \"TypeScript definitions for Lo-Dash\",\n \"license\": \"MIT\",\n \"contributors\": [\n {\n \"name\": \"Brian Zengel\",\n \"url\": \"https://github.com/bczengel\"\n },\n {\n \"name\": \"Ilya Mochalov\",\n \"url\": \"https://github.com/chrootsu\"\n },\n {\n \"name\": \"Stepan Mikhaylyuk\",\n \"url\": \"https://github.com/stepancar\"\n },\n {\n \"name\": \"Eric L Anderson\",\n \"url\": \"https://github.com/ericanderson\"\n },\n {\n \"name\": \"AJ Richardson\",\n \"url\": \"https://github.com/aj-r\"\n },\n {\n \"name\": \"Junyoung Clare Jang\",\n \"url\": \"https://github.com/ailrun\"\n }\n ],\n \"main\": \"\",\n \"repository\": {\n \"type\": \"git\",\n \"url\": \"https://www.github.com/DefinitelyTyped/DefinitelyTyped.git\"\n },\n \"scripts\": {},\n \"dependencies\": {},\n \"typesPublisherContentHash\": \"12af578ffaf8d86d2df37e591857906a86b983fa9258414326544a0fe6af0de8\",\n \"typeScriptVersion\": \"2.2\"\n}" }, + { "path": "/a/b/node_modules/.staging/lodash-b0733faa/index.js", "content": "module.exports = require('./lodash');" }, + { "path": "/a/b/node_modules/.staging/typescript-8493ea5d/package.json.3017591594" } + ); + // Since we didnt add any supported extension file, there wont be any timeout scheduled + verifyAfterPartialOrCompleteNpmInstall(0); + + // Remove file "/a/b/node_modules/.staging/typescript-8493ea5d/package.json.3017591594" + filesAndFoldersToAdd.length--; + verifyAfterPartialOrCompleteNpmInstall(0); + + filesAndFoldersToAdd.push( + { "path": "/a/b/node_modules/.staging/rxjs-22375c61/bundles" }, + { "path": "/a/b/node_modules/.staging/rxjs-22375c61/operator" }, + { "path": "/a/b/node_modules/.staging/rxjs-22375c61/src/add/observable/dom" }, + { "path": "/a/b/node_modules/.staging/@types/lodash-e56c4fe7/index.d.ts", "content": "// Type definitions for Lo-Dash 4.14\n// Project: http://lodash.com/\n// Definitions by: Brian Zengel ,\n// Ilya Mochalov ,\n// Stepan Mikhaylyuk ,\n// Eric L Anderson ,\n// AJ Richardson ,\n// Junyoung Clare Jang \n// Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped\n// TypeScript Version: 2.2\n\n/**\n### 4.0.0 Changelog (https://github.com/lodash/lodash/wiki/Changelog)\n\n#### TODO:\nremoved:\n- [x] Removed _.support\n- [x] Removed _.findWhere in favor of _.find with iteratee shorthand\n- [x] Removed _.where in favor of _.filter with iteratee shorthand\n- [x] Removed _.pluck in favor of _.map with iteratee shorthand\n\nrenamed:\n- [x] Renamed _.first to _.head\n- [x] Renamed _.indexBy to _.keyBy\n- [x] Renamed _.invoke to _.invokeMap\n- [x] Renamed _.overArgs to _.overArgs\n- [x] Renamed _.padLeft & _.padRight to _.padStart & _.padEnd\n- [x] Renamed _.pairs to _.toPairs\n- [x] Renamed _.rest to _.tail\n- [x] Renamed _.restParam to _.rest\n- [x] Renamed _.sortByOrder to _.orderBy\n- [x] Renamed _.trimLeft & _.trimRight to _.trimStart & _.trimEnd\n- [x] Renamed _.trunc to _.truncate\n\nsplit:\n- [x] Split _.indexOf & _.lastIndexOf into _.sortedIndexOf & _.sortedLastIndexOf\n- [x] Split _.max & _.min into _.maxBy & _.minBy\n- [x] Split _.omit & _.pick into _.omitBy & _.pickBy\n- [x] Split _.sample into _.sampleSize\n- [x] Split _.sortedIndex into _.sortedIndexBy\n- [x] Split _.sortedLastIndex into _.sortedLastIndexBy\n- [x] Split _.uniq into _.sortedUniq, _.sortedUniqBy, & _.uniqBy\n\nchanges:\n- [x] Absorbed _.sortByAll into _.sortBy\n- [x] Changed the category of _.at to “Object”\n- [x] Changed the category of _.bindAll to “Utility”\n- [x] Made _.capitalize uppercase the first character & lowercase the rest\n- [x] Made _.functions return only own method names\n\nadded 23 array methods:\n- [x] _.concat\n- [x] _.differenceBy\n- [x] _.differenceWith\n- [x] _.flatMap\n- [x] _.fromPairs\n- [x] _.intersectionBy\n- [x] _.intersectionWith\n- [x] _.join\n- [x] _.pullAll\n- [x] _.pullAllBy\n- [x] _.reverse\n- [x] _.sortedIndexBy\n- [x] _.sortedIndexOf\n- [x] _.sortedLastIndexBy\n- [x] _.sortedLastIndexOf\n- [x] _.sortedUniq\n- [x] _.sortedUniqBy\n- [x] _.unionBy\n- [x] _.unionWith\n- [x] _.uniqBy\n- [x] _.uniqWith\n- [x] _.xorBy\n- [x] _.xorWith\n\nadded 20 lang methods:\n- [x] _.cloneDeepWith\n- [x] _.cloneWith\n- [x] _.eq\n- [x] _.isArrayLike\n- [x] _.isArrayLikeObject\n- [x] _.isEqualWith\n- [x] _.isInteger\n- [x] _.isLength\n- [x] _.isMatchWith\n- [x] _.isNil\n- [x] _.isObjectLike\n- [x] _.isSafeInteger\n- [x] _.isSymbol\n- [x] _.toInteger\n- [x] _.toLength\n- [x] _.toNumber\n- [x] _.toSafeInteger\n- [x] _.toString\n- [X] _.conforms\n- [X] _.conformsTo\n\nadded 13 object methods:\n- [x] _.assignIn\n- [x] _.assignInWith\n- [x] _.assignWith\n- [x] _.functionsIn\n- [x] _.hasIn\n- [x] _.mergeWith\n- [x] _.omitBy\n- [x] _.pickBy\n\nadded 8 string methods:\n- [x] _.lowerCase\n- [x] _.lowerFirst\n- [x] _.upperCase\n- [x] _.upperFirst\n- [x] _.toLower\n- [x] _.toUpper\n\nadded 8 utility methods:\n- [x] _.toPath\n\nadded 4 math methods:\n- [x] _.maxBy\n- [x] _.mean\n- [x] _.minBy\n- [x] _.sumBy\n\nadded 2 function methods:\n- [x] _.flip\n- [x] _.unary\n\nadded 2 number methods:\n- [x] _.clamp\n- [x] _.subtract\n\nadded collection method:\n- [x] _.sampleSize\n\nAdded 3 aliases\n\n- [x] _.first as an alias of _.head\n\nRemoved 17 aliases\n- [x] Removed aliase _.all\n- [x] Removed aliase _.any\n- [x] Removed aliase _.backflow\n- [x] Removed aliase _.callback\n- [x] Removed aliase _.collect\n- [x] Removed aliase _.compose\n- [x] Removed aliase _.contains\n- [x] Removed aliase _.detect\n- [x] Removed aliase _.foldl\n- [x] Removed aliase _.foldr\n- [x] Removed aliase _.include\n- [x] Removed aliase _.inject\n- [x] Removed aliase _.methods\n- [x] Removed aliase _.object\n- [x] Removed aliase _.run\n- [x] Removed aliase _.select\n- [x] Removed aliase _.unique\n\nOther changes\n- [x] Added support for array buffers to _.isEqual\n- [x] Added support for converting iterators to _.toArray\n- [x] Added support for deep paths to _.zipObject\n- [x] Changed UMD to export to window or self when available regardless of other exports\n- [x] Ensured debounce cancel clears args & thisArg references\n- [x] Ensured _.add, _.subtract, & _.sum don’t skip NaN values\n- [x] Ensured _.clone treats generators like functions\n- [x] Ensured _.clone produces clones with the source’s [[Prototype]]\n- [x] Ensured _.defaults assigns properties that shadow Object.prototype\n- [x] Ensured _.defaultsDeep doesn’t merge a string into an array\n- [x] Ensured _.defaultsDeep & _.merge don’t modify sources\n- [x] Ensured _.defaultsDeep works with circular references\n- [x] Ensured _.keys skips “length” on strict mode arguments objects in Safari 9\n- [x] Ensured _.merge doesn’t convert strings to arrays\n- [x] Ensured _.merge merges plain-objects onto non plain-objects\n- [x] Ensured _#plant resets iterator data of cloned sequences\n- [x] Ensured _.random swaps min & max if min is greater than max\n- [x] Ensured _.range preserves the sign of start of -0\n- [x] Ensured _.reduce & _.reduceRight use getIteratee in their array branch\n- [x] Fixed rounding issue with the precision param of _.floor\n- [x] Added flush method to debounced & throttled functions\n\n** LATER **\nMisc:\n- [ ] Made _.forEach, _.forIn, _.forOwn, & _.times implicitly end a chain sequence\n- [ ] Removed thisArg params from most methods\n- [ ] Made “By” methods provide a single param to iteratees\n- [ ] Made _.words chainable by default\n- [ ] Removed isDeep params from _.clone & _.flatten\n- [ ] Removed _.bindAll support for binding all methods when no names are provided\n- [ ] Removed func-first param signature from _.before & _.after\n- [ ] _.extend as an alias of _.assignIn\n- [ ] _.extendWith as an alias of _.assignInWith\n- [ ] Added clear method to _.memoize.Cache\n- [ ] Added support for ES6 maps, sets, & symbols to _.clone, _.isEqual, & _.toArray\n- [x] Enabled _.flow & _.flowRight to accept an array of functions\n- [ ] Ensured “Collection” methods treat functions as objects\n- [ ] Ensured _.assign, _.defaults, & _.merge coerce object values to objects\n- [ ] Ensured _.bindKey bound functions call object[key] when called with the new operator\n- [ ] Ensured _.isFunction returns true for generator functions\n- [ ] Ensured _.merge assigns typed arrays directly\n- [ ] Made _(...) an iterator & iterable\n- [ ] Made _.drop, _.take, & right forms coerce n of undefined to 0\n\nMethods:\n- [ ] _.concat\n- [ ] _.differenceBy\n- [ ] _.differenceWith\n- [ ] _.flatMap\n- [ ] _.fromPairs\n- [ ] _.intersectionBy\n- [ ] _.intersectionWith\n- [ ] _.join\n- [ ] _.pullAll\n- [ ] _.pullAllBy\n- [ ] _.reverse\n- [ ] _.sortedLastIndexOf\n- [ ] _.unionBy\n- [ ] _.unionWith\n- [ ] _.uniqWith\n- [ ] _.xorBy\n- [ ] _.xorWith\n- [ ] _.toString\n\n- [ ] _.invoke\n- [ ] _.setWith\n- [ ] _.toPairs\n- [ ] _.toPairsIn\n- [ ] _.unset\n\n- [ ] _.replace\n- [ ] _.split\n\n- [ ] _.cond\n- [ ] _.nthArg\n- [ ] _.over\n- [ ] _.overEvery\n- [ ] _.overSome\n- [ ] _.rangeRight\n\n- [ ] _.next\n*/\n\nexport = _;\nexport as namespace _;\n\ndeclare var _: _.LoDashStatic;\n\ntype PartialObject = Partial;\n\ndeclare namespace _ {\n type Many = T | T[];\n\n interface LoDashStatic {\n /**\n * Creates a lodash object which wraps the given value to enable intuitive method chaining.\n *\n * In addition to Lo-Dash methods, wrappers also have the following Array methods:\n * concat, join, pop, push, reverse, shift, slice, sort, splice, and unshift\n *\n * Chaining is supported in custom builds as long as the value method is implicitly or\n * explicitly included in the build.\n *\n * The chainable wrapper functions are:\n * after, assign, bind, bindAll, bindKey, chain, chunk, compact, compose, concat, countBy,\n * createCallback, curry, debounce, defaults, defer, delay, difference, filter, flatten,\n * forEach, forEachRight, forIn, forInRight, forOwn, forOwnRight, functions, groupBy,\n * keyBy, initial, intersection, invert, invoke, keys, map, max, memoize, merge, min,\n * object, omit, once, pairs, partial, partialRight, pick, pluck, pull, push, range, reject,\n * remove, rest, reverse, sample, shuffle, slice, sort, sortBy, splice, tap, throttle, times,\n * toArray, transform, union, uniq, unset, unshift, unzip, values, where, without, wrap, and zip\n *\n * The non-chainable wrapper functions are:\n * clone, cloneDeep, contains, escape, every, find, findIndex, findKey, findLast,\n * findLastIndex, findLastKey, has, identity, indexOf, isArguments, isArray, isBoolean,\n * isDate, isElement, isEmpty, isEqual, isFinite, isFunction, isNaN, isNull, isNumber,\n * isObject, isPlainObject, isRegExp, isString, isUndefined, join, lastIndexOf, mixin,\n * noConflict, parseInt, pop, random, reduce, reduceRight, result, shift, size, some,\n * sortedIndex, runInContext, template, unescape, uniqueId, and value\n *\n * The wrapper functions first and last return wrapped values when n is provided, otherwise\n * they return unwrapped values.\n *\n * Explicit chaining can be enabled by using the _.chain method.\n **/\n (value: number): LoDashImplicitWrapper;\n (value: string): LoDashImplicitStringWrapper;\n (value: boolean): LoDashImplicitWrapper;\n (value: null | undefined): LoDashImplicitWrapper;\n (value: number[]): LoDashImplicitNumberArrayWrapper;\n (value: T[]): LoDashImplicitArrayWrapper;\n (value: T[] | null | undefined): LoDashImplicitNillableArrayWrapper;\n (value: T): LoDashImplicitObjectWrapper;\n (value: T | null | undefined): LoDashImplicitNillableObjectWrapper;\n (value: any): LoDashImplicitWrapper;\n\n /**\n * The semantic version number.\n **/\n VERSION: string;\n\n /**\n * By default, the template delimiters used by Lo-Dash are similar to those in embedded Ruby\n * (ERB). Change the following template settings to use alternative delimiters.\n **/\n templateSettings: TemplateSettings;\n }\n\n /**\n * By default, the template delimiters used by Lo-Dash are similar to those in embedded Ruby\n * (ERB). Change the following template settings to use alternative delimiters.\n **/\n interface TemplateSettings {\n /**\n * The \"escape\" delimiter.\n **/\n escape?: RegExp;\n\n /**\n * The \"evaluate\" delimiter.\n **/\n evaluate?: RegExp;\n\n /**\n * An object to import into the template as local variables.\n **/\n imports?: Dictionary;\n\n /**\n * The \"interpolate\" delimiter.\n **/\n interpolate?: RegExp;\n\n /**\n * Used to reference the data object in the template text.\n **/\n variable?: string;\n }\n\n /**\n * Creates a cache object to store key/value pairs.\n */\n interface MapCache {\n /**\n * Removes `key` and its value from the cache.\n * @param key The key of the value to remove.\n * @return Returns `true` if the entry was removed successfully, else `false`.\n */\n delete(key: string): boolean;\n\n /**\n * Gets the cached value for `key`.\n * @param key The key of the value to get.\n * @return Returns the cached value.\n */\n get(key: string): any;\n\n /**\n * Checks if a cached value for `key` exists.\n * @param key The key of the entry to check.\n * @return Returns `true` if an entry for `key` exists, else `false`.\n */\n has(key: string): boolean;\n\n /**\n * Sets `value` to `key` of the cache.\n * @param key The key of the value to cache.\n * @param value The value to cache.\n * @return Returns the cache object.\n */\n set(key: string, value: any): _.Dictionary;\n\n /**\n * Removes all key-value entries from the map.\n */\n clear(): void;\n }\n interface MapCacheConstructor {\n new (): MapCache;\n }\n\n interface LoDashWrapperBase { }\n\n interface LoDashImplicitWrapperBase extends LoDashWrapperBase { }\n\n interface LoDashExplicitWrapperBase extends LoDashWrapperBase { }\n\n interface LoDashImplicitWrapper extends LoDashImplicitWrapperBase> { }\n\n interface LoDashExplicitWrapper extends LoDashExplicitWrapperBase> { }\n\n interface LoDashImplicitStringWrapper extends LoDashImplicitWrapper { }\n\n interface LoDashExplicitStringWrapper extends LoDashExplicitWrapper { }\n\n interface LoDashImplicitObjectWrapperBase extends LoDashImplicitWrapperBase { }\n\n interface LoDashImplicitObjectWrapper extends LoDashImplicitObjectWrapperBase> { }\n\n interface LoDashImplicitNillableObjectWrapper extends LoDashImplicitObjectWrapperBase> { }\n\n interface LoDashExplicitObjectWrapperBase extends LoDashExplicitWrapperBase { }\n\n interface LoDashExplicitObjectWrapper extends LoDashExplicitObjectWrapperBase> { }\n\n interface LoDashExplicitNillableObjectWrapper extends LoDashExplicitObjectWrapperBase> { }\n\n interface LoDashImplicitArrayWrapperBase extends LoDashImplicitWrapperBase {\n pop(): T | undefined;\n push(...items: T[]): TWrapper;\n shift(): T | undefined;\n sort(compareFn?: (a: T, b: T) => number): TWrapper;\n splice(start: number): TWrapper;\n splice(start: number, deleteCount: number, ...items: T[]): TWrapper;\n unshift(...items: T[]): TWrapper;\n }\n\n interface LoDashImplicitArrayWrapper extends LoDashImplicitArrayWrapperBase> { }\n\n interface LoDashImplicitNillableArrayWrapper extends LoDashImplicitArrayWrapperBase> { }\n\n interface LoDashImplicitNumberArrayWrapperBase extends LoDashImplicitArrayWrapperBase { }\n\n interface LoDashImplicitNumberArrayWrapper extends LoDashImplicitArrayWrapper { }\n\n interface LoDashExplicitArrayWrapperBase extends LoDashExplicitWrapperBase {\n pop(): LoDashExplicitObjectWrapper;\n push(...items: T[]): TWrapper;\n shift(): LoDashExplicitObjectWrapper;\n sort(compareFn?: (a: T, b: T) => number): TWrapper;\n splice(start: number): TWrapper;\n splice(start: number, deleteCount: number, ...items: T[]): TWrapper;\n unshift(...items: T[]): TWrapper;\n }\n\n interface LoDashExplicitArrayWrapper extends LoDashExplicitArrayWrapperBase> { }\n\n interface LoDashExplicitNillableArrayWrapper extends LoDashExplicitArrayWrapperBase> { }\n\n interface LoDashExplicitNumberArrayWrapper extends LoDashExplicitArrayWrapper { }\n\n /*********\n * Array *\n *********/\n\n //_.chunk\n interface LoDashStatic {\n /**\n * Creates an array of elements split into groups the length of size. If collection can’t be split evenly, the\n * final chunk will be the remaining elements.\n *\n * @param array The array to process.\n * @param size The length of each chunk.\n * @return Returns the new array containing chunks.\n */\n chunk(\n array: List | null | undefined,\n size?: number\n ): T[][];\n }\n\n interface LoDashImplicitArrayWrapperBase {\n /**\n * @see _.chunk\n */\n chunk(size?: number): LoDashImplicitArrayWrapper;\n }\n\n interface LoDashImplicitObjectWrapperBase {\n /**\n * @see _.chunk\n */\n chunk(size?: number): LoDashImplicitArrayWrapper;\n }\n\n interface LoDashExplicitArrayWrapperBase {\n /**\n * @see _.chunk\n */\n chunk(size?: number): LoDashExplicitArrayWrapper;\n }\n\n interface LoDashExplicitObjectWrapperBase {\n /**\n * @see _.chunk\n */\n chunk(size?: number): LoDashExplicitArrayWrapper;\n }\n\n //_.compact\n interface LoDashStatic {\n /**\n * Creates an array with all falsey values removed. The values false, null, 0, \"\", undefined, and NaN are\n * falsey.\n *\n * @param array The array to compact.\n * @return (Array) Returns the new array of filtered values.\n */\n compact(array?: List | null | undefined): T[];\n }\n\n interface LoDashImplicitArrayWrapperBase {\n /**\n * @see _.compact\n */\n compact(): LoDashImplicitArrayWrapper;\n }\n\n interface LoDashImplicitObjectWrapperBase {\n /**\n * @see _.compact\n */\n compact(): LoDashImplicitArrayWrapper;\n }\n\n interface LoDashExplicitArrayWrapperBase {\n /**\n * @see _.compact\n */\n compact(): LoDashExplicitArrayWrapper;\n }\n\n interface LoDashExplicitObjectWrapperBase {\n /**\n * @see _.compact\n */\n compact(): LoDashExplicitArrayWrapper;\n }\n\n //_.concat DUMMY\n interface LoDashStatic {\n /**\n * Creates a new array concatenating `array` with any additional arrays\n * and/or values.\n *\n * @static\n * @memberOf _\n * @category Array\n * @param {Array} array The array to concatenate.\n * @param {...*} [values] The values to concatenate.\n * @returns {Array} Returns the new concatenated array.\n * @example\n *\n * var array = [1];\n * var other = _.concat(array, 2, [3], [[4]]);\n *\n * console.log(other);\n * // => [1, 2, 3, [4]]\n *\n * console.log(array);\n * // => [1]\n */\n concat(array: List, ...values: Array>): T[];\n }\n\n //_.difference\n interface LoDashStatic {\n /**\n * Creates an array of unique array values not included in the other provided arrays using SameValueZero for\n * equality comparisons.\n *\n * @param array The array to inspect.\n * @param values The arrays of values to exclude.\n * @return Returns the new array of filtered values.\n */\n difference(\n array: List | null | undefined,\n ...values: Array>\n ): T[];\n }\n\n interface LoDashImplicitArrayWrapperBase {\n /**\n * @see _.difference\n */\n difference(...values: Array>): LoDashImplicitArrayWrapper;\n }\n\n interface LoDashImplicitObjectWrapperBase {\n /**\n * @see _.difference\n */\n difference(...values: Array>): LoDashImplicitArrayWrapper;\n }\n\n interface LoDashExplicitArrayWrapperBase {\n /**\n * @see _.difference\n */\n difference(...values: Array>): LoDashExplicitArrayWrapper;\n }\n\n interface LoDashExplicitObjectWrapperBase {\n /**\n * @see _.difference\n */\n difference(...values: Array>): LoDashExplicitArrayWrapper;\n }\n\n //_.differenceBy\n interface LoDashStatic {\n /**\n * This method is like _.difference except that it accepts iteratee which is invoked for each element of array\n * and values to generate the criterion by which uniqueness is computed. The iteratee is invoked with one\n * argument: (value).\n *\n * @param array The array to inspect.\n * @param values The values to exclude.\n * @param iteratee The iteratee invoked per element.\n * @returns Returns the new array of filtered values.\n */\n differenceBy(\n array: List | null | undefined,\n values?: List,\n iteratee?: ((value: T) => any)|string\n ): T[];\n\n /**\n * @see _.differenceBy\n */\n differenceBy(\n array: List | null | undefined,\n values?: List,\n iteratee?: W\n ): T[];\n\n /**\n * @see _.differenceBy\n */\n differenceBy(\n array: List | null | undefined,\n values1?: List,\n values2?: List,\n iteratee?: ((value: T) => any)|string\n ): T[];\n\n /**\n * @see _.differenceBy\n */\n differenceBy(\n array: List | null | undefined,\n values1?: List,\n values2?: List,\n iteratee?: W\n ): T[];\n\n /**\n * @see _.differenceBy\n */\n differenceBy(\n array: List | null | undefined,\n values1?: List,\n values2?: List,\n values3?: List,\n iteratee?: ((value: T) => any)|string\n ): T[];\n\n /**\n * @see _.differenceBy\n */\n differenceBy(\n array: List | null | undefined,\n values1?: List,\n values2?: List,\n values3?: List,\n iteratee?: W\n ): T[];\n\n /**\n * @see _.differenceBy\n */\n differenceBy(\n array: List | null | undefined,\n values1?: List,\n values2?: List,\n values3?: List,\n values4?: List,\n iteratee?: W\n ): T[];\n\n /**\n * @see _.differenceBy\n */\n differenceBy(\n array: List | null | undefined,\n values1?: List,\n values2?: List,\n values3?: List,\n values4?: List,\n iteratee?: ((value: T) => any)|string\n ): T[];\n\n /**\n * @see _.differenceBy\n */\n differenceBy(\n array: List | null | undefined,\n values1?: List,\n values2?: List,\n values3?: List,\n values4?: List,\n values5?: List,\n iteratee?: ((value: T) => any)|string\n ): T[];\n\n /**\n * @see _.differenceBy\n */\n differenceBy(\n array: List | null | undefined,\n values1?: List,\n values2?: List,\n values3?: List,\n values4?: List,\n values5?: List,\n iteratee?: W\n ): T[];\n\n /**\n * @see _.differenceBy\n */\n differenceBy(\n array: List | null | undefined,\n ...values: any[]\n ): T[];\n }\n\n interface LoDashImplicitArrayWrapperBase {\n /**\n * @see _.differenceBy\n */\n differenceBy(\n values?: List,\n iteratee?: ((value: T) => any)|string\n ): LoDashImplicitArrayWrapper;\n\n /**\n * @see _.differenceBy\n */\n differenceBy(\n values?: List,\n iteratee?: W\n ): LoDashImplicitArrayWrapper;\n\n /**\n * @see _.differenceBy\n */\n differenceBy(\n values1?: List,\n values2?: List,\n iteratee?: ((value: T) => any)|string\n ): LoDashImplicitArrayWrapper;\n\n /**\n * @see _.differenceBy\n */\n differenceBy(\n values1?: List,\n values2?: List,\n iteratee?: W\n ): LoDashImplicitArrayWrapper;\n\n /**\n * @see _.differenceBy\n */\n differenceBy(\n values1?: List,\n values2?: List,\n values3?: List,\n iteratee?: ((value: T) => any)|string\n ): LoDashImplicitArrayWrapper;\n\n /**\n * @see _.differenceBy\n */\n differenceBy(\n values1?: List,\n values2?: List,\n values3?: List,\n iteratee?: W\n ): LoDashImplicitArrayWrapper;\n\n /**\n * @see _.differenceBy\n */\n differenceBy(\n values1?: List,\n values2?: List,\n values3?: List,\n values4?: List,\n iteratee?: ((value: T) => any)|string\n ): LoDashImplicitArrayWrapper;\n\n /**\n * @see _.differenceBy\n */\n differenceBy(\n values1?: List,\n values2?: List,\n values3?: List,\n values4?: List,\n iteratee?: W\n ): LoDashImplicitArrayWrapper;\n\n /**\n * @see _.differenceBy\n */\n differenceBy(\n values1?: List,\n values2?: List,\n values3?: List,\n values4?: List,\n values5?: List,\n iteratee?: ((value: T) => any)|string\n ): LoDashImplicitArrayWrapper;\n\n /**\n * @see _.differenceBy\n */\n differenceBy(\n values1?: List,\n values2?: List,\n values3?: List,\n values4?: List,\n values5?: List,\n iteratee?: W\n ): LoDashImplicitArrayWrapper;\n\n /**\n * @see _.differenceBy\n */\n differenceBy(\n ...values: any[]\n ): LoDashImplicitArrayWrapper;\n }\n\n interface LoDashImplicitObjectWrapperBase {\n /**\n * @see _.differenceBy\n */\n differenceBy(\n values?: List,\n iteratee?: ((value: T) => any)|string\n ): LoDashImplicitArrayWrapper;\n\n /**\n * @see _.differenceBy\n */\n differenceBy(\n values?: List,\n iteratee?: W\n ): LoDashImplicitArrayWrapper;\n\n /**\n * @see _.differenceBy\n */\n differenceBy(\n values1?: List,\n values2?: List,\n iteratee?: ((value: T) => any)|string\n ): LoDashImplicitArrayWrapper;\n\n /**\n * @see _.differenceBy\n */\n differenceBy(\n values1?: List,\n values2?: List,\n iteratee?: W\n ): LoDashImplicitArrayWrapper;\n\n /**\n * @see _.differenceBy\n */\n differenceBy(\n values1?: List,\n values2?: List,\n values3?: List,\n iteratee?: ((value: T) => any)|string\n ): LoDashImplicitArrayWrapper;\n\n /**\n * @see _.differenceBy\n */\n differenceBy(\n values1?: List,\n values2?: List,\n values3?: List,\n iteratee?: W\n ): LoDashImplicitArrayWrapper;\n\n /**\n * @see _.differenceBy\n */\n differenceBy(\n values1?: List,\n values2?: List,\n values3?: List,\n values4?: List,\n iteratee?: ((value: T) => any)|string\n ): LoDashImplicitArrayWrapper;\n\n /**\n * @see _.differenceBy\n */\n differenceBy(\n values1?: List,\n values2?: List,\n values3?: List,\n values4?: List,\n iteratee?: W\n ): LoDashImplicitArrayWrapper;\n\n /**\n * @see _.differenceBy\n */\n differenceBy(\n values1?: List,\n values2?: List,\n values3?: List,\n values4?: List,\n values5?: List,\n iteratee?: ((value: T) => any)|string\n ): LoDashImplicitArrayWrapper;\n\n /**\n * @see _.differenceBy\n */\n differenceBy(\n values1?: List,\n values2?: List,\n values3?: List,\n values4?: List,\n values5?: List,\n iteratee?: W\n ): LoDashImplicitArrayWrapper;\n\n /**\n * @see _.differenceBy\n */\n differenceBy(\n ...values: any[]\n ): LoDashImplicitArrayWrapper;\n }\n\n interface LoDashExplicitArrayWrapperBase {\n /**\n * @see _.differenceBy\n */\n differenceBy(\n values?: List,\n iteratee?: ((value: T) => any)|string\n ): LoDashExplicitArrayWrapper;\n\n /**\n * @see _.differenceBy\n */\n differenceBy(\n values?: List,\n iteratee?: W\n ): LoDashExplicitArrayWrapper;\n\n /**\n * @see _.differenceBy\n */\n differenceBy(\n values1?: List,\n values2?: List,\n iteratee?: ((value: T) => any)|string\n ): LoDashExplicitArrayWrapper;\n\n /**\n * @see _.differenceBy\n */\n differenceBy(\n values1?: List,\n values2?: List,\n iteratee?: W\n ): LoDashExplicitArrayWrapper;\n\n /**\n * @see _.differenceBy\n */\n differenceBy(\n values1?: List,\n values2?: List,\n values3?: List,\n iteratee?: ((value: T) => any)|string\n ): LoDashExplicitArrayWrapper;\n\n /**\n * @see _.differenceBy\n */\n differenceBy(\n values1?: List,\n values2?: List,\n values3?: List,\n iteratee?: W\n ): LoDashExplicitArrayWrapper;\n\n /**\n * @see _.differenceBy\n */\n differenceBy(\n values1?: List,\n values2?: List,\n values3?: List,\n values4?: List,\n iteratee?: ((value: T) => any)|string\n ): LoDashExplicitArrayWrapper;\n\n /**\n * @see _.differenceBy\n */\n differenceBy(\n values1?: List,\n values2?: List,\n values3?: List,\n values4?: List,\n iteratee?: W\n ): LoDashExplicitArrayWrapper;\n\n /**\n * @see _.differenceBy\n */\n differenceBy(\n values1?: List,\n values2?: List,\n values3?: List,\n values4?: List,\n values5?: List,\n iteratee?: ((value: T) => any)|string\n ): LoDashExplicitArrayWrapper;\n\n /**\n * @see _.differenceBy\n */\n differenceBy(\n values1?: List,\n values2?: List,\n values3?: List,\n values4?: List,\n values5?: List,\n iteratee?: W\n ): LoDashExplicitArrayWrapper;\n\n /**\n * @see _.differenceBy\n */\n differenceBy(\n ...values: any[]\n ): LoDashExplicitArrayWrapper;\n }\n\n interface LoDashExplicitObjectWrapperBase {\n /**\n * @see _.differenceBy\n */\n differenceBy(\n values?: List,\n iteratee?: ((value: T) => any)|string\n ): LoDashExplicitArrayWrapper;\n\n /**\n * @see _.differenceBy\n */\n differenceBy(\n values?: List,\n iteratee?: W\n ): LoDashExplicitArrayWrapper;\n\n /**\n * @see _.differenceBy\n */\n differenceBy(\n values1?: List,\n values2?: List,\n iteratee?: ((value: T) => any)|string\n ): LoDashExplicitArrayWrapper;\n\n /**\n * @see _.differenceBy\n */\n differenceBy(\n values1?: List,\n values2?: List,\n iteratee?: W\n ): LoDashExplicitArrayWrapper;\n\n /**\n * @see _.differenceBy\n */\n differenceBy(\n values1?: List,\n values2?: List,\n values3?: List,\n iteratee?: ((value: T) => any)|string\n ): LoDashExplicitArrayWrapper;\n\n /**\n * @see _.differenceBy\n */\n differenceBy(\n values1?: List,\n values2?: List,\n values3?: List,\n iteratee?: W\n ): LoDashExplicitArrayWrapper;\n\n /**\n * @see _.differenceBy\n */\n differenceBy(\n values1?: List,\n values2?: List,\n values3?: List,\n values4?: List,\n iteratee?: ((value: T) => any)|string\n ): LoDashExplicitArrayWrapper;\n\n /**\n * @see _.differenceBy\n */\n differenceBy(\n values1?: List,\n values2?: List,\n values3?: List,\n values4?: List,\n iteratee?: W\n ): LoDashExplicitArrayWrapper;\n\n /**\n * @see _.differenceBy\n */\n differenceBy(\n values1?: List,\n values2?: List,\n values3?: List,\n values4?: List,\n values5?: List,\n iteratee?: ((value: T) => any)|string\n ): LoDashExplicitArrayWrapper;\n\n /**\n * @see _.differenceBy\n */\n differenceBy(\n values1?: List,\n values2?: List,\n values3?: List,\n values4?: List,\n values5?: List,\n iteratee?: W\n ): LoDashExplicitArrayWrapper;\n\n /**\n * @see _.differenceBy\n */\n differenceBy(\n ...values: any[]\n ): LoDashExplicitArrayWrapper;\n }\n\n //_.differenceWith DUMMY\n interface LoDashStatic {\n /**\n * Creates an array of unique `array` values not included in the other\n * provided arrays using [`SameValueZero`](http://ecma-international.org/ecma-262/6.0/#sec-samevaluezero)\n * for equality comparisons.\n *\n * @static\n * @memberOf _\n * @category Array\n * @param {Array} array The array to inspect.\n * @param {...Array} [values] The values to exclude.\n * @returns {Array} Returns the new array of filtered values.\n * @example\n *\n * _.difference([3, 2, 1], [4, 2]);\n * // => [3, 1]\n */\n differenceWith(\n array: List,\n ...values: any[]\n ): any[];\n }\n\n //_.drop\n interface LoDashStatic {\n /**\n * Creates a slice of array with n elements dropped from the beginning.\n *\n * @param array The array to query.\n * @param n The number of elements to drop.\n * @return Returns the slice of array.\n */\n drop(array: List | null | undefined, n?: number): T[];\n }\n\n interface LoDashImplicitArrayWrapperBase {\n /**\n * @see _.drop\n */\n drop(n?: number): LoDashImplicitArrayWrapper;\n }\n\n interface LoDashImplicitObjectWrapperBase {\n /**\n * @see _.drop\n */\n drop(n?: number): LoDashImplicitArrayWrapper;\n }\n\n interface LoDashExplicitArrayWrapperBase {\n /**\n * @see _.drop\n */\n drop(n?: number): LoDashExplicitArrayWrapper;\n }\n\n interface LoDashExplicitObjectWrapperBase {\n /**\n * @see _.drop\n */\n drop(n?: number): LoDashExplicitArrayWrapper;\n }\n\n //_.dropRight\n interface LoDashStatic {\n /**\n * Creates a slice of array with n elements dropped from the end.\n *\n * @param array The array to query.\n * @param n The number of elements to drop.\n * @return Returns the slice of array.\n */\n dropRight(\n array: List | null | undefined,\n n?: number\n ): T[];\n }\n\n interface LoDashImplicitArrayWrapperBase {\n /**\n * @see _.dropRight\n */\n dropRight(n?: number): LoDashImplicitArrayWrapper;\n }\n\n interface LoDashImplicitObjectWrapperBase {\n /**\n * @see _.dropRight\n */\n dropRight(n?: number): LoDashImplicitArrayWrapper;\n }\n\n interface LoDashExplicitArrayWrapperBase {\n /**\n * @see _.dropRight\n */\n dropRight(n?: number): LoDashExplicitArrayWrapper;\n }\n\n interface LoDashExplicitObjectWrapperBase {\n /**\n * @see _.dropRight\n */\n dropRight(n?: number): LoDashExplicitArrayWrapper;\n }\n\n //_.dropRightWhile\n interface LoDashStatic {\n /**\n * Creates a slice of array excluding elements dropped from the end. Elements are dropped until predicate\n * returns falsey. The predicate is bound to thisArg and invoked with three arguments: (value, index, array).\n *\n * If a property name is provided for predicate the created _.property style callback returns the property\n * value of the given element.\n *\n * If a value is also provided for thisArg the created _.matchesProperty style callback returns true for\n * elements that have a matching property value, else false.\n *\n * If an object is provided for predicate the created _.matches style callback returns true for elements that\n * match the properties of the given object, else false.\n *\n * @param array The array to query.\n * @param predicate The function invoked per iteration.\n * @param thisArg The this binding of predicate.\n * @return Returns the slice of array.\n */\n dropRightWhile(\n array: List | null | undefined,\n predicate?: ListIterator\n ): TValue[];\n\n /**\n * @see _.dropRightWhile\n */\n dropRightWhile(\n array: List | null | undefined,\n predicate?: string\n ): TValue[];\n\n /**\n * @see _.dropRightWhile\n */\n dropRightWhile(\n array: List | null | undefined,\n predicate?: TWhere\n ): TValue[];\n }\n\n interface LoDashImplicitArrayWrapperBase {\n /**\n * @see _.dropRightWhile\n */\n dropRightWhile(\n predicate?: ListIterator\n ): LoDashImplicitArrayWrapper;\n\n /**\n * @see _.dropRightWhile\n */\n dropRightWhile(\n predicate?: string\n ): LoDashImplicitArrayWrapper;\n\n /**\n * @see _.dropRightWhile\n */\n dropRightWhile(\n predicate?: TWhere\n ): LoDashImplicitArrayWrapper;\n }\n\n interface LoDashImplicitObjectWrapperBase {\n /**\n * @see _.dropRightWhile\n */\n dropRightWhile(\n predicate?: ListIterator\n ): LoDashImplicitArrayWrapper;\n\n /**\n * @see _.dropRightWhile\n */\n dropRightWhile(\n predicate?: string\n ): LoDashImplicitArrayWrapper;\n\n /**\n * @see _.dropRightWhile\n */\n dropRightWhile(\n predicate?: TWhere\n ): LoDashImplicitArrayWrapper;\n }\n\n interface LoDashExplicitArrayWrapperBase {\n /**\n * @see _.dropRightWhile\n */\n dropRightWhile(\n predicate?: ListIterator\n ): LoDashExplicitArrayWrapper;\n\n /**\n * @see _.dropRightWhile\n */\n dropRightWhile(\n predicate?: string\n ): LoDashExplicitArrayWrapper;\n\n /**\n * @see _.dropRightWhile\n */\n dropRightWhile(\n predicate?: TWhere\n ): LoDashExplicitArrayWrapper;\n }\n\n interface LoDashExplicitObjectWrapperBase {\n /**\n * @see _.dropRightWhile\n */\n dropRightWhile(\n predicate?: ListIterator\n ): LoDashExplicitArrayWrapper;\n\n /**\n * @see _.dropRightWhile\n */\n dropRightWhile(\n predicate?: string\n ): LoDashExplicitArrayWrapper;\n\n /**\n * @see _.dropRightWhile\n */\n dropRightWhile(\n predicate?: TWhere\n ): LoDashExplicitArrayWrapper;\n }\n\n //_.dropWhile\n interface LoDashStatic {\n /**\n * Creates a slice of array excluding elements dropped from the beginning. Elements are dropped until predicate\n * returns falsey. The predicate is bound to thisArg and invoked with three arguments: (value, index, array).\n *\n * If a property name is provided for predicate the created _.property style callback returns the property\n * value of the given element.\n *\n * If a value is also provided for thisArg the created _.matchesProperty style callback returns true for\n * elements that have a matching property value, else false.\n *\n * If an object is provided for predicate the created _.matches style callback returns true for elements that\n * have the properties of the given object, else false.\n *\n * @param array The array to query.\n * @param predicate The function invoked per iteration.\n * @param thisArg The this binding of predicate.\n * @return Returns the slice of array.\n */\n dropWhile(\n array: List | null | undefined,\n predicate?: ListIterator\n ): TValue[];\n\n /**\n * @see _.dropWhile\n */\n dropWhile(\n array: List | null | undefined,\n predicate?: string\n ): TValue[];\n\n /**\n * @see _.dropWhile\n */\n dropWhile(\n array: List | null | undefined,\n predicate?: TWhere\n ): TValue[];\n }\n\n interface LoDashImplicitArrayWrapperBase {\n /**\n * @see _.dropWhile\n */\n dropWhile(\n predicate?: ListIterator\n ): LoDashImplicitArrayWrapper;\n\n /**\n * @see _.dropWhile\n */\n dropWhile(\n predicate?: string\n ): LoDashImplicitArrayWrapper;\n\n /**\n * @see _.dropWhile\n */\n dropWhile(\n predicate?: TWhere\n ): LoDashImplicitArrayWrapper;\n }\n\n interface LoDashImplicitObjectWrapperBase {\n /**\n * @see _.dropWhile\n */\n dropWhile(\n predicate?: ListIterator\n ): LoDashImplicitArrayWrapper;\n\n /**\n * @see _.dropWhile\n */\n dropWhile(\n predicate?: string\n ): LoDashImplicitArrayWrapper;\n\n /**\n * @see _.dropWhile\n */\n dropWhile(\n predicate?: TWhere\n ): LoDashImplicitArrayWrapper;\n }\n\n interface LoDashExplicitArrayWrapperBase {\n /**\n * @see _.dropWhile\n */\n dropWhile(\n predicate?: ListIterator\n ): LoDashExplicitArrayWrapper;\n\n /**\n * @see _.dropWhile\n */\n dropWhile(\n predicate?: string\n ): LoDashExplicitArrayWrapper;\n\n /**\n * @see _.dropWhile\n */\n dropWhile(\n predicate?: TWhere\n ): LoDashExplicitArrayWrapper;\n }\n\n interface LoDashExplicitObjectWrapperBase {\n /**\n * @see _.dropWhile\n */\n dropWhile(\n predicate?: ListIterator\n ): LoDashExplicitArrayWrapper;\n\n /**\n * @see _.dropWhile\n */\n dropWhile(\n predicate?: string\n ): LoDashExplicitArrayWrapper;\n\n /**\n * @see _.dropWhile\n */\n dropWhile(\n predicate?: TWhere\n ): LoDashExplicitArrayWrapper;\n }\n\n //_.fill\n interface LoDashStatic {\n /**\n * Fills elements of array with value from start up to, but not including, end.\n *\n * Note: This method mutates array.\n *\n * @param array The array to fill.\n * @param value The value to fill array with.\n * @param start The start position.\n * @param end The end position.\n * @return Returns array.\n */\n fill(\n array: any[] | null | undefined,\n value: T,\n start?: number,\n end?: number\n ): T[];\n\n /**\n * @see _.fill\n */\n fill(\n array: List | null | undefined,\n value: T,\n start?: number,\n end?: number\n ): List;\n }\n\n interface LoDashImplicitArrayWrapperBase {\n /**\n * @see _.fill\n */\n fill(\n value: T,\n start?: number,\n end?: number\n ): LoDashImplicitArrayWrapper;\n }\n\n interface LoDashImplicitObjectWrapperBase {\n /**\n * @see _.fill\n */\n fill(\n value: T,\n start?: number,\n end?: number\n ): LoDashImplicitObjectWrapper>;\n }\n\n interface LoDashExplicitArrayWrapperBase {\n /**\n * @see _.fill\n */\n fill(\n value: T,\n start?: number,\n end?: number\n ): LoDashExplicitArrayWrapper;\n }\n\n interface LoDashExplicitObjectWrapperBase {\n /**\n * @see _.fill\n */\n fill(\n value: T,\n start?: number,\n end?: number\n ): LoDashExplicitObjectWrapper>;\n }\n\n //_.findIndex\n interface LoDashStatic {\n /**\n * This method is like _.find except that it returns the index of the first element predicate returns truthy\n * for instead of the element itself.\n *\n * If a property name is provided for predicate the created _.property style callback returns the property\n * value of the given element.\n *\n * If a value is also provided for thisArg the created _.matchesProperty style callback returns true for\n * elements that have a matching property value, else false.\n *\n * If an object is provided for predicate the created _.matches style callback returns true for elements that\n * have the properties of the given object, else false.\n *\n * @param array The array to search.\n * @param predicate The function invoked per iteration.\n * @param fromIndex The index to search from.\n * @return Returns the index of the found element, else -1.\n */\n findIndex(\n array: List | null | undefined,\n predicate?: ListIterator,\n fromIndex?: number\n ): number;\n\n /**\n * @see _.findIndex\n */\n findIndex(\n array: List | null | undefined,\n predicate?: string,\n fromIndex?: number\n ): number;\n\n /**\n * @see _.findIndex\n */\n findIndex(\n array: List | null | undefined,\n predicate?: W,\n fromIndex?: number\n ): number;\n }\n\n interface LoDashImplicitArrayWrapperBase {\n /**\n * @see _.findIndex\n */\n findIndex(\n predicate?: ListIterator,\n fromIndex?: number\n ): number;\n\n /**\n * @see _.findIndex\n */\n findIndex(\n predicate?: string,\n fromIndex?: number\n ): number;\n\n /**\n * @see _.findIndex\n */\n findIndex(\n predicate?: W,\n fromIndex?: number\n ): number;\n }\n\n interface LoDashImplicitObjectWrapperBase {\n /**\n * @see _.findIndex\n */\n findIndex(\n predicate?: ListIterator,\n fromIndex?: number\n ): number;\n\n /**\n * @see _.findIndex\n */\n findIndex(\n predicate?: string,\n fromIndex?: number\n ): number;\n\n /**\n * @see _.findIndex\n */\n findIndex(\n predicate?: W,\n fromIndex?: number\n ): number;\n }\n\n interface LoDashExplicitArrayWrapperBase {\n /**\n * @see _.findIndex\n */\n findIndex(\n predicate?: ListIterator,\n fromIndex?: number\n ): LoDashExplicitWrapper;\n\n /**\n * @see _.findIndex\n */\n findIndex(\n predicate?: string,\n fromIndex?: number\n ): LoDashExplicitWrapper;\n\n /**\n * @see _.findIndex\n */\n findIndex(\n predicate?: W,\n fromIndex?: number\n ): LoDashExplicitWrapper;\n }\n\n interface LoDashExplicitObjectWrapperBase {\n /**\n * @see _.findIndex\n */\n findIndex(\n predicate?: ListIterator,\n fromIndex?: number\n ): LoDashExplicitWrapper;\n\n /**\n * @see _.findIndex\n */\n findIndex(\n predicate?: string,\n fromIndex?: number\n ): LoDashExplicitWrapper;\n\n /**\n * @see _.findIndex\n */\n findIndex(\n predicate?: W,\n fromIndex?: number\n ): LoDashExplicitWrapper;\n }\n\n //_.findLastIndex\n interface LoDashStatic {\n /**\n * This method is like _.findIndex except that it iterates over elements of collection from right to left.\n *\n * If a property name is provided for predicate the created _.property style callback returns the property\n * value of the given element.\n *\n * If a value is also provided for thisArg the created _.matchesProperty style callback returns true for\n * elements that have a matching property value, else false.\n *\n * If an object is provided for predicate the created _.matches style callback returns true for elements that\n * have the properties of the given object, else false.\n *\n * @param array The array to search.\n * @param predicate The function invoked per iteration.\n * @param fromIndex The index to search from.\n * @return Returns the index of the found element, else -1.\n */\n findLastIndex(\n array: List | null | undefined,\n predicate?: ListIterator,\n fromIndex?: number\n ): number;\n\n /**\n * @see _.findLastIndex\n */\n findLastIndex(\n array: List | null | undefined,\n predicate?: string,\n fromIndex?: number\n ): number;\n\n /**\n * @see _.findLastIndex\n */\n findLastIndex(\n array: List | null | undefined,\n predicate?: W,\n fromIndex?: number\n ): number;\n }\n\n interface LoDashImplicitArrayWrapperBase {\n /**\n * @see _.findLastIndex\n */\n findLastIndex(\n predicate?: ListIterator,\n fromIndex?: number\n ): number;\n\n /**\n * @see _.findLastIndex\n */\n findLastIndex(\n predicate?: string,\n fromIndex?: number\n ): number;\n\n /**\n * @see _.findLastIndex\n */\n findLastIndex(\n predicate?: W,\n fromIndex?: number\n ): number;\n }\n\n interface LoDashImplicitObjectWrapperBase {\n /**\n * @see _.findLastIndex\n */\n findLastIndex(\n predicate?: ListIterator,\n fromIndex?: number\n ): number;\n\n /**\n * @see _.findLastIndex\n */\n findLastIndex(\n predicate?: string,\n fromIndex?: number\n ): number;\n\n /**\n * @see _.findLastIndex\n */\n findLastIndex(\n predicate?: W,\n fromIndex?: number\n ): number;\n }\n\n interface LoDashExplicitArrayWrapperBase {\n /**\n * @see _.findLastIndex\n */\n findLastIndex(\n predicate?: ListIterator,\n fromIndex?: number\n ): LoDashExplicitWrapper;\n\n /**\n * @see _.findLastIndex\n */\n findLastIndex(\n predicate?: string,\n fromIndex?: number\n ): LoDashExplicitWrapper;\n\n /**\n * @see _.findLastIndex\n */\n findLastIndex(\n predicate?: W,\n fromIndex?: number\n ): LoDashExplicitWrapper;\n }\n\n interface LoDashExplicitObjectWrapperBase {\n /**\n * @see _.findLastIndex\n */\n findLastIndex(\n predicate?: ListIterator,\n fromIndex?: number\n ): LoDashExplicitWrapper;\n\n /**\n * @see _.findLastIndex\n */\n findLastIndex(\n predicate?: string,\n fromIndex?: number\n ): LoDashExplicitWrapper;\n\n /**\n * @see _.findLastIndex\n */\n findLastIndex(\n predicate?: W,\n fromIndex?: number\n ): LoDashExplicitWrapper;\n }\n\n //_.first\n interface LoDashStatic {\n /**\n * @see _.head\n */\n first(array: List | null | undefined): T | undefined;\n }\n\n interface LoDashImplicitWrapper {\n /**\n * @see _.head\n */\n first(): string | undefined;\n }\n\n interface LoDashImplicitArrayWrapperBase {\n /**\n * @see _.head\n */\n first(): T | undefined;\n }\n\n interface LoDashImplicitObjectWrapperBase {\n /**\n * @see _.head\n */\n first(): T | undefined;\n }\n\n interface LoDashExplicitWrapper {\n /**\n * @see _.head\n */\n first(): LoDashExplicitWrapper;\n }\n\n interface LoDashExplicitArrayWrapperBase {\n /**\n * @see _.head\n */\n first(): T;\n }\n\n interface LoDashExplicitObjectWrapperBase {\n /**\n * @see _.head\n */\n first(): T;\n }\n\n interface RecursiveArray extends Array> {}\n interface ListOfRecursiveArraysOrValues extends List> {}\n\n //_.flatten\n interface LoDashStatic {\n /**\n * Flattens a nested array. If isDeep is true the array is recursively flattened, otherwise it’s only\n * flattened a single level.\n *\n * @param array The array to flatten.\n * @param isDeep Specify a deep flatten.\n * @return Returns the new flattened array.\n */\n flatten(array: ListOfRecursiveArraysOrValues | null | undefined, isDeep: boolean): T[];\n\n /**\n * @see _.flatten\n */\n flatten(array: List> | null | undefined): T[];\n\n /**\n * @see _.flatten\n */\n flatten(array: ListOfRecursiveArraysOrValues | null | undefined): RecursiveArray;\n }\n\n interface LoDashImplicitWrapper {\n /**\n * @see _.flatten\n */\n flatten(): LoDashImplicitArrayWrapper;\n }\n\n interface LoDashImplicitArrayWrapperBase {\n /**\n * @see _.flatten\n */\n flatten(isDeep?: boolean): LoDashImplicitArrayWrapper;\n }\n\n interface LoDashImplicitObjectWrapperBase {\n /**\n * @see _.flatten\n */\n flatten(isDeep?: boolean): LoDashImplicitArrayWrapper;\n }\n\n interface LoDashExplicitWrapper {\n /**\n * @see _.flatten\n */\n flatten(): LoDashExplicitArrayWrapper;\n }\n\n interface LoDashExplicitArrayWrapperBase {\n /**\n * @see _.flatten\n */\n flatten(isDeep?: boolean): LoDashExplicitArrayWrapper;\n }\n\n interface LoDashExplicitObjectWrapperBase {\n /**\n * @see _.flatten\n */\n flatten(isDeep?: boolean): LoDashExplicitArrayWrapper;\n }\n\n //_.flattenDeep\n interface LoDashStatic {\n /**\n * Recursively flattens a nested array.\n *\n * @param array The array to recursively flatten.\n * @return Returns the new flattened array.\n */\n flattenDeep(array: ListOfRecursiveArraysOrValues | null | undefined): T[];\n }\n\n interface LoDashImplicitWrapper {\n /**\n * @see _.flattenDeep\n */\n flattenDeep(): LoDashImplicitArrayWrapper;\n }\n\n interface LoDashImplicitArrayWrapperBase {\n /**\n * @see _.flattenDeep\n */\n flattenDeep(): LoDashImplicitArrayWrapper;\n }\n\n interface LoDashImplicitObjectWrapperBase {\n /**\n * @see _.flattenDeep\n */\n flattenDeep(): LoDashImplicitArrayWrapper;\n }\n\n interface LoDashExplicitWrapper {\n /**\n * @see _.flattenDeep\n */\n flattenDeep(): LoDashExplicitArrayWrapper;\n }\n\n interface LoDashExplicitArrayWrapperBase {\n /**\n * @see _.flattenDeep\n */\n flattenDeep(): LoDashExplicitArrayWrapper;\n }\n\n interface LoDashExplicitObjectWrapperBase {\n /**\n * @see _.flattenDeep\n */\n flattenDeep(): LoDashExplicitArrayWrapper;\n }\n\n // _.flattenDepth\n interface LoDashStatic {\n /**\n * Recursively flatten array up to depth times.\n *\n * @param array The array to recursively flatten.\n * @param number The maximum recursion depth.\n * @return Returns the new flattened array.\n */\n flattenDepth(array: ListOfRecursiveArraysOrValues | null | undefined, depth?: number): T[];\n }\n\n //_.fromPairs\n interface LoDashStatic {\n /**\n * The inverse of `_.toPairs`; this method returns an object composed\n * from key-value `pairs`.\n *\n * @static\n * @memberOf _\n * @category Array\n * @param {Array} pairs The key-value pairs.\n * @returns {Object} Returns the new object.\n * @example\n *\n * _.fromPairs([['fred', 30], ['barney', 40]]);\n * // => { 'fred': 30, 'barney': 40 }\n */\n fromPairs(\n array: List<[_.StringRepresentable, T]> | null | undefined\n ): Dictionary;\n\n /**\n @see _.fromPairs\n */\n fromPairs(\n array: List | null | undefined\n ): Dictionary;\n }\n\n //_.fromPairs DUMMY\n interface LoDashImplicitArrayWrapperBase {\n /**\n * @see _.fromPairs\n */\n fromPairs(): LoDashImplicitObjectWrapper;\n }\n\n //_.fromPairs DUMMY\n interface LoDashExplicitArrayWrapperBase {\n /**\n * @see _.fromPairs\n */\n fromPairs(): LoDashExplicitObjectWrapper;\n }\n\n //_.head\n interface LoDashStatic {\n /**\n * Gets the first element of array.\n *\n * @alias _.first\n *\n * @param array The array to query.\n * @return Returns the first element of array.\n */\n head(array: List | null | undefined): T | undefined;\n }\n\n interface LoDashImplicitWrapper {\n /**\n * @see _.head\n */\n head(): string | undefined;\n }\n\n interface LoDashImplicitArrayWrapperBase {\n /**\n * @see _.head\n */\n head(): T | undefined;\n }\n\n interface LoDashImplicitObjectWrapperBase {\n /**\n * @see _.head\n */\n head(): T | undefined;\n }\n\n interface LoDashExplicitWrapper {\n /**\n * @see _.head\n */\n head(): LoDashExplicitWrapper;\n }\n\n interface LoDashExplicitArrayWrapperBase {\n /**\n * @see _.head\n */\n head(): T;\n }\n\n interface LoDashExplicitObjectWrapperBase {\n /**\n * @see _.head\n */\n head(): T;\n }\n\n //_.indexOf\n interface LoDashStatic {\n /**\n * Gets the index at which the first occurrence of `value` is found in `array`\n * using [`SameValueZero`](http://ecma-international.org/ecma-262/6.0/#sec-samevaluezero)\n * for equality comparisons. If `fromIndex` is negative, it's used as the offset\n * from the end of `array`. If `array` is sorted providing `true` for `fromIndex`\n * performs a faster binary search.\n *\n * @static\n * @memberOf _\n * @category Array\n * @param {Array} array The array to search.\n * @param {*} value The value to search for.\n * @param {number} [fromIndex=0] The index to search from.\n * @returns {number} Returns the index of the matched value, else `-1`.\n * @example\n *\n * _.indexOf([1, 2, 1, 2], 2);\n * // => 1\n *\n * // using `fromIndex`\n * _.indexOf([1, 2, 1, 2], 2, 2);\n * // => 3\n */\n indexOf(\n array: List | null | undefined,\n value: T,\n fromIndex?: boolean|number\n ): number;\n }\n\n interface LoDashImplicitArrayWrapperBase {\n /**\n * @see _.indexOf\n */\n indexOf(\n value: T,\n fromIndex?: boolean|number\n ): number;\n }\n\n interface LoDashImplicitObjectWrapperBase {\n /**\n * @see _.indexOf\n */\n indexOf(\n value: TValue,\n fromIndex?: boolean|number\n ): number;\n }\n\n interface LoDashExplicitArrayWrapperBase {\n /**\n * @see _.indexOf\n */\n indexOf(\n value: T,\n fromIndex?: boolean|number\n ): LoDashExplicitWrapper;\n }\n\n interface LoDashExplicitObjectWrapperBase {\n /**\n * @see _.indexOf\n */\n indexOf(\n value: TValue,\n fromIndex?: boolean|number\n ): LoDashExplicitWrapper;\n }\n\n //_.intersectionBy DUMMY\n interface LoDashStatic {\n /**\n * This method is like `_.intersection` except that it accepts `iteratee`\n * which is invoked for each element of each `arrays` to generate the criterion\n * by which uniqueness is computed. The iteratee is invoked with one argument: (value).\n *\n * @static\n * @memberOf _\n * @category Array\n * @param {...Array} [arrays] The arrays to inspect.\n * @param {Function|Object|string} [iteratee=_.identity] The iteratee invoked per element.\n * @returns {Array} Returns the new array of shared values.\n * @example\n *\n * _.intersectionBy([2.1, 1.2], [4.3, 2.4], Math.floor);\n * // => [2.1]\n *\n * // using the `_.property` iteratee shorthand\n * _.intersectionBy([{ 'x': 1 }], [{ 'x': 2 }, { 'x': 1 }], 'x');\n * // => [{ 'x': 1 }]\n */\n intersectionBy(\n array: List,\n ...values: any[]\n ): any[];\n }\n\n //_.intersectionWith DUMMY\n interface LoDashStatic {\n /**\n * This method is like `_.intersection` except that it accepts `comparator`\n * which is invoked to compare elements of `arrays`. The comparator is invoked\n * with two arguments: (arrVal, othVal).\n *\n * @static\n * @memberOf _\n * @category Array\n * @param {...Array} [arrays] The arrays to inspect.\n * @param {Function} [comparator] The comparator invoked per element.\n * @returns {Array} Returns the new array of shared values.\n * @example\n *\n * var objects = [{ 'x': 1, 'y': 2 }, { 'x': 2, 'y': 1 }];\n * var others = [{ 'x': 1, 'y': 1 }, { 'x': 1, 'y': 2 }];\n *\n * _.intersectionWith(objects, others, _.isEqual);\n * // => [{ 'x': 1, 'y': 2 }]\n */\n intersectionWith(\n array: List,\n ...values: any[]\n ): any[];\n }\n\n //_.join\n interface LoDashStatic {\n /**\n * Converts all elements in `array` into a string separated by `separator`.\n *\n * @param array The array to convert.\n * @param separator The element separator.\n * @returns Returns the joined string.\n */\n join(\n array: List | null | undefined,\n separator?: string\n ): string;\n }\n\n interface LoDashImplicitWrapper {\n /**\n * @see _.join\n */\n join(separator?: string): string;\n }\n\n interface LoDashImplicitArrayWrapperBase {\n /**\n * @see _.join\n */\n join(separator?: string): string;\n }\n\n interface LoDashImplicitObjectWrapperBase {\n /**\n * @see _.join\n */\n join(separator?: string): string;\n }\n\n interface LoDashExplicitWrapper {\n /**\n * @see _.join\n */\n join(separator?: string): LoDashExplicitWrapper;\n }\n\n interface LoDashExplicitArrayWrapperBase {\n /**\n * @see _.join\n */\n join(separator?: string): LoDashExplicitWrapper;\n }\n\n interface LoDashExplicitObjectWrapperBase {\n /**\n * @see _.join\n */\n join(separator?: string): LoDashExplicitWrapper;\n }\n\n //_.pullAll DUMMY\n interface LoDashStatic {\n /**\n * This method is like `_.pull` except that it accepts an array of values to remove.\n *\n * **Note:** Unlike `_.difference`, this method mutates `array`.\n *\n * @static\n * @memberOf _\n * @category Array\n * @param {Array} array The array to modify.\n * @param {Array} values The values to remove.\n * @returns {Array} Returns `array`.\n * @example\n *\n * var array = [1, 2, 3, 1, 2, 3];\n *\n * _.pull(array, [2, 3]);\n * console.log(array);\n * // => [1, 1]\n */\n pullAll(\n array: List,\n ...values: any[]\n ): any[];\n }\n\n //_.pullAllBy DUMMY\n interface LoDashStatic {\n /**\n * This method is like `_.pullAll` except that it accepts `iteratee` which is\n * invoked for each element of `array` and `values` to to generate the criterion\n * by which uniqueness is computed. The iteratee is invoked with one argument: (value).\n *\n * **Note:** Unlike `_.differenceBy`, this method mutates `array`.\n *\n * @static\n * @memberOf _\n * @category Array\n * @param {Array} array The array to modify.\n * @param {Array} values The values to remove.\n * @param {Function|Object|string} [iteratee=_.identity] The iteratee invoked per element.\n * @returns {Array} Returns `array`.\n * @example\n *\n * var array = [{ 'x': 1 }, { 'x': 2 }, { 'x': 3 }, { 'x': 1 }];\n *\n * _.pullAllBy(array, [{ 'x': 1 }, { 'x': 3 }], 'x');\n * console.log(array);\n * // => [{ 'x': 2 }]\n */\n pullAllBy(\n array: List,\n ...values: any[]\n ): any[];\n }\n\n //_.reverse DUMMY\n interface LoDashStatic {\n /**\n * Reverses `array` so that the first element becomes the last, the second\n * element becomes the second to last, and so on.\n *\n * **Note:** This method mutates `array` and is based on\n * [`Array#reverse`](https://mdn.io/Array/reverse).\n *\n * @memberOf _\n * @category Array\n * @returns {Array} Returns `array`.\n * @example\n *\n * var array = [1, 2, 3];\n *\n * _.reverse(array);\n * // => [3, 2, 1]\n *\n * console.log(array);\n * // => [3, 2, 1]\n */\n reverse(\n array: List,\n ...values: any[]\n ): any[];\n }\n\n //_.sortedIndexOf\n interface LoDashStatic {\n /**\n * This method is like `_.indexOf` except that it performs a binary\n * search on a sorted `array`.\n *\n * @static\n * @memberOf _\n * @category Array\n * @param {Array} array The array to search.\n * @param {*} value The value to search for.\n * @returns {number} Returns the index of the matched value, else `-1`.\n * @example\n *\n * _.sortedIndexOf([1, 1, 2, 2], 2);\n * // => 2\n */\n sortedIndexOf(\n array: List | null | undefined,\n value: T\n ): number;\n }\n\n interface LoDashImplicitArrayWrapperBase {\n /**\n * @see _.sortedIndexOf\n */\n sortedIndexOf(\n value: T\n ): number;\n }\n\n interface LoDashImplicitObjectWrapperBase {\n /**\n * @see _.sortedIndexOf\n */\n sortedIndexOf(\n value: TValue\n ): number;\n }\n\n interface LoDashExplicitArrayWrapperBase {\n /**\n * @see _.sortedIndexOf\n */\n sortedIndexOf(\n value: T\n ): LoDashExplicitWrapper;\n }\n\n interface LoDashExplicitObjectWrapperBase {\n /**\n * @see _.sortedIndexOf\n */\n sortedIndexOf(\n value: TValue\n ): LoDashExplicitWrapper;\n }\n\n //_.initial\n interface LoDashStatic {\n /**\n * Gets all but the last element of array.\n *\n * @param array The array to query.\n * @return Returns the slice of array.\n */\n initial(array: List | null | undefined): T[];\n }\n\n interface LoDashImplicitArrayWrapperBase {\n /**\n * @see _.initial\n */\n initial(): LoDashImplicitArrayWrapper;\n }\n\n interface LoDashImplicitObjectWrapperBase {\n /**\n * @see _.initial\n */\n initial(): LoDashImplicitArrayWrapper;\n }\n\n interface LoDashExplicitArrayWrapperBase {\n /**\n * @see _.initial\n */\n initial(): LoDashExplicitArrayWrapper;\n }\n\n interface LoDashExplicitObjectWrapperBase {\n /**\n * @see _.initial\n */\n initial(): LoDashExplicitArrayWrapper;\n }\n\n //_.intersection\n interface LoDashStatic {\n /**\n * Creates an array of unique values that are included in all of the provided arrays using SameValueZero for\n * equality comparisons.\n *\n * @param arrays The arrays to inspect.\n * @return Returns the new array of shared values.\n */\n intersection(...arrays: Array | null | undefined>): T[];\n }\n\n interface LoDashImplicitArrayWrapperBase {\n /**\n * @see _.intersection\n */\n intersection(...arrays: Array>): LoDashImplicitArrayWrapper;\n }\n\n interface LoDashImplicitObjectWrapperBase {\n /**\n * @see _.intersection\n */\n intersection(...arrays: Array>): LoDashImplicitArrayWrapper;\n }\n\n interface LoDashExplicitArrayWrapperBase {\n /**\n * @see _.intersection\n */\n intersection(...arrays: Array>): LoDashExplicitArrayWrapper;\n }\n\n interface LoDashExplicitObjectWrapperBase {\n /**\n * @see _.intersection\n */\n intersection(...arrays: Array>): LoDashExplicitArrayWrapper;\n }\n\n //_.last\n interface LoDashStatic {\n /**\n * Gets the last element of array.\n *\n * @param array The array to query.\n * @return Returns the last element of array.\n */\n last(array: List | null | undefined): T | undefined;\n }\n\n interface LoDashImplicitWrapper {\n /**\n * @see _.last\n */\n last(): string | undefined;\n }\n\n interface LoDashImplicitArrayWrapperBase {\n /**\n * @see _.last\n */\n last(): T | undefined;\n }\n\n interface LoDashImplicitObjectWrapperBase {\n /**\n * @see _.last\n */\n last(): T | undefined;\n }\n\n interface LoDashExplicitWrapper {\n /**\n * @see _.last\n */\n last(): LoDashExplicitWrapper;\n }\n\n interface LoDashExplicitArrayWrapperBase {\n /**\n * @see _.last\n */\n last(): T;\n }\n\n interface LoDashExplicitObjectWrapperBase {\n /**\n * @see _.last\n */\n last(): T;\n }\n\n //_.lastIndexOf\n interface LoDashStatic {\n /**\n * This method is like _.indexOf except that it iterates over elements of array from right to left.\n *\n * @param array The array to search.\n * @param value The value to search for.\n * @param fromIndex The index to search from or true to perform a binary search on a sorted array.\n * @return Returns the index of the matched value, else -1.\n */\n lastIndexOf(\n array: List | null | undefined,\n value: T,\n fromIndex?: boolean|number\n ): number;\n }\n\n interface LoDashImplicitArrayWrapperBase {\n /**\n * @see _.lastIndexOf\n */\n lastIndexOf(\n value: T,\n fromIndex?: boolean|number\n ): number;\n }\n\n interface LoDashImplicitObjectWrapperBase {\n /**\n * @see _.lastIndexOf\n */\n lastIndexOf(\n value: TResult,\n fromIndex?: boolean|number\n ): number;\n }\n\n interface LoDashExplicitArrayWrapperBase {\n /**\n * @see _.lastIndexOf\n */\n lastIndexOf(\n value: T,\n fromIndex?: boolean|number\n ): LoDashExplicitWrapper;\n }\n\n interface LoDashExplicitObjectWrapperBase {\n /**\n * @see _.lastIndexOf\n */\n lastIndexOf(\n value: TResult,\n fromIndex?: boolean|number\n ): LoDashExplicitWrapper;\n }\n\n //_.nth\n interface LoDashStatic {\n /**\n * Gets the element at index `n` of `array`. If `n` is negative, the nth element from the end is returned.\n *\n * @param array array The array to query.\n * @param value The index of the element to return.\n * @return Returns the nth element of `array`.\n */\n nth(\n array: List | null | undefined,\n n?: number\n ): T | undefined;\n }\n\n interface LoDashImplicitArrayWrapperBase {\n /**\n * @see _.nth\n */\n nth(\n n?: number\n ): T | undefined;\n }\n\n interface LoDashImplicitObjectWrapperBase {\n /**\n * @see _.nth\n */\n nth(\n n?:number\n ): TResult | undefined;\n }\n\n interface LoDashExplicitArrayWrapperBase {\n /**\n * @see _.nth\n */\n nth(\n n?:number\n ): LoDashExplicitWrapper;\n }\n\n interface LoDashExplicitObjectWrapperBase {\n /**\n * @see _.nth\n */\n nth(\n n?:number\n ): LoDashExplicitWrapper;\n }\n\n //_.pull\n interface LoDashStatic {\n /**\n * Removes all provided values from array using SameValueZero for equality comparisons.\n *\n * Note: Unlike _.without, this method mutates array.\n *\n * @param array The array to modify.\n * @param values The values to remove.\n * @return Returns array.\n */\n pull(\n array: T[],\n ...values: T[]\n ): T[];\n\n /**\n * @see _.pull\n */\n pull(\n array: List,\n ...values: T[]\n ): List;\n }\n\n interface LoDashImplicitArrayWrapper {\n /**\n * @see _.pull\n */\n pull(...values: T[]): LoDashImplicitArrayWrapper;\n }\n\n interface LoDashImplicitObjectWrapper {\n /**\n * @see _.pull\n */\n pull(...values: TValue[]): LoDashImplicitObjectWrapper>;\n }\n\n interface LoDashExplicitArrayWrapper {\n /**\n * @see _.pull\n */\n pull(...values: T[]): LoDashExplicitArrayWrapper;\n }\n\n interface LoDashExplicitObjectWrapper {\n /**\n * @see _.pull\n */\n pull(...values: TValue[]): LoDashExplicitObjectWrapper>;\n }\n\n //_.pullAt\n interface LoDashStatic {\n /**\n * Removes elements from array corresponding to the given indexes and returns an array of the removed elements.\n * Indexes may be specified as an array of indexes or as individual arguments.\n *\n * Note: Unlike _.at, this method mutates array.\n *\n * @param array The array to modify.\n * @param indexes The indexes of elements to remove, specified as individual indexes or arrays of indexes.\n * @return Returns the new array of removed elements.\n */\n pullAt(\n array: List,\n ...indexes: Array>\n ): T[];\n }\n\n interface LoDashImplicitArrayWrapper {\n /**\n * @see _.pullAt\n */\n pullAt(...indexes: Array>): LoDashImplicitArrayWrapper;\n }\n\n interface LoDashImplicitObjectWrapper {\n /**\n * @see _.pullAt\n */\n pullAt(...indexes: Array>): LoDashImplicitArrayWrapper;\n }\n\n interface LoDashExplicitArrayWrapper {\n /**\n * @see _.pullAt\n */\n pullAt(...indexes: Array>): LoDashExplicitArrayWrapper;\n }\n\n interface LoDashExplicitObjectWrapper {\n /**\n * @see _.pullAt\n */\n pullAt(...indexes: Array>): LoDashExplicitArrayWrapper;\n }\n\n //_.remove\n interface LoDashStatic {\n /**\n * Removes all elements from array that predicate returns truthy for and returns an array of the removed\n * elements. The predicate is bound to thisArg and invoked with three arguments: (value, index, array).\n *\n * If a property name is provided for predicate the created _.property style callback returns the property\n * value of the given element.\n *\n * If a value is also provided for thisArg the created _.matchesProperty style callback returns true for\n * elements that have a matching property value, else false.\n *\n * If an object is provided for predicate the created _.matches style callback returns true for elements that\n * have the properties of the given object, else false.\n *\n * Note: Unlike _.filter, this method mutates array.\n *\n * @param array The array to modify.\n * @param predicate The function invoked per iteration.\n * @param thisArg The this binding of predicate.\n * @return Returns the new array of removed elements.\n */\n remove(\n array: List,\n predicate?: ListIterator\n ): T[];\n\n /**\n * @see _.remove\n */\n remove(\n array: List,\n predicate?: string\n ): T[];\n\n /**\n * @see _.remove\n */\n remove(\n array: List,\n predicate?: W\n ): T[];\n }\n\n interface LoDashImplicitArrayWrapper {\n /**\n * @see _.remove\n */\n remove(\n predicate?: ListIterator\n ): LoDashImplicitArrayWrapper;\n\n /**\n * @see _.remove\n */\n remove(\n predicate?: string\n ): LoDashImplicitArrayWrapper;\n\n /**\n * @see _.remove\n */\n remove(\n predicate?: W\n ): LoDashImplicitArrayWrapper;\n }\n\n interface LoDashImplicitObjectWrapper {\n /**\n * @see _.remove\n */\n remove(\n predicate?: ListIterator\n ): LoDashImplicitArrayWrapper;\n\n /**\n * @see _.remove\n */\n remove(\n predicate?: string\n ): LoDashImplicitArrayWrapper;\n\n /**\n * @see _.remove\n */\n remove(\n predicate?: W\n ): LoDashImplicitArrayWrapper;\n }\n\n interface LoDashExplicitArrayWrapper {\n /**\n * @see _.remove\n */\n remove(\n predicate?: ListIterator\n ): LoDashExplicitArrayWrapper;\n\n /**\n * @see _.remove\n */\n remove(\n predicate?: string\n ): LoDashExplicitArrayWrapper;\n\n /**\n * @see _.remove\n */\n remove(\n predicate?: W\n ): LoDashExplicitArrayWrapper;\n }\n\n interface LoDashExplicitObjectWrapper {\n /**\n * @see _.remove\n */\n remove(\n predicate?: ListIterator\n ): LoDashExplicitArrayWrapper;\n\n /**\n * @see _.remove\n */\n remove(\n predicate?: string\n ): LoDashExplicitArrayWrapper;\n\n /**\n * @see _.remove\n */\n remove(\n predicate?: W\n ): LoDashExplicitArrayWrapper;\n }\n\n //_.tail\n interface LoDashStatic {\n /**\n * Gets all but the first element of array.\n *\n * @alias _.tail\n *\n * @param array The array to query.\n * @return Returns the slice of array.\n */\n tail(array: List | null | undefined): T[];\n }\n\n interface LoDashImplicitArrayWrapperBase {\n /**\n * @see _.tail\n */\n tail(): LoDashImplicitArrayWrapper;\n }\n\n interface LoDashImplicitObjectWrapperBase {\n /**\n * @see _.tail\n */\n tail(): LoDashImplicitArrayWrapper;\n }\n\n interface LoDashExplicitArrayWrapperBase {\n /**\n * @see _.tail\n */\n tail(): LoDashExplicitArrayWrapper;\n }\n\n interface LoDashExplicitObjectWrapperBase {\n /**\n * @see _.tail\n */\n tail(): LoDashExplicitArrayWrapper;\n }\n\n //_.slice\n interface LoDashStatic {\n /**\n * Creates a slice of array from start up to, but not including, end.\n *\n * @param array The array to slice.\n * @param start The start position.\n * @param end The end position.\n * @return Returns the slice of array.\n */\n slice(\n array: T[] | null | undefined,\n start?: number,\n end?: number\n ): T[];\n }\n\n interface LoDashImplicitArrayWrapperBase {\n /**\n * @see _.slice\n */\n slice(\n start?: number,\n end?: number\n ): LoDashImplicitArrayWrapper;\n }\n\n interface LoDashExplicitArrayWrapperBase {\n /**\n * @see _.slice\n */\n slice(\n start?: number,\n end?: number\n ): LoDashExplicitArrayWrapper;\n }\n\n //_.sortedIndex\n interface LoDashStatic {\n /**\n * Uses a binary search to determine the lowest index at which `value` should\n * be inserted into `array` in order to maintain its sort order.\n *\n * @static\n * @memberOf _\n * @category Array\n * @param {Array} array The sorted array to inspect.\n * @param {*} value The value to evaluate.\n * @returns {number} Returns the index at which `value` should be inserted into `array`.\n * @example\n *\n * _.sortedIndex([30, 50], 40);\n * // => 1\n *\n * _.sortedIndex([4, 5], 4);\n * // => 0\n */\n sortedIndex(\n array: List | null | undefined,\n value: T\n ): number;\n }\n\n interface LoDashImplicitWrapper {\n /**\n * @see _.sortedIndex\n */\n sortedIndex(\n value: string\n ): number;\n }\n\n interface LoDashImplicitArrayWrapperBase {\n /**\n * @see _.sortedIndex\n */\n sortedIndex(\n value: T\n ): number;\n }\n\n interface LoDashImplicitObjectWrapperBase {\n /**\n * @see _.sortedIndex\n */\n sortedIndex(\n value: T\n ): number;\n }\n\n interface LoDashExplicitWrapper {\n /**\n * @see _.sortedIndex\n */\n sortedIndex(\n value: string\n ): LoDashExplicitWrapper;\n }\n\n interface LoDashExplicitArrayWrapperBase {\n /**\n * @see _.sortedIndex\n */\n sortedIndex(\n value: T\n ): LoDashExplicitWrapper;\n }\n\n interface LoDashExplicitObjectWrapperBase {\n /**\n * @see _.sortedIndex\n */\n sortedIndex(\n value: T\n ): LoDashExplicitWrapper;\n }\n\n // _.sortedIndexBy\n interface LoDashStatic {\n /**\n * This method is like `_.sortedIndex` except that it accepts `iteratee`\n * which is invoked for `value` and each element of `array` to compute their\n * sort ranking. The iteratee is invoked with one argument: (value).\n *\n * @static\n * @memberOf _\n * @category Array\n * @param {Array} array The sorted array to inspect.\n * @param {*} value The value to evaluate.\n * @param {Function|Object|string} [iteratee=_.identity] The iteratee invoked per element.\n * @returns {number} Returns the index at which `value` should be inserted into `array`.\n * @example\n *\n * var dict = { 'thirty': 30, 'forty': 40, 'fifty': 50 };\n *\n * _.sortedIndexBy(['thirty', 'fifty'], 'forty', _.propertyOf(dict));\n * // => 1\n *\n * // using the `_.property` iteratee shorthand\n * _.sortedIndexBy([{ 'x': 4 }, { 'x': 5 }], { 'x': 4 }, 'x');\n * // => 0\n */\n sortedIndexBy(\n array: List | null | undefined,\n value: T,\n iteratee: (x: T) => TSort\n ): number;\n\n /**\n * @see _.sortedIndexBy\n */\n sortedIndexBy(\n array: List | null | undefined,\n value: T,\n iteratee: (x: T) => any\n ): number;\n\n /**\n * @see _.sortedIndexBy\n */\n sortedIndexBy(\n array: List | null | undefined,\n value: T,\n iteratee: string\n ): number;\n\n /**\n * @see _.sortedIndexBy\n */\n sortedIndexBy(\n array: List | null | undefined,\n value: T,\n iteratee: W\n ): number;\n\n /**\n * @see _.sortedIndexBy\n */\n sortedIndexBy(\n array: List | null | undefined,\n value: T,\n iteratee: Object\n ): number;\n }\n\n interface LoDashImplicitWrapper {\n /**\n * @see _.sortedIndexBy\n */\n sortedIndexBy(\n value: string,\n iteratee: (x: string) => TSort\n ): number;\n }\n\n interface LoDashImplicitArrayWrapperBase {\n /**\n * @see _.sortedIndexBy\n */\n sortedIndexBy(\n value: T,\n iteratee: (x: T) => TSort\n ): number;\n\n /**\n * @see _.sortedIndexBy\n */\n sortedIndexBy(\n value: T,\n iteratee: string\n ): number;\n\n /**\n * @see _.sortedIndexBy\n */\n sortedIndexBy(\n value: T,\n iteratee: W\n ): number;\n }\n\n interface LoDashImplicitObjectWrapperBase {\n /**\n * @see _.sortedIndexBy\n */\n sortedIndexBy(\n value: T,\n iteratee: (x: T) => TSort\n ): number;\n\n /**\n * @see _.sortedIndexBy\n */\n sortedIndexBy(\n value: T,\n iteratee: (x: T) => any\n ): number;\n\n /**\n * @see _.sortedIndexBy\n */\n sortedIndexBy(\n value: T,\n iteratee: string\n ): number;\n\n /**\n * @see _.sortedIndexBy\n */\n sortedIndexBy(\n value: T,\n iteratee: W\n ): number;\n\n /**\n * @see _.sortedIndexBy\n */\n sortedIndexBy(\n value: T,\n iteratee: Object\n ): number;\n }\n\n interface LoDashExplicitWrapper {\n /**\n * @see _.sortedIndexBy\n */\n sortedIndexBy(\n value: string,\n iteratee: (x: string) => TSort\n ): LoDashExplicitWrapper;\n }\n\n interface LoDashExplicitArrayWrapperBase {\n /**\n * @see _.sortedIndexBy\n */\n sortedIndexBy(\n value: T,\n iteratee: (x: T) => TSort\n ): LoDashExplicitWrapper;\n\n /**\n * @see _.sortedIndexBy\n */\n sortedIndexBy(\n value: T,\n iteratee: string\n ): LoDashExplicitWrapper;\n\n /**\n * @see _.sortedIndexBy\n */\n sortedIndexBy(\n value: T,\n iteratee: W\n ): LoDashExplicitWrapper;\n }\n\n interface LoDashExplicitObjectWrapperBase {\n /**\n * @see _.sortedIndexBy\n */\n sortedIndexBy(\n value: T,\n iteratee: (x: T) => TSort\n ): LoDashExplicitWrapper;\n\n /**\n * @see _.sortedIndexBy\n */\n sortedIndexBy(\n value: T,\n iteratee: (x: T) => any\n ): LoDashExplicitWrapper;\n\n /**\n * @see _.sortedIndexBy\n */\n sortedIndexBy(\n value: T,\n iteratee: string\n ): LoDashExplicitWrapper;\n\n /**\n * @see _.sortedIndexBy\n */\n sortedIndexBy(\n value: T,\n iteratee: W\n ): LoDashExplicitWrapper;\n\n /**\n * @see _.sortedIndexBy\n */\n sortedIndexBy(\n value: T,\n iteratee: Object\n ): LoDashExplicitWrapper;\n }\n\n //_.sortedLastIndex\n interface LoDashStatic {\n /**\n * This method is like `_.sortedIndex` except that it returns the highest\n * index at which `value` should be inserted into `array` in order to\n * maintain its sort order.\n *\n * @static\n * @memberOf _\n * @category Array\n * @param {Array} array The sorted array to inspect.\n * @param {*} value The value to evaluate.\n * @returns {number} Returns the index at which `value` should be inserted into `array`.\n * @example\n *\n * _.sortedLastIndex([4, 5], 4);\n * // => 1\n */\n sortedLastIndex(\n array: List | null | undefined,\n value: T\n ): number;\n }\n\n interface LoDashImplicitWrapper {\n /**\n * @see _.sortedLastIndex\n */\n sortedLastIndex(\n value: string\n ): number;\n }\n\n interface LoDashImplicitArrayWrapperBase {\n /**\n * @see _.sortedLastIndex\n */\n sortedLastIndex(\n value: T\n ): number;\n }\n\n interface LoDashImplicitObjectWrapperBase {\n /**\n * @see _.sortedLastIndex\n */\n sortedLastIndex(\n value: T\n ): number;\n }\n\n interface LoDashExplicitWrapper {\n /**\n * @see _.sortedLastIndex\n */\n sortedLastIndex(\n value: string\n ): LoDashExplicitWrapper;\n }\n\n interface LoDashExplicitArrayWrapperBase {\n /**\n * @see _.sortedLastIndex\n */\n sortedLastIndex(\n value: T\n ): LoDashExplicitWrapper;\n }\n\n interface LoDashExplicitObjectWrapperBase {\n /**\n * @see _.sortedLastIndex\n */\n sortedLastIndex(\n value: T\n ): LoDashExplicitWrapper;\n }\n\n //_.sortedLastIndexBy\n interface LoDashStatic {\n /**\n * This method is like `_.sortedLastIndex` except that it accepts `iteratee`\n * which is invoked for `value` and each element of `array` to compute their\n * sort ranking. The iteratee is invoked with one argument: (value).\n *\n * @static\n * @memberOf _\n * @category Array\n * @param {Array} array The sorted array to inspect.\n * @param {*} value The value to evaluate.\n * @param {Function|Object|string} [iteratee=_.identity] The iteratee invoked per element.\n * @returns {number} Returns the index at which `value` should be inserted into `array`.\n * @example\n *\n * // using the `_.property` iteratee shorthand\n * _.sortedLastIndexBy([{ 'x': 4 }, { 'x': 5 }], { 'x': 4 }, 'x');\n * // => 1\n */\n sortedLastIndexBy(\n array: List | null | undefined,\n value: T,\n iteratee: (x: T) => TSort\n ): number;\n\n /**\n * @see _.sortedLastIndexBy\n */\n sortedLastIndexBy(\n array: List | null | undefined,\n value: T,\n iteratee: (x: T) => any\n ): number;\n\n /**\n * @see _.sortedLastIndexBy\n */\n sortedLastIndexBy(\n array: List | null | undefined,\n value: T,\n iteratee: string\n ): number;\n\n /**\n * @see _.sortedLastIndexBy\n */\n sortedLastIndexBy(\n array: List | null | undefined,\n value: T,\n iteratee: W\n ): number;\n\n /**\n * @see _.sortedLastIndexBy\n */\n sortedLastIndexBy(\n array: List | null | undefined,\n value: T,\n iteratee: Object\n ): number;\n }\n\n interface LoDashImplicitWrapper {\n /**\n * @see _.sortedLastIndexBy\n */\n sortedLastIndexBy(\n value: string,\n iteratee: (x: string) => TSort\n ): number;\n }\n\n interface LoDashImplicitArrayWrapperBase {\n /**\n * @see _.sortedLastIndexBy\n */\n sortedLastIndexBy(\n value: T,\n iteratee: (x: T) => TSort\n ): number;\n\n /**\n * @see _.sortedLastIndexBy\n */\n sortedLastIndexBy(\n value: T,\n iteratee: string\n ): number;\n\n /**\n * @see _.sortedLastIndexBy\n */\n sortedLastIndexBy(\n value: T,\n iteratee: W\n ): number;\n }\n\n interface LoDashImplicitObjectWrapperBase {\n /**\n * @see _.sortedLastIndexBy\n */\n sortedLastIndexBy(\n value: T,\n iteratee: (x: T) => TSort\n ): number;\n\n /**\n * @see _.sortedLastIndexBy\n */\n sortedLastIndexBy(\n value: T,\n iteratee: (x: T) => any\n ): number;\n\n /**\n * @see _.sortedLastIndexBy\n */\n sortedLastIndexBy(\n value: T,\n iteratee: string\n ): number;\n\n /**\n * @see _.sortedLastIndexBy\n */\n sortedLastIndexBy(\n value: T,\n iteratee: W\n ): number;\n\n /**\n * @see _.sortedLastIndexBy\n */\n sortedLastIndexBy(\n value: T,\n iteratee: Object\n ): number;\n }\n\n interface LoDashExplicitWrapper {\n /**\n * @see _.sortedLastIndexBy\n */\n sortedLastIndexBy(\n value: string,\n iteratee: (x: string) => TSort\n ): LoDashExplicitWrapper;\n }\n\n interface LoDashExplicitArrayWrapperBase {\n /**\n * @see _.sortedLastIndexBy\n */\n sortedLastIndexBy(\n value: T,\n iteratee: (x: T) => TSort\n ): LoDashExplicitWrapper;\n\n /**\n * @see _.sortedLastIndexBy\n */\n sortedLastIndexBy(\n value: T,\n iteratee: string\n ): LoDashExplicitWrapper;\n\n /**\n * @see _.sortedLastIndexBy\n */\n sortedLastIndexBy(\n value: T,\n iteratee: W\n ): LoDashExplicitWrapper;\n }\n\n interface LoDashExplicitObjectWrapperBase {\n /**\n * @see _.sortedLastIndexBy\n */\n sortedLastIndexBy(\n value: T,\n iteratee: (x: T) => TSort\n ): LoDashExplicitWrapper;\n\n /**\n * @see _.sortedLastIndexBy\n */\n sortedLastIndexBy(\n value: T,\n iteratee: (x: T) => any\n ): LoDashExplicitWrapper;\n\n /**\n * @see _.sortedLastIndexBy\n */\n sortedLastIndexBy(\n value: T,\n iteratee: string\n ): LoDashExplicitWrapper;\n\n /**\n * @see _.sortedLastIndexBy\n */\n sortedLastIndexBy(\n value: T,\n iteratee: W\n ): LoDashExplicitWrapper;\n\n /**\n * @see _.sortedLastIndexBy\n */\n sortedLastIndexBy(\n value: T,\n iteratee: Object\n ): LoDashExplicitWrapper;\n }\n\n //_.sortedLastIndexOf DUMMY\n interface LoDashStatic {\n /**\n * This method is like `_.lastIndexOf` except that it performs a binary\n * search on a sorted `array`.\n *\n * @static\n * @memberOf _\n * @category Array\n * @param {Array} array The array to search.\n * @param {*} value The value to search for.\n * @returns {number} Returns the index of the matched value, else `-1`.\n * @example\n *\n * _.sortedLastIndexOf([1, 1, 2, 2], 2);\n * // => 3\n */\n sortedLastIndexOf(\n array: List | null | undefined,\n value: T\n ): number;\n }\n\n //_.tail\n interface LoDashStatic {\n /**\n * @see _.rest\n */\n tail(array: List | null | undefined): T[];\n }\n\n interface LoDashImplicitArrayWrapperBase {\n /**\n * @see _.rest\n */\n tail(): LoDashImplicitArrayWrapper;\n }\n\n interface LoDashImplicitObjectWrapperBase {\n /**\n * @see _.rest\n */\n tail(): LoDashImplicitArrayWrapper;\n }\n\n interface LoDashExplicitArrayWrapperBase {\n /**\n * @see _.rest\n */\n tail(): LoDashExplicitArrayWrapper;\n }\n\n interface LoDashExplicitObjectWrapperBase {\n /**\n * @see _.rest\n */\n tail(): LoDashExplicitArrayWrapper;\n }\n\n //_.take\n interface LoDashStatic {\n /**\n * Creates a slice of array with n elements taken from the beginning.\n *\n * @param array The array to query.\n * @param n The number of elements to take.\n * @return Returns the slice of array.\n */\n take(\n array: List | null | undefined,\n n?: number\n ): T[];\n }\n\n interface LoDashImplicitArrayWrapperBase {\n /**\n * @see _.take\n */\n take(n?: number): LoDashImplicitArrayWrapper;\n }\n\n interface LoDashImplicitObjectWrapperBase {\n /**\n * @see _.take\n */\n take(n?: number): LoDashImplicitArrayWrapper;\n }\n\n interface LoDashExplicitArrayWrapperBase {\n /**\n * @see _.take\n */\n take(n?: number): LoDashExplicitArrayWrapper;\n }\n\n interface LoDashExplicitObjectWrapperBase {\n /**\n * @see _.take\n */\n take(n?: number): LoDashExplicitArrayWrapper;\n }\n\n //_.takeRight\n interface LoDashStatic {\n /**\n * Creates a slice of array with n elements taken from the end.\n *\n * @param array The array to query.\n * @param n The number of elements to take.\n * @return Returns the slice of array.\n */\n takeRight(\n array: List | null | undefined,\n n?: number\n ): T[];\n }\n\n interface LoDashImplicitArrayWrapperBase {\n /**\n * @see _.takeRight\n */\n takeRight(n?: number): LoDashImplicitArrayWrapper;\n }\n\n interface LoDashImplicitObjectWrapperBase {\n /**\n * @see _.takeRight\n */\n takeRight(n?: number): LoDashImplicitArrayWrapper;\n }\n\n interface LoDashExplicitArrayWrapperBase {\n /**\n * @see _.takeRight\n */\n takeRight(n?: number): LoDashExplicitArrayWrapper;\n }\n\n interface LoDashExplicitObjectWrapperBase {\n /**\n * @see _.takeRight\n */\n takeRight(n?: number): LoDashExplicitArrayWrapper;\n }\n\n //_.takeRightWhile\n interface LoDashStatic {\n /**\n * Creates a slice of array with elements taken from the end. Elements are taken until predicate returns\n * falsey. The predicate is bound to thisArg and invoked with three arguments: (value, index, array).\n *\n * If a property name is provided for predicate the created _.property style callback returns the property\n * value of the given element.\n *\n * If a value is also provided for thisArg the created _.matchesProperty style callback returns true for\n * elements that have a matching property value, else false.\n *\n * If an object is provided for predicate the created _.matches style callback returns true for elements that\n * have the properties of the given object, else false.\n *\n * @param array The array to query.\n * @param predicate The function invoked per iteration.\n * @param thisArg The this binding of predicate.\n * @return Returns the slice of array.\n */\n takeRightWhile(\n array: List | null | undefined,\n predicate?: ListIterator\n ): TValue[];\n\n /**\n * @see _.takeRightWhile\n */\n takeRightWhile(\n array: List | null | undefined,\n predicate?: string\n ): TValue[];\n\n /**\n * @see _.takeRightWhile\n */\n takeRightWhile(\n array: List | null | undefined,\n predicate?: TWhere\n ): TValue[];\n }\n\n interface LoDashImplicitArrayWrapperBase {\n /**\n * @see _.takeRightWhile\n */\n takeRightWhile(\n predicate?: ListIterator\n ): LoDashImplicitArrayWrapper;\n\n /**\n * @see _.takeRightWhile\n */\n takeRightWhile(\n predicate?: string\n ): LoDashImplicitArrayWrapper;\n\n /**\n * @see _.takeRightWhile\n */\n takeRightWhile(\n predicate?: TWhere\n ): LoDashImplicitArrayWrapper;\n }\n\n interface LoDashImplicitObjectWrapperBase {\n /**\n * @see _.takeRightWhile\n */\n takeRightWhile(\n predicate?: ListIterator\n ): LoDashImplicitArrayWrapper;\n\n /**\n * @see _.takeRightWhile\n */\n takeRightWhile(\n predicate?: string\n ): LoDashImplicitArrayWrapper;\n\n /**\n * @see _.takeRightWhile\n */\n takeRightWhile(\n predicate?: TWhere\n ): LoDashImplicitArrayWrapper;\n }\n\n interface LoDashExplicitArrayWrapperBase {\n /**\n * @see _.takeRightWhile\n */\n takeRightWhile(\n predicate?: ListIterator\n ): LoDashExplicitArrayWrapper;\n\n /**\n * @see _.takeRightWhile\n */\n takeRightWhile(\n predicate?: string\n ): LoDashExplicitArrayWrapper;\n\n /**\n * @see _.takeRightWhile\n */\n takeRightWhile(\n predicate?: TWhere\n ): LoDashExplicitArrayWrapper;\n }\n\n interface LoDashExplicitObjectWrapperBase {\n /**\n * @see _.takeRightWhile\n */\n takeRightWhile(\n predicate?: ListIterator\n ): LoDashExplicitArrayWrapper;\n\n /**\n * @see _.takeRightWhile\n */\n takeRightWhile(\n predicate?: string\n ): LoDashExplicitArrayWrapper;\n\n /**\n * @see _.takeRightWhile\n */\n takeRightWhile(\n predicate?: TWhere\n ): LoDashExplicitArrayWrapper;\n }\n\n //_.takeWhile\n interface LoDashStatic {\n /**\n * Creates a slice of array with elements taken from the beginning. Elements are taken until predicate returns\n * falsey. The predicate is bound to thisArg and invoked with three arguments: (value, index, array).\n *\n * If a property name is provided for predicate the created _.property style callback returns the property\n * value of the given element.\n *\n * If a value is also provided for thisArg the created _.matchesProperty style callback returns true for\n * elements that have a matching property value, else false.\n *\n * If an object is provided for predicate the created _.matches style callback returns true for elements that\n * have the properties of the given object, else false.\n *\n * @param array The array to query.\n * @param predicate The function invoked per iteration.\n * @param thisArg The this binding of predicate.\n * @return Returns the slice of array.\n */\n takeWhile(\n array: List | null | undefined,\n predicate?: ListIterator\n ): TValue[];\n\n /**\n * @see _.takeWhile\n */\n takeWhile(\n array: List | null | undefined,\n predicate?: string\n ): TValue[];\n\n /**\n * @see _.takeWhile\n */\n takeWhile(\n array: List | null | undefined,\n predicate?: TWhere\n ): TValue[];\n }\n\n interface LoDashImplicitArrayWrapperBase {\n /**\n * @see _.takeWhile\n */\n takeWhile(\n predicate?: ListIterator\n ): LoDashImplicitArrayWrapper;\n\n /**\n * @see _.takeWhile\n */\n takeWhile(\n predicate?: string\n ): LoDashImplicitArrayWrapper;\n\n /**\n * @see _.takeWhile\n */\n takeWhile(\n predicate?: TWhere\n ): LoDashImplicitArrayWrapper;\n }\n\n interface LoDashImplicitObjectWrapperBase {\n /**\n * @see _.takeWhile\n */\n takeWhile(\n predicate?: ListIterator\n ): LoDashImplicitArrayWrapper;\n\n /**\n * @see _.takeWhile\n */\n takeWhile(\n predicate?: string\n ): LoDashImplicitArrayWrapper;\n\n /**\n * @see _.takeWhile\n */\n takeWhile(\n predicate?: TWhere\n ): LoDashImplicitArrayWrapper;\n }\n\n interface LoDashExplicitArrayWrapperBase {\n /**\n * @see _.takeWhile\n */\n takeWhile(\n predicate?: ListIterator\n ): LoDashExplicitArrayWrapper;\n\n /**\n * @see _.takeWhile\n */\n takeWhile(\n predicate?: string\n ): LoDashExplicitArrayWrapper;\n\n /**\n * @see _.takeWhile\n */\n takeWhile(\n predicate?: TWhere\n ): LoDashExplicitArrayWrapper;\n }\n\n interface LoDashExplicitObjectWrapperBase {\n /**\n * @see _.takeWhile\n */\n takeWhile(\n predicate?: ListIterator\n ): LoDashExplicitArrayWrapper;\n\n /**\n * @see _.takeWhile\n */\n takeWhile(\n predicate?: string\n ): LoDashExplicitArrayWrapper;\n\n /**\n * @see _.takeWhile\n */\n takeWhile(\n predicate?: TWhere\n ): LoDashExplicitArrayWrapper;\n }\n\n //_.union\n interface LoDashStatic {\n /**\n * Creates an array of unique values, in order, from all of the provided arrays using SameValueZero for\n * equality comparisons.\n *\n * @param arrays The arrays to inspect.\n * @return Returns the new array of combined values.\n */\n union(...arrays: Array | null | undefined>): T[];\n }\n\n interface LoDashImplicitArrayWrapperBase {\n /**\n * @see _.union\n */\n union(...arrays: Array | null | undefined>): LoDashImplicitArrayWrapper;\n\n /**\n * @see _.union\n */\n union(...arrays: Array | null | undefined>): LoDashImplicitArrayWrapper;\n }\n\n interface LoDashImplicitObjectWrapperBase {\n /**\n * @see _.union\n */\n union(...arrays: Array | null | undefined>): LoDashImplicitArrayWrapper;\n }\n\n interface LoDashExplicitArrayWrapperBase {\n /**\n * @see _.union\n */\n union(...arrays: Array | null | undefined>): LoDashExplicitArrayWrapper;\n\n /**\n * @see _.union\n */\n union(...arrays: Array | null | undefined>): LoDashExplicitArrayWrapper;\n }\n\n interface LoDashExplicitObjectWrapperBase {\n /**\n * @see _.union\n */\n union(...arrays: Array | null | undefined>): LoDashExplicitArrayWrapper;\n }\n\n //_.unionBy\n interface LoDashStatic {\n /**\n * This method is like `_.union` except that it accepts `iteratee` which is\n * invoked for each element of each `arrays` to generate the criterion by which\n * uniqueness is computed. The iteratee is invoked with one argument: (value).\n *\n * @param arrays The arrays to inspect.\n * @param iteratee The iteratee invoked per element.\n * @return Returns the new array of combined values.\n */\n unionBy(\n arrays: List | null | undefined,\n iteratee?: (value: T) => any\n ): T[];\n\n /**\n * @see _.unionBy\n */\n unionBy(\n arrays: List | null | undefined,\n iteratee?: W\n ): T[];\n\n /**\n * @see _.unionBy\n */\n unionBy(\n arrays1: List | null | undefined,\n arrays2: List | null | undefined,\n iteratee?: (value: T) => any\n ): T[];\n\n /**\n * @see _.unionBy\n */\n unionBy(\n arrays1: List | null | undefined,\n arrays2: List | null | undefined,\n iteratee?: W\n ): T[];\n\n /**\n * @see _.unionBy\n */\n unionBy(\n arrays1: List | null | undefined,\n arrays2: List | null | undefined,\n arrays3: List | null | undefined,\n iteratee?: (value: T) => any\n ): T[];\n\n /**\n * @see _.unionBy\n */\n unionBy(\n arrays1: List | null | undefined,\n arrays2: List | null | undefined,\n arrays3: List | null | undefined,\n iteratee?: W\n ): T[];\n\n /**\n * @see _.unionBy\n */\n unionBy(\n arrays1: List | null | undefined,\n arrays2: List | null | undefined,\n arrays3: List | null | undefined,\n arrays4: List | null | undefined,\n iteratee?: (value: T) => any\n ): T[];\n\n /**\n * @see _.unionBy\n */\n unionBy(\n arrays1: List | null | undefined,\n arrays2: List | null | undefined,\n arrays3: List | null | undefined,\n arrays4: List | null | undefined,\n iteratee?: W\n ): T[];\n\n /**\n * @see _.unionBy\n */\n unionBy(\n arrays1: List | null | undefined,\n arrays2: List | null | undefined,\n arrays3: List | null | undefined,\n arrays4: List | null | undefined,\n arrays5: List | null | undefined,\n iteratee?: (value: T) => any\n ): T[];\n\n /**\n * @see _.unionBy\n */\n unionBy(\n arrays1: List | null | undefined,\n arrays2: List | null | undefined,\n arrays3: List | null | undefined,\n arrays4: List | null | undefined,\n arrays5: List | null | undefined,\n iteratee?: W\n ): T[];\n\n /**\n * @see _.unionBy\n */\n unionBy(\n arrays: List | null | undefined,\n ...iteratee: any[]\n ): T[];\n }\n\n interface LoDashImplicitArrayWrapperBase {\n /**\n * @see _.unionBy\n */\n unionBy(\n iteratee?: (value: T) => any\n ): LoDashImplicitArrayWrapper;\n\n /**\n * @see _.unionBy\n */\n unionBy(\n iteratee?: W\n ): LoDashImplicitArrayWrapper;\n\n /**\n * @see _.unionBy\n */\n unionBy(\n arrays2: List | null | undefined,\n iteratee?: (value: T) => any\n ): LoDashImplicitArrayWrapper;\n\n /**\n * @see _.unionBy\n */\n unionBy(\n arrays2: List | null | undefined,\n iteratee?: W\n ): LoDashImplicitArrayWrapper;\n\n /**\n * @see _.unionBy\n */\n unionBy(\n arrays2: List | null | undefined,\n arrays3: List | null | undefined,\n iteratee?: (value: T) => any\n ): LoDashImplicitArrayWrapper;\n\n /**\n * @see _.unionBy\n */\n unionBy(\n arrays2: List | null | undefined,\n arrays3: List | null | undefined,\n iteratee?: W\n ): LoDashImplicitArrayWrapper;\n\n /**\n * @see _.unionBy\n */\n unionBy(\n arrays2: List | null | undefined,\n arrays3: List | null | undefined,\n arrays4: List | null | undefined,\n iteratee?: (value: T) => any\n ): LoDashImplicitArrayWrapper;\n\n /**\n * @see _.unionBy\n */\n unionBy(\n arrays2: List | null | undefined,\n arrays3: List | null | undefined,\n arrays4: List | null | undefined,\n iteratee?: W\n ): LoDashImplicitArrayWrapper;\n\n /**\n * @see _.unionBy\n */\n unionBy(\n arrays2: List | null | undefined,\n arrays3: List | null | undefined,\n arrays4: List | null | undefined,\n arrays5: List | null | undefined,\n iteratee?: (value: T) => any\n ): LoDashImplicitArrayWrapper;\n\n /**\n * @see _.unionBy\n */\n unionBy(\n arrays2: List | null | undefined,\n arrays3: List | null | undefined,\n arrays4: List | null | undefined,\n arrays5: List | null | undefined,\n iteratee?: W\n ): LoDashImplicitArrayWrapper;\n\n /**\n * @see _.unionBy\n */\n unionBy(\n ...iteratee: any[]\n ): LoDashImplicitArrayWrapper;\n }\n\n interface LoDashImplicitObjectWrapperBase {\n /**\n * @see _.unionBy\n */\n unionBy(\n iteratee?: (value: T) => any\n ): LoDashImplicitArrayWrapper;\n\n /**\n * @see _.unionBy\n */\n unionBy(\n iteratee?: W\n ): LoDashImplicitArrayWrapper;\n\n /**\n * @see _.unionBy\n */\n unionBy(\n arrays2: List | null | undefined,\n iteratee?: (value: T) => any\n ): LoDashImplicitArrayWrapper;\n\n /**\n * @see _.unionBy\n */\n unionBy(\n arrays2: List | null | undefined,\n iteratee?: W\n ): LoDashImplicitArrayWrapper;\n\n /**\n * @see _.unionBy\n */\n unionBy(\n arrays2: List | null | undefined,\n arrays3: List | null | undefined,\n iteratee?: (value: T) => any\n ): LoDashImplicitArrayWrapper;\n\n /**\n * @see _.unionBy\n */\n unionBy(\n arrays2: List | null | undefined,\n arrays3: List | null | undefined,\n iteratee?: W\n ): LoDashImplicitArrayWrapper;\n\n /**\n * @see _.unionBy\n */\n unionBy(\n arrays2: List | null | undefined,\n arrays3: List | null | undefined,\n arrays4: List | null | undefined,\n iteratee?: (value: T) => any\n ): LoDashImplicitArrayWrapper;\n\n /**\n * @see _.unionBy\n */\n unionBy(\n arrays2: List | null | undefined,\n arrays3: List | null | undefined,\n arrays4: List | null | undefined,\n iteratee?: W\n ): LoDashImplicitArrayWrapper;\n\n /**\n * @see _.unionBy\n */\n unionBy(\n arrays2: List | null | undefined,\n arrays3: List | null | undefined,\n arrays4: List | null | undefined,\n arrays5: List | null | undefined,\n iteratee?: (value: T) => any\n ): LoDashImplicitArrayWrapper;\n\n /**\n * @see _.unionBy\n */\n unionBy(\n arrays2: List | null | undefined,\n arrays3: List | null | undefined,\n arrays4: List | null | undefined,\n arrays5: List | null | undefined,\n iteratee?: W\n ): LoDashImplicitArrayWrapper;\n\n /**\n * @see _.unionBy\n */\n unionBy(\n ...iteratee: any[]\n ): LoDashImplicitArrayWrapper;\n }\n\n interface LoDashExplicitArrayWrapperBase {\n /**\n * @see _.unionBy\n */\n unionBy(\n iteratee?: (value: T) => any\n ): LoDashExplicitArrayWrapper;\n\n /**\n * @see _.unionBy\n */\n unionBy(\n iteratee?: W\n ): LoDashExplicitArrayWrapper;\n\n /**\n * @see _.unionBy\n */\n unionBy(\n arrays2: List | null | undefined,\n iteratee?: (value: T) => any\n ): LoDashExplicitArrayWrapper;\n\n /**\n * @see _.unionBy\n */\n unionBy(\n arrays2: List | null | undefined,\n iteratee?: W\n ): LoDashExplicitArrayWrapper;\n\n /**\n * @see _.unionBy\n */\n unionBy(\n arrays2: List | null | undefined,\n arrays3: List | null | undefined,\n iteratee?: (value: T) => any\n ): LoDashExplicitArrayWrapper;\n\n /**\n * @see _.unionBy\n */\n unionBy(\n arrays2: List | null | undefined,\n arrays3: List | null | undefined,\n iteratee?: W\n ): LoDashExplicitArrayWrapper;\n\n /**\n * @see _.unionBy\n */\n unionBy(\n arrays2: List | null | undefined,\n arrays3: List | null | undefined,\n arrays4: List | null | undefined,\n iteratee?: (value: T) => any\n ): LoDashExplicitArrayWrapper;\n\n /**\n * @see _.unionBy\n */\n unionBy(\n arrays2: List | null | undefined,\n arrays3: List | null | undefined,\n arrays4: List | null | undefined,\n iteratee?: W\n ): LoDashExplicitArrayWrapper;\n\n /**\n * @see _.unionBy\n */\n unionBy(\n arrays2: List | null | undefined,\n arrays3: List | null | undefined,\n arrays4: List | null | undefined,\n arrays5: List | null | undefined,\n iteratee?: (value: T) => any\n ): LoDashExplicitArrayWrapper;\n\n /**\n * @see _.unionBy\n */\n unionBy(\n arrays2: List | null | undefined,\n arrays3: List | null | undefined,\n arrays4: List | null | undefined,\n arrays5: List | null | undefined,\n iteratee?: W\n ): LoDashExplicitArrayWrapper;\n\n /**\n * @see _.unionBy\n */\n unionBy(\n ...iteratee: any[]\n ): LoDashExplicitArrayWrapper;\n }\n\n interface LoDashExplicitObjectWrapperBase {\n /**\n * @see _.unionBy\n */\n unionBy(\n iteratee?: (value: T) => any\n ): LoDashExplicitArrayWrapper;\n\n /**\n * @see _.unionBy\n */\n unionBy(\n iteratee?: W\n ): LoDashExplicitArrayWrapper;\n\n /**\n * @see _.unionBy\n */\n unionBy(\n arrays2: List | null | undefined,\n iteratee?: (value: T) => any\n ): LoDashExplicitArrayWrapper;\n\n /**\n * @see _.unionBy\n */\n unionBy(\n arrays2: List | null | undefined,\n iteratee?: W\n ): LoDashExplicitArrayWrapper;\n\n /**\n * @see _.unionBy\n */\n unionBy(\n arrays2: List | null | undefined,\n arrays3: List | null | undefined,\n iteratee?: (value: T) => any\n ): LoDashExplicitArrayWrapper;\n\n /**\n * @see _.unionBy\n */\n unionBy(\n arrays2: List | null | undefined,\n arrays3: List | null | undefined,\n iteratee?: W\n ): LoDashExplicitArrayWrapper;\n\n /**\n * @see _.unionBy\n */\n unionBy(\n arrays2: List | null | undefined,\n arrays3: List | null | undefined,\n arrays4: List | null | undefined,\n iteratee?: (value: T) => any\n ): LoDashExplicitArrayWrapper;\n\n /**\n * @see _.unionBy\n */\n unionBy(\n arrays2: List | null | undefined,\n arrays3: List | null | undefined,\n arrays4: List | null | undefined,\n iteratee?: W\n ): LoDashExplicitArrayWrapper;\n\n /**\n * @see _.unionBy\n */\n unionBy(\n arrays2: List | null | undefined,\n arrays3: List | null | undefined,\n arrays4: List | null | undefined,\n arrays5: List | null | undefined,\n iteratee?: (value: T) => any\n ): LoDashExplicitArrayWrapper;\n\n /**\n * @see _.unionBy\n */\n unionBy(\n arrays2: List | null | undefined,\n arrays3: List | null | undefined,\n arrays4: List | null | undefined,\n arrays5: List | null | undefined,\n iteratee?: W\n ): LoDashExplicitArrayWrapper;\n\n /**\n * @see _.unionBy\n */\n unionBy(\n ...iteratee: any[]\n ): LoDashExplicitArrayWrapper;\n }\n\n //_.uniq\n interface LoDashStatic {\n /**\n * Creates a duplicate-free version of an array, using\n * [`SameValueZero`](http://ecma-international.org/ecma-262/6.0/#sec-samevaluezero)\n * for equality comparisons, in which only the first occurrence of each element\n * is kept.\n *\n * @static\n * @memberOf _\n * @category Array\n * @param {Array} array The array to inspect.\n * @returns {Array} Returns the new duplicate free array.\n * @example\n *\n * _.uniq([2, 1, 2]);\n * // => [2, 1]\n */\n uniq(\n array: List | null | undefined\n ): T[];\n\n /**\n * @see _.uniq\n */\n uniq(\n array: List | null | undefined\n ): T[];\n }\n\n interface LoDashImplicitWrapper {\n /**\n * @see _.uniq\n */\n uniq(): LoDashImplicitArrayWrapper;\n }\n\n interface LoDashImplicitArrayWrapperBase {\n /**\n * @see _.uniq\n */\n uniq(): LoDashImplicitArrayWrapper;\n\n /**\n * @see _.uniq\n */\n uniq(): LoDashImplicitArrayWrapper;\n }\n\n interface LoDashImplicitObjectWrapperBase {\n uniq(): LoDashImplicitArrayWrapper;\n\n /**\n * @see _.uniq\n */\n uniq(): LoDashImplicitArrayWrapper;\n }\n\n interface LoDashExplicitWrapper {\n /**\n * @see _.uniq\n */\n uniq(): LoDashExplicitArrayWrapper;\n }\n\n interface LoDashExplicitArrayWrapperBase {\n /**\n * @see _.uniq\n */\n uniq(): LoDashExplicitArrayWrapper;\n\n /**\n * @see _.uniq\n */\n uniq(): LoDashExplicitArrayWrapper;\n }\n\n interface LoDashExplicitObjectWrapperBase {\n /**\n * @see _.uniq\n */\n uniq(): LoDashExplicitArrayWrapper;\n\n /**\n * @see _.uniq\n */\n uniq(): LoDashExplicitArrayWrapper;\n }\n\n //_.uniqBy\n interface LoDashStatic {\n /**\n * This method is like `_.uniq` except that it accepts `iteratee` which is\n * invoked for each element in `array` to generate the criterion by which\n * uniqueness is computed. The iteratee is invoked with one argument: (value).\n *\n * @static\n * @memberOf _\n * @category Array\n * @param {Array} array The array to inspect.\n * @param {Function|Object|string} [iteratee=_.identity] The iteratee invoked per element.\n * @returns {Array} Returns the new duplicate free array.\n * @example\n *\n * _.uniqBy([2.1, 1.2, 2.3], Math.floor);\n * // => [2.1, 1.2]\n *\n * // using the `_.property` iteratee shorthand\n * _.uniqBy([{ 'x': 1 }, { 'x': 2 }, { 'x': 1 }], 'x');\n * // => [{ 'x': 1 }, { 'x': 2 }]\n */\n uniqBy(\n array: List | null | undefined,\n iteratee: ListIterator\n ): T[];\n\n /**\n * @see _.uniqBy\n */\n uniqBy(\n array: List | null | undefined,\n iteratee: ListIterator\n ): T[];\n\n /**\n * @see _.uniqBy\n */\n uniqBy(\n array: List | null | undefined,\n iteratee: string\n ): T[];\n\n /**\n * @see _.uniqBy\n */\n uniqBy(\n array: List | null | undefined,\n iteratee: Object\n ): T[];\n\n /**\n * @see _.uniqBy\n */\n uniqBy(\n array: List | null | undefined,\n iteratee: TWhere\n ): T[];\n }\n\n interface LoDashImplicitWrapper {\n /**\n * @see _.uniqBy\n */\n uniqBy(\n iteratee: ListIterator\n ): LoDashImplicitArrayWrapper;\n }\n\n interface LoDashImplicitArrayWrapperBase {\n /**\n * @see _.uniqBy\n */\n uniqBy(\n iteratee: ListIterator\n ): LoDashImplicitArrayWrapper;\n\n /**\n * @see _.uniqBy\n */\n uniqBy(\n iteratee: string\n ): LoDashImplicitArrayWrapper;\n\n /**\n * @see _.uniqBy\n */\n uniqBy(\n iteratee: TWhere\n ): LoDashImplicitArrayWrapper;\n }\n\n interface LoDashImplicitObjectWrapperBase {\n /**\n * @see _.uniqBy\n */\n uniqBy(\n iteratee: ListIterator\n ): LoDashImplicitArrayWrapper;\n\n /**\n * @see _.uniqBy\n */\n uniqBy(\n iteratee: ListIterator\n ): LoDashImplicitArrayWrapper;\n\n /**\n * @see _.uniqBy\n */\n uniqBy(\n iteratee: string\n ): LoDashImplicitArrayWrapper;\n\n /**\n * @see _.uniqBy\n */\n uniqBy(\n iteratee: Object\n ): LoDashImplicitArrayWrapper;\n\n /**\n * @see _.uniqBy\n */\n uniqBy(\n iteratee: TWhere\n ): LoDashImplicitArrayWrapper;\n }\n\n interface LoDashExplicitWrapper {\n /**\n * @see _.uniqBy\n */\n uniqBy(\n iteratee: ListIterator\n ): LoDashExplicitArrayWrapper;\n }\n\n interface LoDashExplicitArrayWrapperBase {\n /**\n * @see _.uniqBy\n */\n uniqBy(\n iteratee: ListIterator\n ): LoDashExplicitArrayWrapper;\n\n /**\n * @see _.uniqBy\n */\n uniqBy(\n iteratee: string\n ): LoDashExplicitArrayWrapper;\n\n /**\n * @see _.uniqBy\n */\n uniqBy(\n iteratee: TWhere\n ): LoDashExplicitArrayWrapper;\n }\n\n interface LoDashExplicitObjectWrapperBase {\n /**\n * @see _.uniqBy\n */\n uniqBy(\n iteratee: ListIterator\n ): LoDashExplicitArrayWrapper;\n\n /**\n * @see _.uniqBy\n */\n uniqBy(\n iteratee: ListIterator\n ): LoDashExplicitArrayWrapper;\n\n /**\n * @see _.uniqBy\n */\n uniqBy(\n iteratee: string\n ): LoDashExplicitArrayWrapper;\n\n /**\n * @see _.uniqBy\n */\n uniqBy(\n iteratee: Object\n ): LoDashExplicitArrayWrapper;\n\n /**\n * @see _.uniqBy\n */\n uniqBy(\n iteratee: TWhere\n ): LoDashExplicitArrayWrapper;\n }\n\n //_.sortedUniq\n interface LoDashStatic {\n /**\n * This method is like `_.uniq` except that it's designed and optimized\n * for sorted arrays.\n *\n * @static\n * @memberOf _\n * @category Array\n * @param {Array} array The array to inspect.\n * @returns {Array} Returns the new duplicate free array.\n * @example\n *\n * _.sortedUniq([1, 1, 2]);\n * // => [1, 2]\n */\n sortedUniq(\n array: List | null | undefined\n ): T[];\n\n /**\n * @see _.sortedUniq\n */\n sortedUniq(\n array: List | null | undefined\n ): T[];\n }\n\n interface LoDashImplicitWrapper {\n /**\n * @see _.sortedUniq\n */\n sortedUniq(): LoDashImplicitArrayWrapper;\n }\n\n interface LoDashImplicitArrayWrapperBase {\n /**\n * @see _.sortedUniq\n */\n sortedUniq(): LoDashImplicitArrayWrapper;\n\n /**\n * @see _.sortedUniq\n */\n sortedUniq(): LoDashImplicitArrayWrapper;\n }\n\n interface LoDashImplicitObjectWrapperBase {\n sortedUniq(): LoDashImplicitArrayWrapper;\n\n /**\n * @see _.sortedUniq\n */\n sortedUniq(): LoDashImplicitArrayWrapper;\n }\n\n interface LoDashExplicitWrapper {\n /**\n * @see _.sortedUniq\n */\n sortedUniq(): LoDashExplicitArrayWrapper;\n }\n\n interface LoDashExplicitArrayWrapperBase {\n /**\n * @see _.sortedUniq\n */\n sortedUniq(): LoDashExplicitArrayWrapper;\n\n /**\n * @see _.sortedUniq\n */\n sortedUniq(): LoDashExplicitArrayWrapper;\n }\n\n interface LoDashExplicitObjectWrapperBase {\n /**\n * @see _.sortedUniq\n */\n sortedUniq(): LoDashExplicitArrayWrapper;\n\n /**\n * @see _.sortedUniq\n */\n sortedUniq(): LoDashExplicitArrayWrapper;\n }\n\n //_.sortedUniqBy\n interface LoDashStatic {\n /**\n * This method is like `_.uniqBy` except that it's designed and optimized\n * for sorted arrays.\n *\n * @static\n * @memberOf _\n * @category Array\n * @param {Array} array The array to inspect.\n * @param {Function} [iteratee] The iteratee invoked per element.\n * @returns {Array} Returns the new duplicate free array.\n * @example\n *\n * _.sortedUniqBy([1.1, 1.2, 2.3, 2.4], Math.floor);\n * // => [1.1, 2.2]\n */\n sortedUniqBy(\n array: List | null | undefined,\n iteratee: ListIterator\n ): T[];\n\n /**\n * @see _.sortedUniqBy\n */\n sortedUniqBy(\n array: List | null | undefined,\n iteratee: ListIterator\n ): T[];\n\n /**\n * @see _.sortedUniqBy\n */\n sortedUniqBy(\n array: List | null | undefined,\n iteratee: string\n ): T[];\n\n /**\n * @see _.sortedUniqBy\n */\n sortedUniqBy(\n array: List | null | undefined,\n iteratee: Object\n ): T[];\n\n /**\n * @see _.sortedUniqBy\n */\n sortedUniqBy(\n array: List | null | undefined,\n iteratee: TWhere\n ): T[];\n }\n\n interface LoDashImplicitWrapper {\n /**\n * @see _.sortedUniqBy\n */\n sortedUniqBy(\n iteratee: ListIterator\n ): LoDashImplicitArrayWrapper;\n }\n\n interface LoDashImplicitArrayWrapperBase {\n /**\n * @see _.sortedUniqBy\n */\n sortedUniqBy(\n iteratee: ListIterator\n ): LoDashImplicitArrayWrapper;\n\n /**\n * @see _.sortedUniqBy\n */\n sortedUniqBy(\n iteratee: string\n ): LoDashImplicitArrayWrapper;\n\n /**\n * @see _.sortedUniqBy\n */\n sortedUniqBy(\n iteratee: TWhere\n ): LoDashImplicitArrayWrapper;\n }\n\n interface LoDashImplicitObjectWrapperBase {\n /**\n * @see _.sortedUniqBy\n */\n sortedUniqBy(\n iteratee: ListIterator\n ): LoDashImplicitArrayWrapper;\n\n /**\n * @see _.sortedUniqBy\n */\n sortedUniqBy(\n iteratee: ListIterator\n ): LoDashImplicitArrayWrapper;\n\n /**\n * @see _.sortedUniqBy\n */\n sortedUniqBy(\n iteratee: string\n ): LoDashImplicitArrayWrapper;\n\n /**\n * @see _.sortedUniqBy\n */\n sortedUniqBy(\n iteratee: Object\n ): LoDashImplicitArrayWrapper;\n\n /**\n * @see _.sortedUniqBy\n */\n sortedUniqBy(\n iteratee: TWhere\n ): LoDashImplicitArrayWrapper;\n }\n\n interface LoDashExplicitWrapper {\n /**\n * @see _.sortedUniqBy\n */\n sortedUniqBy(\n iteratee: ListIterator\n ): LoDashExplicitArrayWrapper;\n }\n\n interface LoDashExplicitArrayWrapperBase {\n /**\n * @see _.sortedUniqBy\n */\n sortedUniqBy(\n iteratee: ListIterator\n ): LoDashExplicitArrayWrapper;\n\n /**\n * @see _.sortedUniqBy\n */\n sortedUniqBy(\n iteratee: string\n ): LoDashExplicitArrayWrapper;\n\n /**\n * @see _.sortedUniqBy\n */\n sortedUniqBy(\n iteratee: TWhere\n ): LoDashExplicitArrayWrapper;\n }\n\n interface LoDashExplicitObjectWrapperBase {\n /**\n * @see _.sortedUniqBy\n */\n sortedUniqBy(\n iteratee: ListIterator\n ): LoDashExplicitArrayWrapper;\n\n /**\n * @see _.sortedUniqBy\n */\n sortedUniqBy(\n iteratee: ListIterator\n ): LoDashExplicitArrayWrapper;\n\n /**\n * @see _.sortedUniqBy\n */\n sortedUniqBy(\n iteratee: string\n ): LoDashExplicitArrayWrapper;\n\n /**\n * @see _.sortedUniqBy\n */\n sortedUniqBy(\n iteratee: Object\n ): LoDashExplicitArrayWrapper;\n\n /**\n * @see _.sortedUniqBy\n */\n sortedUniqBy(\n iteratee: TWhere\n ): LoDashExplicitArrayWrapper;\n }\n\n //_.unionWith DUMMY\n interface LoDashStatic {\n /**\n * This method is like `_.union` except that it accepts `comparator` which\n * is invoked to compare elements of `arrays`. The comparator is invoked\n * with two arguments: (arrVal, othVal).\n *\n * @static\n * @memberOf _\n * @category Array\n * @param {...Array} [arrays] The arrays to inspect.\n * @param {Function} [comparator] The comparator invoked per element.\n * @returns {Array} Returns the new array of combined values.\n * @example\n *\n * var objects = [{ 'x': 1, 'y': 2 }, { 'x': 2, 'y': 1 }];\n * var others = [{ 'x': 1, 'y': 1 }, { 'x': 1, 'y': 2 }];\n *\n * _.unionWith(objects, others, _.isEqual);\n * // => [{ 'x': 1, 'y': 2 }, { 'x': 2, 'y': 1 }, { 'x': 1, 'y': 1 }]\n */\n unionWith(\n array: List,\n ...values: any[]\n ): any[];\n }\n\n //_.uniqWith DUMMY\n interface LoDashStatic {\n /**\n * This method is like `_.uniq` except that it accepts `comparator` which\n * is invoked to compare elements of `array`. The comparator is invoked with\n * two arguments: (arrVal, othVal).\n *\n * @static\n * @memberOf _\n * @category Array\n * @param {Array} array The array to inspect.\n * @param {Function} [comparator] The comparator invoked per element.\n * @returns {Array} Returns the new duplicate free array.\n * @example\n *\n * var objects = [{ 'x': 1, 'y': 2 }, { 'x': 2, 'y': 1 }, { 'x': 1, 'y': 2 }];\n *\n * _.uniqWith(objects, _.isEqual);\n * // => [{ 'x': 1, 'y': 2 }, { 'x': 2, 'y': 1 }]\n */\n uniqWith(\n array: List,\n ...values: any[]\n ): any[];\n }\n\n //_.unzip\n interface LoDashStatic {\n /**\n * This method is like _.zip except that it accepts an array of grouped elements and creates an array\n * regrouping the elements to their pre-zip configuration.\n *\n * @param array The array of grouped elements to process.\n * @return Returns the new array of regrouped elements.\n */\n unzip(array: List> | null | undefined): T[][];\n }\n\n interface LoDashImplicitArrayWrapperBase {\n /**\n * @see _.unzip\n */\n unzip(): LoDashImplicitArrayWrapper;\n }\n\n interface LoDashImplicitObjectWrapperBase {\n /**\n * @see _.unzip\n */\n unzip(): LoDashImplicitArrayWrapper;\n }\n\n interface LoDashExplicitArrayWrapperBase {\n /**\n * @see _.unzip\n */\n unzip(): LoDashExplicitArrayWrapper;\n }\n\n interface LoDashExplicitObjectWrapperBase {\n /**\n * @see _.unzip\n */\n unzip(): LoDashExplicitArrayWrapper;\n }\n\n //_.unzipWith\n interface LoDashStatic {\n /**\n * This method is like _.unzip except that it accepts an iteratee to specify how regrouped values should be\n * combined. The iteratee is bound to thisArg and invoked with four arguments: (accumulator, value, index,\n * group).\n *\n * @param array The array of grouped elements to process.\n * @param iteratee The function to combine regrouped values.\n * @param thisArg The this binding of iteratee.\n * @return Returns the new array of regrouped elements.\n */\n unzipWith(\n array: List> | null | undefined,\n iteratee?: MemoIterator\n ): TResult[];\n }\n\n interface LoDashImplicitArrayWrapperBase {\n /**\n * @see _.unzipWith\n */\n unzipWith(\n iteratee?: MemoIterator\n ): LoDashImplicitArrayWrapper;\n }\n\n interface LoDashImplicitObjectWrapperBase {\n /**\n * @see _.unzipWith\n */\n unzipWith(\n iteratee?: MemoIterator\n ): LoDashImplicitArrayWrapper;\n }\n\n //_.without\n interface LoDashStatic {\n /**\n * Creates an array excluding all provided values using SameValueZero for equality comparisons.\n *\n * @param array The array to filter.\n * @param values The values to exclude.\n * @return Returns the new array of filtered values.\n */\n without(\n array: List | null | undefined,\n ...values: T[]\n ): T[];\n }\n\n interface LoDashImplicitArrayWrapperBase {\n /**\n * @see _.without\n */\n without(...values: T[]): LoDashImplicitArrayWrapper;\n }\n\n interface LoDashImplicitObjectWrapperBase {\n /**\n * @see _.without\n */\n without(...values: T[]): LoDashImplicitArrayWrapper;\n }\n\n interface LoDashExplicitArrayWrapperBase {\n /**\n * @see _.without\n */\n without(...values: T[]): LoDashExplicitArrayWrapper;\n }\n\n interface LoDashExplicitObjectWrapperBase {\n /**\n * @see _.without\n */\n without(...values: T[]): LoDashExplicitArrayWrapper;\n }\n\n //_.xor\n interface LoDashStatic {\n /**\n * Creates an array of unique values that is the symmetric difference of the provided arrays.\n *\n * @param arrays The arrays to inspect.\n * @return Returns the new array of values.\n */\n xor(...arrays: Array | null | undefined>): T[];\n }\n\n interface LoDashImplicitArrayWrapperBase {\n /**\n * @see _.xor\n */\n xor(...arrays: Array | null | undefined>): LoDashImplicitArrayWrapper;\n }\n\n interface LoDashImplicitObjectWrapperBase {\n /**\n * @see _.xor\n */\n xor(...arrays: Array | null | undefined>): LoDashImplicitArrayWrapper;\n }\n\n interface LoDashExplicitArrayWrapperBase {\n /**\n * @see _.xor\n */\n xor(...arrays: Array | null | undefined>): LoDashExplicitArrayWrapper;\n }\n\n interface LoDashExplicitObjectWrapperBase {\n /**\n * @see _.xor\n */\n xor(...arrays: Array | null | undefined>): LoDashExplicitArrayWrapper;\n }\n\n //_.xorBy DUMMY\n interface LoDashStatic {\n /**\n * This method is like `_.xor` except that it accepts `iteratee` which is\n * invoked for each element of each `arrays` to generate the criterion by which\n * uniqueness is computed. The iteratee is invoked with one argument: (value).\n *\n * @static\n * @memberOf _\n * @category Array\n * @param {...Array} [arrays] The arrays to inspect.\n * @param {Function|Object|string} [iteratee=_.identity] The iteratee invoked per element.\n * @returns {Array} Returns the new array of values.\n * @example\n *\n * _.xorBy([2.1, 1.2], [4.3, 2.4], Math.floor);\n * // => [1.2, 4.3]\n *\n * // using the `_.property` iteratee shorthand\n * _.xorBy([{ 'x': 1 }], [{ 'x': 2 }, { 'x': 1 }], 'x');\n * // => [{ 'x': 2 }]\n */\n xorBy(\n array: List,\n ...values: any[]\n ): any[];\n }\n\n //_.xorWith DUMMY\n interface LoDashStatic {\n /**\n * This method is like `_.xor` except that it accepts `comparator` which is\n * invoked to compare elements of `arrays`. The comparator is invoked with\n * two arguments: (arrVal, othVal).\n *\n * @static\n * @memberOf _\n * @category Array\n * @param {...Array} [arrays] The arrays to inspect.\n * @param {Function} [comparator] The comparator invoked per element.\n * @returns {Array} Returns the new array of values.\n * @example\n *\n * var objects = [{ 'x': 1, 'y': 2 }, { 'x': 2, 'y': 1 }];\n * var others = [{ 'x': 1, 'y': 1 }, { 'x': 1, 'y': 2 }];\n *\n * _.xorWith(objects, others, _.isEqual);\n * // => [{ 'x': 2, 'y': 1 }, { 'x': 1, 'y': 1 }]\n */\n xorWith(\n array: List,\n ...values: any[]\n ): any[];\n }\n\n //_.zip\n interface LoDashStatic {\n /**\n * Creates an array of grouped elements, the first of which contains the first elements of the given arrays,\n * the second of which contains the second elements of the given arrays, and so on.\n *\n * @param arrays The arrays to process.\n * @return Returns the new array of grouped elements.\n */\n zip(...arrays: Array | null | undefined>): T[][];\n }\n\n interface LoDashImplicitArrayWrapperBase {\n /**\n * @see _.zip\n */\n zip(...arrays: Array | null | undefined>): _.LoDashImplicitArrayWrapper;\n }\n\n interface LoDashImplicitObjectWrapperBase {\n /**\n * @see _.zip\n */\n zip(...arrays: Array | null | undefined>): _.LoDashImplicitArrayWrapper;\n }\n\n interface LoDashExplicitArrayWrapperBase {\n /**\n * @see _.zip\n */\n zip(...arrays: Array | null | undefined>): _.LoDashExplicitArrayWrapper;\n }\n\n interface LoDashExplicitObjectWrapperBase {\n /**\n * @see _.zip\n */\n zip(...arrays: Array | null | undefined>): _.LoDashExplicitArrayWrapper;\n }\n\n //_.zipObject\n interface LoDashStatic {\n /**\n * This method is like _.fromPairs except that it accepts two arrays, one of property\n * identifiers and one of corresponding values.\n *\n * @param props The property names.\n * @param values The property values.\n * @return Returns the new object.\n */\n zipObject(\n props: List|List>,\n values?: List\n ): TResult;\n /**\n * This method is like _.zipObject except that it supports property paths.\n *\n * @param props The property names.\n * @param values The property values.\n * @return Returns the new object.\n */\n zipObjectDeep(\n props: List|List>,\n values?: List\n ): TResult;\n\n /**\n * @see _.zipObject\n */\n zipObject(\n props: List|List>,\n values?: List\n ): TResult;\n /**\n * @see _.zipObjectDeep\n */\n zipObjectDeep(\n props: List|List>,\n values?: List\n ): TResult;\n\n /**\n * @see _.zipObject\n */\n zipObject(\n props: List|List>,\n values?: List\n ): _.Dictionary;\n /**\n * @see _.zipObjectDeep\n */\n zipObjectDeep(\n props: List|List>,\n values?: List\n ): _.Dictionary;\n }\n\n interface LoDashImplicitArrayWrapper {\n /**\n * @see _.zipObject\n */\n zipObject(\n values?: List\n ): _.LoDashImplicitObjectWrapper;\n /**\n * @see _.zipObjectDeep\n */\n zipObjectDeep(\n values?: List\n ): _.LoDashImplicitObjectWrapper;\n\n /**\n * @see _.zipObject\n */\n zipObject(\n values?: List\n ): _.LoDashImplicitObjectWrapper;\n /**\n * @see _.zipObjectDeep\n */\n zipObjectDeep(\n values?: List\n ): _.LoDashImplicitObjectWrapper;\n\n /**\n * @see _.zipObject\n */\n zipObject(\n values?: List\n ): _.LoDashImplicitObjectWrapper<_.Dictionary>;\n /**\n * @see _.zipObjectDeep\n */\n zipObjectDeep(\n values?: List\n ): _.LoDashImplicitObjectWrapper<_.Dictionary>;\n }\n\n interface LoDashImplicitObjectWrapper {\n /**\n * @see _.zipObject\n */\n zipObject(\n values?: List\n ): _.LoDashImplicitObjectWrapper;\n /**\n * @see _.zipObjectDeep\n */\n zipObjectDeep(\n values?: List\n ): _.LoDashImplicitObjectWrapper;\n\n /**\n * @see _.zipObject\n */\n zipObject(\n values?: List\n ): _.LoDashImplicitObjectWrapper;\n /**\n * @see _.zipObjectDeep\n */\n zipObjectDeep(\n values?: List\n ): _.LoDashImplicitObjectWrapper;\n\n /**\n * @see _.zipObject\n */\n zipObject(\n values?: List\n ): _.LoDashImplicitObjectWrapper<_.Dictionary>;\n /**\n * @see _.zipObjectDeep\n */\n zipObjectDeep(\n values?: List\n ): _.LoDashImplicitObjectWrapper<_.Dictionary>;\n }\n\n interface LoDashExplicitArrayWrapper {\n /**\n * @see _.zipObject\n */\n zipObject(\n values?: List\n ): _.LoDashExplicitObjectWrapper;\n /**\n * @see _.zipObjectDeep\n */\n zipObjectDeep(\n values?: List\n ): _.LoDashExplicitObjectWrapper;\n\n /**\n * @see _.zipObject\n */\n zipObject(\n values?: List\n ): _.LoDashExplicitObjectWrapper;\n /**\n * @see _.zipObjectDeep\n */\n zipObjectDeep(\n values?: List\n ): _.LoDashExplicitObjectWrapper;\n\n /**\n * @see _.zipObject\n */\n zipObject(\n values?: List\n ): _.LoDashExplicitObjectWrapper<_.Dictionary>;\n /**\n * @see _.zipObjectDeep\n */\n zipObjectDeep(\n values?: List\n ): _.LoDashExplicitObjectWrapper<_.Dictionary>;\n }\n\n interface LoDashExplicitObjectWrapper {\n /**\n * @see _.zipObject\n */\n zipObject(\n values?: List\n ): _.LoDashExplicitObjectWrapper;\n /**\n * @see _.zipObjectDeep\n */\n zipObjectDeep(\n values?: List\n ): _.LoDashExplicitObjectWrapper;\n\n /**\n * @see _.zipObject\n */\n zipObject(\n values?: List\n ): _.LoDashExplicitObjectWrapper;\n /**\n * @see _.zipObjectDeep\n */\n zipObjectDeep(\n values?: List\n ): _.LoDashExplicitObjectWrapper;\n\n /**\n * @see _.zipObject\n */\n zipObject(\n values?: List\n ): _.LoDashExplicitObjectWrapper<_.Dictionary>;\n /**\n * @see _.zipObjectDeep\n */\n zipObjectDeep(\n values?: List\n ): _.LoDashExplicitObjectWrapper<_.Dictionary>;\n }\n\n //_.zipWith\n interface LoDashStatic {\n /**\n * This method is like _.zip except that it accepts an iteratee to specify how grouped values should be\n * combined. The iteratee is bound to thisArg and invoked with four arguments: (accumulator, value, index,\n * group).\n * @param {...Array} [arrays] The arrays to process.\n * @param {Function} [iteratee] The function to combine grouped values.\n * @param {*} [thisArg] The `this` binding of `iteratee`.\n * @return Returns the new array of grouped elements.\n */\n zipWith(...args: any[]): TResult[];\n }\n\n interface LoDashImplicitArrayWrapper {\n /**\n * @see _.zipWith\n */\n zipWith(...args: any[]): LoDashImplicitArrayWrapper;\n }\n\n /*********\n * Chain *\n *********/\n\n //_.chain\n interface LoDashStatic {\n /**\n * Creates a lodash object that wraps value with explicit method chaining enabled.\n *\n * @param value The value to wrap.\n * @return Returns the new lodash wrapper instance.\n */\n chain(value: number): LoDashExplicitWrapper;\n chain(value: string): LoDashExplicitWrapper;\n chain(value: boolean): LoDashExplicitWrapper;\n chain(value: null | undefined): LoDashExplicitWrapper;\n chain(value: T[]): LoDashExplicitArrayWrapper;\n chain(value: ReadonlyArray): LoDashExplicitArrayWrapper;\n chain(value: T[] | null | undefined): LoDashExplicitNillableArrayWrapper;\n chain(value: ReadonlyArray | null | undefined): LoDashExplicitNillableArrayWrapper;\n chain(value: T): LoDashExplicitObjectWrapper;\n chain(value: T | null | undefined): LoDashExplicitObjectWrapper;\n chain(value: any): LoDashExplicitWrapper;\n }\n\n interface LoDashImplicitWrapper {\n /**\n * @see _.chain\n */\n chain(): LoDashExplicitWrapper;\n }\n\n interface LoDashImplicitArrayWrapper {\n /**\n * @see _.chain\n */\n chain(): LoDashExplicitArrayWrapper;\n }\n\n interface LoDashImplicitNillableArrayWrapper {\n /**\n * @see _.chain\n */\n chain(): LoDashExplicitNillableArrayWrapper;\n }\n\n interface LoDashImplicitObjectWrapper {\n /**\n * @see _.chain\n */\n chain(): LoDashExplicitObjectWrapper;\n }\n\n interface LoDashImplicitNillableObjectWrapper {\n /**\n * @see _.chain\n */\n chain(): LoDashExplicitNillableObjectWrapper;\n }\n\n interface LoDashExplicitWrapperBase {\n /**\n * @see _.chain\n */\n chain(): TWrapper;\n }\n\n //_.tap\n interface LoDashStatic {\n /**\n * This method invokes interceptor and returns value. The interceptor is bound to thisArg and invoked with one\n * argument; (value). The purpose of this method is to \"tap into\" a method chain in order to perform operations\n * on intermediate results within the chain.\n *\n * @param value The value to provide to interceptor.\n * @param interceptor The function to invoke.\n * @parem thisArg The this binding of interceptor.\n * @return Returns value.\n **/\n tap(\n value: T,\n interceptor: (value: T) => void\n ): T;\n }\n\n interface LoDashImplicitWrapperBase {\n /**\n * @see _.tap\n */\n tap(\n interceptor: (value: T) => void\n ): TWrapper;\n }\n\n interface LoDashExplicitWrapperBase {\n /**\n * @see _.tap\n */\n tap(\n interceptor: (value: T) => void\n ): TWrapper;\n }\n\n //_.thru\n interface LoDashStatic {\n /**\n * This method is like _.tap except that it returns the result of interceptor.\n *\n * @param value The value to provide to interceptor.\n * @param interceptor The function to invoke.\n * @param thisArg The this binding of interceptor.\n * @return Returns the result of interceptor.\n */\n thru(\n value: T,\n interceptor: (value: T) => TResult\n ): TResult;\n }\n\n interface LoDashImplicitWrapperBase {\n /**\n * @see _.thru\n */\n thru(\n interceptor: (value: T) => TResult): LoDashImplicitWrapper;\n\n /**\n * @see _.thru\n */\n thru(\n interceptor: (value: T) => TResult): LoDashImplicitWrapper;\n\n /**\n * @see _.thru\n */\n thru(\n interceptor: (value: T) => TResult): LoDashImplicitWrapper;\n\n /**\n * @see _.thru\n */\n thru(\n interceptor: (value: T) => TResult): LoDashImplicitObjectWrapper;\n\n /**\n * @see _.thru\n */\n thru(\n interceptor: (value: T) => TResult[]): LoDashImplicitArrayWrapper;\n }\n\n interface LoDashExplicitWrapperBase {\n /**\n * @see _.thru\n */\n thru(\n interceptor: (value: T) => TResult\n ): LoDashExplicitWrapper;\n\n /**\n * @see _.thru\n */\n thru(\n interceptor: (value: T) => TResult\n ): LoDashExplicitWrapper;\n\n /**\n * @see _.thru\n */\n thru(\n interceptor: (value: T) => TResult\n ): LoDashExplicitWrapper;\n\n /**\n * @see _.thru\n */\n thru(\n interceptor: (value: T) => TResult\n ): LoDashExplicitObjectWrapper;\n\n /**\n * @see _.thru\n */\n thru(\n interceptor: (value: T) => TResult[]\n ): LoDashExplicitArrayWrapper;\n }\n\n //_.prototype.commit\n interface LoDashImplicitWrapperBase {\n /**\n * Executes the chained sequence and returns the wrapped result.\n *\n * @return Returns the new lodash wrapper instance.\n */\n commit(): TWrapper;\n }\n\n interface LoDashExplicitWrapperBase {\n /**\n * @see _.commit\n */\n commit(): TWrapper;\n }\n\n //_.prototype.concat\n interface LoDashImplicitWrapperBase {\n /**\n * Creates a new array joining a wrapped array with any additional arrays and/or values.\n *\n * @param items\n * @return Returns the new concatenated array.\n */\n concat(...items: Array>): LoDashImplicitArrayWrapper;\n\n /**\n * @see _.concat\n */\n concat(...items: Array>): LoDashImplicitArrayWrapper;\n }\n\n interface LoDashExplicitWrapperBase {\n /**\n * @see _.concat\n */\n concat(...items: Array>): LoDashExplicitArrayWrapper;\n\n /**\n * @see _.concat\n */\n concat(...items: Array>): LoDashExplicitArrayWrapper;\n }\n\n //_.prototype.plant\n interface LoDashImplicitWrapperBase {\n /**\n * Creates a clone of the chained sequence planting value as the wrapped value.\n * @param value The value to plant as the wrapped value.\n * @return Returns the new lodash wrapper instance.\n */\n plant(value: number): LoDashImplicitWrapper;\n\n /**\n * @see _.plant\n */\n plant(value: string): LoDashImplicitStringWrapper;\n\n /**\n * @see _.plant\n */\n plant(value: boolean): LoDashImplicitWrapper;\n\n /**\n * @see _.plant\n */\n plant(value: number[]): LoDashImplicitNumberArrayWrapper;\n\n /**\n * @see _.plant\n */\n plant(value: T[]): LoDashImplicitArrayWrapper;\n\n /**\n * @see _.plant\n */\n plant(value: ReadonlyArray): LoDashImplicitArrayWrapper;\n\n /**\n * @see _.plant\n */\n plant(value: T): LoDashImplicitObjectWrapper;\n\n /**\n * @see _.plant\n */\n plant(value: any): LoDashImplicitWrapper;\n }\n\n interface LoDashExplicitWrapperBase {\n /**\n * @see _.plant\n */\n plant(value: number): LoDashExplicitWrapper;\n\n /**\n * @see _.plant\n */\n plant(value: string): LoDashExplicitStringWrapper;\n\n /**\n * @see _.plant\n */\n plant(value: boolean): LoDashExplicitWrapper;\n\n /**\n * @see _.plant\n */\n plant(value: number[]): LoDashExplicitNumberArrayWrapper;\n\n /**\n * @see _.plant\n */\n plant(value: T[]): LoDashExplicitArrayWrapper;\n\n /**\n * @see _.plant\n */\n plant(value: ReadonlyArray): LoDashExplicitArrayWrapper;\n\n /**\n * @see _.plant\n */\n plant(value: T): LoDashExplicitObjectWrapper;\n\n /**\n * @see _.plant\n */\n plant(value: any): LoDashExplicitWrapper;\n }\n\n //_.prototype.reverse\n interface LoDashImplicitArrayWrapper {\n /**\n * Reverses the wrapped array so the first element becomes the last, the second element becomes the second to\n * last, and so on.\n *\n * Note: This method mutates the wrapped array.\n *\n * @return Returns the new reversed lodash wrapper instance.\n */\n reverse(): LoDashImplicitArrayWrapper;\n }\n\n interface LoDashExplicitArrayWrapper {\n /**\n * @see _.reverse\n */\n reverse(): LoDashExplicitArrayWrapper;\n }\n\n //_.prototype.toJSON\n interface LoDashWrapperBase {\n /**\n * @see _.value\n */\n toJSON(): T;\n }\n\n //_.prototype.toString\n interface LoDashWrapperBase {\n /**\n * Produces the result of coercing the unwrapped value to a string.\n *\n * @return Returns the coerced string value.\n */\n toString(): string;\n }\n\n //_.prototype.value\n interface LoDashWrapperBase {\n /**\n * Executes the chained sequence to extract the unwrapped value.\n *\n * @alias _.toJSON, _.valueOf\n *\n * @return Returns the resolved unwrapped value.\n */\n value(): T;\n }\n\n //_.valueOf\n interface LoDashWrapperBase {\n /**\n * @see _.value\n */\n valueOf(): T;\n }\n\n /**************\n * Collection *\n **************/\n\n //_.at\n interface LoDashStatic {\n /**\n * Creates an array of elements corresponding to the given keys, or indexes, of collection. Keys may be\n * specified as individual arguments or as arrays of keys.\n *\n * @param collection The collection to iterate over.\n * @param props The property names or indexes of elements to pick, specified individually or in arrays.\n * @return Returns the new array of picked elements.\n */\n at(\n collection: List|Dictionary | null | undefined,\n ...props: Array>\n ): T[];\n }\n\n interface LoDashImplicitArrayWrapperBase {\n /**\n * @see _.at\n */\n at(...props: Array>): LoDashImplicitArrayWrapper;\n }\n\n interface LoDashImplicitObjectWrapperBase {\n /**\n * @see _.at\n */\n at(...props: Array>): LoDashImplicitArrayWrapper;\n }\n\n interface LoDashExplicitArrayWrapperBase {\n /**\n * @see _.at\n */\n at(...props: Array>): LoDashExplicitArrayWrapper;\n }\n\n interface LoDashExplicitObjectWrapperBase {\n /**\n * @see _.at\n */\n at(...props: Array>): LoDashExplicitArrayWrapper;\n }\n\n //_.countBy\n interface LoDashStatic {\n /**\n * Creates an object composed of keys generated from the results of running each element of collection through\n * iteratee. The corresponding value of each key is the number of times the key was returned by iteratee. The\n * iteratee is bound to thisArg and invoked with three arguments:\n * (value, index|key, collection).\n *\n * If a property name is provided for iteratee the created _.property style callback returns the property\n * value of the given element.\n *\n * If a value is also provided for thisArg the created _.matchesProperty style callback returns true for\n * elements that have a matching property value, else false.\n *\n * If an object is provided for iteratee the created _.matches style callback returns true for elements that\n * have the properties of the given object, else false.\n *\n * @param collection The collection to iterate over.\n * @param iteratee The function invoked per iteration.\n * @param thisArg The this binding of iteratee.\n * @return Returns the composed aggregate object.\n */\n countBy(\n collection: List | null | undefined,\n iteratee?: ListIterator\n ): Dictionary;\n\n /**\n * @see _.countBy\n */\n countBy(\n collection: Dictionary | null | undefined,\n iteratee?: DictionaryIterator\n ): Dictionary;\n\n /**\n * @see _.countBy\n */\n countBy(\n collection: NumericDictionary | null | undefined,\n iteratee?: NumericDictionaryIterator\n ): Dictionary;\n\n /**\n * @see _.countBy\n */\n countBy(\n collection: List|Dictionary|NumericDictionary | null | undefined,\n iteratee?: string\n ): Dictionary;\n\n /**\n * @see _.countBy\n */\n countBy(\n collection: List|Dictionary|NumericDictionary | null | undefined,\n iteratee?: W\n ): Dictionary;\n\n /**\n * @see _.countBy\n */\n countBy(\n collection: List|Dictionary|NumericDictionary | null | undefined,\n iteratee?: Object\n ): Dictionary;\n }\n\n interface LoDashImplicitWrapper {\n /**\n * @see _.countBy\n */\n countBy(\n iteratee?: ListIterator\n ): LoDashImplicitObjectWrapper>;\n }\n\n interface LoDashImplicitArrayWrapperBase {\n /**\n * @see _.countBy\n */\n countBy(\n iteratee?: ListIterator\n ): LoDashImplicitObjectWrapper>;\n\n /**\n * @see _.countBy\n */\n countBy(\n iteratee?: string\n ): LoDashImplicitObjectWrapper>;\n\n /**\n * @see _.countBy\n */\n countBy(\n iteratee?: W\n ): LoDashImplicitObjectWrapper>;\n }\n\n interface LoDashImplicitObjectWrapperBase {\n /**\n * @see _.countBy\n */\n countBy(\n iteratee?: ListIterator|DictionaryIterator|NumericDictionaryIterator\n ): LoDashImplicitObjectWrapper>;\n\n /**\n * @see _.countBy\n */\n countBy(\n iteratee?: string\n ): LoDashImplicitObjectWrapper>;\n\n /**\n * @see _.countBy\n */\n countBy(\n iteratee?: W\n ): LoDashImplicitObjectWrapper>;\n }\n\n interface LoDashExplicitWrapper {\n /**\n * @see _.countBy\n */\n countBy(\n iteratee?: ListIterator\n ): LoDashExplicitObjectWrapper>;\n }\n\n interface LoDashExplicitArrayWrapperBase {\n /**\n * @see _.countBy\n */\n countBy(\n iteratee?: ListIterator\n ): LoDashExplicitObjectWrapper>;\n\n /**\n * @see _.countBy\n */\n countBy(\n iteratee?: string\n ): LoDashExplicitObjectWrapper>;\n\n /**\n * @see _.countBy\n */\n countBy(\n iteratee?: W\n ): LoDashExplicitObjectWrapper>;\n }\n\n interface LoDashExplicitObjectWrapperBase {\n /**\n * @see _.countBy\n */\n countBy(\n iteratee?: ListIterator|DictionaryIterator|NumericDictionaryIterator\n ): LoDashExplicitObjectWrapper>;\n\n /**\n * @see _.countBy\n */\n countBy(\n iteratee?: string\n ): LoDashExplicitObjectWrapper>;\n\n /**\n * @see _.countBy\n */\n countBy(\n iteratee?: W\n ): LoDashExplicitObjectWrapper>;\n }\n\n //_.each\n interface LoDashStatic {\n each: typeof _.forEach;\n }\n\n interface LoDashImplicitWrapper {\n /**\n * @see _.forEach\n */\n each(\n iteratee: StringIterator\n ): LoDashImplicitWrapper;\n }\n\n interface LoDashImplicitArrayWrapperBase {\n /**\n * @see _.forEach\n */\n each(\n iteratee: ListIterator\n ): TWrapper;\n }\n\n interface LoDashImplicitObjectWrapperBase {\n /**\n * @see _.forEach\n */\n each(\n iteratee?: ListIterator|DictionaryIterator\n ): TWrapper;\n }\n\n interface LoDashExplicitWrapper {\n /**\n * @see _.forEach\n */\n each(\n iteratee: StringIterator\n ): LoDashExplicitWrapper;\n }\n\n interface LoDashExplicitArrayWrapperBase {\n /**\n * @see _.forEach\n */\n each(\n iteratee: ListIterator\n ): TWrapper;\n }\n\n interface LoDashExplicitObjectWrapperBase {\n /**\n * @see _.forEach\n */\n each(\n iteratee?: ListIterator|DictionaryIterator\n ): TWrapper;\n }\n\n //_.eachRight\n interface LoDashStatic {\n eachRight: typeof _.forEachRight;\n }\n\n interface LoDashImplicitWrapper {\n /**\n * @see _.forEachRight\n */\n eachRight(\n iteratee: StringIterator\n ): LoDashImplicitWrapper;\n }\n\n interface LoDashImplicitArrayWrapperBase {\n /**\n * @see _.forEachRight\n */\n eachRight(\n iteratee: ListIterator\n ): TWrapper;\n }\n\n interface LoDashImplicitObjectWrapperBase {\n /**\n * @see _.forEachRight\n */\n eachRight(\n iteratee?: ListIterator|DictionaryIterator\n ): TWrapper;\n }\n\n interface LoDashExplicitWrapper {\n /**\n * @see _.forEachRight\n */\n eachRight(\n iteratee: StringIterator\n ): LoDashExplicitWrapper;\n }\n\n interface LoDashExplicitArrayWrapperBase {\n /**\n * @see _.forEachRight\n */\n eachRight(\n iteratee: ListIterator\n ): TWrapper;\n }\n\n interface LoDashExplicitObjectWrapperBase {\n /**\n * @see _.forEachRight\n */\n eachRight(\n iteratee?: ListIterator|DictionaryIterator\n ): TWrapper;\n }\n\n //_.every\n interface LoDashStatic {\n /**\n * Checks if predicate returns truthy for all elements of collection. Iteration is stopped once predicate\n * returns falsey. The predicate is invoked with three arguments: (value, index|key, collection).\n *\n * @param collection The collection to iterate over.\n * @param predicate The function invoked per iteration.\n * @return Returns true if all elements pass the predicate check, else false.\n */\n every(\n collection: List | null | undefined,\n predicate?: ListIterator\n ): boolean;\n\n /**\n * @see _.every\n */\n every(\n collection: Dictionary | null | undefined,\n predicate?: DictionaryIterator\n ): boolean;\n\n /**\n * @see _.every\n */\n every(\n collection: NumericDictionary | null | undefined,\n predicate?: NumericDictionaryIterator\n ): boolean;\n\n /**\n * @see _.every\n */\n every(\n collection: List|Dictionary|NumericDictionary | null | undefined,\n predicate?: string|any[]|PartialObject\n ): boolean;\n }\n\n interface LoDashImplicitArrayWrapperBase {\n /**\n * @see _.every\n */\n every(\n predicate?: ListIterator|NumericDictionaryIterator\n ): boolean;\n\n /**\n * @see _.every\n */\n every(\n predicate?: string|any[]\n ): boolean;\n\n /**\n * @see _.every\n */\n every(\n predicate?: PartialObject\n ): boolean;\n }\n\n interface LoDashImplicitObjectWrapperBase {\n /**\n * @see _.every\n */\n every(\n predicate?: ListIterator|DictionaryIterator|NumericDictionaryIterator\n ): boolean;\n\n /**\n * @see _.every\n */\n every(\n predicate?: string|any[]\n ): boolean;\n\n /**\n * @see _.every\n */\n every(\n predicate?: PartialObject\n ): boolean;\n }\n\n interface LoDashExplicitArrayWrapperBase {\n /**\n * @see _.every\n */\n every(\n predicate?: ListIterator|NumericDictionaryIterator\n ): LoDashExplicitWrapper;\n\n /**\n * @see _.every\n */\n every(\n predicate?: string|any[]\n ): LoDashExplicitWrapper;\n\n /**\n * @see _.every\n */\n every(\n predicate?: PartialObject\n ): LoDashExplicitWrapper;\n }\n\n interface LoDashExplicitObjectWrapperBase {\n /**\n * @see _.every\n */\n every(\n predicate?: ListIterator|DictionaryIterator|NumericDictionaryIterator\n ): LoDashExplicitWrapper;\n\n /**\n * @see _.every\n */\n every(\n predicate?: string|any[]\n ): LoDashExplicitWrapper;\n\n /**\n * @see _.every\n */\n every(\n predicate?: PartialObject\n ): LoDashExplicitWrapper;\n }\n\n //_.filter\n interface LoDashStatic {\n /**\n * Iterates over elements of collection, returning an array of all elements predicate returns truthy for. The\n * predicate is bound to thisArg and invoked with three arguments: (value, index|key, collection).\n *\n * If a property name is provided for predicate the created _.property style callback returns the property\n * value of the given element.\n *\n * If a value is also provided for thisArg the created _.matchesProperty style callback returns true for\n * elements that have a matching property value, else false.\n *\n * If an object is provided for predicate the created _.matches style callback returns true for elements that\n * have the properties of the given object, else false.\n *\n * @param collection The collection to iterate over.\n * @param predicate The function invoked per iteration.\n * @param thisArg The this binding of predicate.\n * @return Returns the new filtered array.\n */\n filter(\n collection: List | null | undefined,\n predicate: ListIteratorTypeGuard\n ): S[];\n\n /**\n * @see _.filter\n */\n filter(\n collection: List | null | undefined,\n predicate?: ListIterator\n ): T[];\n\n /**\n * @see _.filter\n */\n filter(\n collection: Dictionary | null | undefined,\n predicate: DictionaryIteratorTypeGuard\n ): S[];\n\n /**\n * @see _.filter\n */\n filter(\n collection: Dictionary | null | undefined,\n predicate?: DictionaryIterator\n ): T[];\n\n /**\n * @see _.filter\n */\n filter(\n collection: string | null | undefined,\n predicate?: StringIterator\n ): string[];\n\n /**\n * @see _.filter\n */\n filter(\n collection: List|Dictionary | null | undefined,\n predicate: string | [string, any] | RegExp | PartialObject\n ): T[];\n }\n\n interface LoDashImplicitWrapper {\n /**\n * @see _.filter\n */\n filter(\n predicate?: StringIterator\n ): LoDashImplicitArrayWrapper;\n }\n\n interface LoDashImplicitArrayWrapperBase {\n /**\n * @see _.filter\n */\n filter(\n predicate: ListIteratorTypeGuard\n ): LoDashImplicitArrayWrapper;\n\n /**\n * @see _.filter\n */\n filter(\n predicate: ListIterator | string | [string, any] | RegExp | PartialObject\n ): LoDashImplicitArrayWrapper;\n }\n\n interface LoDashImplicitObjectWrapperBase {\n /**\n * @see _.filter\n */\n filter(\n predicate: ListIteratorTypeGuard\n ): LoDashImplicitArrayWrapper;\n\n /**\n * @see _.filter\n */\n filter(\n predicate: ListIterator | DictionaryIterator | string | [string, any] | RegExp | PartialObject\n ): LoDashImplicitArrayWrapper;\n }\n\n interface LoDashExplicitWrapper {\n /**\n * @see _.filter\n */\n filter(\n predicate?: StringIterator\n ): LoDashExplicitArrayWrapper;\n }\n\n interface LoDashExplicitArrayWrapperBase {\n /**\n * @see _.filter\n */\n filter(\n predicate: ListIteratorTypeGuard\n ): LoDashExplicitArrayWrapper;\n\n /**\n * @see _.filter\n */\n filter(\n predicate: ListIterator | string | [string, any] | RegExp | PartialObject\n ): LoDashExplicitArrayWrapper;\n }\n\n interface LoDashExplicitObjectWrapperBase {\n /**\n * @see _.filter\n */\n filter(\n predicate: ListIteratorTypeGuard\n ): LoDashExplicitArrayWrapper;\n\n /**\n * @see _.filter\n */\n filter(\n predicate: ListIterator | DictionaryIterator | string | [string, any] | RegExp | PartialObject\n ): LoDashExplicitArrayWrapper;\n }\n\n //_.find\n interface LoDashStatic {\n /**\n * Iterates over elements of collection, returning the first element predicate returns truthy for.\n * The predicate is bound to thisArg and invoked with three arguments: (value, index|key, collection).\n *\n * If a property name is provided for predicate the created _.property style callback returns the property\n * value of the given element.\n *\n * If a value is also provided for thisArg the created _.matchesProperty style callback returns true for\n * elements that have a matching property value, else false.\n *\n * If an object is provided for predicate the created _.matches style callback returns true for elements that\n * have the properties of the given object, else false.\n *\n * @param collection The collection to search.\n * @param predicate The function invoked per iteration.\n * @param fromIndex The index to search from.\n * @return Returns the matched element, else undefined.\n */\n find(\n collection: List | null | undefined,\n predicate: ListIteratorTypeGuard,\n fromIndex?: number\n ): S|undefined;\n \n /**\n * @see _.find\n */\n find(\n collection: List | null | undefined,\n predicate?: ListIterator,\n fromIndex?: number\n ): T|undefined;\n\n /**\n * @see _.find\n */\n find(\n collection: Dictionary | null | undefined,\n predicate: DictionaryIteratorTypeGuard,\n fromIndex?: number\n ): S|undefined;\n\n /**\n * @see _.find\n */\n find(\n collection: Dictionary | null | undefined,\n predicate?: DictionaryIterator,\n fromIndex?: number\n ): T|undefined;\n\n /**\n * @see _.find\n */\n find(\n collection: List|Dictionary | null | undefined,\n predicate?: string | PartialObject | [string, any],\n fromIndex?: number\n ): T|undefined;\n }\n\n interface LoDashImplicitArrayWrapperBase {\n /**\n * @see _.find\n */\n find(\n predicate: ListIteratorTypeGuard,\n fromIndex?: number\n ): S|undefined;\n\n /**\n * @see _.find\n */\n find(\n predicate?: ListIterator | string | PartialObject | [string, any],\n fromIndex?: number\n ): T|undefined;\n }\n\n interface LoDashImplicitObjectWrapperBase {\n /**\n * @see _.find\n */\n find(\n predicate?: ListIterator | DictionaryIterator | string | PartialObject | [string, any],\n fromIndex?: number\n ): TResult|undefined;\n }\n\n interface LoDashExplicitWrapperBase {\n /**\n * @see _.find\n */\n find(\n predicate?: ListIterator | string | PartialObject | [string, any],\n fromIndex?: number\n ): any;\n }\n\n //_.findLast\n interface LoDashStatic {\n /**\n * This method is like _.find except that it iterates over elements of a collection from\n * right to left.\n * @param collection Searches for a value in this list.\n * @param predicate The function called per iteration.\n * @param fromIndex The index to search from.\n * @return The found element, else undefined.\n **/\n findLast(\n collection: List | null | undefined,\n predicate: ListIteratorTypeGuard,\n fromIndex?: number\n ): S|undefined;\n \n /**\n * @see _.findLast\n */\n findLast(\n collection: List | null | undefined,\n predicate?: ListIterator,\n fromIndex?: number\n ): T|undefined;\n\n /**\n * @see _.findLast\n */\n findLast(\n collection: Dictionary | null | undefined,\n predicate: DictionaryIteratorTypeGuard,\n fromIndex?: number\n ): S|undefined;\n\n /**\n * @see _.findLast\n */\n findLast(\n collection: Dictionary | null | undefined,\n predicate?: DictionaryIterator,\n fromIndex?: number\n ): T|undefined;\n\n /**\n * @see _.findLast\n */\n findLast(\n collection: List|Dictionary | null | undefined,\n predicate?: string | PartialObject | [string, any],\n fromIndex?: number\n ): T|undefined;\n }\n\n interface LoDashImplicitArrayWrapperBase {\n /**\n * @see _.findLast\n */\n findLast(\n predicate: ListIteratorTypeGuard,\n fromIndex?: number\n ): S|undefined;\n\n /**\n * @see _.findLast\n */\n findLast(\n predicate?: ListIterator | string | PartialObject | [string, any],\n fromIndex?: number\n ): T|undefined;\n }\n\n interface LoDashImplicitObjectWrapperBase {\n /**\n * @see _.findLast\n */\n findLast(\n predicate?: ListIterator | DictionaryIterator | string | PartialObject | [string, any],\n fromIndex?: number\n ): TResult|undefined;\n }\n\n interface LoDashExplicitWrapperBase {\n /**\n * @see _.findLast\n */\n findLast(\n predicate?: ListIterator | string | PartialObject | [string, any],\n fromIndex?: number\n ): any;\n }\n\n //_.flatMap\n interface LoDashStatic {\n /**\n * Creates an array of flattened values by running each element in collection through iteratee\n * and concating its result to the other mapped values. The iteratee is invoked with three arguments:\n * (value, index|key, collection).\n *\n * @param collection The collection to iterate over.\n * @param iteratee The function invoked per iteration.\n * @return Returns the new flattened array.\n */\n flatMap(\n collection: List> | Dictionary> | NumericDictionary> | null | undefined\n ): T[];\n\n /**\n * @see _.flatMap\n */\n flatMap(\n collection: List | null | undefined,\n iteratee: ListIterator> | string\n ): TResult[];\n\n /**\n * @see _.flatMap\n */\n flatMap(\n collection: Dictionary | null | undefined,\n iteratee: DictionaryIterator> | string\n ): TResult[];\n\n /**\n * @see _.flatMap\n */\n flatMap(\n collection: NumericDictionary | null | undefined,\n iteratee: NumericDictionaryIterator> | string\n ): TResult[];\n\n /**\n * @see _.flatMap\n */\n flatMap(\n collection: object | null | undefined,\n iteratee?: ObjectIterator> | string\n ): TResult[];\n\n /**\n * @see _.flatMap\n */\n flatMap(\n collection: object | null | undefined,\n iteratee: object\n ): boolean[];\n }\n\n interface LoDashImplicitWrapper {\n /**\n * @see _.flatMap\n */\n flatMap(\n iteratee: StringIterator>\n ): LoDashImplicitArrayWrapper;\n\n /**\n * @see _.flatMap\n */\n flatMap(): LoDashImplicitArrayWrapper;\n }\n\n interface LoDashImplicitArrayWrapperBase {\n /**\n * @see _.flatMap\n */\n flatMap(\n iteratee: ListIterator>|string\n ): LoDashImplicitArrayWrapper;\n\n /**\n * @see _.flatMap\n */\n flatMap(\n iteratee: object\n ): LoDashImplicitArrayWrapper;\n\n /**\n * @see _.flatMap\n */\n flatMap(): LoDashImplicitArrayWrapper;\n }\n\n interface LoDashImplicitObjectWrapperBase {\n /**\n * @see _.flatMap\n */\n flatMap(\n iteratee: ListIterator>|DictionaryIterator>|NumericDictionaryIterator>\n ): LoDashImplicitArrayWrapper;\n\n /**\n * @see _.flatMap\n */\n flatMap(\n iteratee: ObjectIterator>|string\n ): LoDashImplicitArrayWrapper;\n\n /**\n * @see _.flatMap\n */\n flatMap(\n iteratee: object\n ): LoDashImplicitArrayWrapper;\n\n /**\n * @see _.flatMap\n */\n flatMap(): LoDashImplicitArrayWrapper;\n }\n\n interface LoDashExplicitWrapper {\n /**\n * @see _.flatMap\n */\n flatMap(\n iteratee: StringIterator>\n ): LoDashExplicitArrayWrapper;\n\n /**\n * @see _.flatMap\n */\n flatMap(): LoDashExplicitArrayWrapper;\n }\n\n interface LoDashExplicitArrayWrapperBase {\n /**\n * @see _.flatMap\n */\n flatMap(\n iteratee: ListIterator>|string\n ): LoDashExplicitArrayWrapper;\n\n /**\n * @see _.flatMap\n */\n flatMap(\n iteratee: object\n ): LoDashExplicitArrayWrapper;\n\n /**\n * @see _.flatMap\n */\n flatMap(): LoDashExplicitArrayWrapper;\n }\n\n interface LoDashExplicitObjectWrapperBase {\n /**\n * @see _.flatMap\n */\n flatMap(\n iteratee: ListIterator>|DictionaryIterator>|NumericDictionaryIterator>\n ): LoDashExplicitArrayWrapper;\n\n /**\n * @see _.flatMap\n */\n flatMap(\n iteratee: ObjectIterator>|string\n ): LoDashExplicitArrayWrapper;\n\n /**\n * @see _.flatMap\n */\n flatMap(\n iteratee: object\n ): LoDashExplicitArrayWrapper;\n\n /**\n * @see _.flatMap\n */\n flatMap(): LoDashExplicitArrayWrapper;\n }\n\n //_.forEach\n interface LoDashStatic {\n /**\n * Iterates over elements of collection invoking iteratee for each element. The iteratee is bound to thisArg\n * and invoked with three arguments:\n * (value, index|key, collection). Iteratee functions may exit iteration early by explicitly returning false.\n *\n * Note: As with other \"Collections\" methods, objects with a \"length\" property are iterated like arrays. To\n * avoid this behavior _.forIn or _.forOwn may be used for object iteration.\n *\n * @alias _.each\n *\n * @param collection The collection to iterate over.\n * @param iteratee The function invoked per iteration.\n * @param thisArg The this binding of iteratee.\n */\n forEach(\n collection: TString,\n iteratee?: StringIterator\n ): TString;\n\n /**\n * @see _.forEach\n */\n forEach | null | undefined>(\n collection: TList & (List | null | undefined),\n iteratee?: ListIterator\n ): TList;\n\n /**\n * @see _.forEach\n */\n forEach | null | undefined>(\n collection: TDictionary & (Dictionary | null | undefined),\n iteratee?: DictionaryIterator\n ): TDictionary;\n\n /**\n * @see _.forEach\n */\n forEach(\n collection: T,\n iteratee?: ObjectIterator\n ): T;\n }\n\n interface LoDashImplicitWrapper {\n /**\n * @see _.forEach\n */\n forEach(\n iteratee: StringIterator\n ): LoDashImplicitWrapper;\n }\n\n interface LoDashImplicitArrayWrapperBase {\n /**\n * @see _.forEach\n */\n forEach(\n iteratee: ListIterator\n ): TWrapper;\n }\n\n interface LoDashImplicitObjectWrapperBase {\n /**\n * @see _.forEach\n */\n forEach(\n iteratee?: ListIterator|DictionaryIterator\n ): TWrapper;\n }\n\n interface LoDashExplicitWrapper {\n /**\n * @see _.forEach\n */\n forEach(\n iteratee: StringIterator\n ): LoDashExplicitWrapper;\n }\n\n interface LoDashExplicitArrayWrapperBase {\n /**\n * @see _.forEach\n */\n forEach(\n iteratee: ListIterator\n ): TWrapper;\n }\n\n interface LoDashExplicitObjectWrapperBase {\n /**\n * @see _.forEach\n */\n forEach(\n iteratee?: ListIterator|DictionaryIterator\n ): TWrapper;\n }\n\n //_.forEachRight\n interface LoDashStatic {\n /**\n * This method is like _.forEach except that it iterates over elements of collection from right to left.\n *\n * @alias _.eachRight\n *\n * @param collection The collection to iterate over.\n * @param iteratee The function called per iteration.\n * @param thisArg The this binding of callback.\n */\n forEachRight(\n collection: TString,\n iteratee?: StringIterator\n ): TString;\n\n /**\n * @see _.forEachRight\n */\n forEachRight | null | undefined>(\n collection: TList & (List | null | undefined),\n iteratee?: ListIterator\n ): TList;\n\n /**\n * @see _.forEachRight\n */\n forEachRight | null | undefined>(\n collection: TDictionary & (Dictionary | null | undefined),\n iteratee?: DictionaryIterator\n ): TDictionary;\n\n /**\n * @see _.forEachRight\n */\n forEachRight(\n collection: T,\n iteratee?: ObjectIterator\n ): T;\n }\n\n interface LoDashImplicitWrapper {\n /**\n * @see _.forEachRight\n */\n forEachRight(\n iteratee: StringIterator\n ): LoDashImplicitWrapper;\n }\n\n interface LoDashImplicitArrayWrapperBase {\n /**\n * @see _.forEachRight\n */\n forEachRight(\n iteratee: ListIterator\n ): TWrapper;\n }\n\n interface LoDashImplicitObjectWrapperBase {\n /**\n * @see _.forEachRight\n */\n forEachRight(\n iteratee?: ListIterator|DictionaryIterator\n ): TWrapper;\n }\n\n interface LoDashExplicitWrapper {\n /**\n * @see _.forEachRight\n */\n forEachRight(\n iteratee: StringIterator\n ): LoDashExplicitWrapper;\n }\n\n interface LoDashExplicitArrayWrapperBase {\n /**\n * @see _.forEachRight\n */\n forEachRight(\n iteratee: ListIterator\n ): TWrapper;\n }\n\n interface LoDashExplicitObjectWrapperBase {\n /**\n * @see _.forEachRight\n */\n forEachRight(\n iteratee?: ListIterator|DictionaryIterator\n ): TWrapper;\n }\n\n //_.groupBy\n interface LoDashStatic {\n /**\n * Creates an object composed of keys generated from the results of running each element of collection through\n * iteratee. The corresponding value of each key is an array of the elements responsible for generating the\n * key. The iteratee is bound to thisArg and invoked with three arguments:\n * (value, index|key, collection).\n *\n * If a property name is provided for iteratee the created _.property style callback returns the property\n * value of the given element.\n *\n * If a value is also provided for thisArg the created _.matchesProperty style callback returns true for\n * elements that have a matching property value, else false.\n *\n * If an object is provided for iteratee the created _.matches style callback returns true for elements that\n * have the properties of the given object, else false.\n *\n * @param collection The collection to iterate over.\n * @param iteratee The function invoked per iteration.\n * @param thisArg The this binding of iteratee.\n * @return Returns the composed aggregate object.\n */\n groupBy(\n collection: List | null | undefined,\n iteratee?: ListIterator\n ): Dictionary;\n\n /**\n * @see _.groupBy\n */\n groupBy(\n collection: List | null | undefined,\n iteratee?: ListIterator\n ): Dictionary;\n\n /**\n * @see _.groupBy\n */\n groupBy(\n collection: Dictionary | null | undefined,\n iteratee?: DictionaryIterator\n ): Dictionary;\n\n /**\n * @see _.groupBy\n */\n groupBy(\n collection: Dictionary | null | undefined,\n iteratee?: DictionaryIterator\n ): Dictionary;\n\n /**\n * @see _.groupBy\n */\n groupBy(\n collection: List|Dictionary | null | undefined,\n iteratee?: string\n ): Dictionary;\n\n /**\n * @see _.groupBy\n */\n groupBy(\n collection: List|Dictionary | null | undefined,\n iteratee?: string\n ): Dictionary;\n\n /**\n * @see _.groupBy\n */\n groupBy(\n collection: List|Dictionary | null | undefined,\n iteratee?: TWhere\n ): Dictionary;\n\n /**\n * @see _.groupBy\n */\n groupBy(\n collection: List|Dictionary | null | undefined,\n iteratee?: Object\n ): Dictionary;\n }\n\n interface LoDashImplicitWrapper {\n /**\n * @see _.groupBy\n */\n groupBy(\n iteratee?: ListIterator\n ): LoDashImplicitObjectWrapper>;\n }\n\n interface LoDashImplicitArrayWrapperBase {\n /**\n * @see _.groupBy\n */\n groupBy(\n iteratee?: ListIterator\n ): LoDashImplicitObjectWrapper>;\n\n /**\n * @see _.groupBy\n */\n groupBy(\n iteratee?: string\n ): LoDashImplicitObjectWrapper>;\n\n /**\n * @see _.groupBy\n */\n groupBy(\n iteratee?: TWhere\n ): LoDashImplicitObjectWrapper>;\n }\n\n interface LoDashImplicitObjectWrapperBase {\n /**\n * @see _.groupBy\n */\n groupBy(\n iteratee?: ListIterator|DictionaryIterator\n ): LoDashImplicitObjectWrapper>;\n\n /**\n * @see _.groupBy\n */\n groupBy(\n iteratee?: ListIterator|DictionaryIterator\n ): LoDashImplicitObjectWrapper>;\n\n /**\n * @see _.groupBy\n */\n groupBy(\n iteratee?: string\n ): LoDashImplicitObjectWrapper>;\n\n /**\n * @see _.groupBy\n */\n groupBy(\n iteratee?: string\n ): LoDashImplicitObjectWrapper>;\n\n /**\n * @see _.groupBy\n */\n groupBy(\n iteratee?: TWhere\n ): LoDashImplicitObjectWrapper>;\n\n /**\n * @see _.groupBy\n */\n groupBy(\n iteratee?: Object\n ): LoDashImplicitObjectWrapper>;\n }\n\n interface LoDashExplicitWrapper {\n /**\n * @see _.groupBy\n */\n groupBy(\n iteratee?: ListIterator\n ): LoDashExplicitObjectWrapper>;\n }\n\n interface LoDashExplicitArrayWrapperBase {\n /**\n * @see _.groupBy\n */\n groupBy(\n iteratee?: ListIterator\n ): LoDashExplicitObjectWrapper>;\n\n /**\n * @see _.groupBy\n */\n groupBy(\n iteratee?: string\n ): LoDashExplicitObjectWrapper>;\n\n /**\n * @see _.groupBy\n */\n groupBy(\n iteratee?: TWhere\n ): LoDashExplicitObjectWrapper>;\n }\n\n interface LoDashExplicitObjectWrapperBase {\n /**\n * @see _.groupBy\n */\n groupBy(\n iteratee?: ListIterator|DictionaryIterator\n ): LoDashExplicitObjectWrapper>;\n\n /**\n * @see _.groupBy\n */\n groupBy(\n iteratee?: ListIterator|DictionaryIterator\n ): LoDashExplicitObjectWrapper>;\n\n /**\n * @see _.groupBy\n */\n groupBy(\n iteratee?: string\n ): LoDashExplicitObjectWrapper>;\n\n /**\n * @see _.groupBy\n */\n groupBy(\n iteratee?: string\n ): LoDashExplicitObjectWrapper>;\n\n /**\n * @see _.groupBy\n */\n groupBy(\n iteratee?: TWhere\n ): LoDashExplicitObjectWrapper>;\n\n /**\n * @see _.groupBy\n */\n groupBy(\n iteratee?: Object\n ): LoDashExplicitObjectWrapper>;\n }\n\n //_.includes\n interface LoDashStatic {\n /**\n * Checks if target is in collection using SameValueZero for equality comparisons. If fromIndex is negative,\n * it’s used as the offset from the end of collection.\n *\n * @param collection The collection to search.\n * @param target The value to search for.\n * @param fromIndex The index to search from.\n * @return True if the target element is found, else false.\n */\n includes(\n collection: List|Dictionary | null | undefined,\n target: T,\n fromIndex?: number\n ): boolean;\n\n /**\n * @see _.includes\n */\n includes(\n collection: string | null | undefined,\n target: string,\n fromIndex?: number\n ): boolean;\n }\n\n interface LoDashImplicitArrayWrapperBase {\n /**\n * @see _.includes\n */\n includes(\n target: T,\n fromIndex?: number\n ): boolean;\n }\n\n interface LoDashImplicitObjectWrapperBase {\n /**\n * @see _.includes\n */\n includes(\n target: TValue,\n fromIndex?: number\n ): boolean;\n }\n\n interface LoDashImplicitWrapper {\n /**\n * @see _.includes\n */\n includes(\n target: string,\n fromIndex?: number\n ): boolean;\n }\n\n interface LoDashExplicitArrayWrapperBase {\n /**\n * @see _.includes\n */\n includes(\n target: T,\n fromIndex?: number\n ): LoDashExplicitWrapper;\n }\n\n interface LoDashExplicitObjectWrapperBase {\n /**\n * @see _.includes\n */\n includes(\n target: TValue,\n fromIndex?: number\n ): LoDashExplicitWrapper;\n }\n\n interface LoDashExplicitWrapper {\n /**\n * @see _.includes\n */\n includes(\n target: string,\n fromIndex?: number\n ): LoDashExplicitWrapper;\n }\n\n //_.keyBy\n interface LoDashStatic {\n /**\n * Creates an object composed of keys generated from the results of running each element of collection through\n * iteratee. The corresponding value of each key is the last element responsible for generating the key. The\n * iteratee function is bound to thisArg and invoked with three arguments:\n * (value, index|key, collection).\n *\n * If a property name is provided for iteratee the created _.property style callback returns the property\n * value of the given element.\n *\n * If a value is also provided for thisArg the created _.matchesProperty style callback returns true for\n * elements that have a matching property value, else false.\n *\n * If an object is provided for iteratee the created _.matches style callback returns true for elements that\n * have the properties of the given object, else false.\n *\n * @param collection The collection to iterate over.\n * @param iteratee The function invoked per iteration.\n * @param thisArg The this binding of iteratee.\n * @return Returns the composed aggregate object.\n */\n keyBy(\n collection: List | null | undefined,\n iteratee?: ListIterator\n ): Dictionary;\n\n /**\n * @see _.keyBy\n */\n keyBy(\n collection: NumericDictionary | null | undefined,\n iteratee?: NumericDictionaryIterator\n ): Dictionary;\n\n /**\n * @see _.keyBy\n */\n keyBy(\n collection: Dictionary | null | undefined,\n iteratee?: DictionaryIterator\n ): Dictionary;\n\n /**\n * @see _.keyBy\n */\n keyBy(\n collection: List|NumericDictionary|Dictionary | null | undefined,\n iteratee?: string\n ): Dictionary;\n\n /**\n * @see _.keyBy\n */\n keyBy(\n collection: List|NumericDictionary|Dictionary | null | undefined,\n iteratee?: W\n ): Dictionary;\n\n /**\n * @see _.keyBy\n */\n keyBy(\n collection: List|NumericDictionary|Dictionary | null | undefined,\n iteratee?: Object\n ): Dictionary;\n }\n\n interface LoDashImplicitWrapper {\n /**\n * @see _.keyBy\n */\n keyBy(\n iteratee?: ListIterator\n ): LoDashImplicitObjectWrapper>;\n }\n\n interface LoDashImplicitArrayWrapperBase {\n /**\n * @see _.keyBy\n */\n keyBy(\n iteratee?: ListIterator\n ): LoDashImplicitObjectWrapper>;\n\n /**\n * @see _.keyBy\n */\n keyBy(\n iteratee?: string\n ): LoDashImplicitObjectWrapper>;\n\n /**\n * @see _.keyBy\n */\n keyBy(\n iteratee?: W\n ): LoDashImplicitObjectWrapper>;\n }\n\n interface LoDashImplicitObjectWrapperBase {\n /**\n * @see _.keyBy\n */\n keyBy(\n iteratee?: ListIterator|NumericDictionaryIterator|DictionaryIterator\n ): LoDashImplicitObjectWrapper>;\n\n /**\n * @see _.keyBy\n */\n keyBy(\n iteratee?: string\n ): LoDashImplicitObjectWrapper>;\n\n /**\n * @see _.keyBy\n */\n keyBy(\n iteratee?: W\n ): LoDashImplicitObjectWrapper>;\n\n /**\n * @see _.keyBy\n */\n keyBy(\n iteratee?: Object\n ): LoDashImplicitObjectWrapper>;\n }\n\n interface LoDashExplicitWrapper {\n /**\n * @see _.keyBy\n */\n keyBy(\n iteratee?: ListIterator\n ): LoDashExplicitObjectWrapper>;\n }\n\n interface LoDashExplicitArrayWrapperBase {\n /**\n * @see _.keyBy\n */\n keyBy(\n iteratee?: ListIterator\n ): LoDashExplicitObjectWrapper>;\n\n /**\n * @see _.keyBy\n */\n keyBy(\n iteratee?: string\n ): LoDashExplicitObjectWrapper>;\n\n /**\n * @see _.keyBy\n */\n keyBy(\n iteratee?: W\n ): LoDashExplicitObjectWrapper>;\n }\n\n interface LoDashExplicitObjectWrapperBase {\n /**\n * @see _.keyBy\n */\n keyBy(\n iteratee?: ListIterator|NumericDictionaryIterator|DictionaryIterator\n ): LoDashExplicitObjectWrapper>;\n\n /**\n * @see _.keyBy\n */\n keyBy(\n iteratee?: string\n ): LoDashExplicitObjectWrapper>;\n\n /**\n * @see _.keyBy\n */\n keyBy(\n iteratee?: W\n ): LoDashExplicitObjectWrapper>;\n\n /**\n * @see _.keyBy\n */\n keyBy(\n iteratee?: Object\n ): LoDashExplicitObjectWrapper>;\n }\n\n //_.invoke\n interface LoDashStatic {\n /**\n * Invokes the method at path of object.\n * @param object The object to query.\n * @param path The path of the method to invoke.\n * @param args The arguments to invoke the method with.\n **/\n invoke(\n object: TObject,\n path: Many,\n ...args: any[]): TResult;\n\n /**\n * @see _.invoke\n **/\n invoke(\n object: Dictionary|TValue[],\n path: Many,\n ...args: any[]): TResult;\n\n /**\n * @see _.invoke\n **/\n invoke(\n object: any,\n path: Many,\n ...args: any[]): TResult;\n }\n\n interface LoDashImplicitArrayWrapper {\n /**\n * @see _.invoke\n **/\n invoke(\n path: Many,\n ...args: any[]): TResult;\n }\n\n interface LoDashImplicitObjectWrapper {\n /**\n * @see _.invoke\n **/\n invoke(\n path: Many,\n ...args: any[]): TResult;\n }\n\n interface LoDashExplicitArrayWrapper {\n /**\n * @see _.invoke\n **/\n invoke(\n path: Many,\n ...args: any[]): TResult;\n }\n\n interface LoDashExplicitObjectWrapper {\n /**\n * @see _.invoke\n **/\n invoke(\n path: Many,\n ...args: any[]): TResult;\n }\n\n //_.invokeMap\n interface LoDashStatic {\n /**\n * Invokes the method named by methodName on each element in the collection returning\n * an array of the results of each invoked method. Additional arguments will be provided\n * to each invoked method. If methodName is a function it will be invoked for, and this\n * bound to, each element in the collection.\n * @param collection The collection to iterate over.\n * @param methodName The name of the method to invoke.\n * @param args Arguments to invoke the method with.\n **/\n invokeMap(\n collection: TValue[] | null | undefined,\n methodName: string,\n ...args: any[]): TResult[];\n\n /**\n * @see _.invokeMap\n **/\n invokeMap(\n collection: Dictionary | null | undefined,\n methodName: string,\n ...args: any[]): TResult[];\n\n /**\n * @see _.invokeMap\n **/\n invokeMap(\n collection: Array<{}> | null | undefined,\n methodName: string,\n ...args: any[]): TResult[];\n\n /**\n * @see _.invokeMap\n **/\n invokeMap(\n collection: Dictionary<{}> | null | undefined,\n methodName: string,\n ...args: any[]): TResult[];\n\n /**\n * @see _.invokeMap\n **/\n invokeMap(\n collection: TValue[] | null | undefined,\n method: (...args: any[]) => TResult,\n ...args: any[]): TResult[];\n\n /**\n * @see _.invokeMap\n **/\n invokeMap(\n collection: Dictionary | null | undefined,\n method: (...args: any[]) => TResult,\n ...args: any[]): TResult[];\n\n /**\n * @see _.invokeMap\n **/\n invokeMap(\n collection: Array<{}> | null | undefined,\n method: (...args: any[]) => TResult,\n ...args: any[]): TResult[];\n\n /**\n * @see _.invokeMap\n **/\n invokeMap(\n collection: Dictionary<{}> | null | undefined,\n method: (...args: any[]) => TResult,\n ...args: any[]): TResult[];\n }\n\n interface LoDashImplicitArrayWrapperBase {\n /**\n * @see _.invokeMap\n **/\n invokeMap(\n methodName: string,\n ...args: any[]): LoDashImplicitArrayWrapper;\n\n /**\n * @see _.invokeMap\n **/\n invokeMap(\n method: (...args: any[]) => TResult,\n ...args: any[]): LoDashImplicitArrayWrapper;\n }\n\n interface LoDashImplicitObjectWrapperBase {\n /**\n * @see _.invokeMap\n **/\n invokeMap(\n methodName: string,\n ...args: any[]): LoDashImplicitArrayWrapper;\n\n /**\n * @see _.invokeMap\n **/\n invokeMap(\n method: (...args: any[]) => TResult,\n ...args: any[]): LoDashImplicitArrayWrapper;\n }\n\n interface LoDashExplicitArrayWrapperBase {\n /**\n * @see _.invokeMap\n **/\n invokeMap(\n methodName: string,\n ...args: any[]): LoDashExplicitArrayWrapper;\n\n /**\n * @see _.invokeMap\n **/\n invokeMap(\n method: (...args: any[]) => TResult,\n ...args: any[]): LoDashExplicitArrayWrapper;\n }\n\n interface LoDashExplicitObjectWrapperBase {\n /**\n * @see _.invokeMap\n **/\n invokeMap(\n methodName: string,\n ...args: any[]): LoDashExplicitArrayWrapper;\n\n /**\n * @see _.invokeMap\n **/\n invokeMap(\n method: (...args: any[]) => TResult,\n ...args: any[]): LoDashExplicitArrayWrapper;\n }\n\n //_.map\n interface LoDashStatic {\n /**\n * Creates an array of values by running each element in collection through iteratee. The iteratee is bound to\n * thisArg and invoked with three arguments: (value, index|key, collection).\n *\n * If a property name is provided for iteratee the created _.property style callback returns the property value\n * of the given element.\n *\n * If a value is also provided for thisArg the created _.matchesProperty style callback returns true for\n * elements that have a matching property value, else false.\n *\n * If an object is provided for iteratee the created _.matches style callback returns true for elements that\n * have the properties of the given object, else false.\n *\n * Many lodash methods are guarded to work as iteratees for methods like _.every, _.filter, _.map, _.mapValues,\n * _.reject, and _.some.\n *\n * The guarded methods are:\n * ary, callback, chunk, clone, create, curry, curryRight, drop, dropRight, every, fill, flatten, invert, max,\n * min, parseInt, slice, sortBy, take, takeRight, template, trim, trimLeft, trimRight, trunc, random, range,\n * sample, some, sum, uniq, and words\n *\n * @param collection The collection to iterate over.\n * @param iteratee The function invoked per iteration.\n * @param thisArg The this binding of iteratee.\n * @return Returns the new mapped array.\n */\n map(\n collection: List | null | undefined,\n iteratee: ListIterator\n ): TResult[];\n\n /**\n * @see _.map\n */\n map(collection: List | null | undefined): T[];\n\n /**\n * @see _.map\n */\n map(\n collection: Dictionary | null | undefined,\n iteratee: DictionaryIterator\n ): TResult[];\n\n /** @see _.map */\n map(\n collection: Dictionary | null | undefined,\n iteratee: K\n ): T[K][];\n\n /** @see _.map */\n map(collection: Dictionary | null | undefined): T[];\n\n map(\n collection: NumericDictionary | null | undefined,\n iteratee?: NumericDictionaryIterator\n ): TResult[];\n\n /** @see _.map */\n map(collection: List | null | undefined, iteratee: K): T[K][];\n\n /**\n * @see _.map\n */\n map(\n collection: List|Dictionary|NumericDictionary | null | undefined,\n iteratee?: string\n ): TResult[];\n\n /**\n * @see _.map\n */\n map(\n collection: List|Dictionary|NumericDictionary | null | undefined,\n iteratee?: TObject\n ): boolean[];\n }\n\n interface LoDashImplicitArrayWrapperBase {\n /**\n * @see _.map\n */\n map(\n iteratee: ListIterator\n ): LoDashImplicitArrayWrapper;\n\n /**\n * @see _.map\n */\n map(): LoDashImplicitArrayWrapper;\n\n /** @see _.map */\n map(iteratee: K): LoDashImplicitArrayWrapper;\n\n /**\n * @see _.map\n */\n map(\n iteratee: string\n ): LoDashImplicitArrayWrapper;\n\n /**\n * @see _.map\n */\n map(\n iteratee: TObject\n ): LoDashImplicitArrayWrapper;\n }\n\n interface LoDashImplicitObjectWrapperBase {\n /**\n * @see _.map\n */\n map(\n iteratee: ListIterator|DictionaryIterator\n ): LoDashImplicitArrayWrapper;\n\n /** @see _.map */\n map(): LoDashImplicitArrayWrapper;\n\n /** @see _.map */\n map(iteratee: K): LoDashImplicitArrayWrapper;\n\n /**\n * @see _.map\n */\n map(\n iteratee: string\n ): LoDashImplicitArrayWrapper;\n\n /**\n * @see _.map\n */\n map(\n iteratee: TObject\n ): LoDashImplicitArrayWrapper;\n }\n\n interface LoDashExplicitArrayWrapperBase {\n /**\n * @see _.map\n */\n map(\n iteratee: ListIterator\n ): LoDashExplicitArrayWrapper;\n\n /** @see _.map */\n map(): LoDashExplicitArrayWrapper;\n\n /** @see _.map */\n map(iteratee: K): LoDashExplicitArrayWrapper;\n\n /**\n * @see _.map\n */\n map(\n iteratee: string\n ): LoDashExplicitArrayWrapper;\n\n /**\n * @see _.map\n */\n map(\n iteratee: TObject\n ): LoDashExplicitArrayWrapper;\n }\n\n interface LoDashExplicitObjectWrapperBase {\n /**\n * @see _.map\n */\n map(\n iteratee: ListIterator|DictionaryIterator\n ): LoDashExplicitArrayWrapper;\n\n /** @see _.map */\n map(): LoDashExplicitArrayWrapper;\n\n /**\n * @see _.map\n */\n map(\n iteratee: string\n ): LoDashExplicitArrayWrapper;\n\n /**\n * @see _.map\n */\n map(\n iteratee: TObject\n ): LoDashExplicitArrayWrapper;\n }\n\n //_.partition\n interface LoDashStatic {\n /**\n * Creates an array of elements split into two groups, the first of which contains elements predicate returns truthy for,\n * while the second of which contains elements predicate returns falsey for.\n * The predicate is bound to thisArg and invoked with three arguments: (value, index|key, collection).\n *\n * If a property name is provided for predicate the created _.property style callback\n * returns the property value of the given element.\n *\n * If a value is also provided for thisArg the created _.matchesProperty style callback\n * returns true for elements that have a matching property value, else false.\n *\n * If an object is provided for predicate the created _.matches style callback returns\n * true for elements that have the properties of the given object, else false.\n *\n * @param collection The collection to iterate over.\n * @param callback The function called per iteration.\n * @param thisArg The this binding of predicate.\n * @return Returns the array of grouped elements.\n **/\n partition(\n collection: List | null | undefined,\n callback: ListIterator): T[][];\n\n /**\n * @see _.partition\n **/\n partition(\n collection: Dictionary | null | undefined,\n callback: DictionaryIterator): T[][];\n\n /**\n * @see _.partition\n **/\n partition(\n collection: List | null | undefined,\n whereValue: W): T[][];\n\n /**\n * @see _.partition\n **/\n partition(\n collection: Dictionary | null | undefined,\n whereValue: W): T[][];\n\n /**\n * @see _.partition\n **/\n partition(\n collection: List | null | undefined,\n path: string,\n srcValue: any): T[][];\n\n /**\n * @see _.partition\n **/\n partition(\n collection: Dictionary | null | undefined,\n path: string,\n srcValue: any): T[][];\n\n /**\n * @see _.partition\n **/\n partition(\n collection: List | null | undefined,\n pluckValue: string): T[][];\n\n /**\n * @see _.partition\n **/\n partition(\n collection: Dictionary | null | undefined,\n pluckValue: string): T[][];\n }\n\n interface LoDashImplicitStringWrapper {\n /**\n * @see _.partition\n */\n partition(\n callback: ListIterator): LoDashImplicitArrayWrapper;\n }\n\n interface LoDashImplicitArrayWrapperBase {\n /**\n * @see _.partition\n */\n partition(\n callback: ListIterator): LoDashImplicitArrayWrapper;\n /**\n * @see _.partition\n */\n partition(\n whereValue: W): LoDashImplicitArrayWrapper;\n /**\n * @see _.partition\n */\n partition(\n path: string,\n srcValue: any): LoDashImplicitArrayWrapper;\n /**\n * @see _.partition\n */\n partition(\n pluckValue: string): LoDashImplicitArrayWrapper;\n }\n\n interface LoDashImplicitObjectWrapperBase {\n /**\n * @see _.partition\n */\n partition(\n callback: ListIterator): LoDashImplicitArrayWrapper;\n\n /**\n * @see _.partition\n */\n partition(\n callback: DictionaryIterator): LoDashImplicitArrayWrapper;\n\n /**\n * @see _.partition\n */\n partition(\n whereValue: W): LoDashImplicitArrayWrapper;\n\n /**\n * @see _.partition\n */\n partition(\n path: string,\n srcValue: any): LoDashImplicitArrayWrapper;\n\n /**\n * @see _.partition\n */\n partition(\n pluckValue: string): LoDashImplicitArrayWrapper;\n }\n\n //_.reduce\n interface LoDashStatic {\n /**\n * Reduces a collection to a value which is the accumulated result of running each\n * element in the collection through the callback, where each successive callback execution\n * consumes the return value of the previous execution. If accumulator is not provided the\n * first element of the collection will be used as the initial accumulator value. The callback\n * is bound to thisArg and invoked with four arguments; (accumulator, value, index|key, collection).\n * @param collection The collection to iterate over.\n * @param callback The function called per iteration.\n * @param accumulator Initial value of the accumulator.\n * @param thisArg The this binding of callback.\n * @return Returns the accumulated value.\n **/\n reduce(\n collection: T[] | null | undefined,\n callback: MemoIterator,\n accumulator: TResult): TResult;\n\n /**\n * @see _.reduce\n **/\n reduce(\n collection: List | null | undefined,\n callback: MemoIterator,\n accumulator: TResult): TResult;\n\n /**\n * @see _.reduce\n **/\n reduce(\n collection: Dictionary | null | undefined,\n callback: MemoIterator,\n accumulator: TResult): TResult;\n\n /**\n * @see _.reduce\n **/\n reduce(\n collection: NumericDictionary | null | undefined,\n callback: MemoIterator,\n accumulator: TResult): TResult;\n\n /**\n * @see _.reduce\n **/\n reduce(\n collection: T[] | null | undefined,\n callback: MemoIterator): TResult | undefined;\n\n /**\n * @see _.reduce\n **/\n reduce(\n collection: List | null | undefined,\n callback: MemoIterator): TResult | undefined;\n\n /**\n * @see _.reduce\n **/\n reduce(\n collection: Dictionary | null | undefined,\n callback: MemoIterator): TResult | undefined;\n\n /**\n * @see _.reduce\n **/\n reduce(\n collection: NumericDictionary | null | undefined,\n callback: MemoIterator): TResult | undefined;\n }\n\n interface LoDashImplicitArrayWrapperBase {\n /**\n * @see _.reduce\n **/\n reduce(\n callback: MemoIterator,\n accumulator: TResult): TResult;\n\n /**\n * @see _.reduce\n **/\n reduce(\n callback: MemoIterator): TResult | undefined;\n }\n\n interface LoDashImplicitObjectWrapperBase {\n /**\n * @see _.reduce\n **/\n reduce(\n callback: MemoIterator,\n accumulator: TResult): TResult;\n\n /**\n * @see _.reduce\n **/\n reduce(\n callback: MemoIterator): TResult | undefined;\n }\n\n interface LoDashExplicitObjectWrapperBase {\n /**\n * @see _.reduce\n **/\n reduce(\n callback: MemoIterator,\n accumulator: TResult): LoDashExplicitObjectWrapper;\n\n /**\n * @see _.reduce\n **/\n reduce(\n callback: MemoIterator): LoDashExplicitObjectWrapper;\n }\n\n interface LoDashExplicitArrayWrapperBase {\n /**LoDashExplicitWrapper\n * @see _.reduce\n */\n reduce(\n callback: MemoIterator,\n accumulator: TResult): LoDashExplicitWrapper;\n\n /**\n * @see _.reduce\n */\n reduce(\n callback: MemoIterator): LoDashExplicitWrapper;\n }\n\n //_.reduceRight\n interface LoDashStatic {\n /**\n * This method is like _.reduce except that it iterates over elements of a collection from\n * right to left.\n * @param collection The collection to iterate over.\n * @param callback The function called per iteration.\n * @param accumulator Initial value of the accumulator.\n * @param thisArg The this binding of callback.\n * @return The accumulated value.\n **/\n reduceRight(\n collection: T[] | null | undefined,\n callback: MemoIterator,\n accumulator: TResult): TResult;\n\n /**\n * @see _.reduceRight\n **/\n reduceRight(\n collection: List | null | undefined,\n callback: MemoIterator,\n accumulator: TResult): TResult;\n\n /**\n * @see _.reduceRight\n **/\n reduceRight(\n collection: Dictionary | null | undefined,\n callback: MemoIterator,\n accumulator: TResult): TResult;\n\n /**\n * @see _.reduceRight\n **/\n reduceRight(\n collection: T[] | null | undefined,\n callback: MemoIterator): TResult | undefined;\n\n /**\n * @see _.reduceRight\n **/\n reduceRight(\n collection: List | null | undefined,\n callback: MemoIterator): TResult | undefined;\n\n /**\n * @see _.reduceRight\n **/\n reduceRight(\n collection: Dictionary | null | undefined,\n callback: MemoIterator): TResult | undefined;\n }\n\n //_.reject\n interface LoDashStatic {\n /**\n * The opposite of _.filter; this method returns the elements of collection that predicate does not return\n * truthy for.\n *\n * @param collection The collection to iterate over.\n * @param predicate The function invoked per iteration.\n * @param thisArg The this binding of predicate.\n * @return Returns the new filtered array.\n */\n reject(\n collection: List | null | undefined,\n predicate?: ListIterator\n ): T[];\n\n /**\n * @see _.reject\n */\n reject(\n collection: Dictionary | null | undefined,\n predicate?: DictionaryIterator\n ): T[];\n\n /**\n * @see _.reject\n */\n reject(\n collection: string | null | undefined,\n predicate?: StringIterator\n ): string[];\n\n /**\n * @see _.reject\n */\n reject(\n collection: List|Dictionary | null | undefined,\n predicate: string\n ): T[];\n\n /**\n * @see _.reject\n */\n reject(\n collection: List|Dictionary | null | undefined,\n predicate: W\n ): T[];\n }\n\n interface LoDashImplicitWrapper {\n /**\n * @see _.reject\n */\n reject(\n predicate?: StringIterator\n ): LoDashImplicitArrayWrapper;\n }\n\n interface LoDashImplicitArrayWrapperBase {\n /**\n * @see _.reject\n */\n reject(\n predicate: ListIterator\n ): LoDashImplicitArrayWrapper;\n\n /**\n * @see _.reject\n */\n reject(\n predicate: string\n ): LoDashImplicitArrayWrapper;\n\n /**\n * @see _.reject\n */\n reject(predicate: W): LoDashImplicitArrayWrapper;\n }\n\n interface LoDashImplicitObjectWrapperBase {\n /**\n * @see _.reject\n */\n reject(\n predicate: ListIterator|DictionaryIterator\n ): LoDashImplicitArrayWrapper;\n\n /**\n * @see _.reject\n */\n reject(\n predicate: string\n ): LoDashImplicitArrayWrapper;\n\n /**\n * @see _.reject\n */\n reject(predicate: W): LoDashImplicitArrayWrapper;\n }\n\n interface LoDashExplicitWrapper {\n /**\n * @see _.reject\n */\n reject(\n predicate?: StringIterator\n ): LoDashExplicitArrayWrapper;\n }\n\n interface LoDashExplicitArrayWrapperBase {\n /**\n * @see _.reject\n */\n reject(\n predicate: ListIterator\n ): LoDashExplicitArrayWrapper;\n\n /**\n * @see _.reject\n */\n reject(\n predicate: string\n ): LoDashExplicitArrayWrapper;\n\n /**\n * @see _.reject\n */\n reject(predicate: W): LoDashExplicitArrayWrapper;\n }\n\n interface LoDashExplicitObjectWrapperBase {\n /**\n * @see _.reject\n */\n reject(\n predicate: ListIterator|DictionaryIterator\n ): LoDashExplicitArrayWrapper;\n\n /**\n * @see _.reject\n */\n reject(\n predicate: string\n ): LoDashExplicitArrayWrapper;\n\n /**\n * @see _.reject\n */\n reject(predicate: W): LoDashExplicitArrayWrapper;\n }\n\n //_.sample\n interface LoDashStatic {\n /**\n * Gets a random element from collection.\n *\n * @param collection The collection to sample.\n * @return Returns the random element.\n */\n sample(\n collection: List | Dictionary | NumericDictionary | object | null | undefined\n ): T | undefined;\n }\n\n interface LoDashImplicitWrapper {\n /**\n * @see _.sample\n */\n sample(): string | undefined;\n }\n\n interface LoDashImplicitArrayWrapperBase {\n /**\n * @see _.sample\n */\n sample(): T | undefined;\n }\n\n interface LoDashImplicitObjectWrapperBase {\n /**\n * @see _.sample\n */\n sample(): T | undefined;\n }\n\n interface LoDashExplicitWrapper {\n /**\n * @see _.sample\n */\n sample(): LoDashExplicitWrapper;\n }\n\n interface LoDashExplicitArrayWrapperBase {\n /**\n * @see _.sample\n */\n sample(): TWrapper;\n }\n\n interface LoDashExplicitObjectWrapperBase {\n /**\n * @see _.sample\n */\n sample(): TWrapper;\n }\n\n //_.sampleSize\n interface LoDashStatic {\n /**\n * Gets n random elements at unique keys from collection up to the size of collection.\n *\n * @param collection The collection to sample.\n * @param n The number of elements to sample.\n * @return Returns the random elements.\n */\n sampleSize(\n collection: List|Dictionary|NumericDictionary | null | undefined,\n n?: number\n ): T[];\n\n /**\n * @see _.sampleSize\n */\n sampleSize(\n collection: O | null | undefined,\n n?: number\n ): T[];\n\n /**\n * @see _.sampleSize\n */\n sampleSize(\n collection: Object | null | undefined,\n n?: number\n ): T[];\n }\n\n interface LoDashImplicitWrapper {\n /**\n * @see _.sampleSize\n */\n sampleSize(\n n?: number\n ): LoDashImplicitArrayWrapper;\n }\n\n interface LoDashImplicitArrayWrapperBase {\n /**\n * @see _.sampleSize\n */\n sampleSize(\n n?: number\n ): LoDashImplicitArrayWrapper;\n }\n\n interface LoDashImplicitObjectWrapperBase {\n /**\n * @see _.sampleSize\n */\n sampleSize(\n n?: number\n ): LoDashImplicitArrayWrapper;\n }\n\n interface LoDashExplicitWrapper {\n /**\n * @see _.sampleSize\n */\n sampleSize(\n n?: number\n ): LoDashExplicitArrayWrapper;\n }\n\n interface LoDashExplicitArrayWrapperBase {\n /**\n * @see _.sampleSize\n */\n sampleSize(\n n?: number\n ): LoDashExplicitArrayWrapper;\n }\n\n interface LoDashExplicitObjectWrapperBase {\n /**\n * @see _.sampleSize\n */\n sampleSize(\n n?: number\n ): LoDashExplicitArrayWrapper;\n }\n\n //_.shuffle\n interface LoDashStatic {\n /**\n * Creates an array of shuffled values, using a version of the Fisher-Yates shuffle.\n *\n * @param collection The collection to shuffle.\n * @return Returns the new shuffled array.\n */\n shuffle(collection: List|Dictionary | null | undefined): T[];\n\n /**\n * @see _.shuffle\n */\n shuffle(collection: string | null | undefined): string[];\n }\n\n interface LoDashImplicitWrapper {\n /**\n * @see _.shuffle\n */\n shuffle(): LoDashImplicitArrayWrapper;\n }\n\n interface LoDashImplicitArrayWrapperBase {\n /**\n * @see _.shuffle\n */\n shuffle(): LoDashImplicitArrayWrapper;\n }\n\n interface LoDashImplicitObjectWrapperBase {\n /**\n * @see _.shuffle\n */\n shuffle(): LoDashImplicitArrayWrapper;\n }\n\n interface LoDashExplicitWrapper {\n /**\n * @see _.shuffle\n */\n shuffle(): LoDashExplicitArrayWrapper;\n }\n\n interface LoDashExplicitArrayWrapperBase {\n /**\n * @see _.shuffle\n */\n shuffle(): LoDashExplicitArrayWrapper;\n }\n\n interface LoDashExplicitObjectWrapperBase {\n /**\n * @see _.shuffle\n */\n shuffle(): LoDashExplicitArrayWrapper;\n }\n\n //_.size\n interface LoDashStatic {\n /**\n * Gets the size of collection by returning its length for array-like values or the number of own enumerable\n * properties for objects.\n *\n * @param collection The collection to inspect.\n * @return Returns the size of collection.\n */\n size(collection: List|Dictionary | null | undefined): number;\n\n /**\n * @see _.size\n */\n size(collection: string | null | undefined): number;\n }\n\n interface LoDashImplicitWrapper {\n /**\n * @see _.size\n */\n size(): number;\n }\n\n interface LoDashImplicitArrayWrapperBase {\n /**\n * @see _.size\n */\n size(): number;\n }\n\n interface LoDashImplicitObjectWrapperBase {\n /**\n * @see _.size\n */\n size(): number;\n }\n\n interface LoDashExplicitWrapper {\n /**\n * @see _.size\n */\n size(): LoDashExplicitWrapper;\n }\n\n interface LoDashExplicitArrayWrapperBase {\n /**\n * @see _.size\n */\n size(): LoDashExplicitWrapper;\n }\n\n interface LoDashExplicitObjectWrapperBase {\n /**\n * @see _.size\n */\n size(): LoDashExplicitWrapper;\n }\n\n //_.some\n interface LoDashStatic {\n /**\n * Checks if predicate returns truthy for any element of collection. Iteration is stopped once predicate\n * returns truthy. The predicate is invoked with three arguments: (value, index|key, collection).\n *\n * @param collection The collection to iterate over.\n * @param predicate The function invoked per iteration.\n * @return Returns true if any element passes the predicate check, else false.\n */\n some(\n collection: List | null | undefined,\n predicate?: ListIterator\n ): boolean;\n\n /**\n * @see _.some\n */\n some(\n collection: Dictionary | null | undefined,\n predicate?: DictionaryIterator\n ): boolean;\n\n /**\n * @see _.some\n */\n some(\n collection: NumericDictionary | null | undefined,\n predicate?: NumericDictionaryIterator\n ): boolean;\n\n /**\n * @see _.some\n */\n some(\n collection: Object | null | undefined,\n predicate?: ObjectIterator\n ): boolean;\n\n /**\n * @see _.some\n */\n some(\n collection: List|Dictionary|NumericDictionary | null | undefined,\n predicate?: string|[string, any]\n ): boolean;\n\n /**\n * @see _.some\n */\n some(\n collection: Object | null | undefined,\n predicate?: string|[string, any]\n ): boolean;\n\n /**\n * @see _.some\n */\n some(\n collection: List|Dictionary|NumericDictionary | null | undefined,\n predicate?: PartialObject\n ): boolean;\n\n /**\n * @see _.some\n */\n some(\n collection: List|Dictionary|NumericDictionary | null | undefined,\n predicate?: PartialObject\n ): boolean;\n\n /**\n * @see _.some\n */\n some(\n collection: Object | null | undefined,\n predicate?: PartialObject\n ): boolean;\n }\n\n interface LoDashImplicitArrayWrapperBase {\n /**\n * @see _.some\n */\n some(\n predicate?: ListIterator|NumericDictionaryIterator\n ): boolean;\n\n /**\n * @see _.some\n */\n some(\n predicate?: string|[string, any]\n ): boolean;\n\n /**\n * @see _.some\n */\n some(\n predicate?: PartialObject\n ): boolean;\n }\n\n interface LoDashImplicitObjectWrapperBase {\n /**\n * @see _.some\n */\n some(\n predicate?: ListIterator|DictionaryIterator|NumericDictionaryIterator|ObjectIterator\n ): boolean;\n /**\n * @see _.some\n */\n some(\n predicate?: string|[string, any]\n ): boolean;\n\n /**\n * @see _.some\n */\n some(\n predicate?: PartialObject\n ): boolean;\n }\n\n interface LoDashExplicitArrayWrapperBase {\n /**\n * @see _.some\n */\n some(\n predicate?: ListIterator|NumericDictionaryIterator\n ): LoDashExplicitWrapper;\n\n /**\n * @see _.some\n */\n some(\n predicate?: string|[string, any]\n ): LoDashExplicitWrapper;\n\n /**\n * @see _.some\n */\n some(\n predicate?: PartialObject\n ): LoDashExplicitWrapper;\n }\n\n interface LoDashExplicitObjectWrapperBase {\n /**\n * @see _.some\n */\n some(\n predicate?: ListIterator|DictionaryIterator|NumericDictionaryIterator|ObjectIterator\n ): LoDashExplicitWrapper;\n\n /**\n * @see _.some\n */\n some(\n predicate?: string|[string, any]\n ): LoDashExplicitWrapper;\n\n /**\n * @see _.some\n */\n some(\n predicate?: PartialObject\n ): LoDashExplicitWrapper;\n }\n\n //_.sortBy\n interface LoDashStatic {\n /**\n * Creates an array of elements, sorted in ascending order by the results of\n * running each element in a collection through each iteratee. This method\n * performs a stable sort, that is, it preserves the original sort order of\n * equal elements. The iteratees are invoked with one argument: (value).\n *\n * @static\n * @memberOf _\n * @category Collection\n * @param {Array|Object} collection The collection to iterate over.\n * @param {...(Function|Function[]|Object|Object[]|string|string[])} [iteratees=[_.identity]]\n * The iteratees to sort by, specified individually or in arrays.\n * @returns {Array} Returns the new sorted array.\n * @example\n *\n * var users = [\n * { 'user': 'fred', 'age': 48 },\n * { 'user': 'barney', 'age': 36 },\n * { 'user': 'fred', 'age': 42 },\n * { 'user': 'barney', 'age': 34 }\n * ];\n *\n * _.sortBy(users, function(o) { return o.user; });\n * // => objects for [['barney', 36], ['barney', 34], ['fred', 48], ['fred', 42]]\n *\n * _.sortBy(users, ['user', 'age']);\n * // => objects for [['barney', 34], ['barney', 36], ['fred', 42], ['fred', 48]]\n *\n * _.sortBy(users, 'user', function(o) {\n * return Math.floor(o.age / 10);\n * });\n * // => objects for [['barney', 36], ['barney', 34], ['fred', 48], ['fred', 42]]\n */\n sortBy(\n collection: List | null | undefined,\n iteratee?: ListIterator\n ): T[];\n\n /**\n * @see _.sortBy\n */\n sortBy(\n collection: Dictionary | null | undefined,\n iteratee?: DictionaryIterator\n ): T[];\n\n /**\n * @see _.sortBy\n */\n sortBy(\n collection: List|Dictionary | null | undefined,\n iteratee: string\n ): T[];\n\n /**\n * @see _.sortBy\n */\n sortBy(\n collection: List|Dictionary | null | undefined,\n whereValue: W\n ): T[];\n\n /**\n * @see _.sortBy\n */\n sortBy(\n collection: List|Dictionary | null | undefined\n ): T[];\n\n /**\n * @see _.sortBy\n */\n sortBy(\n collection: List | null | undefined,\n iteratees: Array|string|Object>\n ): T[];\n\n /**\n * @see _.sortBy\n */\n sortBy(\n collection: List | null | undefined,\n ...iteratees: Array|Object|string>\n ): T[];\n }\n\n interface LoDashImplicitArrayWrapperBase {\n /**\n * @see _.sortBy\n */\n sortBy(\n iteratee?: ListIterator\n ): LoDashImplicitArrayWrapper;\n\n /**\n * @see _.sortBy\n */\n sortBy(iteratee: string): LoDashImplicitArrayWrapper;\n\n /**\n * @see _.sortBy\n */\n sortBy(whereValue: W): LoDashImplicitArrayWrapper;\n\n /**\n * @see _.sortBy\n */\n sortBy(): LoDashImplicitArrayWrapper;\n\n /**\n * @see _.sortBy\n */\n sortBy(...iteratees: Array|Object|string>): LoDashImplicitArrayWrapper;\n\n /**\n * @see _.sortBy\n **/\n sortBy(iteratees: Array|string|Object>): LoDashImplicitArrayWrapper;\n }\n\n interface LoDashImplicitObjectWrapperBase {\n /**\n * @see _.sortBy\n */\n sortBy(\n iteratee?: ListIterator|DictionaryIterator\n ): LoDashImplicitArrayWrapper;\n\n /**\n * @see _.sortBy\n */\n sortBy(iteratee: string): LoDashImplicitArrayWrapper;\n\n /**\n * @see _.sortBy\n */\n sortBy(whereValue: W): LoDashImplicitArrayWrapper;\n\n /**\n * @see _.sortBy\n */\n sortBy(): LoDashImplicitArrayWrapper;\n }\n\n interface LoDashExplicitArrayWrapperBase {\n /**\n * @see _.sortBy\n */\n sortBy(\n iteratee?: ListIterator\n ): LoDashExplicitArrayWrapper;\n\n /**\n * @see _.sortBy\n */\n sortBy(iteratee: string): LoDashExplicitArrayWrapper;\n\n /**\n * @see _.sortBy\n */\n sortBy(whereValue: W): LoDashExplicitArrayWrapper;\n\n /**\n * @see _.sortBy\n */\n sortBy(): LoDashExplicitArrayWrapper;\n }\n\n interface LoDashExplicitObjectWrapperBase {\n /**\n * @see _.sortBy\n */\n sortBy(\n iteratee?: ListIterator|DictionaryIterator\n ): LoDashExplicitArrayWrapper;\n\n /**\n * @see _.sortBy\n */\n sortBy(iteratee: string): LoDashExplicitArrayWrapper;\n\n /**\n * @see _.sortBy\n */\n sortBy(whereValue: W): LoDashExplicitArrayWrapper;\n\n /**\n * @see _.sortBy\n */\n sortBy(): LoDashExplicitArrayWrapper;\n }\n\n //_.orderBy\n interface LoDashStatic {\n /**\n * This method is like `_.sortBy` except that it allows specifying the sort\n * orders of the iteratees to sort by. If `orders` is unspecified, all values\n * are sorted in ascending order. Otherwise, specify an order of \"desc\" for\n * descending or \"asc\" for ascending sort order of corresponding values.\n *\n * @static\n * @memberOf _\n * @category Collection\n * @param {Array|Object} collection The collection to iterate over.\n * @param {Function[]|Object[]|string[]} [iteratees=[_.identity]] The iteratees to sort by.\n * @param {string[]} [orders] The sort orders of `iteratees`.\n * @param- {Object} [guard] Enables use as an iteratee for functions like `_.reduce`.\n * @returns {Array} Returns the new sorted array.\n * @example\n *\n * var users = [\n * { 'user': 'fred', 'age': 48 },\n * { 'user': 'barney', 'age': 34 },\n * { 'user': 'fred', 'age': 42 },\n * { 'user': 'barney', 'age': 36 }\n * ];\n *\n * // sort by `user` in ascending order and by `age` in descending order\n * _.orderBy(users, ['user', 'age'], ['asc', 'desc']);\n * // => objects for [['barney', 36], ['barney', 34], ['fred', 48], ['fred', 42]]\n */\n orderBy(\n collection: List | null | undefined,\n iteratees?: Many|string|W>,\n orders?: Many\n ): T[];\n\n /**\n * @see _.orderBy\n */\n orderBy(\n collection: List | null | undefined,\n iteratees?: Many|string|Object>,\n orders?: Many\n ): T[];\n\n /**\n * @see _.orderBy\n */\n orderBy(\n collection: NumericDictionary | null | undefined,\n iteratees?: Many|string|W>,\n orders?: Many\n ): T[];\n\n /**\n * @see _.orderBy\n */\n orderBy(\n collection: NumericDictionary | null | undefined,\n iteratees?: Many|string|Object>,\n orders?: Many\n ): T[];\n\n /**\n * @see _.orderBy\n */\n orderBy(\n collection: Dictionary | null | undefined,\n iteratees?: Many|string|W>,\n orders?: Many\n ): T[];\n\n /**\n * @see _.orderBy\n */\n orderBy(\n collection: Dictionary | null | undefined,\n iteratees?: Many|string|Object>,\n orders?: Many\n ): T[];\n }\n\n interface LoDashImplicitWrapper {\n /**\n * @see _.orderBy\n */\n orderBy(\n iteratees?: Many|string>,\n orders?: Many\n ): LoDashImplicitArrayWrapper;\n }\n\n interface LoDashImplicitArrayWrapperBase {\n /**\n * @see _.orderBy\n */\n orderBy(\n iteratees?: Many|string|W>,\n orders?: Many\n ): LoDashImplicitArrayWrapper;\n }\n\n interface LoDashImplicitObjectWrapperBase {\n /**\n * @see _.orderBy\n */\n orderBy(\n iteratees?: Many|string|W>,\n orders?: Many\n ): LoDashImplicitArrayWrapper;\n\n /**\n * @see _.orderBy\n */\n orderBy(\n iteratees?: Many|string|Object>,\n orders?: Many\n ): LoDashImplicitArrayWrapper;\n\n /**\n * @see _.orderBy\n */\n orderBy(\n iteratees?: Many|string|W>,\n orders?: Many\n ): LoDashImplicitArrayWrapper;\n\n /**\n * @see _.orderBy\n */\n orderBy(\n iteratees?: Many|string|Object>,\n orders?: Many\n ): LoDashImplicitArrayWrapper;\n\n /**\n * @see _.orderBy\n */\n orderBy(\n iteratees?: Many|string|W>,\n orders?: Many\n ): LoDashImplicitArrayWrapper;\n\n /**\n * @see _.orderBy\n */\n orderBy(\n iteratees?: Many|string|Object>,\n orders?: Many\n ): LoDashImplicitArrayWrapper;\n }\n\n interface LoDashExplicitWrapper {\n /**\n * @see _.orderBy\n */\n orderBy(\n iteratees?: Many|string>,\n orders?: Many\n ): LoDashExplicitArrayWrapper;\n }\n\n interface LoDashExplicitArrayWrapperBase {\n /**\n * @see _.orderBy\n */\n orderBy(\n iteratees?: Many|string|W|(ListIterator|string|W)>,\n orders?: Many\n ): LoDashExplicitArrayWrapper;\n }\n\n interface LoDashExplicitObjectWrapperBase {\n /**\n * @see _.orderBy\n */\n orderBy(\n iteratees?: Many|string|W>,\n orders?: Many\n ): LoDashExplicitArrayWrapper;\n\n /**\n * @see _.orderBy\n */\n orderBy(\n iteratees?: Many|string|Object>,\n orders?: Many\n ): LoDashExplicitArrayWrapper;\n\n /**\n * @see _.orderBy\n */\n orderBy(\n iteratees?: Many|string|W>,\n orders?: Many\n ): LoDashExplicitArrayWrapper;\n\n /**\n * @see _.orderBy\n */\n orderBy(\n iteratees?: Many|string|Object>,\n orders?: Many\n ): LoDashExplicitArrayWrapper;\n\n /**\n * @see _.orderBy\n */\n orderBy(\n iteratees?: Many|string|W>,\n orders?: Many\n ): LoDashExplicitArrayWrapper;\n\n /**\n * @see _.orderBy\n */\n orderBy(\n iteratees?: Many|string|Object>,\n orders?: Many\n ): LoDashExplicitArrayWrapper;\n }\n\n /********\n * Date *\n ********/\n\n //_.now\n interface LoDashStatic {\n /**\n * Gets the number of milliseconds that have elapsed since the Unix epoch (1 January 1970 00:00:00 UTC).\n *\n * @return The number of milliseconds.\n */\n now(): number;\n }\n\n interface LoDashImplicitWrapperBase {\n /**\n * @see _.now\n */\n now(): number;\n }\n\n interface LoDashExplicitWrapperBase {\n /**\n * @see _.now\n */\n now(): LoDashExplicitWrapper;\n }\n\n /*************\n * Functions *\n *************/\n\n //_.after\n interface LoDashStatic {\n /**\n * The opposite of _.before; this method creates a function that invokes func once it’s called n or more times.\n *\n * @param n The number of calls before func is invoked.\n * @param func The function to restrict.\n * @return Returns the new restricted function.\n */\n after(\n n: number,\n func: TFunc\n ): TFunc;\n }\n\n interface LoDashImplicitWrapper {\n /**\n * @see _.after\n **/\n after(func: TFunc): LoDashImplicitObjectWrapper;\n }\n\n interface LoDashExplicitWrapper {\n /**\n * @see _.after\n **/\n after(func: TFunc): LoDashExplicitObjectWrapper;\n }\n\n //_.ary\n interface LoDashStatic {\n /**\n * Creates a function that accepts up to n arguments ignoring any additional arguments.\n *\n * @param func The function to cap arguments for.\n * @param n The arity cap.\n * @returns Returns the new function.\n */\n ary(\n func: Function,\n n?: number\n ): TResult;\n\n ary(\n func: T,\n n?: number\n ): TResult;\n }\n\n interface LoDashImplicitObjectWrapper {\n /**\n * @see _.ary\n */\n ary(n?: number): LoDashImplicitObjectWrapper;\n }\n\n interface LoDashExplicitObjectWrapper {\n /**\n * @see _.ary\n */\n ary(n?: number): LoDashExplicitObjectWrapper;\n }\n\n //_.before\n interface LoDashStatic {\n /**\n * Creates a function that invokes func, with the this binding and arguments of the created function, while\n * it’s called less than n times. Subsequent calls to the created function return the result of the last func\n * invocation.\n *\n * @param n The number of calls at which func is no longer invoked.\n * @param func The function to restrict.\n * @return Returns the new restricted function.\n */\n before(\n n: number,\n func: TFunc\n ): TFunc;\n }\n\n interface LoDashImplicitWrapper {\n /**\n * @see _.before\n **/\n before(func: TFunc): LoDashImplicitObjectWrapper;\n }\n\n interface LoDashExplicitWrapper {\n /**\n * @see _.before\n **/\n before(func: TFunc): LoDashExplicitObjectWrapper;\n }\n\n //_.bind\n interface FunctionBind {\n placeholder: any;\n\n (\n func: T,\n thisArg: any,\n ...partials: any[]\n ): TResult;\n\n (\n func: Function,\n thisArg: any,\n ...partials: any[]\n ): TResult;\n }\n\n interface LoDashStatic {\n /**\n * Creates a function that invokes func with the this binding of thisArg and prepends any additional _.bind\n * arguments to those provided to the bound function.\n *\n * The _.bind.placeholder value, which defaults to _ in monolithic builds, may be used as a placeholder for\n * partially applied arguments.\n *\n * Note: Unlike native Function#bind this method does not set the \"length\" property of bound functions.\n *\n * @param func The function to bind.\n * @param thisArg The this binding of func.\n * @param partials The arguments to be partially applied.\n * @return Returns the new bound function.\n */\n bind: FunctionBind;\n }\n\n interface LoDashImplicitObjectWrapper {\n /**\n * @see _.bind\n */\n bind(\n thisArg: any,\n ...partials: any[]\n ): LoDashImplicitObjectWrapper;\n }\n\n interface LoDashExplicitObjectWrapper {\n /**\n * @see _.bind\n */\n bind(\n thisArg: any,\n ...partials: any[]\n ): LoDashExplicitObjectWrapper;\n }\n\n //_.bindAll\n interface LoDashStatic {\n /**\n * Binds methods of an object to the object itself, overwriting the existing method. Method names may be\n * specified as individual arguments or as arrays of method names. If no method names are provided all\n * enumerable function properties, own and inherited, of object are bound.\n *\n * Note: This method does not set the \"length\" property of bound functions.\n *\n * @param object The object to bind and assign the bound methods to.\n * @param methodNames The object method names to bind, specified as individual method names or arrays of\n * method names.\n * @return Returns object.\n */\n bindAll(\n object: T,\n ...methodNames: Array>\n ): T;\n }\n\n interface LoDashImplicitObjectWrapper {\n /**\n * @see _.bindAll\n */\n bindAll(...methodNames: Array>): LoDashImplicitObjectWrapper;\n }\n\n interface LoDashExplicitObjectWrapper {\n /**\n * @see _.bindAll\n */\n bindAll(...methodNames: Array>): LoDashExplicitObjectWrapper;\n }\n\n //_.bindKey\n interface FunctionBindKey {\n placeholder: any;\n\n (\n object: T,\n key: any,\n ...partials: any[]\n ): TResult;\n\n (\n object: Object,\n key: any,\n ...partials: any[]\n ): TResult;\n }\n\n interface LoDashStatic {\n /**\n * Creates a function that invokes the method at object[key] and prepends any additional _.bindKey arguments\n * to those provided to the bound function.\n *\n * This method differs from _.bind by allowing bound functions to reference methods that may be redefined\n * or don’t yet exist. See Peter Michaux’s article for more details.\n *\n * The _.bindKey.placeholder value, which defaults to _ in monolithic builds, may be used as a placeholder\n * for partially applied arguments.\n *\n * @param object The object the method belongs to.\n * @param key The key of the method.\n * @param partials The arguments to be partially applied.\n * @return Returns the new bound function.\n */\n bindKey: FunctionBindKey;\n }\n\n interface LoDashImplicitObjectWrapper {\n /**\n * @see _.bindKey\n */\n bindKey(\n key: any,\n ...partials: any[]\n ): LoDashImplicitObjectWrapper;\n }\n\n interface LoDashExplicitObjectWrapper {\n /**\n * @see _.bindKey\n */\n bindKey(\n key: any,\n ...partials: any[]\n ): LoDashExplicitObjectWrapper;\n }\n\n //_.createCallback\n interface LoDashStatic {\n /**\n * Produces a callback bound to an optional thisArg. If func is a property name the created\n * callback will return the property value for a given element. If func is an object the created\n * callback will return true for elements that contain the equivalent object properties,\n * otherwise it will return false.\n * @param func The value to convert to a callback.\n * @param thisArg The this binding of the created callback.\n * @param argCount The number of arguments the callback accepts.\n * @return A callback function.\n **/\n createCallback(\n func: string,\n argCount?: number): () => any;\n\n /**\n * @see _.createCallback\n **/\n createCallback(\n func: Dictionary,\n argCount?: number): () => boolean;\n }\n\n interface LoDashImplicitWrapper {\n /**\n * @see _.createCallback\n **/\n createCallback(\n argCount?: number): LoDashImplicitObjectWrapper<() => any>;\n }\n\n interface LoDashImplicitObjectWrapper {\n /**\n * @see _.createCallback\n **/\n createCallback(\n argCount?: number): LoDashImplicitObjectWrapper<() => any>;\n }\n\n //_.curry\n interface LoDashStatic {\n /**\n * Creates a function that accepts one or more arguments of func that when called either invokes func returning\n * its result, if all func arguments have been provided, or returns a function that accepts one or more of the\n * remaining func arguments, and so on. The arity of func may be specified if func.length is not sufficient.\n * @param func The function to curry.\n * @return Returns the new curried function.\n */\n curry(func: (t1: T1) => R):\n CurriedFunction1;\n /**\n * Creates a function that accepts one or more arguments of func that when called either invokes func returning\n * its result, if all func arguments have been provided, or returns a function that accepts one or more of the\n * remaining func arguments, and so on. The arity of func may be specified if func.length is not sufficient.\n * @param func The function to curry.\n * @return Returns the new curried function.\n */\n curry(func: (t1: T1, t2: T2) => R):\n CurriedFunction2;\n /**\n * Creates a function that accepts one or more arguments of func that when called either invokes func returning\n * its result, if all func arguments have been provided, or returns a function that accepts one or more of the\n * remaining func arguments, and so on. The arity of func may be specified if func.length is not sufficient.\n * @param func The function to curry.\n * @return Returns the new curried function.\n */\n curry(func: (t1: T1, t2: T2, t3: T3) => R):\n CurriedFunction3;\n /**\n * Creates a function that accepts one or more arguments of func that when called either invokes func returning\n * its result, if all func arguments have been provided, or returns a function that accepts one or more of the\n * remaining func arguments, and so on. The arity of func may be specified if func.length is not sufficient.\n * @param func The function to curry.\n * @return Returns the new curried function.\n */\n curry(func: (t1: T1, t2: T2, t3: T3, t4: T4) => R):\n CurriedFunction4;\n /**\n * Creates a function that accepts one or more arguments of func that when called either invokes func returning\n * its result, if all func arguments have been provided, or returns a function that accepts one or more of the\n * remaining func arguments, and so on. The arity of func may be specified if func.length is not sufficient.\n * @param func The function to curry.\n * @return Returns the new curried function.\n */\n curry(func: (t1: T1, t2: T2, t3: T3, t4: T4, t5: T5) => R):\n CurriedFunction5;\n /**\n * Creates a function that accepts one or more arguments of func that when called either invokes func returning\n * its result, if all func arguments have been provided, or returns a function that accepts one or more of the\n * remaining func arguments, and so on. The arity of func may be specified if func.length is not sufficient.\n * @param func The function to curry.\n * @param arity The arity of func.\n * @return Returns the new curried function.\n */\n curry(\n func: Function,\n arity?: number): TResult;\n }\n\n interface CurriedFunction1 {\n (): CurriedFunction1;\n (t1: T1): R;\n }\n\n interface CurriedFunction2 {\n (): CurriedFunction2;\n (t1: T1): CurriedFunction1;\n (t1: T1, t2: T2): R;\n }\n\n interface CurriedFunction3 {\n (): CurriedFunction3;\n (t1: T1): CurriedFunction2;\n (t1: T1, t2: T2): CurriedFunction1;\n (t1: T1, t2: T2, t3: T3): R;\n }\n\n interface CurriedFunction4 {\n (): CurriedFunction4;\n (t1: T1): CurriedFunction3;\n (t1: T1, t2: T2): CurriedFunction2;\n (t1: T1, t2: T2, t3: T3): CurriedFunction1;\n (t1: T1, t2: T2, t3: T3, t4: T4): R;\n }\n\n interface CurriedFunction5 {\n (): CurriedFunction5;\n (t1: T1): CurriedFunction4;\n (t1: T1, t2: T2): CurriedFunction3;\n (t1: T1, t2: T2, t3: T3): CurriedFunction2;\n (t1: T1, t2: T2, t3: T3, t4: T4): CurriedFunction1;\n (t1: T1, t2: T2, t3: T3, t4: T4, t5: T5): R;\n }\n interface RightCurriedFunction1{\n ():RightCurriedFunction1\n (t1:T1):R\n }\n interface RightCurriedFunction2{\n ():RightCurriedFunction2\n (t2:T2):RightCurriedFunction1\n (t1:T1,t2:T2):R\n }\n interface RightCurriedFunction3{\n ():RightCurriedFunction3\n (t3:T3):RightCurriedFunction2\n (t2:T2,t3:T3):RightCurriedFunction1\n (t1:T1,t2:T2,t3:T3):R\n }\n interface RightCurriedFunction4{\n ():RightCurriedFunction4\n (t4:T4):RightCurriedFunction3\n (t3:T3,t4:T4):RightCurriedFunction2\n (t2:T2,t3:T3,t4:T4):RightCurriedFunction1\n (t1:T1,t2:T2,t3:T3,t4:T4):R\n }\n interface RightCurriedFunction5{\n ():RightCurriedFunction5\n (t5:T5):RightCurriedFunction4\n (t4:T4,t5:T5):RightCurriedFunction3\n (t3:T3,t4:T4,t5:T5):RightCurriedFunction2\n (t2:T2,t3:T3,t4:T4,t5:T5):RightCurriedFunction1\n (t1:T1,t2:T2,t3:T3,t4:T4,t5:T5):R\n }\n\n interface LoDashImplicitObjectWrapper {\n /**\n * @see _.curry\n **/\n curry(arity?: number): LoDashImplicitObjectWrapper;\n }\n\n //_.curryRight\n interface LoDashStatic {\n /**\n * This method is like _.curry except that arguments are applied to func in the manner of _.partialRight\n * instead of _.partial.\n * @param func The function to curry.\n * @return Returns the new curried function.\n */\n curryRight(func: (t1: T1) => R):\n RightCurriedFunction1;\n /**\n * This method is like _.curry except that arguments are applied to func in the manner of _.partialRight\n * instead of _.partial.\n * @param func The function to curry.\n * @return Returns the new curried function.\n */\n curryRight(func: (t1: T1, t2: T2) => R):\n RightCurriedFunction2;\n /**\n * This method is like _.curry except that arguments are applied to func in the manner of _.partialRight\n * instead of _.partial.\n * @param func The function to curry.\n * @return Returns the new curried function.\n */\n curryRight(func: (t1: T1, t2: T2, t3: T3) => R):\n RightCurriedFunction3;\n /**\n * This method is like _.curry except that arguments are applied to func in the manner of _.partialRight\n * instead of _.partial.\n * @param func The function to curry.\n * @return Returns the new curried function.\n */\n curryRight(func: (t1: T1, t2: T2, t3: T3, t4: T4) => R):\n RightCurriedFunction4;\n /**\n * This method is like _.curry except that arguments are applied to func in the manner of _.partialRight\n * instead of _.partial.\n * @param func The function to curry.\n * @return Returns the new curried function.\n */\n curryRight(func: (t1: T1, t2: T2, t3: T3, t4: T4, t5: T5) => R):\n RightCurriedFunction5;\n /**\n * This method is like _.curry except that arguments are applied to func in the manner of _.partialRight\n * instead of _.partial.\n * @param func The function to curry.\n * @param arity The arity of func.\n * @return Returns the new curried function.\n */\n curryRight(\n func: Function,\n arity?: number): TResult;\n }\n\n interface LoDashImplicitObjectWrapper {\n /**\n * @see _.curryRight\n **/\n curryRight(arity?: number): LoDashImplicitObjectWrapper;\n }\n\n //_.debounce\n interface DebounceSettings {\n /**\n * Specify invoking on the leading edge of the timeout.\n */\n leading?: boolean;\n\n /**\n * The maximum time func is allowed to be delayed before it’s invoked.\n */\n maxWait?: number;\n\n /**\n * Specify invoking on the trailing edge of the timeout.\n */\n trailing?: boolean;\n }\n\n interface LoDashStatic {\n /**\n * Creates a debounced function that delays invoking func until after wait milliseconds have elapsed since\n * the last time the debounced function was invoked. The debounced function comes with a cancel method to\n * cancel delayed invocations and a flush method to immediately invoke them. Provide an options object to\n * indicate that func should be invoked on the leading and/or trailing edge of the wait timeout. Subsequent\n * calls to the debounced function return the result of the last func invocation.\n *\n * Note: If leading and trailing options are true, func is invoked on the trailing edge of the timeout only\n * if the the debounced function is invoked more than once during the wait timeout.\n *\n * See David Corbacho’s article for details over the differences between _.debounce and _.throttle.\n *\n * @param func The function to debounce.\n * @param wait The number of milliseconds to delay.\n * @param options The options object.\n * @param options.leading Specify invoking on the leading edge of the timeout.\n * @param options.maxWait The maximum time func is allowed to be delayed before it’s invoked.\n * @param options.trailing Specify invoking on the trailing edge of the timeout.\n * @return Returns the new debounced function.\n */\n debounce(\n func: T,\n wait?: number,\n options?: DebounceSettings\n ): T & Cancelable;\n }\n\n interface LoDashImplicitObjectWrapper {\n /**\n * @see _.debounce\n */\n debounce(\n wait?: number,\n options?: DebounceSettings\n ): LoDashImplicitObjectWrapper;\n }\n\n interface LoDashExplicitObjectWrapper {\n /**\n * @see _.debounce\n */\n debounce(\n wait?: number,\n options?: DebounceSettings\n ): LoDashExplicitObjectWrapper;\n }\n\n //_.defer\n interface LoDashStatic {\n /**\n * Defers invoking the func until the current call stack has cleared. Any additional arguments are provided to\n * func when it’s invoked.\n *\n * @param func The function to defer.\n * @param args The arguments to invoke the function with.\n * @return Returns the timer id.\n */\n defer(\n func: T,\n ...args: any[]\n ): number;\n }\n\n interface LoDashImplicitObjectWrapper {\n /**\n * @see _.defer\n */\n defer(...args: any[]): LoDashImplicitWrapper;\n }\n\n interface LoDashExplicitObjectWrapper {\n /**\n * @see _.defer\n */\n defer(...args: any[]): LoDashExplicitWrapper;\n }\n\n //_.delay\n interface LoDashStatic {\n /**\n * Invokes func after wait milliseconds. Any additional arguments are provided to func when it’s invoked.\n *\n * @param func The function to delay.\n * @param wait The number of milliseconds to delay invocation.\n * @param args The arguments to invoke the function with.\n * @return Returns the timer id.\n */\n delay(\n func: T,\n wait: number,\n ...args: any[]\n ): number;\n }\n\n interface LoDashImplicitObjectWrapper {\n /**\n * @see _.delay\n */\n delay(\n wait: number,\n ...args: any[]\n ): LoDashImplicitWrapper;\n }\n\n interface LoDashExplicitObjectWrapper {\n /**\n * @see _.delay\n */\n delay(\n wait: number,\n ...args: any[]\n ): LoDashExplicitWrapper;\n }\n\n interface LoDashStatic {\n /**\n * Creates a function that invokes `func` with arguments reversed.\n *\n * @static\n * @memberOf _\n * @category Function\n * @param {Function} func The function to flip arguments for.\n * @returns {Function} Returns the new function.\n * @example\n *\n * var flipped = _.flip(function() {\n * return _.toArray(arguments);\n * });\n *\n * flipped('a', 'b', 'c', 'd');\n * // => ['d', 'c', 'b', 'a']\n */\n flip(func: T): T;\n }\n\n interface LoDashImplicitObjectWrapper {\n /**\n * @see _.flip\n */\n flip(): LoDashImplicitObjectWrapper;\n }\n\n interface LoDashExplicitObjectWrapper {\n /**\n * @see _.flip\n */\n flip(): LoDashExplicitObjectWrapper;\n }\n\n //_.flow\n interface LoDashStatic {\n /**\n * Creates a function that returns the result of invoking the provided functions with the this binding of the\n * created function, where each successive invocation is supplied the return value of the previous.\n *\n * @param funcs Functions to invoke.\n * @return Returns the new function.\n */\n // 0-argument first function\n flow(f1: () => R1, f2: (a: R1) => R2): () => R2;\n flow(f1: () => R1, f2: (a: R1) => R2, f3: (a: R2) => R3): () => R3;\n flow(f1: () => R1, f2: (a: R1) => R2, f3: (a: R2) => R3, f4: (a: R3) => R4): () => R4;\n flow(f1: () => R1, f2: (a: R1) => R2, f3: (a: R2) => R3, f4: (a: R3) => R4, f5: (a: R4) => R5): () => R5;\n flow(f1: () => R1, f2: (a: R1) => R2, f3: (a: R2) => R3, f4: (a: R3) => R4, f5: (a: R4) => R5, f6: (a: R5) => R6): () => R6;\n flow(f1: () => R1, f2: (a: R1) => R2, f3: (a: R2) => R3, f4: (a: R3) => R4, f5: (a: R4) => R5, f6: (a: R5) => R6, f7: (a: R6) => R7): () => R7;\n // 1-argument first function\n flow(f1: (a1: A1) => R1, f2: (a: R1) => R2): (a1: A1) => R2;\n flow(f1: (a1: A1) => R1, f2: (a: R1) => R2, f3: (a: R2) => R3): (a1: A1) => R3;\n flow(f1: (a1: A1) => R1, f2: (a: R1) => R2, f3: (a: R2) => R3, f4: (a: R3) => R4): (a1: A1) => R4;\n flow(f1: (a1: A1) => R1, f2: (a: R1) => R2, f3: (a: R2) => R3, f4: (a: R3) => R4, f5: (a: R4) => R5): (a1: A1) => R5;\n flow(f1: (a1: A1) => R1, f2: (a: R1) => R2, f3: (a: R2) => R3, f4: (a: R3) => R4, f5: (a: R4) => R5, f6: (a: R5) => R6): (a1: A1) => R6;\n flow(f1: (a1: A1) => R1, f2: (a: R1) => R2, f3: (a: R2) => R3, f4: (a: R3) => R4, f5: (a: R4) => R5, f6: (a: R5) => R6, f7: (a: R6) => R7): (a1: A1) => R7;\n // 2-argument first function\n flow(f1: (a1: A1, a2: A2) => R1, f2: (a: R1) => R2): (a1: A1, a2: A2) => R2;\n flow(f1: (a1: A1, a2: A2) => R1, f2: (a: R1) => R2, f3: (a: R2) => R3): (a1: A1, a2: A2) => R3;\n flow(f1: (a1: A1, a2: A2) => R1, f2: (a: R1) => R2, f3: (a: R2) => R3, f4: (a: R3) => R4): (a1: A1, a2: A2) => R4;\n flow(f1: (a1: A1, a2: A2) => R1, f2: (a: R1) => R2, f3: (a: R2) => R3, f4: (a: R3) => R4, f5: (a: R4) => R5): (a1: A1, a2: A2) => R5;\n flow(f1: (a1: A1, a2: A2) => R1, f2: (a: R1) => R2, f3: (a: R2) => R3, f4: (a: R3) => R4, f5: (a: R4) => R5, f6: (a: R5) => R6): (a1: A1, a2: A2) => R6;\n flow(f1: (a1: A1, a2: A2) => R1, f2: (a: R1) => R2, f3: (a: R2) => R3, f4: (a: R3) => R4, f5: (a: R4) => R5, f6: (a: R5) => R6, f7: (a: R6) => R7): (a1: A1, a2: A2) => R7;\n // 3-argument first function\n flow(f1: (a1: A1, a2: A2, a3: A3) => R1, f2: (a: R1) => R2): (a1: A1, a2: A2, a3: A3) => R2;\n flow(f1: (a1: A1, a2: A2, a3: A3) => R1, f2: (a: R1) => R2, f3: (a: R2) => R3): (a1: A1, a2: A2, a3: A3) => R3;\n flow(f1: (a1: A1, a2: A2, a3: A3) => R1, f2: (a: R1) => R2, f3: (a: R2) => R3, f4: (a: R3) => R4): (a1: A1, a2: A2, a3: A3) => R4;\n flow(f1: (a1: A1, a2: A2, a3: A3) => R1, f2: (a: R1) => R2, f3: (a: R2) => R3, f4: (a: R3) => R4, f5: (a: R4) => R5): (a1: A1, a2: A2, a3: A3) => R5;\n flow(f1: (a1: A1, a2: A2, a3: A3) => R1, f2: (a: R1) => R2, f3: (a: R2) => R3, f4: (a: R3) => R4, f5: (a: R4) => R5, f6: (a: R5) => R6): (a1: A1, a2: A2, a3: A3) => R6;\n flow(f1: (a1: A1, a2: A2, a3: A3) => R1, f2: (a: R1) => R2, f3: (a: R2) => R3, f4: (a: R3) => R4, f5: (a: R4) => R5, f6: (a: R5) => R6, f7: (a: R6) => R7): (a1: A1, a2: A2, a3: A3) => R7;\n // 4-argument first function\n flow(f1: (a1: A1, a2: A2, a3: A3, a4: A4) => R1, f2: (a: R1) => R2): (a1: A1, a2: A2, a3: A3, a4: A4) => R2;\n flow(f1: (a1: A1, a2: A2, a3: A3, a4: A4) => R1, f2: (a: R1) => R2, f3: (a: R2) => R3): (a1: A1, a2: A2, a3: A3, a4: A4) => R3;\n flow(f1: (a1: A1, a2: A2, a3: A3, a4: A4) => R1, f2: (a: R1) => R2, f3: (a: R2) => R3, f4: (a: R3) => R4): (a1: A1, a2: A2, a3: A3, a4: A4) => R4;\n flow(f1: (a1: A1, a2: A2, a3: A3, a4: A4) => R1, f2: (a: R1) => R2, f3: (a: R2) => R3, f4: (a: R3) => R4, f5: (a: R4) => R5): (a1: A1, a2: A2, a3: A3, a4: A4) => R5;\n flow(f1: (a1: A1, a2: A2, a3: A3, a4: A4) => R1, f2: (a: R1) => R2, f3: (a: R2) => R3, f4: (a: R3) => R4, f5: (a: R4) => R5, f6: (a: R5) => R6): (a1: A1, a2: A2, a3: A3, a4: A4) => R6;\n flow(f1: (a1: A1, a2: A2, a3: A3, a4: A4) => R1, f2: (a: R1) => R2, f3: (a: R2) => R3, f4: (a: R3) => R4, f5: (a: R4) => R5, f6: (a: R5) => R6, f7: (a: R6) => R7): (a1: A1, a2: A2, a3: A3, a4: A4) => R7;\n // generic function\n flow(...funcs: Function[]): TResult;\n flow(funcs: Function[]): TResult;\n }\n\n interface LoDashImplicitObjectWrapper {\n /**\n * @see _.flow\n */\n flow(...funcs: Function[]): LoDashImplicitObjectWrapper;\n /**\n * @see _.flow\n */\n flow(funcs: Function[]): LoDashImplicitObjectWrapper;\n }\n\n interface LoDashExplicitObjectWrapper {\n /**\n * @see _.flow\n */\n flow(...funcs: Function[]): LoDashExplicitObjectWrapper;\n /**\n * @see _.flow\n */\n flow(funcs: Function[]): LoDashExplicitObjectWrapper;\n }\n\n //_.flowRight\n interface LoDashStatic {\n /**\n * This method is like _.flow except that it creates a function that invokes the provided functions from right\n * to left.\n *\n * @param funcs Functions to invoke.\n * @return Returns the new function.\n */\n flowRight(...funcs: Function[]): TResult;\n /**\n * @see _.flowRight\n */\n flowRight(funcs: Function[]): TResult;\n }\n\n interface LoDashImplicitObjectWrapper {\n /**\n * @see _.flowRight\n */\n flowRight(...funcs: Function[]): LoDashImplicitObjectWrapper;\n /**\n * @see _.flowRight\n */\n flowRight(funcs: Function[]): LoDashImplicitObjectWrapper;\n }\n\n interface LoDashExplicitObjectWrapper {\n /**\n * @see _.flowRight\n */\n flowRight(...funcs: Function[]): LoDashExplicitObjectWrapper;\n /**\n * @see _.flowRight\n */\n flowRight(funcs: Function[]): LoDashExplicitObjectWrapper;\n }\n\n //_.memoize\n interface MemoizedFunction extends Function {\n cache: MapCache;\n }\n\n interface LoDashStatic {\n /**\n * Creates a function that memoizes the result of func. If resolver is provided it determines the cache key for\n * storing the result based on the arguments provided to the memoized function. By default, the first argument\n * provided to the memoized function is coerced to a string and used as the cache key. The func is invoked with\n * the this binding of the memoized function.\n *\n * @param func The function to have its output memoized.\n * @param resolver The function to resolve the cache key.\n * @return Returns the new memoizing function.\n */\n memoize: {\n (func: T, resolver?: Function): T & MemoizedFunction;\n Cache: MapCacheConstructor;\n };\n }\n\n interface LoDashImplicitObjectWrapper {\n /**\n * @see _.memoize\n */\n memoize(resolver?: Function): LoDashImplicitObjectWrapper;\n }\n\n interface LoDashExplicitObjectWrapper {\n /**\n * @see _.memoize\n */\n memoize(resolver?: Function): LoDashExplicitObjectWrapper;\n }\n\n //_.overArgs (was _.modArgs)\n interface LoDashStatic {\n /**\n * Creates a function that runs each argument through a corresponding transform function.\n *\n * @param func The function to wrap.\n * @param transforms The functions to transform arguments, specified as individual functions or arrays\n * of functions.\n * @return Returns the new function.\n */\n overArgs(\n func: T,\n ...transforms: Function[]\n ): TResult;\n\n /**\n * @see _.overArgs\n */\n overArgs(\n func: T,\n transforms: Function[]\n ): TResult;\n }\n\n interface LoDashImplicitObjectWrapper {\n /**\n * @see _.overArgs\n */\n overArgs(...transforms: Function[]): LoDashImplicitObjectWrapper;\n\n /**\n * @see _.overArgs\n */\n overArgs(transforms: Function[]): LoDashImplicitObjectWrapper;\n }\n\n interface LoDashExplicitObjectWrapper {\n /**\n * @see _.overArgs\n */\n overArgs(...transforms: Function[]): LoDashExplicitObjectWrapper;\n\n /**\n * @see _.overArgs\n */\n overArgs(transforms: Function[]): LoDashExplicitObjectWrapper;\n }\n\n //_.negate\n interface LoDashStatic {\n /**\n * Creates a function that negates the result of the predicate func. The func predicate is invoked with\n * the this binding and arguments of the created function.\n *\n * @param predicate The predicate to negate.\n * @return Returns the new function.\n */\n negate(predicate: T): (...args: any[]) => boolean;\n\n /**\n * @see _.negate\n */\n negate(predicate: T): TResult;\n }\n\n interface LoDashImplicitObjectWrapper {\n /**\n * @see _.negate\n */\n negate(): LoDashImplicitObjectWrapper<(...args: any[]) => boolean>;\n\n /**\n * @see _.negate\n */\n negate(): LoDashImplicitObjectWrapper;\n }\n\n interface LoDashExplicitObjectWrapper {\n /**\n * @see _.negate\n */\n negate(): LoDashExplicitObjectWrapper<(...args: any[]) => boolean>;\n\n /**\n * @see _.negate\n */\n negate(): LoDashExplicitObjectWrapper;\n }\n\n //_.once\n interface LoDashStatic {\n /**\n * Creates a function that is restricted to invoking func once. Repeat calls to the function return the value\n * of the first call. The func is invoked with the this binding and arguments of the created function.\n *\n * @param func The function to restrict.\n * @return Returns the new restricted function.\n */\n once(func: T): T;\n }\n\n interface LoDashImplicitObjectWrapper {\n /**\n * @see _.once\n */\n once(): LoDashImplicitObjectWrapper;\n }\n\n interface LoDashExplicitObjectWrapper {\n /**\n * @see _.once\n */\n once(): LoDashExplicitObjectWrapper;\n }\n\n //_.partial\n interface LoDashStatic {\n /**\n * Creates a function that, when called, invokes func with any additional partial arguments\n * prepended to those provided to the new function. This method is similar to _.bind except\n * it does not alter the this binding.\n * @param func The function to partially apply arguments to.\n * @param args Arguments to be partially applied.\n * @return The new partially applied function.\n **/\n partial: Partial;\n }\n\n type PH = LoDashStatic;\n\n type Function0 = () => R;\n type Function1 = (t1: T1) => R;\n type Function2 = (t1: T1, t2: T2) => R;\n type Function3 = (t1: T1, t2: T2, t3: T3) => R;\n type Function4 = (t1: T1, t2: T2, t3: T3, t4: T4) => R;\n\n interface Partial {\n // arity 0\n (func: Function0): Function0;\n // arity 1\n (func: Function1): Function1;\n (func: Function1, arg1: T1): Function0;\n // arity 2\n (func: Function2): Function2;\n (func: Function2, arg1: T1): Function1< T2, R>;\n (func: Function2, plc1: PH, arg2: T2): Function1;\n (func: Function2, arg1: T1, arg2: T2): Function0< R>;\n // arity 3\n (func: Function3): Function3;\n (func: Function3, arg1: T1): Function2< T2, T3, R>;\n (func: Function3, plc1: PH, arg2: T2): Function2;\n (func: Function3, arg1: T1, arg2: T2): Function1< T3, R>;\n (func: Function3, plc1: PH, plc2: PH, arg3: T3): Function2;\n (func: Function3, arg1: T1, plc2: PH, arg3: T3): Function1< T2, R>;\n (func: Function3, plc1: PH, arg2: T2, arg3: T3): Function1;\n (func: Function3, arg1: T1, arg2: T2, arg3: T3): Function0< R>;\n // arity 4\n (func: Function4): Function4;\n (func: Function4, arg1: T1): Function3< T2, T3, T4, R>;\n (func: Function4, plc1: PH, arg2: T2): Function3;\n (func: Function4, arg1: T1, arg2: T2): Function2< T3, T4, R>;\n (func: Function4, plc1: PH, plc2: PH, arg3: T3): Function3;\n (func: Function4, arg1: T1, plc2: PH, arg3: T3): Function2< T2, T4, R>;\n (func: Function4, plc1: PH, arg2: T2, arg3: T3): Function2;\n (func: Function4, arg1: T1, arg2: T2, arg3: T3): Function1< T4, R>;\n (func: Function4, plc1: PH, plc2: PH, plc3: PH, arg4: T4): Function3;\n (func: Function4, arg1: T1, plc2: PH, plc3: PH, arg4: T4): Function2< T2, T3, R>;\n (func: Function4, plc1: PH, arg2: T2, plc3: PH, arg4: T4): Function2;\n (func: Function4, arg1: T1, arg2: T2, plc3: PH, arg4: T4): Function1< T3, R>;\n (func: Function4, plc1: PH, plc2: PH, arg3: T3, arg4: T4): Function2;\n (func: Function4, arg1: T1, plc2: PH, arg3: T3, arg4: T4): Function1< T2, R>;\n (func: Function4, plc1: PH, arg2: T2, arg3: T3, arg4: T4): Function1;\n (func: Function4, arg1: T1, arg2: T2, arg3: T3, arg4: T4): Function0< R>;\n // catch-all\n (func: Function, ...args: any[]): Function;\n }\n\n //_.partialRight\n interface LoDashStatic {\n /**\n * This method is like _.partial except that partial arguments are appended to those provided\n * to the new function.\n * @param func The function to partially apply arguments to.\n * @param args Arguments to be partially applied.\n * @return The new partially applied function.\n **/\n partialRight: PartialRight;\n }\n\n interface PartialRight {\n // arity 0\n (func: Function0): Function0;\n // arity 1\n (func: Function1): Function1;\n (func: Function1, arg1: T1): Function0;\n // arity 2\n (func: Function2): Function2;\n (func: Function2, arg1: T1, plc2: PH): Function1< T2, R>;\n (func: Function2, arg2: T2): Function1;\n (func: Function2, arg1: T1, arg2: T2): Function0< R>;\n // arity 3\n (func: Function3): Function3;\n (func: Function3, arg1: T1, plc2: PH, plc3: PH): Function2< T2, T3, R>;\n (func: Function3, arg2: T2, plc3: PH): Function2;\n (func: Function3, arg1: T1, arg2: T2, plc3: PH): Function1< T3, R>;\n (func: Function3, arg3: T3): Function2;\n (func: Function3, arg1: T1, plc2: PH, arg3: T3): Function1< T2, R>;\n (func: Function3, arg2: T2, arg3: T3): Function1;\n (func: Function3, arg1: T1, arg2: T2, arg3: T3): Function0< R>;\n // arity 4\n (func: Function4): Function4;\n (func: Function4, arg1: T1, plc2: PH, plc3: PH, plc4: PH): Function3< T2, T3, T4, R>;\n (func: Function4, arg2: T2, plc3: PH, plc4: PH): Function3;\n (func: Function4, arg1: T1, arg2: T2, plc3: PH, plc4: PH): Function2< T3, T4, R>;\n (func: Function4, arg3: T3, plc4: PH): Function3;\n (func: Function4, arg1: T1, plc2: PH, arg3: T3, plc4: PH): Function2< T2, T4, R>;\n (func: Function4, arg2: T2, arg3: T3, plc4: PH): Function2;\n (func: Function4, arg1: T1, arg2: T2, arg3: T3, plc4: PH): Function1< T4, R>;\n (func: Function4, arg4: T4): Function3;\n (func: Function4, arg1: T1, plc2: PH, plc3: PH, arg4: T4): Function2< T2, T3, R>;\n (func: Function4, arg2: T2, plc3: PH, arg4: T4): Function2;\n (func: Function4, arg1: T1, arg2: T2, plc3: PH, arg4: T4): Function1< T3, R>;\n (func: Function4, arg3: T3, arg4: T4): Function2;\n (func: Function4, arg1: T1, plc2: PH, arg3: T3, arg4: T4): Function1< T2, R>;\n (func: Function4, arg2: T2, arg3: T3, arg4: T4): Function1;\n (func: Function4, arg1: T1, arg2: T2, arg3: T3, arg4: T4): Function0< R>;\n // catch-all\n (func: Function, ...args: any[]): Function;\n }\n\n //_.rearg\n interface LoDashStatic {\n /**\n * Creates a function that invokes func with arguments arranged according to the specified indexes where the\n * argument value at the first index is provided as the first argument, the argument value at the second index\n * is provided as the second argument, and so on.\n * @param func The function to rearrange arguments for.\n * @param indexes The arranged argument indexes, specified as individual indexes or arrays of indexes.\n * @return Returns the new function.\n */\n rearg(func: Function, indexes: number[]): TResult;\n\n /**\n * @see _.rearg\n */\n rearg(func: Function, ...indexes: number[]): TResult;\n }\n\n interface LoDashImplicitObjectWrapper {\n /**\n * @see _.rearg\n */\n rearg(indexes: number[]): LoDashImplicitObjectWrapper;\n\n /**\n * @see _.rearg\n */\n rearg(...indexes: number[]): LoDashImplicitObjectWrapper;\n }\n\n //_.rest\n interface LoDashStatic {\n /**\n * Creates a function that invokes func with the this binding of the created function and arguments from start\n * and beyond provided as an array.\n *\n * Note: This method is based on the rest parameter.\n *\n * @param func The function to apply a rest parameter to.\n * @param start The start position of the rest parameter.\n * @return Returns the new function.\n */\n rest(\n func: Function,\n start?: number\n ): TResult;\n\n /**\n * @see _.rest\n */\n rest(\n func: TFunc,\n start?: number\n ): TResult;\n }\n\n interface LoDashImplicitObjectWrapper {\n /**\n * @see _.rest\n */\n rest(start?: number): LoDashImplicitObjectWrapper;\n }\n\n interface LoDashExplicitObjectWrapper {\n /**\n * @see _.rest\n */\n rest(start?: number): LoDashExplicitObjectWrapper;\n }\n\n //_.spread\n interface LoDashStatic {\n /**\n * Creates a function that invokes func with the this binding of the created function and an array of arguments\n * much like Function#apply.\n *\n * Note: This method is based on the spread operator.\n *\n * @param func The function to spread arguments over.\n * @return Returns the new function.\n */\n spread(func: F): T;\n\n /**\n * @see _.spread\n */\n spread(func: Function): T;\n }\n\n interface LoDashImplicitObjectWrapper {\n /**\n * @see _.spread\n */\n spread(): LoDashImplicitObjectWrapper;\n }\n\n interface LoDashExplicitObjectWrapper {\n /**\n * @see _.spread\n */\n spread(): LoDashExplicitObjectWrapper;\n }\n\n //_.throttle\n interface ThrottleSettings {\n /**\n * If you'd like to disable the leading-edge call, pass this as false.\n */\n leading?: boolean;\n\n /**\n * If you'd like to disable the execution on the trailing-edge, pass false.\n */\n trailing?: boolean;\n }\n\n interface LoDashStatic {\n /**\n * Creates a throttled function that only invokes func at most once per every wait milliseconds. The throttled\n * function comes with a cancel method to cancel delayed invocations and a flush method to immediately invoke\n * them. Provide an options object to indicate that func should be invoked on the leading and/or trailing edge\n * of the wait timeout. Subsequent calls to the throttled function return the result of the last func call.\n *\n * Note: If leading and trailing options are true, func is invoked on the trailing edge of the timeout only if\n * the the throttled function is invoked more than once during the wait timeout.\n *\n * @param func The function to throttle.\n * @param wait The number of milliseconds to throttle invocations to.\n * @param options The options object.\n * @param options.leading Specify invoking on the leading edge of the timeout.\n * @param options.trailing Specify invoking on the trailing edge of the timeout.\n * @return Returns the new throttled function.\n */\n throttle(\n func: T,\n wait?: number,\n options?: ThrottleSettings\n ): T & Cancelable;\n }\n\n interface LoDashImplicitObjectWrapper {\n /**\n * @see _.throttle\n */\n throttle(\n wait?: number,\n options?: ThrottleSettings\n ): LoDashImplicitObjectWrapper;\n }\n\n interface LoDashExplicitObjectWrapper {\n /**\n * @see _.throttle\n */\n throttle(\n wait?: number,\n options?: ThrottleSettings\n ): LoDashExplicitObjectWrapper;\n }\n\n //_.unary\n interface LoDashStatic {\n /**\n * Creates a function that accepts up to one argument, ignoring any\n * additional arguments.\n *\n * @static\n * @memberOf _\n * @category Function\n * @param {Function} func The function to cap arguments for.\n * @returns {Function} Returns the new function.\n * @example\n *\n * _.map(['6', '8', '10'], _.unary(parseInt));\n * // => [6, 8, 10]\n */\n unary(func: T): T;\n }\n\n interface LoDashImplicitObjectWrapper {\n /**\n * @see _.unary\n */\n unary(): LoDashImplicitObjectWrapper;\n }\n\n interface LoDashExplicitObjectWrapper {\n /**\n * @see _.unary\n */\n unary(): LoDashExplicitObjectWrapper;\n }\n\n //_.wrap\n interface LoDashStatic {\n /**\n * Creates a function that provides value to the wrapper function as its first argument. Any additional\n * arguments provided to the function are appended to those provided to the wrapper function. The wrapper is\n * invoked with the this binding of the created function.\n *\n * @param value The value to wrap.\n * @param wrapper The wrapper function.\n * @return Returns the new function.\n */\n wrap(\n value: V,\n wrapper: W\n ): R;\n\n /**\n * @see _.wrap\n */\n wrap(\n value: V,\n wrapper: Function\n ): R;\n\n /**\n * @see _.wrap\n */\n wrap(\n value: any,\n wrapper: Function\n ): R;\n }\n\n interface LoDashImplicitWrapper {\n /**\n * @see _.wrap\n */\n wrap(wrapper: W): LoDashImplicitObjectWrapper;\n\n /**\n * @see _.wrap\n */\n wrap(wrapper: Function): LoDashImplicitObjectWrapper;\n }\n\n interface LoDashImplicitArrayWrapperBase {\n /**\n * @see _.wrap\n */\n wrap(wrapper: W): LoDashImplicitObjectWrapper;\n\n /**\n * @see _.wrap\n */\n wrap(wrapper: Function): LoDashImplicitObjectWrapper;\n }\n\n interface LoDashImplicitObjectWrapperBase {\n /**\n * @see _.wrap\n */\n wrap(wrapper: W): LoDashImplicitObjectWrapper;\n\n /**\n * @see _.wrap\n */\n wrap(wrapper: Function): LoDashImplicitObjectWrapper;\n }\n\n interface LoDashExplicitWrapper {\n /**\n * @see _.wrap\n */\n wrap(wrapper: W): LoDashExplicitObjectWrapper;\n\n /**\n * @see _.wrap\n */\n wrap(wrapper: Function): LoDashExplicitObjectWrapper;\n }\n\n interface LoDashExplicitArrayWrapperBase {\n /**\n * @see _.wrap\n */\n wrap(wrapper: W): LoDashExplicitObjectWrapper;\n\n /**\n * @see _.wrap\n */\n wrap(wrapper: Function): LoDashExplicitObjectWrapper;\n }\n\n interface LoDashExplicitObjectWrapperBase {\n /**\n * @see _.wrap\n */\n wrap(wrapper: W): LoDashExplicitObjectWrapper;\n\n /**\n * @see _.wrap\n */\n wrap(wrapper: Function): LoDashExplicitObjectWrapper;\n }\n\n /********\n * Lang *\n ********/\n\n //_.castArray\n interface LoDashStatic {\n /**\n * Casts value as an array if it’s not one.\n *\n * @param value The value to inspect.\n * @return Returns the cast array.\n */\n castArray(value?: Many): T[];\n }\n\n interface LoDashImplicitWrapper {\n /**\n * @see _.castArray\n */\n castArray(): LoDashImplicitArrayWrapper;\n }\n\n interface LoDashImplicitArrayWrapperBase {\n /**\n * @see _.castArray\n */\n castArray(): TWrapper;\n }\n\n interface LoDashImplicitObjectWrapperBase {\n /**\n * @see _.castArray\n */\n castArray(): LoDashImplicitArrayWrapper;\n }\n\n interface LoDashExplicitWrapper {\n /**\n * @see _.castArray\n */\n castArray(): LoDashExplicitArrayWrapper;\n }\n\n interface LoDashExplicitArrayWrapperBase {\n /**\n * @see _.castArray\n */\n castArray(): TWrapper;\n }\n\n interface LoDashExplicitObjectWrapperBase {\n /**\n * @see _.castArray\n */\n castArray(): LoDashExplicitArrayWrapper;\n }\n\n //_.clone\n interface LoDashStatic {\n /**\n * Creates a shallow clone of value.\n *\n * Note: This method is loosely based on the structured clone algorithm and supports cloning arrays,\n * array buffers, booleans, date objects, maps, numbers, Object objects, regexes, sets, strings, symbols,\n * and typed arrays. The own enumerable properties of arguments objects are cloned as plain objects. An empty\n * object is returned for uncloneable values such as error objects, functions, DOM nodes, and WeakMaps.\n *\n * @param value The value to clone.\n * @return Returns the cloned value.\n */\n clone(value: T): T;\n }\n\n interface LoDashImplicitWrapper {\n /**\n * @see _.clone\n */\n clone(): T;\n }\n\n interface LoDashImplicitArrayWrapperBase {\n /**\n * @see _.clone\n */\n clone(): TArray;\n }\n\n interface LoDashImplicitObjectWrapperBase {\n /**\n * @see _.clone\n */\n clone(): TObject;\n }\n\n interface LoDashExplicitWrapper {\n /**\n * @see _.clone\n */\n clone(): LoDashExplicitWrapper;\n }\n\n interface LoDashExplicitArrayWrapperBase {\n /**\n * @see _.clone\n */\n clone(): TWrapper;\n }\n\n interface LoDashExplicitObjectWrapperBase {\n /**\n * @see _.clone\n */\n clone(): TWrapper;\n }\n\n //_.cloneDeep\n interface LoDashStatic {\n /**\n * This method is like _.clone except that it recursively clones value.\n *\n * @param value The value to recursively clone.\n * @return Returns the deep cloned value.\n */\n cloneDeep(value: T): T;\n }\n\n interface LoDashImplicitWrapper {\n /**\n * @see _.cloneDeep\n */\n cloneDeep(): T;\n }\n\n interface LoDashImplicitArrayWrapperBase {\n /**\n * @see _.cloneDeep\n */\n cloneDeep(): TArray;\n }\n\n interface LoDashImplicitObjectWrapperBase {\n /**\n * @see _.cloneDeep\n */\n cloneDeep(): TObject;\n }\n\n interface LoDashExplicitWrapper {\n /**\n * @see _.cloneDeep\n */\n cloneDeep(): LoDashExplicitWrapper;\n }\n\n interface LoDashExplicitArrayWrapperBase {\n /**\n * @see _.cloneDeep\n */\n cloneDeep(): TWrapper;\n }\n\n interface LoDashExplicitObjectWrapperBase {\n /**\n * @see _.cloneDeep\n */\n cloneDeep(): TWrapper;\n }\n\n //_.cloneDeepWith\n type CloneDeepWithCustomizer = (value: TValue, key?: number|string, object?: any, stack?: any) => TResult;\n\n interface LoDashStatic {\n /**\n * This method is like _.cloneWith except that it recursively clones value.\n *\n * @param value The value to recursively clone.\n * @param customizer The function to customize cloning.\n * @return Returns the deep cloned value.\n */\n cloneDeepWith(\n value: any,\n customizer?: CloneDeepWithCustomizer\n ): TResult;\n\n /**\n * @see _.clonDeepeWith\n */\n cloneDeepWith(\n value: T,\n customizer?: CloneDeepWithCustomizer\n ): TResult;\n }\n\n interface LoDashImplicitWrapper {\n /**\n * @see _.cloneDeepWith\n */\n cloneDeepWith(\n customizer?: CloneDeepWithCustomizer\n ): TResult;\n }\n\n interface LoDashImplicitArrayWrapperBase {\n /**\n * @see _.cloneDeepWith\n */\n cloneDeepWith(\n customizer?: CloneDeepWithCustomizer\n ): TResult;\n }\n\n interface LoDashImplicitObjectWrapperBase {\n /**\n * @see _.cloneDeepWith\n */\n cloneDeepWith(\n customizer?: CloneDeepWithCustomizer\n ): TResult;\n }\n\n interface LoDashExplicitWrapper {\n /**\n * @see _.cloneDeepWith\n */\n cloneDeepWith(\n customizer?: CloneDeepWithCustomizer\n ): LoDashExplicitWrapper;\n\n /**\n * @see _.cloneDeepWith\n */\n cloneDeepWith(\n customizer?: CloneDeepWithCustomizer\n ): LoDashExplicitArrayWrapper;\n\n /**\n * @see _.cloneDeepWith\n */\n cloneDeepWith(\n customizer?: CloneDeepWithCustomizer\n ): LoDashExplicitObjectWrapper;\n }\n\n interface LoDashExplicitArrayWrapperBase {\n /**\n * @see _.cloneDeepWith\n */\n cloneDeepWith(\n customizer?: CloneDeepWithCustomizer\n ): LoDashExplicitWrapper;\n\n /**\n * @see _.cloneDeepWith\n */\n cloneDeepWith(\n customizer?: CloneDeepWithCustomizer\n ): LoDashExplicitArrayWrapper;\n\n /**\n * @see _.cloneDeepWith\n */\n cloneDeepWith(\n customizer?: CloneDeepWithCustomizer\n ): LoDashExplicitObjectWrapper;\n }\n\n interface LoDashExplicitObjectWrapperBase {\n /**\n * @see _.cloneDeepWith\n */\n cloneDeepWith(\n customizer?: CloneDeepWithCustomizer\n ): LoDashExplicitWrapper;\n\n /**\n * @see _.cloneDeepWith\n */\n cloneDeepWith(\n customizer?: CloneDeepWithCustomizer\n ): LoDashExplicitArrayWrapper;\n\n /**\n * @see _.cloneDeepWith\n */\n cloneDeepWith(\n customizer?: CloneDeepWithCustomizer\n ): LoDashExplicitObjectWrapper;\n }\n\n //_.cloneWith\n type CloneWithCustomizer = (value: TValue, key?: number|string, object?: any, stack?: any) => TResult;\n\n interface LoDashStatic {\n /**\n * This method is like _.clone except that it accepts customizer which is invoked to produce the cloned value.\n * If customizer returns undefined cloning is handled by the method instead.\n *\n * @param value The value to clone.\n * @param customizer The function to customize cloning.\n * @return Returns the cloned value.\n */\n cloneWith(\n value: any,\n customizer?: CloneWithCustomizer\n ): TResult;\n\n /**\n * @see _.cloneWith\n */\n cloneWith(\n value: T,\n customizer?: CloneWithCustomizer\n ): TResult;\n }\n\n interface LoDashImplicitWrapper {\n /**\n * @see _.cloneWith\n */\n cloneWith(\n customizer?: CloneWithCustomizer\n ): TResult;\n }\n\n interface LoDashImplicitArrayWrapperBase {\n /**\n * @see _.cloneWith\n */\n cloneWith(\n customizer?: CloneWithCustomizer\n ): TResult;\n }\n\n interface LoDashImplicitObjectWrapperBase {\n /**\n * @see _.cloneWith\n */\n cloneWith(\n customizer?: CloneWithCustomizer\n ): TResult;\n }\n\n interface LoDashExplicitWrapper {\n /**\n * @see _.cloneWith\n */\n cloneWith(\n customizer?: CloneWithCustomizer\n ): LoDashExplicitWrapper;\n\n /**\n * @see _.cloneWith\n */\n cloneWith(\n customizer?: CloneWithCustomizer\n ): LoDashExplicitArrayWrapper;\n\n /**\n * @see _.cloneWith\n */\n cloneWith(\n customizer?: CloneWithCustomizer\n ): LoDashExplicitObjectWrapper;\n }\n\n interface LoDashExplicitArrayWrapperBase {\n /**\n * @see _.cloneWith\n */\n cloneWith(\n customizer?: CloneWithCustomizer\n ): LoDashExplicitWrapper;\n\n /**\n * @see _.cloneWith\n */\n cloneWith(\n customizer?: CloneWithCustomizer\n ): LoDashExplicitArrayWrapper;\n\n /**\n * @see _.cloneWith\n */\n cloneWith(\n customizer?: CloneWithCustomizer\n ): LoDashExplicitObjectWrapper;\n }\n\n interface LoDashExplicitObjectWrapperBase {\n /**\n * @see _.cloneWith\n */\n cloneWith(\n customizer?: CloneWithCustomizer\n ): LoDashExplicitWrapper;\n\n /**\n * @see _.cloneWith\n */\n cloneWith(\n customizer?: CloneWithCustomizer\n ): LoDashExplicitArrayWrapper;\n\n /**\n * @see _.cloneWith\n */\n cloneWith(\n customizer?: CloneWithCustomizer\n ): LoDashExplicitObjectWrapper;\n }\n\n /**\n * An object containing predicate functions for each property of T\n */\n type ConformsPredicateObject = {\n [P in keyof T]: (val: T[P]) => boolean;\n };\n\n //_.conforms\n interface LoDashStatic {\n /**\n * Creates a function that invokes the predicate properties of `source` with the corresponding\n * property values of a given object, returning true if all predicates return truthy, else false.\n */\n conforms(source: ConformsPredicateObject): (Target: T) => boolean;\n }\n\n //_.conformsTo\n interface LoDashStatic {\n /**\n * Checks if object conforms to source by invoking the predicate properties of source with the\n * corresponding property values of object.\n *\n * Note: This method is equivalent to _.conforms when source is partially applied.\n */\n conformsTo(object: T, source: ConformsPredicateObject): boolean;\n }\n\n //_.eq\n interface LoDashStatic {\n /**\n * Performs a [`SameValueZero`](http://ecma-international.org/ecma-262/6.0/#sec-samevaluezero)\n * comparison between two values to determine if they are equivalent.\n *\n * @static\n * @memberOf _\n * @category Lang\n * @param {*} value The value to compare.\n * @param {*} other The other value to compare.\n * @returns {boolean} Returns `true` if the values are equivalent, else `false`.\n * @example\n *\n * var object = { 'user': 'fred' };\n * var other = { 'user': 'fred' };\n *\n * _.eq(object, object);\n * // => true\n *\n * _.eq(object, other);\n * // => false\n *\n * _.eq('a', 'a');\n * // => true\n *\n * _.eq('a', Object('a'));\n * // => false\n *\n * _.eq(NaN, NaN);\n * // => true\n */\n eq(\n value: any,\n other: any\n ): boolean;\n }\n\n interface LoDashImplicitWrapperBase {\n /**\n * @see _.isEqual\n */\n eq(\n other: any\n ): boolean;\n }\n\n interface LoDashExplicitWrapperBase {\n /**\n * @see _.isEqual\n */\n eq(\n other: any\n ): LoDashExplicitWrapper;\n }\n\n //_.gt\n interface LoDashStatic {\n /**\n * Checks if value is greater than other.\n *\n * @param value The value to compare.\n * @param other The other value to compare.\n * @return Returns true if value is greater than other, else false.\n */\n gt(\n value: any,\n other: any\n ): boolean;\n }\n\n interface LoDashImplicitWrapperBase {\n /**\n * @see _.gt\n */\n gt(other: any): boolean;\n }\n\n interface LoDashExplicitWrapperBase {\n /**\n * @see _.gt\n */\n gt(other: any): LoDashExplicitWrapper;\n }\n\n //_.gte\n interface LoDashStatic {\n /**\n * Checks if value is greater than or equal to other.\n *\n * @param value The value to compare.\n * @param other The other value to compare.\n * @return Returns true if value is greater than or equal to other, else false.\n */\n gte(\n value: any,\n other: any\n ): boolean;\n }\n\n interface LoDashImplicitWrapperBase {\n /**\n * @see _.gte\n */\n gte(other: any): boolean;\n }\n\n interface LoDashExplicitWrapperBase {\n /**\n * @see _.gte\n */\n gte(other: any): LoDashExplicitWrapper;\n }\n\n //_.isArguments\n interface LoDashStatic {\n /**\n * Checks if value is classified as an arguments object.\n *\n * @param value The value to check.\n * @return Returns true if value is correctly classified, else false.\n */\n isArguments(value?: any): value is IArguments;\n }\n\n interface LoDashImplicitWrapperBase {\n /**\n * @see _.isArguments\n */\n isArguments(): boolean;\n }\n\n interface LoDashExplicitWrapperBase {\n /**\n * @see _.isArguments\n */\n isArguments(): LoDashExplicitWrapper;\n }\n\n //_.isArray\n interface LoDashStatic {\n /**\n * Checks if value is classified as an Array object.\n * @param value The value to check.\n *\n * @return Returns true if value is correctly classified, else false.\n */\n isArray(value?: any): value is any[];\n\n /**\n * DEPRECATED\n */\n isArray(value?: any): value is any[];\n }\n\n interface LoDashImplicitWrapperBase {\n /**\n * @see _.isArray\n */\n isArray(): boolean;\n }\n\n interface LoDashExplicitWrapperBase {\n /**\n * @see _.isArray\n */\n isArray(): LoDashExplicitWrapper;\n }\n\n //_.isArrayBuffer\n interface LoDashStatic {\n /**\n * Checks if value is classified as an ArrayBuffer object.\n *\n * @param value The value to check.\n * @return Returns true if value is correctly classified, else false.\n */\n isArrayBuffer(value?: any): value is ArrayBuffer;\n }\n\n interface LoDashImplicitWrapperBase {\n /**\n * @see _.isArrayBuffer\n */\n isArrayBuffer(): boolean;\n }\n\n interface LoDashExplicitWrapperBase {\n /**\n * @see _.isArrayBuffer\n */\n isArrayBuffer(): LoDashExplicitWrapper;\n }\n\n //_.isArrayLike\n interface LoDashStatic {\n /**\n * Checks if `value` is array-like. A value is considered array-like if it's\n * not a function and has a `value.length` that's an integer greater than or\n * equal to `0` and less than or equal to `Number.MAX_SAFE_INTEGER`.\n *\n * @static\n * @memberOf _\n * @type Function\n * @category Lang\n * @param {*} value The value to check.\n * @returns {boolean} Returns `true` if `value` is array-like, else `false`.\n * @example\n *\n * _.isArrayLike([1, 2, 3]);\n * // => true\n *\n * _.isArrayLike(document.body.children);\n * // => true\n *\n * _.isArrayLike('abc');\n * // => true\n *\n * _.isArrayLike(_.noop);\n * // => false\n */\n isArrayLike(value: T & string & number): boolean; // should only match if T = any\n\n /**\n * @see _.isArrayLike\n */\n isArrayLike(value?: Function): value is never;\n\n /**\n * @see _.isArrayLike\n */\n isArrayLike(value: T | Function): value is T & { length: number };\n\n /**\n * DEPRECATED\n */\n isArrayLike(value?: any): value is any[];\n }\n\n interface LoDashImplicitWrapperBase {\n /**\n * @see _.isArrayLike\n */\n isArrayLike(): boolean;\n }\n\n interface LoDashExplicitWrapperBase {\n /**\n * @see _.isArrayLike\n */\n isArrayLike(): LoDashExplicitWrapper;\n }\n\n //_.isArrayLikeObject\n interface LoDashStatic {\n /**\n * This method is like `_.isArrayLike` except that it also checks if `value`\n * is an object.\n *\n * @static\n * @memberOf _\n * @type Function\n * @category Lang\n * @param {*} value The value to check.\n * @returns {boolean} Returns `true` if `value` is an array-like object, else `false`.\n * @example\n *\n * _.isArrayLikeObject([1, 2, 3]);\n * // => true\n *\n * _.isArrayLikeObject(document.body.children);\n * // => true\n *\n * _.isArrayLikeObject('abc');\n * // => false\n *\n * _.isArrayLikeObject(_.noop);\n * // => false\n */\n isArrayLikeObject(value: T & string & number): boolean; // should only match if T = any\n\n /**\n * @see _.isArrayLike\n */\n isArrayLikeObject(value?: Function | string | boolean | number): value is never;\n\n /**\n * @see _.isArrayLike\n */\n isArrayLikeObject(value: T | Function | string | boolean | number): value is T & { length: number };\n\n /**\n * DEPRECATED\n */\n isArrayLikeObject(value?: any): value is any[];\n }\n\n interface LoDashImplicitWrapperBase {\n /**\n * @see _.isArrayLikeObject\n */\n isArrayLikeObject(): boolean;\n }\n\n interface LoDashExplicitWrapperBase {\n /**\n * @see _.isArrayLikeObject\n */\n isArrayLikeObject(): LoDashExplicitWrapper;\n }\n\n //_.isBoolean\n interface LoDashStatic {\n /**\n * Checks if value is classified as a boolean primitive or object.\n *\n * @param value The value to check.\n * @return Returns true if value is correctly classified, else false.\n */\n isBoolean(value?: any): value is boolean;\n }\n\n interface LoDashImplicitWrapperBase {\n /**\n * @see _.isBoolean\n */\n isBoolean(): boolean;\n }\n\n interface LoDashExplicitWrapperBase {\n /**\n * @see _.isBoolean\n */\n isBoolean(): LoDashExplicitWrapper;\n }\n\n //_.isBuffer\n interface LoDashStatic {\n /**\n * Checks if value is a buffer.\n *\n * @param value The value to check.\n * @return Returns true if value is a buffer, else false.\n */\n isBuffer(value?: any): boolean;\n }\n\n interface LoDashImplicitWrapperBase {\n /**\n * @see _.isBuffer\n */\n isBuffer(): boolean;\n }\n\n interface LoDashExplicitWrapperBase {\n /**\n * @see _.isBuffer\n */\n isBuffer(): LoDashExplicitWrapper;\n }\n\n //_.isDate\n interface LoDashStatic {\n /**\n * Checks if value is classified as a Date object.\n * @param value The value to check.\n *\n * @return Returns true if value is correctly classified, else false.\n */\n isDate(value?: any): value is Date;\n }\n\n interface LoDashImplicitWrapperBase {\n /**\n * @see _.isDate\n */\n isDate(): boolean;\n }\n\n interface LoDashExplicitWrapperBase {\n /**\n * @see _.isDate\n */\n isDate(): LoDashExplicitWrapper;\n }\n\n //_.isElement\n interface LoDashStatic {\n /**\n * Checks if value is a DOM element.\n *\n * @param value The value to check.\n * @return Returns true if value is a DOM element, else false.\n */\n isElement(value?: any): boolean;\n }\n\n interface LoDashImplicitWrapperBase {\n /**\n * @see _.isElement\n */\n isElement(): boolean;\n }\n\n interface LoDashExplicitWrapperBase {\n /**\n * @see _.isElement\n */\n isElement(): LoDashExplicitWrapper;\n }\n\n //_.isEmpty\n interface LoDashStatic {\n /**\n * Checks if value is empty. A value is considered empty unless it’s an arguments object, array, string, or\n * jQuery-like collection with a length greater than 0 or an object with own enumerable properties.\n *\n * @param value The value to inspect.\n * @return Returns true if value is empty, else false.\n */\n isEmpty(value?: any): boolean;\n }\n\n interface LoDashImplicitWrapperBase {\n /**\n * @see _.isEmpty\n */\n isEmpty(): boolean;\n }\n\n interface LoDashExplicitWrapperBase {\n /**\n * @see _.isEmpty\n */\n isEmpty(): LoDashExplicitWrapper;\n }\n\n //_.isEqual\n interface LoDashStatic {\n /**\n * Performs a deep comparison between two values to determine if they are\n * equivalent.\n *\n * **Note:** This method supports comparing arrays, array buffers, booleans,\n * date objects, error objects, maps, numbers, `Object` objects, regexes,\n * sets, strings, symbols, and typed arrays. `Object` objects are compared\n * by their own, not inherited, enumerable properties. Functions and DOM\n * nodes are **not** supported.\n *\n * @static\n * @memberOf _\n * @category Lang\n * @param {*} value The value to compare.\n * @param {*} other The other value to compare.\n * @returns {boolean} Returns `true` if the values are equivalent, else `false`.\n * @example\n *\n * var object = { 'user': 'fred' };\n * var other = { 'user': 'fred' };\n *\n * _.isEqual(object, other);\n * // => true\n *\n * object === other;\n * // => false\n */\n isEqual(\n value: any,\n other: any\n ): boolean;\n }\n\n interface LoDashImplicitWrapperBase {\n /**\n * @see _.isEqual\n */\n isEqual(\n other: any\n ): boolean;\n }\n\n interface LoDashExplicitWrapperBase {\n /**\n * @see _.isEqual\n */\n isEqual(\n other: any\n ): LoDashExplicitWrapper;\n }\n\n // _.isEqualWith\n type IsEqualCustomizer = (value: any, other: any, indexOrKey: number|string|undefined, parent: any, otherParent: any, stack: any) => boolean|undefined;\n\n interface LoDashStatic {\n /**\n * This method is like `_.isEqual` except that it accepts `customizer` which is\n * invoked to compare values. If `customizer` returns `undefined` comparisons are\n * handled by the method instead. The `customizer` is invoked with up to seven arguments:\n * (objValue, othValue [, index|key, object, other, stack]).\n *\n * @static\n * @memberOf _\n * @category Lang\n * @param {*} value The value to compare.\n * @param {*} other The other value to compare.\n * @param {Function} [customizer] The function to customize comparisons.\n * @returns {boolean} Returns `true` if the values are equivalent, else `false`.\n * @example\n *\n * function isGreeting(value) {\n * return /^h(?:i|ello)$/.test(value);\n * }\n *\n * function customizer(objValue, othValue) {\n * if (isGreeting(objValue) && isGreeting(othValue)) {\n * return true;\n * }\n * }\n *\n * var array = ['hello', 'goodbye'];\n * var other = ['hi', 'goodbye'];\n *\n * _.isEqualWith(array, other, customizer);\n * // => true\n */\n isEqualWith(\n value: any,\n other: any,\n customizer?: IsEqualCustomizer\n ): boolean;\n }\n\n interface LoDashImplicitWrapperBase {\n /**\n * @see _.isEqualWith\n */\n isEqualWith(\n other: any,\n customizer?: IsEqualCustomizer\n ): boolean;\n }\n\n interface LoDashExplicitWrapperBase {\n /**\n * @see _.isEqualWith\n */\n isEqualWith(\n other: any,\n customizer?: IsEqualCustomizer\n ): LoDashExplicitWrapper;\n }\n\n //_.isError\n interface LoDashStatic {\n /**\n * Checks if value is an Error, EvalError, RangeError, ReferenceError, SyntaxError, TypeError, or URIError\n * object.\n *\n * @param value The value to check.\n * @return Returns true if value is an error object, else false.\n */\n isError(value: any): value is Error;\n }\n\n interface LoDashImplicitWrapperBase {\n /**\n * @see _.isError\n */\n isError(): boolean;\n }\n\n interface LoDashExplicitWrapperBase {\n /**\n * @see _.isError\n */\n isError(): LoDashExplicitWrapper;\n }\n\n //_.isFinite\n interface LoDashStatic {\n /**\n * Checks if value is a finite primitive number.\n *\n * Note: This method is based on Number.isFinite.\n *\n * @param value The value to check.\n * @return Returns true if value is a finite number, else false.\n */\n isFinite(value?: any): boolean;\n }\n\n interface LoDashImplicitWrapperBase {\n /**\n * @see _.isFinite\n */\n isFinite(): boolean;\n }\n\n interface LoDashExplicitWrapperBase {\n /**\n * @see _.isFinite\n */\n isFinite(): LoDashExplicitWrapper;\n }\n\n //_.isFunction\n interface LoDashStatic {\n /**\n * Checks if value is classified as a Function object.\n *\n * @param value The value to check.\n * @return Returns true if value is correctly classified, else false.\n */\n isFunction(value?: any): value is Function;\n }\n\n interface LoDashImplicitWrapperBase {\n /**\n * @see _.isFunction\n */\n isFunction(): boolean;\n }\n\n interface LoDashExplicitWrapperBase {\n /**\n * @see _.isFunction\n */\n isFunction(): LoDashExplicitWrapper;\n }\n\n //_.isInteger\n interface LoDashStatic {\n /**\n * Checks if `value` is an integer.\n *\n * **Note:** This method is based on [`Number.isInteger`](https://mdn.io/Number/isInteger).\n *\n * @static\n * @memberOf _\n * @category Lang\n * @param {*} value The value to check.\n * @returns {boolean} Returns `true` if `value` is an integer, else `false`.\n * @example\n *\n * _.isInteger(3);\n * // => true\n *\n * _.isInteger(Number.MIN_VALUE);\n * // => false\n *\n * _.isInteger(Infinity);\n * // => false\n *\n * _.isInteger('3');\n * // => false\n */\n isInteger(value?: any): boolean;\n }\n\n interface LoDashImplicitWrapperBase {\n /**\n * @see _.isInteger\n */\n isInteger(): boolean;\n }\n\n interface LoDashExplicitWrapperBase {\n /**\n * @see _.isInteger\n */\n isInteger(): LoDashExplicitWrapper;\n }\n\n //_.isLength\n interface LoDashStatic {\n /**\n * Checks if `value` is a valid array-like length.\n *\n * **Note:** This function is loosely based on [`ToLength`](http://ecma-international.org/ecma-262/6.0/#sec-tolength).\n *\n * @static\n * @memberOf _\n * @category Lang\n * @param {*} value The value to check.\n * @returns {boolean} Returns `true` if `value` is a valid length, else `false`.\n * @example\n *\n * _.isLength(3);\n * // => true\n *\n * _.isLength(Number.MIN_VALUE);\n * // => false\n *\n * _.isLength(Infinity);\n * // => false\n *\n * _.isLength('3');\n * // => false\n */\n isLength(value?: any): boolean;\n }\n\n interface LoDashImplicitWrapperBase {\n /**\n * @see _.isLength\n */\n isLength(): boolean;\n }\n\n interface LoDashExplicitWrapperBase {\n /**\n * @see _.isLength\n */\n isLength(): LoDashExplicitWrapper;\n }\n\n //_.isMap\n interface LoDashStatic {\n /**\n * Checks if value is classified as a Map object.\n *\n * @param value The value to check.\n * @returns Returns true if value is correctly classified, else false.\n */\n isMap(value?: any): value is Map;\n }\n\n interface LoDashImplicitWrapperBase {\n /**\n * @see _.isMap\n */\n isMap(): boolean;\n }\n\n interface LoDashExplicitWrapperBase {\n /**\n * @see _.isMap\n */\n isMap(): LoDashExplicitWrapper;\n }\n\n //_.isMatch\n type isMatchCustomizer = (value: any, other: any, indexOrKey?: number|string) => boolean;\n\n interface LoDashStatic {\n /**\n * Performs a deep comparison between `object` and `source` to determine if\n * `object` contains equivalent property values.\n *\n * **Note:** This method supports comparing the same values as `_.isEqual`.\n *\n * @static\n * @memberOf _\n * @category Lang\n * @param {Object} object The object to inspect.\n * @param {Object} source The object of property values to match.\n * @returns {boolean} Returns `true` if `object` is a match, else `false`.\n * @example\n *\n * var object = { 'user': 'fred', 'age': 40 };\n *\n * _.isMatch(object, { 'age': 40 });\n * // => true\n *\n * _.isMatch(object, { 'age': 36 });\n * // => false\n */\n isMatch(object: Object, source: Object): boolean;\n }\n\n interface LoDashImplicitObjectWrapper {\n /**\n * @see _.isMatch\n */\n isMatch(source: Object): boolean;\n }\n\n //_.isMatchWith\n type isMatchWithCustomizer = (value: any, other: any, indexOrKey?: number|string) => boolean;\n\n interface LoDashStatic {\n /**\n * This method is like `_.isMatch` except that it accepts `customizer` which\n * is invoked to compare values. If `customizer` returns `undefined` comparisons\n * are handled by the method instead. The `customizer` is invoked with three\n * arguments: (objValue, srcValue, index|key, object, source).\n *\n * @static\n * @memberOf _\n * @category Lang\n * @param {Object} object The object to inspect.\n * @param {Object} source The object of property values to match.\n * @param {Function} [customizer] The function to customize comparisons.\n * @returns {boolean} Returns `true` if `object` is a match, else `false`.\n * @example\n *\n * function isGreeting(value) {\n * return /^h(?:i|ello)$/.test(value);\n * }\n *\n * function customizer(objValue, srcValue) {\n * if (isGreeting(objValue) && isGreeting(srcValue)) {\n * return true;\n * }\n * }\n *\n * var object = { 'greeting': 'hello' };\n * var source = { 'greeting': 'hi' };\n *\n * _.isMatchWith(object, source, customizer);\n * // => true\n */\n isMatchWith(object: Object, source: Object, customizer: isMatchWithCustomizer): boolean;\n }\n\n interface LoDashImplicitObjectWrapper {\n /**\n * @see _.isMatchWith\n */\n isMatchWith(source: Object, customizer: isMatchWithCustomizer): boolean;\n }\n\n //_.isNaN\n interface LoDashStatic {\n /**\n * Checks if value is NaN.\n *\n * Note: This method is not the same as isNaN which returns true for undefined and other non-numeric values.\n *\n * @param value The value to check.\n * @return Returns true if value is NaN, else false.\n */\n isNaN(value?: any): boolean;\n }\n\n interface LoDashImplicitWrapperBase {\n /**\n * @see _.isNaN\n */\n isNaN(): boolean;\n }\n\n interface LoDashExplicitWrapperBase {\n /**\n * @see _.isNaN\n */\n isNaN(): LoDashExplicitWrapper;\n }\n\n //_.isNative\n interface LoDashStatic {\n /**\n * Checks if value is a native function.\n * @param value The value to check.\n *\n * @retrun Returns true if value is a native function, else false.\n */\n isNative(value: any): value is Function;\n }\n\n interface LoDashImplicitWrapperBase {\n /**\n * see _.isNative\n */\n isNative(): boolean;\n }\n\n interface LoDashExplicitWrapperBase {\n /**\n * see _.isNative\n */\n isNative(): LoDashExplicitWrapper;\n }\n\n //_.isNil\n interface LoDashStatic {\n /**\n * Checks if `value` is `null` or `undefined`.\n *\n * @static\n * @memberOf _\n * @category Lang\n * @param {*} value The value to check.\n * @returns {boolean} Returns `true` if `value` is nullish, else `false`.\n * @example\n *\n * _.isNil(null);\n * // => true\n *\n * _.isNil(void 0);\n * // => true\n *\n * _.isNil(NaN);\n * // => false\n */\n isNil(value: any): value is null | undefined;\n }\n\n interface LoDashImplicitWrapperBase {\n /**\n * see _.isNil\n */\n isNil(): boolean;\n }\n\n interface LoDashExplicitWrapperBase {\n /**\n * see _.isNil\n */\n isNil(): LoDashExplicitWrapper;\n }\n\n //_.isNull\n interface LoDashStatic {\n /**\n * Checks if value is null.\n *\n * @param value The value to check.\n * @return Returns true if value is null, else false.\n */\n isNull(value: any): value is null;\n }\n\n interface LoDashImplicitWrapperBase {\n /**\n * see _.isNull\n */\n isNull(): boolean;\n }\n\n interface LoDashExplicitWrapperBase {\n /**\n * see _.isNull\n */\n isNull(): LoDashExplicitWrapper;\n }\n\n //_.isNumber\n interface LoDashStatic {\n /**\n * Checks if value is classified as a Number primitive or object.\n *\n * Note: To exclude Infinity, -Infinity, and NaN, which are classified as numbers, use the _.isFinite method.\n *\n * @param value The value to check.\n * @return Returns true if value is correctly classified, else false.\n */\n isNumber(value?: any): value is number;\n }\n\n interface LoDashImplicitWrapperBase {\n /**\n * see _.isNumber\n */\n isNumber(): boolean;\n }\n\n interface LoDashExplicitWrapperBase {\n /**\n * see _.isNumber\n */\n isNumber(): LoDashExplicitWrapper;\n }\n\n //_.isObject\n interface LoDashStatic {\n /**\n * Checks if value is the language type of Object. (e.g. arrays, functions, objects, regexes, new Number(0),\n * and new String(''))\n *\n * @param value The value to check.\n * @return Returns true if value is an object, else false.\n */\n isObject(value?: any): boolean;\n }\n\n interface LoDashImplicitWrapperBase {\n /**\n * see _.isObject\n */\n isObject(): boolean;\n }\n\n interface LoDashExplicitWrapperBase {\n /**\n * see _.isObject\n */\n isObject(): LoDashExplicitWrapper;\n }\n\n //_.isObjectLike\n interface LoDashStatic {\n /**\n * Checks if `value` is object-like. A value is object-like if it's not `null`\n * and has a `typeof` result of \"object\".\n *\n * @static\n * @memberOf _\n * @category Lang\n * @param {*} value The value to check.\n * @returns {boolean} Returns `true` if `value` is object-like, else `false`.\n * @example\n *\n * _.isObjectLike({});\n * // => true\n *\n * _.isObjectLike([1, 2, 3]);\n * // => true\n *\n * _.isObjectLike(_.noop);\n * // => false\n *\n * _.isObjectLike(null);\n * // => false\n */\n isObjectLike(value?: any): boolean;\n }\n\n interface LoDashImplicitWrapperBase {\n /**\n * see _.isObjectLike\n */\n isObjectLike(): boolean;\n }\n\n interface LoDashExplicitWrapperBase {\n /**\n * see _.isObjectLike\n */\n isObjectLike(): LoDashExplicitWrapper;\n }\n\n //_.isPlainObject\n interface LoDashStatic {\n /**\n * Checks if value is a plain object, that is, an object created by the Object constructor or one with a\n * [[Prototype]] of null.\n *\n * Note: This method assumes objects created by the Object constructor have no inherited enumerable properties.\n *\n * @param value The value to check.\n * @return Returns true if value is a plain object, else false.\n */\n isPlainObject(value?: any): boolean;\n }\n\n interface LoDashImplicitWrapperBase {\n /**\n * see _.isPlainObject\n */\n isPlainObject(): boolean;\n }\n\n interface LoDashExplicitWrapperBase {\n /**\n * see _.isPlainObject\n */\n isPlainObject(): LoDashExplicitWrapper;\n }\n\n //_.isRegExp\n interface LoDashStatic {\n /**\n * Checks if value is classified as a RegExp object.\n * @param value The value to check.\n *\n * @return Returns true if value is correctly classified, else false.\n */\n isRegExp(value?: any): value is RegExp;\n }\n\n interface LoDashImplicitWrapperBase {\n /**\n * see _.isRegExp\n */\n isRegExp(): boolean;\n }\n\n interface LoDashExplicitWrapperBase {\n /**\n * see _.isRegExp\n */\n isRegExp(): LoDashExplicitWrapper;\n }\n\n //_.isSafeInteger\n interface LoDashStatic {\n /**\n * Checks if `value` is a safe integer. An integer is safe if it's an IEEE-754\n * double precision number which isn't the result of a rounded unsafe integer.\n *\n * **Note:** This method is based on [`Number.isSafeInteger`](https://mdn.io/Number/isSafeInteger).\n *\n * @static\n * @memberOf _\n * @category Lang\n * @param {*} value The value to check.\n * @returns {boolean} Returns `true` if `value` is a safe integer, else `false`.\n * @example\n *\n * _.isSafeInteger(3);\n * // => true\n *\n * _.isSafeInteger(Number.MIN_VALUE);\n * // => false\n *\n * _.isSafeInteger(Infinity);\n * // => false\n *\n * _.isSafeInteger('3');\n * // => false\n */\n isSafeInteger(value: any): boolean;\n }\n\n interface LoDashImplicitWrapperBase {\n /**\n * see _.isSafeInteger\n */\n isSafeInteger(): boolean;\n }\n\n interface LoDashExplicitWrapperBase {\n /**\n * see _.isSafeInteger\n */\n isSafeInteger(): LoDashExplicitWrapper;\n }\n\n //_.isSet\n interface LoDashStatic {\n /**\n * Checks if value is classified as a Set object.\n *\n * @param value The value to check.\n * @returns Returns true if value is correctly classified, else false.\n */\n isSet(value?: any): value is Set;\n }\n\n interface LoDashImplicitWrapperBase {\n /**\n * @see _.isSet\n */\n isSet(): boolean;\n }\n\n interface LoDashExplicitWrapperBase {\n /**\n * @see _.isSet\n */\n isSet(): LoDashExplicitWrapper;\n }\n\n //_.isString\n interface LoDashStatic {\n /**\n * Checks if value is classified as a String primitive or object.\n *\n * @param value The value to check.\n * @return Returns true if value is correctly classified, else false.\n */\n isString(value?: any): value is string;\n }\n\n interface LoDashImplicitWrapperBase {\n /**\n * see _.isString\n */\n isString(): boolean;\n }\n\n interface LoDashExplicitWrapperBase {\n /**\n * see _.isString\n */\n isString(): LoDashExplicitWrapper;\n }\n\n //_.isSymbol\n interface LoDashStatic {\n /**\n * Checks if `value` is classified as a `Symbol` primitive or object.\n *\n * @static\n * @memberOf _\n * @category Lang\n * @param {*} value The value to check.\n * @returns {boolean} Returns `true` if `value` is correctly classified, else `false`.\n * @example\n *\n * _.isSymbol(Symbol.iterator);\n * // => true\n *\n * _.isSymbol('abc');\n * // => false\n */\n isSymbol(value: any): boolean;\n }\n\n interface LoDashImplicitWrapperBase {\n /**\n * see _.isSymbol\n */\n isSymbol(): boolean;\n }\n\n interface LoDashExplicitWrapperBase {\n /**\n * see _.isSymbol\n */\n isSymbol(): LoDashExplicitWrapper;\n }\n\n //_.isTypedArray\n interface LoDashStatic {\n /**\n * Checks if value is classified as a typed array.\n *\n * @param value The value to check.\n * @return Returns true if value is correctly classified, else false.\n */\n isTypedArray(value: any): boolean;\n }\n\n interface LoDashImplicitWrapperBase {\n /**\n * see _.isTypedArray\n */\n isTypedArray(): boolean;\n }\n\n interface LoDashExplicitWrapperBase {\n /**\n * see _.isTypedArray\n */\n isTypedArray(): LoDashExplicitWrapper;\n }\n\n //_.isUndefined\n interface LoDashStatic {\n /**\n * Checks if value is undefined.\n *\n * @param value The value to check.\n * @return Returns true if value is undefined, else false.\n */\n isUndefined(value: any): value is undefined;\n }\n\n interface LoDashImplicitWrapperBase {\n /**\n * see _.isUndefined\n */\n isUndefined(): boolean;\n }\n\n interface LoDashExplicitWrapperBase {\n /**\n * see _.isUndefined\n */\n isUndefined(): LoDashExplicitWrapper;\n }\n\n //_.isWeakMap\n interface LoDashStatic {\n /**\n * Checks if value is classified as a WeakMap object.\n *\n * @param value The value to check.\n * @returns Returns true if value is correctly classified, else false.\n */\n isWeakMap(value?: any): value is WeakMap;\n }\n\n interface LoDashImplicitWrapperBase {\n /**\n * @see _.isSet\n */\n isWeakMap(): boolean;\n }\n\n interface LoDashExplicitWrapperBase {\n /**\n * @see _.isSet\n */\n isWeakMap(): LoDashExplicitWrapper;\n }\n\n //_.isWeakSet\n interface LoDashStatic {\n /**\n * Checks if value is classified as a WeakSet object.\n *\n * @param value The value to check.\n * @returns Returns true if value is correctly classified, else false.\n */\n isWeakSet(value?: any): value is WeakSet;\n }\n\n interface LoDashImplicitWrapperBase {\n /**\n * @see _.isWeakSet\n */\n isWeakSet(): boolean;\n }\n\n interface LoDashExplicitWrapperBase {\n /**\n * @see _.isWeakSet\n */\n isWeakSet(): LoDashExplicitWrapper;\n }\n\n //_.lt\n interface LoDashStatic {\n /**\n * Checks if value is less than other.\n *\n * @param value The value to compare.\n * @param other The other value to compare.\n * @return Returns true if value is less than other, else false.\n */\n lt(\n value: any,\n other: any\n ): boolean;\n }\n\n interface LoDashImplicitWrapperBase {\n /**\n * @see _.lt\n */\n lt(other: any): boolean;\n }\n\n interface LoDashExplicitWrapperBase {\n /**\n * @see _.lt\n */\n lt(other: any): LoDashExplicitWrapper;\n }\n\n //_.lte\n interface LoDashStatic {\n /**\n * Checks if value is less than or equal to other.\n *\n * @param value The value to compare.\n * @param other The other value to compare.\n * @return Returns true if value is less than or equal to other, else false.\n */\n lte(\n value: any,\n other: any\n ): boolean;\n }\n\n interface LoDashImplicitWrapperBase {\n /**\n * @see _.lte\n */\n lte(other: any): boolean;\n }\n\n interface LoDashExplicitWrapperBase {\n /**\n * @see _.lte\n */\n lte(other: any): LoDashExplicitWrapper;\n }\n\n //_.toArray\n interface LoDashStatic {\n /**\n * Converts value to an array.\n *\n * @param value The value to convert.\n * @return Returns the converted array.\n */\n toArray(value: List|Dictionary|NumericDictionary): T[];\n\n /**\n * @see _.toArray\n */\n toArray(value: TValue): TResult[];\n\n /**\n * @see _.toArray\n */\n toArray(value?: any): TResult[];\n }\n\n interface LoDashImplicitWrapper {\n /**\n * @see _.toArray\n */\n toArray(): LoDashImplicitArrayWrapper;\n }\n\n interface LoDashImplicitArrayWrapperBase {\n /**\n * @see _.toArray\n */\n toArray(): LoDashImplicitArrayWrapper;\n }\n\n interface LoDashImplicitObjectWrapperBase {\n /**\n * @see _.toArray\n */\n toArray(): LoDashImplicitArrayWrapper;\n }\n\n interface LoDashExplicitWrapper {\n /**\n * @see _.toArray\n */\n toArray(): LoDashExplicitArrayWrapper;\n }\n\n interface LoDashExplicitArrayWrapperBase {\n /**\n * @see _.toArray\n */\n toArray(): LoDashExplicitArrayWrapper;\n }\n\n interface LoDashExplicitObjectWrapperBase {\n /**\n * @see _.toArray\n */\n toArray(): LoDashExplicitArrayWrapper;\n }\n\n //_.toPlainObject\n interface LoDashStatic {\n /**\n * Converts value to a plain object flattening inherited enumerable properties of value to own properties\n * of the plain object.\n *\n * @param value The value to convert.\n * @return Returns the converted plain object.\n */\n toPlainObject(value?: any): TResult;\n }\n\n interface LoDashImplicitWrapperBase {\n /**\n * @see _.toPlainObject\n */\n toPlainObject(): LoDashImplicitObjectWrapper;\n }\n\n //_.toFinite\n interface LoDashStatic {\n /**\n * Converts `value` to a finite number.\n *\n * @static\n * @memberOf _\n * @since 4.12.0\n * @category Lang\n * @param {*} value The value to convert.\n * @returns {number} Returns the converted number.\n * @example\n *\n * _.toFinite(3.2);\n * // => 3.2\n *\n * _.toFinite(Number.MIN_VALUE);\n * // => 5e-324\n *\n * _.toFinite(Infinity);\n * // => 1.7976931348623157e+308\n *\n * _.toFinite('3.2');\n * // => 3.2\n */\n toFinite(value: any): number;\n }\n\n interface LoDashImplicitWrapperBase {\n /**\n * @see _.toFinite\n */\n toFinite(): LoDashImplicitWrapper;\n }\n\n interface LoDashExplicitWrapperBase {\n /**\n * @see _.toFinite\n */\n toFinite(): LoDashExplicitWrapper;\n }\n\n //_.toInteger\n interface LoDashStatic {\n /**\n * Converts `value` to an integer.\n *\n * **Note:** This function is loosely based on [`ToInteger`](http://www.ecma-international.org/ecma-262/6.0/#sec-tointeger).\n *\n * @static\n * @memberOf _\n * @category Lang\n * @param {*} value The value to convert.\n * @returns {number} Returns the converted integer.\n * @example\n *\n * _.toInteger(3);\n * // => 3\n *\n * _.toInteger(Number.MIN_VALUE);\n * // => 0\n *\n * _.toInteger(Infinity);\n * // => 1.7976931348623157e+308\n *\n * _.toInteger('3');\n * // => 3\n */\n toInteger(value: any): number;\n }\n\n interface LoDashImplicitWrapperBase {\n /**\n * @see _.toInteger\n */\n toInteger(): LoDashImplicitWrapper;\n }\n\n interface LoDashExplicitWrapperBase {\n /**\n * @see _.toInteger\n */\n toInteger(): LoDashExplicitWrapper;\n }\n\n //_.toLength\n interface LoDashStatic {\n /**\n * Converts `value` to an integer suitable for use as the length of an\n * array-like object.\n *\n * **Note:** This method is based on [`ToLength`](http://ecma-international.org/ecma-262/6.0/#sec-tolength).\n *\n * @static\n * @memberOf _\n * @category Lang\n * @param {*} value The value to convert.\n * @return {number} Returns the converted integer.\n * @example\n *\n * _.toLength(3);\n * // => 3\n *\n * _.toLength(Number.MIN_VALUE);\n * // => 0\n *\n * _.toLength(Infinity);\n * // => 4294967295\n *\n * _.toLength('3');\n * // => 3\n */\n toLength(value: any): number;\n }\n\n interface LoDashImplicitWrapperBase {\n /**\n * @see _.toLength\n */\n toLength(): LoDashImplicitWrapper;\n }\n\n interface LoDashExplicitWrapperBase {\n /**\n * @see _.toLength\n */\n toLength(): LoDashExplicitWrapper;\n }\n\n //_.toNumber\n interface LoDashStatic {\n /**\n * Converts `value` to a number.\n *\n * @static\n * @memberOf _\n * @category Lang\n * @param {*} value The value to process.\n * @returns {number} Returns the number.\n * @example\n *\n * _.toNumber(3);\n * // => 3\n *\n * _.toNumber(Number.MIN_VALUE);\n * // => 5e-324\n *\n * _.toNumber(Infinity);\n * // => Infinity\n *\n * _.toNumber('3');\n * // => 3\n */\n toNumber(value: any): number;\n }\n\n interface LoDashImplicitWrapperBase {\n /**\n * @see _.toNumber\n */\n toNumber(): LoDashImplicitWrapper;\n }\n\n interface LoDashExplicitWrapperBase {\n /**\n * @see _.toNumber\n */\n toNumber(): LoDashExplicitWrapper;\n }\n\n //_.toSafeInteger\n interface LoDashStatic {\n /**\n * Converts `value` to a safe integer. A safe integer can be compared and\n * represented correctly.\n *\n * @static\n * @memberOf _\n * @category Lang\n * @param {*} value The value to convert.\n * @returns {number} Returns the converted integer.\n * @example\n *\n * _.toSafeInteger(3);\n * // => 3\n *\n * _.toSafeInteger(Number.MIN_VALUE);\n * // => 0\n *\n * _.toSafeInteger(Infinity);\n * // => 9007199254740991\n *\n * _.toSafeInteger('3');\n * // => 3\n */\n toSafeInteger(value: any): number;\n }\n\n interface LoDashImplicitWrapperBase {\n /**\n * @see _.toSafeInteger\n */\n toSafeInteger(): LoDashImplicitWrapper;\n }\n\n interface LoDashExplicitWrapperBase {\n /**\n * @see _.toSafeInteger\n */\n toSafeInteger(): LoDashExplicitWrapper;\n }\n\n //_.toString DUMMY\n interface LoDashStatic {\n /**\n * Converts `value` to a string if it's not one. An empty string is returned\n * for `null` and `undefined` values. The sign of `-0` is preserved.\n *\n * @static\n * @memberOf _\n * @category Lang\n * @param {*} value The value to process.\n * @returns {string} Returns the string.\n * @example\n *\n * _.toString(null);\n * // => ''\n *\n * _.toString(-0);\n * // => '-0'\n *\n * _.toString([1, 2, 3]);\n * // => '1,2,3'\n */\n toString(value: any): string;\n }\n\n /********\n * Math *\n ********/\n\n //_.add\n interface LoDashStatic {\n /**\n * Adds two numbers.\n *\n * @param augend The first number to add.\n * @param addend The second number to add.\n * @return Returns the sum.\n */\n add(\n augend: number,\n addend: number\n ): number;\n }\n\n interface LoDashImplicitWrapper {\n /**\n * @see _.add\n */\n add(addend: number): number;\n }\n\n interface LoDashExplicitWrapper {\n /**\n * @see _.add\n */\n add(addend: number): LoDashExplicitWrapper;\n }\n\n //_.ceil\n interface LoDashStatic {\n /**\n * Calculates n rounded up to precision.\n *\n * @param n The number to round up.\n * @param precision The precision to round up to.\n * @return Returns the rounded up number.\n */\n ceil(\n n: number,\n precision?: number\n ): number;\n }\n\n interface LoDashImplicitWrapper {\n /**\n * @see _.ceil\n */\n ceil(precision?: number): number;\n }\n\n interface LoDashExplicitWrapper {\n /**\n * @see _.ceil\n */\n ceil(precision?: number): LoDashExplicitWrapper;\n }\n\n //_.divide\n interface LoDashStatic {\n /**\n * Divide two numbers.\n *\n * @param dividend The first number in a division.\n * @param divisor The second number in a division.\n * @returns Returns the quotient.\n */\n divide(\n dividend: number,\n divisor: number\n ): number;\n }\n\n interface LoDashImplicitWrapper {\n /**\n * @see _.divide\n */\n divide(divisor: number): number;\n }\n\n interface LoDashExplicitWrapper {\n /**\n * @see _.divide\n */\n divide(divisor: number): LoDashExplicitWrapper;\n }\n\n //_.floor\n interface LoDashStatic {\n /**\n * Calculates n rounded down to precision.\n *\n * @param n The number to round down.\n * @param precision The precision to round down to.\n * @return Returns the rounded down number.\n */\n floor(\n n: number,\n precision?: number\n ): number;\n }\n\n interface LoDashImplicitWrapper {\n /**\n * @see _.floor\n */\n floor(precision?: number): number;\n }\n\n interface LoDashExplicitWrapper {\n /**\n * @see _.floor\n */\n floor(precision?: number): LoDashExplicitWrapper;\n }\n\n //_.max\n interface LoDashStatic {\n /**\n * Computes the maximum value of `array`. If `array` is empty or falsey\n * `undefined` is returned.\n *\n * @static\n * @memberOf _\n * @category Math\n * @param {Array} array The array to iterate over.\n * @returns {*} Returns the maximum value.\n */\n max(\n collection: List | null | undefined\n ): T | undefined;\n }\n\n interface LoDashImplicitArrayWrapperBase {\n /**\n * @see _.max\n */\n max(): T | undefined;\n }\n\n interface LoDashImplicitObjectWrapperBase {\n /**\n * @see _.max\n */\n max(): T | undefined;\n }\n\n //_.maxBy\n interface LoDashStatic {\n /**\n * This method is like `_.max` except that it accepts `iteratee` which is\n * invoked for each element in `array` to generate the criterion by which\n * the value is ranked. The iteratee is invoked with one argument: (value).\n *\n * @static\n * @memberOf _\n * @category Math\n * @param {Array} array The array to iterate over.\n * @param {Function|Object|string} [iteratee=_.identity] The iteratee invoked per element.\n * @returns {*} Returns the maximum value.\n * @example\n *\n * var objects = [{ 'n': 1 }, { 'n': 2 }];\n *\n * _.maxBy(objects, function(o) { return o.a; });\n * // => { 'n': 2 }\n *\n * // using the `_.property` iteratee shorthand\n * _.maxBy(objects, 'n');\n * // => { 'n': 2 }\n */\n maxBy(\n collection: List | null | undefined,\n iteratee?: ListIterator\n ): T | undefined;\n\n /**\n * @see _.maxBy\n */\n maxBy(\n collection: Dictionary | null | undefined,\n iteratee?: DictionaryIterator\n ): T | undefined;\n\n /**\n * @see _.maxBy\n */\n maxBy(\n collection: List|Dictionary | null | undefined,\n iteratee?: string\n ): T | undefined;\n\n /**\n * @see _.maxBy\n */\n maxBy(\n collection: List|Dictionary | null | undefined,\n whereValue?: TObject\n ): T | undefined;\n }\n\n interface LoDashImplicitArrayWrapperBase {\n /**\n * @see _.maxBy\n */\n maxBy(\n iteratee?: ListIterator\n ): T | undefined;\n\n /**\n * @see _.maxBy\n */\n maxBy(\n iteratee?: string\n ): T | undefined;\n\n /**\n * @see _.maxBy\n */\n maxBy(\n whereValue?: TObject\n ): T | undefined;\n }\n\n interface LoDashImplicitObjectWrapperBase {\n /**\n * @see _.maxBy\n */\n maxBy(\n iteratee?: ListIterator|DictionaryIterator\n ): T | undefined;\n\n /**\n * @see _.maxBy\n */\n maxBy(\n iteratee?: string\n ): T | undefined;\n\n /**\n * @see _.maxBy\n */\n maxBy(\n whereValue?: TObject\n ): T | undefined;\n }\n\n //_.mean\n interface LoDashStatic {\n /**\n * Computes the mean of the values in `array`.\n *\n * @static\n * @memberOf _\n * @category Math\n * @param {Array} array The array to iterate over.\n * @returns {number} Returns the mean.\n * @example\n *\n * _.mean([4, 2, 8, 6]);\n * // => 5\n */\n mean(\n collection: List | null | undefined\n ): number;\n }\n\n //_.meanBy\n interface LoDashStatic {\n /**\n * Computes the mean of the provided propties of the objects in the `array`\n *\n * @static\n * @memberOf _\n * @category Math\n * @param {Array} array The array to iterate over.\n * @param {Function|Object|string} [iteratee=_.identity] The iteratee invoked per element.\n * @returns {number} Returns the mean.\n * @example\n *\n * _.mean([{ 'n': 4 }, { 'n': 2 }, { 'n': 8 }, { 'n': 6 }], 'n');\n * // => 5\n */\n meanBy(\n collection: List | null | undefined,\n iteratee?: ListIterator | string\n ): number;\n\n meanBy(\n collection: Dictionary | null | undefined,\n iteratee?: DictionaryIterator | string\n ): number;\n }\n\n interface LoDashImplicitArrayWrapperBase {\n /**\n * @see _.mean\n */\n mean(): number;\n\n /**\n * @see _.mean\n */\n mean(): number;\n }\n\n //_.min\n interface LoDashStatic {\n /**\n * Computes the minimum value of `array`. If `array` is empty or falsey\n * `undefined` is returned.\n *\n * @static\n * @memberOf _\n * @category Math\n * @param {Array} array The array to iterate over.\n * @returns {*} Returns the minimum value.\n */\n min(\n collection: List | null | undefined\n ): T | undefined;\n }\n\n interface LoDashImplicitArrayWrapperBase {\n /**\n * @see _.min\n */\n min(): T | undefined;\n }\n\n interface LoDashImplicitObjectWrapperBase {\n /**\n * @see _.min\n */\n min(): T | undefined;\n }\n\n //_.minBy\n interface LoDashStatic {\n /**\n * This method is like `_.min` except that it accepts `iteratee` which is\n * invoked for each element in `array` to generate the criterion by which\n * the value is ranked. The iteratee is invoked with one argument: (value).\n *\n * @static\n * @memberOf _\n * @category Math\n * @param {Array} array The array to iterate over.\n * @param {Function|Object|string} [iteratee=_.identity] The iteratee invoked per element.\n * @returns {*} Returns the minimum value.\n * @example\n *\n * var objects = [{ 'n': 1 }, { 'n': 2 }];\n *\n * _.minBy(objects, function(o) { return o.a; });\n * // => { 'n': 1 }\n *\n * // using the `_.property` iteratee shorthand\n * _.minBy(objects, 'n');\n * // => { 'n': 1 }\n */\n minBy(\n collection: List | null | undefined,\n iteratee?: ListIterator\n ): T | undefined;\n\n /**\n * @see _.minBy\n */\n minBy(\n collection: Dictionary | null | undefined,\n iteratee?: DictionaryIterator\n ): T | undefined;\n\n /**\n * @see _.minBy\n */\n minBy(\n collection: List|Dictionary | null | undefined,\n iteratee?: string\n ): T | undefined;\n\n /**\n * @see _.minBy\n */\n minBy(\n collection: List|Dictionary | null | undefined,\n whereValue?: TObject\n ): T | undefined;\n }\n\n interface LoDashImplicitArrayWrapperBase {\n /**\n * @see _.minBy\n */\n minBy(\n iteratee?: ListIterator\n ): T | undefined;\n\n /**\n * @see _.minBy\n */\n minBy(\n iteratee?: string\n ): T | undefined;\n\n /**\n * @see _.minBy\n */\n minBy(\n whereValue?: TObject\n ): T | undefined;\n }\n\n interface LoDashImplicitObjectWrapperBase {\n /**\n * @see _.minBy\n */\n minBy(\n iteratee?: ListIterator|DictionaryIterator\n ): T | undefined;\n\n /**\n * @see _.minBy\n */\n minBy(\n iteratee?: string\n ): T | undefined;\n\n /**\n * @see _.minBy\n */\n minBy(\n whereValue?: TObject\n ): T | undefined;\n }\n\n //_.multiply\n interface LoDashStatic {\n /**\n * Multiply two numbers.\n * @param multiplier The first number in a multiplication.\n * @param multiplicand The second number in a multiplication.\n * @returns Returns the product.\n */\n multiply(\n multiplier: number,\n multiplicand: number\n ): number;\n }\n\n interface LoDashImplicitWrapper {\n /**\n * @see _.multiply\n */\n multiply(multiplicand: number): number;\n }\n\n interface LoDashExplicitWrapper {\n /**\n * @see _.multiply\n */\n multiply(multiplicand: number): LoDashExplicitWrapper;\n }\n\n //_.round\n interface LoDashStatic {\n /**\n * Calculates n rounded to precision.\n *\n * @param n The number to round.\n * @param precision The precision to round to.\n * @return Returns the rounded number.\n */\n round(\n n: number,\n precision?: number\n ): number;\n }\n\n interface LoDashImplicitWrapper {\n /**\n * @see _.round\n */\n round(precision?: number): number;\n }\n\n interface LoDashExplicitWrapper {\n /**\n * @see _.round\n */\n round(precision?: number): LoDashExplicitWrapper;\n }\n\n //_.sum\n interface LoDashStatic {\n /**\n * Computes the sum of the values in `array`.\n *\n * @static\n * @memberOf _\n * @category Math\n * @param {Array} array The array to iterate over.\n * @returns {number} Returns the sum.\n * @example\n *\n * _.sum([4, 2, 8, 6]);\n * // => 20\n */\n sum(collection: List | null | undefined): number;\n\n /**\n * @see _.sum\n */\n sum(collection: List|Dictionary | null | undefined): number;\n }\n\n interface LoDashImplicitArrayWrapperBase {\n /**\n * @see _.sum\n */\n sum(): number;\n }\n\n interface LoDashImplicitObjectWrapperBase {\n /**\n * @see _.sum\n **/\n sum(): number;\n\n /**\n * @see _.sum\n */\n sum(): number;\n }\n\n interface LoDashExplicitArrayWrapperBase {\n /**\n * @see _.sum\n */\n sum(): LoDashExplicitWrapper;\n }\n\n interface LoDashExplicitObjectWrapperBase {\n /**\n * @see _.sum\n */\n sum(): LoDashExplicitWrapper;\n\n /**\n * @see _.sum\n */\n sum(): LoDashExplicitWrapper;\n }\n\n //_.sumBy\n interface LoDashStatic {\n /**\n * This method is like `_.sum` except that it accepts `iteratee` which is\n * invoked for each element in `array` to generate the value to be summed.\n * The iteratee is invoked with one argument: (value).\n *\n * @static\n * @memberOf _\n * @category Math\n * @param {Array} array The array to iterate over.\n * @param {Function|Object|string} [iteratee=_.identity] The iteratee invoked per element.\n * @returns {number} Returns the sum.\n * @example\n *\n * var objects = [{ 'n': 4 }, { 'n': 2 }, { 'n': 8 }, { 'n': 6 }];\n *\n * _.sumBy(objects, function(o) { return o.n; });\n * // => 20\n *\n * // using the `_.property` iteratee shorthand\n * _.sumBy(objects, 'n');\n * // => 20\n */\n sumBy(\n collection: List | null | undefined,\n iteratee: ListIterator\n ): number;\n\n /**\n * @see _.sumBy\n */\n sumBy(\n collection: List<{}> | null | undefined,\n iteratee: string\n ): number;\n\n /**\n * @see _.sumBy\n */\n sumBy(\n collection: List | null | undefined\n ): number;\n\n /**\n * @see _.sumBy\n */\n sumBy(\n collection: List<{}> | null | undefined,\n iteratee: Dictionary<{}>\n ): number;\n }\n\n interface LoDashImplicitArrayWrapperBase {\n /**\n * @see _.sumBy\n */\n sumBy(\n iteratee: ListIterator\n ): number;\n\n /**\n * @see _.sumBy\n */\n sumBy(iteratee: string): number;\n\n /**\n * @see _.sumBy\n */\n sumBy(iteratee: Dictionary<{}>): number;\n }\n\n interface LoDashImplicitObjectWrapperBase {\n /**\n * @see _.sumBy\n */\n sumBy(\n iteratee: ListIterator<{}, number>\n ): number;\n\n /**\n * @see _.sumBy\n */\n sumBy(iteratee: string): number;\n\n /**\n * @see _.sumBy\n */\n sumBy(iteratee: Dictionary<{}>): number;\n }\n\n interface LoDashExplicitArrayWrapperBase {\n /**\n * @see _.sumBy\n */\n sumBy(\n iteratee: ListIterator\n ): LoDashExplicitWrapper;\n\n /**\n * @see _.sumBy\n */\n sumBy(iteratee: string): LoDashExplicitWrapper;\n\n /**\n * @see _.sumBy\n */\n sumBy(): LoDashExplicitWrapper;\n\n /**\n * @see _.sumBy\n */\n sumBy(iteratee: Dictionary<{}>): LoDashExplicitWrapper;\n }\n\n interface LoDashExplicitObjectWrapperBase {\n /**\n * @see _.sumBy\n */\n sumBy(\n iteratee: ListIterator<{}, number>\n ): LoDashExplicitWrapper;\n\n /**\n * @see _.sumBy\n */\n sumBy(iteratee: string): LoDashExplicitWrapper;\n\n /**\n * @see _.sumBy\n */\n sumBy(iteratee: Dictionary<{}>): LoDashExplicitWrapper;\n }\n\n /**********\n * Number *\n **********/\n\n //_.subtract\n interface LoDashStatic {\n /**\n * Subtract two numbers.\n *\n * @static\n * @memberOf _\n * @category Math\n * @param {number} minuend The first number in a subtraction.\n * @param {number} subtrahend The second number in a subtraction.\n * @returns {number} Returns the difference.\n * @example\n *\n * _.subtract(6, 4);\n * // => 2\n */\n subtract(\n minuend: number,\n subtrahend: number\n ): number;\n }\n\n interface LoDashImplicitWrapper {\n /**\n * @see _.subtract\n */\n subtract(\n subtrahend: number\n ): number;\n }\n\n interface LoDashExplicitWrapper {\n /**\n * @see _.subtract\n */\n subtract(\n subtrahend: number\n ): LoDashExplicitWrapper;\n }\n\n //_.clamp\n interface LoDashStatic {\n /**\n * Clamps `number` within the inclusive `lower` and `upper` bounds.\n *\n * @static\n * @memberOf _\n * @category Number\n * @param {number} number The number to clamp.\n * @param {number} [lower] The lower bound.\n * @param {number} upper The upper bound.\n * @returns {number} Returns the clamped number.\n * @example\n *\n * _.clamp(-10, -5, 5);\n * // => -5\n *\n * _.clamp(10, -5, 5);\n * // => 5\n */\n clamp(\n number: number,\n lower: number,\n upper: number\n ): number;\n }\n\n interface LoDashImplicitWrapper {\n /**\n * @see _.clamp\n */\n clamp(\n lower: number,\n upper: number\n ): number;\n }\n\n interface LoDashExplicitWrapper {\n /**\n * @see _.clamp\n */\n clamp(\n lower: number,\n upper: number\n ): LoDashExplicitWrapper;\n }\n\n //_.inRange\n interface LoDashStatic {\n /**\n * Checks if n is between start and up to but not including, end. If end is not specified it’s set to start\n * with start then set to 0.\n *\n * @param n The number to check.\n * @param start The start of the range.\n * @param end The end of the range.\n * @return Returns true if n is in the range, else false.\n */\n inRange(\n n: number,\n start: number,\n end: number\n ): boolean;\n\n /**\n * @see _.inRange\n */\n inRange(\n n: number,\n end: number\n ): boolean;\n }\n\n interface LoDashImplicitWrapper {\n /**\n * @see _.inRange\n */\n inRange(\n start: number,\n end: number\n ): boolean;\n\n /**\n * @see _.inRange\n */\n inRange(end: number): boolean;\n }\n\n interface LoDashExplicitWrapper {\n /**\n * @see _.inRange\n */\n inRange(\n start: number,\n end: number\n ): LoDashExplicitWrapper;\n\n /**\n * @see _.inRange\n */\n inRange(end: number): LoDashExplicitWrapper;\n }\n\n //_.random\n interface LoDashStatic {\n /**\n * Produces a random number between min and max (inclusive). If only one argument is provided a number between\n * 0 and the given number is returned. If floating is true, or either min or max are floats, a floating-point\n * number is returned instead of an integer.\n *\n * @param min The minimum possible value.\n * @param max The maximum possible value.\n * @param floating Specify returning a floating-point number.\n * @return Returns the random number.\n */\n random(\n min?: number,\n max?: number,\n floating?: boolean\n ): number;\n\n /**\n * @see _.random\n */\n random(\n min?: number,\n floating?: boolean\n ): number;\n\n /**\n * @see _.random\n */\n random(floating?: boolean): number;\n }\n\n interface LoDashImplicitWrapper {\n /**\n * @see _.random\n */\n random(\n max?: number,\n floating?: boolean\n ): number;\n\n /**\n * @see _.random\n */\n random(floating?: boolean): number;\n }\n\n interface LoDashExplicitWrapper {\n /**\n * @see _.random\n */\n random(\n max?: number,\n floating?: boolean\n ): LoDashExplicitWrapper;\n\n /**\n * @see _.random\n */\n random(floating?: boolean): LoDashExplicitWrapper;\n }\n\n /**********\n * Object *\n **********/\n\n //_.assign\n interface LoDashStatic {\n /**\n * Assigns own enumerable properties of source objects to the destination\n * object. Source objects are applied from left to right. Subsequent sources\n * overwrite property assignments of previous sources.\n *\n * **Note:** This method mutates `object` and is loosely based on\n * [`Object.assign`](https://mdn.io/Object/assign).\n *\n * @static\n * @memberOf _\n * @category Object\n * @param {Object} object The destination object.\n * @param {...Object} [sources] The source objects.\n * @returns {Object} Returns `object`.\n * @example\n *\n * function Foo() {\n * this.c = 3;\n * }\n *\n * function Bar() {\n * this.e = 5;\n * }\n *\n * Foo.prototype.d = 4;\n * Bar.prototype.f = 6;\n *\n * _.assign({ 'a': 1 }, new Foo, new Bar);\n * // => { 'a': 1, 'c': 3, 'e': 5 }\n */\n assign(\n object: TObject,\n source: TSource\n ): TObject & TSource;\n\n /**\n * @see assign\n */\n assign(\n object: TObject,\n source1: TSource1,\n source2: TSource2\n ): TObject & TSource1 & TSource2;\n\n /**\n * @see assign\n */\n assign(\n object: TObject,\n source1: TSource1,\n source2: TSource2,\n source3: TSource3\n ): TObject & TSource1 & TSource2 & TSource3;\n\n /**\n * @see assign\n */\n assign(\n object: TObject,\n source1: TSource1,\n source2: TSource2,\n source3: TSource3,\n source4: TSource4\n ): TObject & TSource1 & TSource2 & TSource3 & TSource4;\n\n /**\n * @see _.assign\n */\n assign(object: TObject): TObject;\n\n /**\n * @see _.assign\n */\n assign(\n object: any,\n ...otherArgs: any[]\n ): TResult;\n }\n\n interface LoDashImplicitObjectWrapper {\n /**\n * @see _.assign\n */\n assign(\n source: TSource\n ): LoDashImplicitObjectWrapper;\n\n /**\n * @see assign\n */\n assign(\n source1: TSource1,\n source2: TSource2\n ): LoDashImplicitObjectWrapper;\n\n /**\n * @see assign\n */\n assign(\n source1: TSource1,\n source2: TSource2,\n source3: TSource3\n ): LoDashImplicitObjectWrapper;\n\n /**\n * @see assign\n */\n assign(\n source1: TSource1,\n source2: TSource2,\n source3: TSource3,\n source4: TSource4\n ): LoDashImplicitObjectWrapper;\n\n /**\n * @see _.assign\n */\n assign(): LoDashImplicitObjectWrapper;\n\n /**\n * @see _.assign\n */\n assign(...otherArgs: any[]): LoDashImplicitObjectWrapper;\n }\n\n interface LoDashExplicitObjectWrapper {\n /**\n * @see _.assign\n */\n assign(\n source: TSource\n ): LoDashExplicitObjectWrapper;\n\n /**\n * @see assign\n */\n assign(\n source1: TSource1,\n source2: TSource2\n ): LoDashExplicitObjectWrapper;\n\n /**\n * @see assign\n */\n assign(\n source1: TSource1,\n source2: TSource2,\n source3: TSource3\n ): LoDashExplicitObjectWrapper;\n\n /**\n * @see assign\n */\n assign(\n source1: TSource1,\n source2: TSource2,\n source3: TSource3,\n source4: TSource4\n ): LoDashExplicitObjectWrapper;\n\n /**\n * @see _.assign\n */\n assign(): LoDashExplicitObjectWrapper;\n\n /**\n * @see _.assign\n */\n assign(...otherArgs: any[]): LoDashExplicitObjectWrapper;\n }\n\n interface LoDashStatic {\n /**\n * This method is like `_.assign` except that it accepts `customizer` which\n * is invoked to produce the assigned values. If `customizer` returns `undefined`\n * assignment is handled by the method instead. The `customizer` is invoked\n * with five arguments: (objValue, srcValue, key, object, source).\n *\n * **Note:** This method mutates `object`.\n *\n * @static\n * @memberOf _\n * @category Object\n * @param {Object} object The destination object.\n * @param {...Object} sources The source objects.\n * @param {Function} [customizer] The function to customize assigned values.\n * @returns {Object} Returns `object`.\n * @example\n *\n * function customizer(objValue, srcValue) {\n * return _.isUndefined(objValue) ? srcValue : objValue;\n * }\n *\n * var defaults = _.partialRight(_.assignWith, customizer);\n *\n * defaults({ 'a': 1 }, { 'b': 2 }, { 'a': 3 });\n * // => { 'a': 1, 'b': 2 }\n */\n assignWith(\n object: TObject,\n source: TSource,\n customizer: AssignCustomizer\n ): TObject & TSource;\n\n /**\n * @see assignWith\n */\n assignWith(\n object: TObject,\n source1: TSource1,\n source2: TSource2,\n customizer: AssignCustomizer\n ): TObject & TSource1 & TSource2;\n\n /**\n * @see assignWith\n */\n assignWith(\n object: TObject,\n source1: TSource1,\n source2: TSource2,\n source3: TSource3,\n customizer: AssignCustomizer\n ): TObject & TSource1 & TSource2 & TSource3;\n\n /**\n * @see assignWith\n */\n assignWith(\n object: TObject,\n source1: TSource1,\n source2: TSource2,\n source3: TSource3,\n source4: TSource4,\n customizer: AssignCustomizer\n ): TObject & TSource1 & TSource2 & TSource3 & TSource4;\n\n /**\n * @see _.assignWith\n */\n assignWith(object: TObject): TObject;\n\n /**\n * @see _.assignWith\n */\n assignWith(\n object: any,\n ...otherArgs: any[]\n ): TResult;\n }\n\n interface LoDashImplicitObjectWrapper {\n /**\n * @see _.assignWith\n */\n assignWith(\n source: TSource,\n customizer: AssignCustomizer\n ): LoDashImplicitObjectWrapper;\n\n /**\n * @see assignWith\n */\n assignWith(\n source1: TSource1,\n source2: TSource2,\n customizer: AssignCustomizer\n ): LoDashImplicitObjectWrapper;\n\n /**\n * @see assignWith\n */\n assignWith(\n source1: TSource1,\n source2: TSource2,\n source3: TSource3,\n customizer: AssignCustomizer\n ): LoDashImplicitObjectWrapper;\n\n /**\n * @see assignWith\n */\n assignWith(\n source1: TSource1,\n source2: TSource2,\n source3: TSource3,\n source4: TSource4,\n customizer: AssignCustomizer\n ): LoDashImplicitObjectWrapper;\n\n /**\n * @see _.assignWith\n */\n assignWith(): LoDashImplicitObjectWrapper;\n\n /**\n * @see _.assignWith\n */\n assignWith(...otherArgs: any[]): LoDashImplicitObjectWrapper;\n }\n\n interface LoDashExplicitObjectWrapper {\n /**\n * @see _.assignWith\n */\n assignWith(\n source: TSource,\n customizer: AssignCustomizer\n ): LoDashExplicitObjectWrapper;\n\n /**\n * @see assignWith\n */\n assignWith(\n source1: TSource1,\n source2: TSource2,\n customizer: AssignCustomizer\n ): LoDashExplicitObjectWrapper;\n\n /**\n * @see assignWith\n */\n assignWith(\n source1: TSource1,\n source2: TSource2,\n source3: TSource3,\n customizer: AssignCustomizer\n ): LoDashExplicitObjectWrapper;\n\n /**\n * @see assignWith\n */\n assignWith(\n source1: TSource1,\n source2: TSource2,\n source3: TSource3,\n source4: TSource4,\n customizer: AssignCustomizer\n ): LoDashExplicitObjectWrapper;\n\n /**\n * @see _.assignWith\n */\n assignWith(): LoDashExplicitObjectWrapper;\n\n /**\n * @see _.assignWith\n */\n assignWith(...otherArgs: any[]): LoDashExplicitObjectWrapper;\n }\n\n //_.assignIn\n interface LoDashStatic {\n /**\n * This method is like `_.assign` except that it iterates over own and\n * inherited source properties.\n *\n * **Note:** This method mutates `object`.\n *\n * @static\n * @memberOf _\n * @alias extend\n * @category Object\n * @param {Object} object The destination object.\n * @param {...Object} [sources] The source objects.\n * @returns {Object} Returns `object`.\n * @example\n *\n * function Foo() {\n * this.b = 2;\n * }\n *\n * function Bar() {\n * this.d = 4;\n * }\n *\n * Foo.prototype.c = 3;\n * Bar.prototype.e = 5;\n *\n * _.assignIn({ 'a': 1 }, new Foo, new Bar);\n * // => { 'a': 1, 'b': 2, 'c': 3, 'd': 4, 'e': 5 }\n */\n assignIn(\n object: TObject,\n source: TSource\n ): TObject & TSource;\n\n /**\n * @see assignIn\n */\n assignIn(\n object: TObject,\n source1: TSource1,\n source2: TSource2\n ): TObject & TSource1 & TSource2;\n\n /**\n * @see assignIn\n */\n assignIn(\n object: TObject,\n source1: TSource1,\n source2: TSource2,\n source3: TSource3\n ): TObject & TSource1 & TSource2 & TSource3;\n\n /**\n * @see assignIn\n */\n assignIn(\n object: TObject,\n source1: TSource1,\n source2: TSource2,\n source3: TSource3,\n source4: TSource4\n ): TObject & TSource1 & TSource2 & TSource3 & TSource4;\n\n /**\n * @see _.assignIn\n */\n assignIn(object: TObject): TObject;\n\n /**\n * @see _.assignIn\n */\n assignIn(\n object: any,\n ...otherArgs: any[]\n ): TResult;\n }\n\n interface LoDashImplicitObjectWrapper {\n /**\n * @see _.assignIn\n */\n assignIn(\n source: TSource\n ): LoDashImplicitObjectWrapper;\n\n /**\n * @see assignIn\n */\n assignIn(\n source1: TSource1,\n source2: TSource2\n ): LoDashImplicitObjectWrapper;\n\n /**\n * @see assignIn\n */\n assignIn(\n source1: TSource1,\n source2: TSource2,\n source3: TSource3\n ): LoDashImplicitObjectWrapper;\n\n /**\n * @see assignIn\n */\n assignIn(\n source1: TSource1,\n source2: TSource2,\n source3: TSource3,\n source4: TSource4\n ): LoDashImplicitObjectWrapper;\n\n /**\n * @see _.assignIn\n */\n assignIn(): LoDashImplicitObjectWrapper;\n\n /**\n * @see _.assignIn\n */\n assignIn(...otherArgs: any[]): LoDashImplicitObjectWrapper;\n }\n\n interface LoDashExplicitObjectWrapper {\n /**\n * @see _.assignIn\n */\n assignIn(\n source: TSource\n ): LoDashExplicitObjectWrapper;\n\n /**\n * @see assignIn\n */\n assignIn(\n source1: TSource1,\n source2: TSource2\n ): LoDashExplicitObjectWrapper;\n\n /**\n * @see assignIn\n */\n assignIn(\n source1: TSource1,\n source2: TSource2,\n source3: TSource3\n ): LoDashExplicitObjectWrapper;\n\n /**\n * @see assignIn\n */\n assignIn(\n source1: TSource1,\n source2: TSource2,\n source3: TSource3,\n source4: TSource4\n ): LoDashExplicitObjectWrapper;\n\n /**\n * @see _.assignIn\n */\n assignIn(): LoDashExplicitObjectWrapper;\n\n /**\n * @see _.assignIn\n */\n assignIn(...otherArgs: any[]): LoDashExplicitObjectWrapper;\n }\n\n //_.assignInWith\n type AssignCustomizer = (objectValue: any, sourceValue: any, key?: string, object?: {}, source?: {}) => any;\n\n interface LoDashStatic {\n /**\n * This method is like `_.assignIn` except that it accepts `customizer` which\n * is invoked to produce the assigned values. If `customizer` returns `undefined`\n * assignment is handled by the method instead. The `customizer` is invoked\n * with five arguments: (objValue, srcValue, key, object, source).\n *\n * **Note:** This method mutates `object`.\n *\n * @static\n * @memberOf _\n * @alias extendWith\n * @category Object\n * @param {Object} object The destination object.\n * @param {...Object} sources The source objects.\n * @param {Function} [customizer] The function to customize assigned values.\n * @returns {Object} Returns `object`.\n * @example\n *\n * function customizer(objValue, srcValue) {\n * return _.isUndefined(objValue) ? srcValue : objValue;\n * }\n *\n * var defaults = _.partialRight(_.assignInWith, customizer);\n *\n * defaults({ 'a': 1 }, { 'b': 2 }, { 'a': 3 });\n * // => { 'a': 1, 'b': 2 }\n */\n assignInWith(\n object: TObject,\n source: TSource,\n customizer: AssignCustomizer\n ): TObject & TSource;\n\n /**\n * @see assignInWith\n */\n assignInWith(\n object: TObject,\n source1: TSource1,\n source2: TSource2,\n customizer: AssignCustomizer\n ): TObject & TSource1 & TSource2;\n\n /**\n * @see assignInWith\n */\n assignInWith(\n object: TObject,\n source1: TSource1,\n source2: TSource2,\n source3: TSource3,\n customizer: AssignCustomizer\n ): TObject & TSource1 & TSource2 & TSource3;\n\n /**\n * @see assignInWith\n */\n assignInWith(\n object: TObject,\n source1: TSource1,\n source2: TSource2,\n source3: TSource3,\n source4: TSource4,\n customizer: AssignCustomizer\n ): TObject & TSource1 & TSource2 & TSource3 & TSource4;\n\n /**\n * @see _.assignInWith\n */\n assignInWith(object: TObject): TObject;\n\n /**\n * @see _.assignInWith\n */\n assignInWith(\n object: any,\n ...otherArgs: any[]\n ): TResult;\n }\n\n interface LoDashImplicitObjectWrapper {\n /**\n * @see _.assignInWith\n */\n assignInWith(\n source: TSource,\n customizer: AssignCustomizer\n ): LoDashImplicitObjectWrapper;\n\n /**\n * @see assignInWith\n */\n assignInWith(\n source1: TSource1,\n source2: TSource2,\n customizer: AssignCustomizer\n ): LoDashImplicitObjectWrapper;\n\n /**\n * @see assignInWith\n */\n assignInWith(\n source1: TSource1,\n source2: TSource2,\n source3: TSource3,\n customizer: AssignCustomizer\n ): LoDashImplicitObjectWrapper;\n\n /**\n * @see assignInWith\n */\n assignInWith(\n source1: TSource1,\n source2: TSource2,\n source3: TSource3,\n source4: TSource4,\n customizer: AssignCustomizer\n ): LoDashImplicitObjectWrapper;\n\n /**\n * @see _.assignInWith\n */\n assignInWith(): LoDashImplicitObjectWrapper;\n\n /**\n * @see _.assignInWith\n */\n assignInWith(...otherArgs: any[]): LoDashImplicitObjectWrapper;\n }\n\n interface LoDashExplicitObjectWrapper {\n /**\n * @see _.assignInWith\n */\n assignInWith(\n source: TSource,\n customizer: AssignCustomizer\n ): LoDashExplicitObjectWrapper;\n\n /**\n * @see assignInWith\n */\n assignInWith(\n source1: TSource1,\n source2: TSource2,\n customizer: AssignCustomizer\n ): LoDashExplicitObjectWrapper;\n\n /**\n * @see assignInWith\n */\n assignInWith(\n source1: TSource1,\n source2: TSource2,\n source3: TSource3,\n customizer: AssignCustomizer\n ): LoDashExplicitObjectWrapper;\n\n /**\n * @see assignInWith\n */\n assignInWith(\n source1: TSource1,\n source2: TSource2,\n source3: TSource3,\n source4: TSource4,\n customizer: AssignCustomizer\n ): LoDashExplicitObjectWrapper;\n\n /**\n * @see _.assignInWith\n */\n assignInWith(): LoDashExplicitObjectWrapper;\n\n /**\n * @see _.assignInWith\n */\n assignInWith(...otherArgs: any[]): LoDashExplicitObjectWrapper;\n }\n\n //_.create\n interface LoDashStatic {\n /**\n * Creates an object that inherits from the given prototype object. If a properties object is provided its own\n * enumerable properties are assigned to the created object.\n *\n * @param prototype The object to inherit from.\n * @param properties The properties to assign to the object.\n * @return Returns the new object.\n */\n create(\n prototype: T,\n properties?: U\n ): T & U;\n }\n\n interface LoDashImplicitObjectWrapper {\n /**\n * @see _.create\n */\n create(properties?: U): LoDashImplicitObjectWrapper;\n }\n\n interface LoDashExplicitObjectWrapper {\n /**\n * @see _.create\n */\n create(properties?: U): LoDashExplicitObjectWrapper;\n }\n\n //_.defaults\n interface LoDashStatic {\n /**\n * Assigns own enumerable properties of source object(s) to the destination object for all destination\n * properties that resolve to undefined. Once a property is set, additional values of the same property are\n * ignored.\n *\n * Note: This method mutates object.\n *\n * @param object The destination object.\n * @param sources The source objects.\n * @return The destination object.\n */\n defaults(\n object: TObject,\n source: TSource\n ): TSource & TObject;\n\n /**\n * @see _.defaults\n */\n defaults(\n object: TObject,\n source1: TSource1,\n source2: TSource2\n ): TSource2 & TSource1 & TObject;\n\n /**\n * @see _.defaults\n */\n defaults(\n object: TObject,\n source1: TSource1,\n source2: TSource2,\n source3: TSource3\n ): TSource3 & TSource2 & TSource1 & TObject;\n\n /**\n * @see _.defaults\n */\n defaults(\n object: TObject,\n source1: TSource1,\n source2: TSource2,\n source3: TSource3,\n source4: TSource4\n ): TSource4 & TSource3 & TSource2 & TSource1 & TObject;\n\n /**\n * @see _.defaults\n */\n defaults(object: TObject): TObject;\n\n /**\n * @see _.defaults\n */\n defaults(\n object: any,\n ...sources: any[]\n ): TResult;\n }\n\n interface LoDashImplicitObjectWrapper {\n /**\n * @see _.defaults\n */\n defaults(\n source: TSource\n ): LoDashImplicitObjectWrapper;\n\n /**\n * @see _.defaults\n */\n defaults(\n source1: TSource1,\n source2: TSource2\n ): LoDashImplicitObjectWrapper;\n\n /**\n * @see _.defaults\n */\n defaults(\n source1: TSource1,\n source2: TSource2,\n source3: TSource3\n ): LoDashImplicitObjectWrapper;\n\n /**\n * @see _.defaults\n */\n defaults(\n source1: TSource1,\n source2: TSource2,\n source3: TSource3,\n source4: TSource4\n ): LoDashImplicitObjectWrapper;\n\n /**\n * @see _.defaults\n */\n defaults(): LoDashImplicitObjectWrapper;\n\n /**\n * @see _.defaults\n */\n defaults(...sources: any[]): LoDashImplicitObjectWrapper;\n }\n\n interface LoDashExplicitObjectWrapper {\n /**\n * @see _.defaults\n */\n defaults(\n source: TSource\n ): LoDashExplicitObjectWrapper;\n\n /**\n * @see _.defaults\n */\n defaults(\n source1: TSource1,\n source2: TSource2\n ): LoDashExplicitObjectWrapper;\n\n /**\n * @see _.defaults\n */\n defaults(\n source1: TSource1,\n source2: TSource2,\n source3: TSource3\n ): LoDashExplicitObjectWrapper;\n\n /**\n * @see _.defaults\n */\n defaults(\n source1: TSource1,\n source2: TSource2,\n source3: TSource3,\n source4: TSource4\n ): LoDashExplicitObjectWrapper;\n\n /**\n * @see _.defaults\n */\n defaults(): LoDashExplicitObjectWrapper;\n\n /**\n * @see _.defaults\n */\n defaults(...sources: any[]): LoDashExplicitObjectWrapper;\n }\n\n //_.defaultsDeep\n interface LoDashStatic {\n /**\n * This method is like _.defaults except that it recursively assigns default properties.\n * @param object The destination object.\n * @param sources The source objects.\n * @return Returns object.\n **/\n defaultsDeep(\n object: T,\n ...sources: any[]): TResult;\n }\n\n interface LoDashImplicitObjectWrapper {\n /**\n * @see _.defaultsDeep\n **/\n defaultsDeep(...sources: any[]): LoDashImplicitObjectWrapper;\n }\n\n // _.extend\n interface LoDashStatic {\n /**\n * @see _.assignIn\n */\n extend(\n object: TObject,\n source: TSource\n ): TObject & TSource;\n\n /**\n * @see _.assignIn\n */\n extend(\n object: TObject,\n source1: TSource1,\n source2: TSource2\n ): TObject & TSource1 & TSource2;\n\n /**\n * @see _.assignIn\n */\n extend(\n object: TObject,\n source1: TSource1,\n source2: TSource2,\n source3: TSource3\n ): TObject & TSource1 & TSource2 & TSource3;\n\n /**\n * @see _.assignIn\n */\n extend(\n object: TObject,\n source1: TSource1,\n source2: TSource2,\n source3: TSource3,\n source4: TSource4\n ): TObject & TSource1 & TSource2 & TSource3 & TSource4;\n\n /**\n * @see _.assignIn\n */\n extend(object: TObject): TObject;\n\n /**\n * @see _.assignIn\n */\n extend(\n object: any,\n ...otherArgs: any[]\n ): TResult;\n }\n\n interface LoDashImplicitObjectWrapper {\n /**\n * @see _.assignIn\n */\n extend(\n source: TSource\n ): LoDashImplicitObjectWrapper;\n\n /**\n * @see _.assignIn\n */\n extend(\n source1: TSource1,\n source2: TSource2\n ): LoDashImplicitObjectWrapper;\n\n /**\n * @see _.assignIn\n */\n extend(\n source1: TSource1,\n source2: TSource2,\n source3: TSource3\n ): LoDashImplicitObjectWrapper;\n\n /**\n * @see _.assignIn\n */\n extend(\n source1: TSource1,\n source2: TSource2,\n source3: TSource3,\n source4: TSource4\n ): LoDashImplicitObjectWrapper;\n\n /**\n * @see _.assignIn\n */\n extend(): LoDashImplicitObjectWrapper;\n\n /**\n * @see _.assignIn\n */\n extend(...otherArgs: any[]): LoDashImplicitObjectWrapper;\n }\n\n interface LoDashExplicitObjectWrapper {\n /**\n * @see _.assignIn\n */\n extend(\n source: TSource\n ): LoDashExplicitObjectWrapper;\n\n /**\n * @see _.assignIn\n */\n extend(\n source1: TSource1,\n source2: TSource2\n ): LoDashExplicitObjectWrapper;\n\n /**\n * @see _.assignIn\n */\n extend(\n source1: TSource1,\n source2: TSource2,\n source3: TSource3\n ): LoDashExplicitObjectWrapper;\n\n /**\n * @see _.assignIn\n */\n extend(\n source1: TSource1,\n source2: TSource2,\n source3: TSource3,\n source4: TSource4\n ): LoDashExplicitObjectWrapper;\n\n /**\n * @see _.assignIn\n */\n extend(): LoDashExplicitObjectWrapper;\n\n /**\n * @see _.assignIn\n */\n extend(...otherArgs: any[]): LoDashExplicitObjectWrapper;\n }\n\n interface LoDashStatic {\n /**\n * @see _.assignInWith\n */\n extendWith(\n object: TObject,\n source: TSource,\n customizer: AssignCustomizer\n ): TObject & TSource;\n\n /**\n * @see _.assignInWith\n */\n extendWith(\n object: TObject,\n source1: TSource1,\n source2: TSource2,\n customizer: AssignCustomizer\n ): TObject & TSource1 & TSource2;\n\n /**\n * @see _.assignInWith\n */\n extendWith(\n object: TObject,\n source1: TSource1,\n source2: TSource2,\n source3: TSource3,\n customizer: AssignCustomizer\n ): TObject & TSource1 & TSource2 & TSource3;\n\n /**\n * @see _.assignInWith\n */\n extendWith(\n object: TObject,\n source1: TSource1,\n source2: TSource2,\n source3: TSource3,\n source4: TSource4,\n customizer: AssignCustomizer\n ): TObject & TSource1 & TSource2 & TSource3 & TSource4;\n\n /**\n * @see _.assignInWith\n */\n extendWith(object: TObject): TObject;\n\n /**\n * @see _.assignInWith\n */\n extendWith(\n object: any,\n ...otherArgs: any[]\n ): TResult;\n }\n\n interface LoDashImplicitObjectWrapper {\n /**\n * @see _.assignInWith\n */\n extendWith(\n source: TSource,\n customizer: AssignCustomizer\n ): LoDashImplicitObjectWrapper;\n\n /**\n * @see _.assignInWith\n */\n extendWith(\n source1: TSource1,\n source2: TSource2,\n customizer: AssignCustomizer\n ): LoDashImplicitObjectWrapper;\n\n /**\n * @see _.assignInWith\n */\n extendWith(\n source1: TSource1,\n source2: TSource2,\n source3: TSource3,\n customizer: AssignCustomizer\n ): LoDashImplicitObjectWrapper;\n\n /**\n * @see _.assignInWith\n */\n extendWith(\n source1: TSource1,\n source2: TSource2,\n source3: TSource3,\n source4: TSource4,\n customizer: AssignCustomizer\n ): LoDashImplicitObjectWrapper;\n\n /**\n * @see _.assignInWith\n */\n extendWith(): LoDashImplicitObjectWrapper;\n\n /**\n * @see _.assignInWith\n */\n extendWith(...otherArgs: any[]): LoDashImplicitObjectWrapper;\n }\n\n interface LoDashExplicitObjectWrapper {\n /**\n * @see _.assignInWith\n */\n extendWith(\n source: TSource,\n customizer: AssignCustomizer\n ): LoDashExplicitObjectWrapper;\n\n /**\n * @see _.assignInWith\n */\n extendWith(\n source1: TSource1,\n source2: TSource2,\n customizer: AssignCustomizer\n ): LoDashExplicitObjectWrapper;\n\n /**\n * @see _.assignInWith\n */\n extendWith(\n source1: TSource1,\n source2: TSource2,\n source3: TSource3,\n customizer: AssignCustomizer\n ): LoDashExplicitObjectWrapper;\n\n /**\n * @see _.assignInWith\n */\n extendWith(\n source1: TSource1,\n source2: TSource2,\n source3: TSource3,\n source4: TSource4,\n customizer: AssignCustomizer\n ): LoDashExplicitObjectWrapper;\n\n /**\n * @see _.assignInWith\n */\n extendWith(): LoDashExplicitObjectWrapper;\n\n /**\n * @see _.assignInWith\n */\n extendWith(...otherArgs: any[]): LoDashExplicitObjectWrapper;\n }\n\n //_.findKey\n interface LoDashStatic {\n /**\n * This method is like _.find except that it returns the key of the first element predicate returns truthy for\n * instead of the element itself.\n *\n * If a property name is provided for predicate the created _.property style callback returns the property\n * value of the given element.\n *\n * If a value is also provided for thisArg the created _.matchesProperty style callback returns true for\n * elements that have a matching property value, else false.\n *\n * If an object is provided for predicate the created _.matches style callback returns true for elements that\n * have the properties of the given object, else false.\n *\n * @param object The object to search.\n * @param predicate The function invoked per iteration.\n * @param thisArg The this binding of predicate.\n * @return Returns the key of the matched element, else undefined.\n */\n findKey(\n object: TObject,\n predicate?: DictionaryIterator\n ): string | undefined;\n\n /**\n * @see _.findKey\n */\n findKey(\n object: TObject,\n predicate?: ObjectIterator\n ): string | undefined;\n\n /**\n * @see _.findKey\n */\n findKey(\n object: TObject,\n predicate?: string\n ): string | undefined;\n\n /**\n * @see _.findKey\n */\n findKey, TObject>(\n object: TObject,\n predicate?: TWhere\n ): string | undefined;\n }\n\n interface LoDashImplicitObjectWrapperBase {\n /**\n * @see _.findKey\n */\n findKey(\n predicate?: DictionaryIterator\n ): string | undefined;\n\n /**\n * @see _.findKey\n */\n findKey(\n predicate?: ObjectIterator\n ): string | undefined;\n\n /**\n * @see _.findKey\n */\n findKey(\n predicate?: string\n ): string | undefined;\n\n /**\n * @see _.findKey\n */\n findKey>(\n predicate?: TWhere\n ): string | undefined;\n }\n\n interface LoDashExplicitObjectWrapperBase {\n /**\n * @see _.findKey\n */\n findKey(\n predicate?: DictionaryIterator\n ): LoDashExplicitWrapper;\n\n /**\n * @see _.findKey\n */\n findKey(\n predicate?: ObjectIterator\n ): LoDashExplicitWrapper;\n\n /**\n * @see _.findKey\n */\n findKey(\n predicate?: string\n ): LoDashExplicitWrapper;\n\n /**\n * @see _.findKey\n */\n findKey>(\n predicate?: TWhere\n ): LoDashExplicitWrapper;\n }\n\n //_.findLastKey\n interface LoDashStatic {\n /**\n * This method is like _.findKey except that it iterates over elements of a collection in the opposite order.\n *\n * If a property name is provided for predicate the created _.property style callback returns the property\n * value of the given element.\n *\n * If a value is also provided for thisArg the created _.matchesProperty style callback returns true for\n * elements that have a matching property value, else false.\n *\n * If an object is provided for predicate the created _.matches style callback returns true for elements that\n * have the properties of the given object, else false.\n *\n * @param object The object to search.\n * @param predicate The function invoked per iteration.\n * @param thisArg The this binding of predicate.\n * @return Returns the key of the matched element, else undefined.\n */\n findLastKey(\n object: TObject,\n predicate?: DictionaryIterator\n ): string | undefined;\n\n /**\n * @see _.findLastKey\n */\n findLastKey(\n object: TObject,\n predicate?: ObjectIterator\n ): string | undefined;\n\n /**\n * @see _.findLastKey\n */\n findLastKey(\n object: TObject,\n predicate?: string\n ): string;\n\n /**\n * @see _.findLastKey\n */\n findLastKey, TObject>(\n object: TObject,\n predicate?: TWhere\n ): string | undefined;\n }\n\n interface LoDashImplicitObjectWrapperBase {\n /**\n * @see _.findLastKey\n */\n findLastKey(\n predicate?: DictionaryIterator\n ): string;\n\n /**\n * @see _.findLastKey\n */\n findLastKey(\n predicate?: ObjectIterator\n ): string | undefined;\n\n /**\n * @see _.findLastKey\n */\n findLastKey(\n predicate?: string\n ): string | undefined;\n\n /**\n * @see _.findLastKey\n */\n findLastKey>(\n predicate?: TWhere\n ): string | undefined;\n }\n\n interface LoDashExplicitObjectWrapperBase {\n /**\n * @see _.findLastKey\n */\n findLastKey(\n predicate?: DictionaryIterator\n ): LoDashExplicitWrapper;\n\n /**\n * @see _.findLastKey\n */\n findLastKey(\n predicate?: ObjectIterator\n ): LoDashExplicitWrapper;\n\n /**\n * @see _.findLastKey\n */\n findLastKey(\n predicate?: string\n ): LoDashExplicitWrapper;\n\n /**\n * @see _.findLastKey\n */\n findLastKey>(\n predicate?: TWhere\n ): LoDashExplicitWrapper;\n }\n\n //_.forIn\n interface LoDashStatic {\n /**\n * Iterates over own and inherited enumerable properties of an object invoking iteratee for each property. The\n * iteratee is bound to thisArg and invoked with three arguments: (value, key, object). Iteratee functions may\n * exit iteration early by explicitly returning false.\n *\n * @param object The object to iterate over.\n * @param iteratee The function invoked per iteration.\n * @param thisArg The this binding of iteratee.\n * @return Returns object.\n */\n forIn(\n object: Dictionary,\n iteratee?: DictionaryIterator\n ): Dictionary;\n\n /**\n * @see _.forIn\n */\n forIn(\n object: Dictionary | null | undefined,\n iteratee?: DictionaryIterator\n ): Dictionary | null | undefined;\n\n /**\n * @see _.forIn\n */\n forIn(\n object: T,\n iteratee?: ObjectIterator\n ): T;\n }\n\n interface LoDashImplicitObjectWrapperBase {\n /**\n * @see _.forIn\n */\n forIn(\n iteratee?: DictionaryIterator\n ): TWrapper;\n }\n\n interface LoDashExplicitObjectWrapperBase {\n /**\n * @see _.forIn\n */\n forIn(\n iteratee?: DictionaryIterator\n ): TWrapper;\n }\n\n //_.forInRight\n interface LoDashStatic {\n /**\n * This method is like _.forIn except that it iterates over properties of object in the opposite order.\n *\n * @param object The object to iterate over.\n * @param iteratee The function invoked per iteration.\n * @param thisArg The this binding of iteratee.\n * @return Returns object.\n */\n forInRight(\n object: Dictionary,\n iteratee?: DictionaryIterator\n ): Dictionary;\n\n /**\n * @see _.forInRight\n */\n forInRight(\n object: Dictionary | null | undefined,\n iteratee?: DictionaryIterator\n ): Dictionary | null | undefined;\n\n /**\n * @see _.forInRight\n */\n forInRight(\n object: T,\n iteratee?: ObjectIterator\n ): T;\n }\n\n interface LoDashImplicitObjectWrapperBase {\n /**\n * @see _.forInRight\n */\n forInRight(\n iteratee?: DictionaryIterator\n ): TWrapper;\n }\n\n interface LoDashExplicitObjectWrapperBase {\n /**\n * @see _.forInRight\n */\n forInRight(\n iteratee?: DictionaryIterator\n ): TWrapper;\n }\n\n //_.forOwn\n interface LoDashStatic {\n /**\n * Iterates over own enumerable properties of an object invoking iteratee for each property. The iteratee is\n * bound to thisArg and invoked with three arguments: (value, key, object). Iteratee functions may exit\n * iteration early by explicitly returning false.\n *\n * @param object The object to iterate over.\n * @param iteratee The function invoked per iteration.\n * @param thisArg The this binding of iteratee.\n * @return Returns object.\n */\n forOwn(\n object: Dictionary,\n iteratee?: DictionaryIterator\n ): Dictionary;\n\n /**\n * @see _.forOwn\n */\n forOwn(\n object: Dictionary | null | undefined,\n iteratee?: DictionaryIterator\n ): Dictionary | null | undefined;\n\n /**\n * @see _.forOwn\n */\n forOwn(\n object: T,\n iteratee?: ObjectIterator\n ): T;\n }\n\n interface LoDashImplicitObjectWrapperBase {\n /**\n * @see _.forOwn\n */\n forOwn(\n iteratee?: DictionaryIterator\n ): TWrapper;\n }\n\n interface LoDashExplicitObjectWrapperBase {\n /**\n * @see _.forOwn\n */\n forOwn(\n iteratee?: DictionaryIterator\n ): TWrapper;\n }\n\n //_.forOwnRight\n interface LoDashStatic {\n /**\n * This method is like _.forOwn except that it iterates over properties of object in the opposite order.\n *\n * @param object The object to iterate over.\n * @param iteratee The function invoked per iteration.\n * @param thisArg The this binding of iteratee.\n * @return Returns object.\n */\n forOwnRight(\n object: Dictionary,\n iteratee?: DictionaryIterator\n ): Dictionary;\n\n /**\n * @see _.forOwnRight\n */\n forOwnRight(\n object: Dictionary | null | undefined,\n iteratee?: DictionaryIterator\n ): Dictionary | null | undefined;\n\n /**\n * @see _.forOwnRight\n */\n forOwnRight(\n object: T,\n iteratee?: ObjectIterator\n ): T;\n }\n\n interface LoDashImplicitObjectWrapperBase {\n /**\n * @see _.forOwnRight\n */\n forOwnRight(\n iteratee?: DictionaryIterator\n ): TWrapper;\n }\n\n interface LoDashExplicitObjectWrapperBase {\n /**\n * @see _.forOwnRight\n */\n forOwnRight(\n iteratee?: DictionaryIterator\n ): TWrapper;\n }\n\n //_.functions\n interface LoDashStatic {\n /**\n * Creates an array of function property names from own enumerable properties\n * of `object`.\n *\n * @static\n * @memberOf _\n * @category Object\n * @param {Object} object The object to inspect.\n * @returns {Array} Returns the new array of property names.\n * @example\n *\n * function Foo() {\n * this.a = _.constant('a');\n * this.b = _.constant('b');\n * }\n *\n * Foo.prototype.c = _.constant('c');\n *\n * _.functions(new Foo);\n * // => ['a', 'b']\n */\n functions(object: any): string[];\n }\n\n interface LoDashImplicitObjectWrapper {\n /**\n * @see _.functions\n */\n functions(): _.LoDashImplicitArrayWrapper;\n }\n\n interface LoDashExplicitObjectWrapper {\n /**\n * @see _.functions\n */\n functions(): _.LoDashExplicitArrayWrapper;\n }\n\n //_.functionsIn\n interface LoDashStatic {\n /**\n * Creates an array of function property names from own and inherited\n * enumerable properties of `object`.\n *\n * @static\n * @memberOf _\n * @category Object\n * @param {Object} object The object to inspect.\n * @returns {Array} Returns the new array of property names.\n * @example\n *\n * function Foo() {\n * this.a = _.constant('a');\n * this.b = _.constant('b');\n * }\n *\n * Foo.prototype.c = _.constant('c');\n *\n * _.functionsIn(new Foo);\n * // => ['a', 'b', 'c']\n */\n functionsIn(object: any): string[];\n }\n\n interface LoDashImplicitObjectWrapper {\n /**\n * @see _.functionsIn\n */\n functionsIn(): _.LoDashImplicitArrayWrapper;\n }\n\n interface LoDashExplicitObjectWrapper {\n /**\n * @see _.functionsIn\n */\n functionsIn(): _.LoDashExplicitArrayWrapper;\n }\n\n //_.get\n interface LoDashStatic {\n /**\n * Gets the property value at path of object. If the resolved value is undefined the defaultValue is used\n * in its place.\n *\n * @param object The object to query.\n * @param path The path of the property to get.\n * @param defaultValue The value returned if the resolved value is undefined.\n * @return Returns the resolved value.\n */\n get(\n object: TObject,\n path: Many,\n defaultValue?: TResult\n ): TResult;\n\n /**\n * @see _.get\n */\n get(\n object: any,\n path: Many,\n defaultValue?: TResult\n ): TResult;\n }\n\n interface LoDashImplicitWrapper {\n /**\n * @see _.get\n */\n get(\n path: Many,\n defaultValue?: TResult\n ): TResult;\n }\n\n interface LoDashImplicitArrayWrapperBase {\n /**\n * @see _.get\n */\n get(\n path: Many,\n defaultValue?: TResult\n ): TResult;\n }\n\n interface LoDashImplicitObjectWrapperBase {\n /**\n * @see _.get\n */\n get(\n path: Many,\n defaultValue?: TResult\n ): TResult;\n }\n\n interface LoDashExplicitWrapper {\n /**\n * @see _.get\n */\n get(\n path: Many,\n defaultValue?: any\n ): TResultWrapper;\n }\n\n interface LoDashExplicitArrayWrapperBase {\n /**\n * @see _.get\n */\n get(\n path: Many,\n defaultValue?: any\n ): TResultWrapper;\n }\n\n interface LoDashExplicitObjectWrapperBase {\n /**\n * @see _.get\n */\n get(\n path: Many,\n defaultValue?: any\n ): TResultWrapper;\n }\n\n //_.has\n interface LoDashStatic {\n /**\n * Checks if `path` is a direct property of `object`.\n *\n * @static\n * @memberOf _\n * @category Object\n * @param {Object} object The object to query.\n * @param {Array|string} path The path to check.\n * @returns {boolean} Returns `true` if `path` exists, else `false`.\n * @example\n *\n * var object = { 'a': { 'b': { 'c': 3 } } };\n * var other = _.create({ 'a': _.create({ 'b': _.create({ 'c': 3 }) }) });\n *\n * _.has(object, 'a');\n * // => true\n *\n * _.has(object, 'a.b.c');\n * // => true\n *\n * _.has(object, ['a', 'b', 'c']);\n * // => true\n *\n * _.has(other, 'a');\n * // => false\n */\n has(\n object: T,\n path: Many\n ): boolean;\n }\n\n interface LoDashImplicitObjectWrapperBase {\n /**\n * @see _.has\n */\n has(path: Many): boolean;\n }\n\n interface LoDashExplicitObjectWrapperBase {\n /**\n * @see _.has\n */\n has(path: Many): LoDashExplicitWrapper;\n }\n\n //_.hasIn\n interface LoDashStatic {\n /**\n * Checks if `path` is a direct or inherited property of `object`.\n *\n * @static\n * @memberOf _\n * @category Object\n * @param {Object} object The object to query.\n * @param {Array|string} path The path to check.\n * @returns {boolean} Returns `true` if `path` exists, else `false`.\n * @example\n *\n * var object = _.create({ 'a': _.create({ 'b': _.create({ 'c': 3 }) }) });\n *\n * _.hasIn(object, 'a');\n * // => true\n *\n * _.hasIn(object, 'a.b.c');\n * // => true\n *\n * _.hasIn(object, ['a', 'b', 'c']);\n * // => true\n *\n * _.hasIn(object, 'b');\n * // => false\n */\n hasIn(\n object: T,\n path: Many\n ): boolean;\n }\n\n interface LoDashImplicitObjectWrapperBase {\n /**\n * @see _.hasIn\n */\n hasIn(path: Many): boolean;\n }\n\n interface LoDashExplicitObjectWrapperBase {\n /**\n * @see _.hasIn\n */\n hasIn(path: Many): LoDashExplicitWrapper;\n }\n\n //_.invert\n interface LoDashStatic {\n /**\n * Creates an object composed of the inverted keys and values of object. If object contains duplicate values,\n * subsequent values overwrite property assignments of previous values unless multiValue is true.\n *\n * @param object The object to invert.\n * @param multiValue Allow multiple values per key.\n * @return Returns the new inverted object.\n */\n invert(\n object: T,\n multiValue?: boolean\n ): TResult;\n\n /**\n * @see _.invert\n */\n invert(\n object: Object,\n multiValue?: boolean\n ): TResult;\n }\n\n interface LoDashImplicitObjectWrapper {\n /**\n * @see _.invert\n */\n invert(multiValue?: boolean): LoDashImplicitObjectWrapper;\n }\n\n interface LoDashExplicitObjectWrapper {\n /**\n * @see _.invert\n */\n invert(multiValue?: boolean): LoDashExplicitObjectWrapper;\n }\n\n //_.inverBy\n type InvertByIterator = (value: T) => any;\n\n interface LoDashStatic {\n /**\n * This method is like _.invert except that the inverted object is generated from the results of running each\n * element of object through iteratee. The corresponding inverted value of each inverted key is an array of\n * keys responsible for generating the inverted value. The iteratee is invoked with one argument: (value).\n *\n * @param object The object to invert.\n * @param interatee The iteratee invoked per element.\n * @return Returns the new inverted object.\n */\n invertBy(\n object: Object,\n interatee?: InvertByIterator|string\n ): Dictionary;\n\n /**\n * @see _.invertBy\n */\n invertBy(\n object: _.Dictionary|_.NumericDictionary,\n interatee?: InvertByIterator|string\n ): Dictionary;\n\n /**\n * @see _.invertBy\n */\n invertBy(\n object: Object,\n interatee?: W\n ): Dictionary;\n\n /**\n * @see _.invertBy\n */\n invertBy(\n object: _.Dictionary,\n interatee?: W\n ): Dictionary;\n }\n\n interface LoDashImplicitWrapper {\n /**\n * @see _.invertBy\n */\n invertBy(\n interatee?: InvertByIterator\n ): LoDashImplicitObjectWrapper>;\n }\n\n interface LoDashImplicitArrayWrapper {\n /**\n * @see _.invertBy\n */\n invertBy(\n interatee?: InvertByIterator|string\n ): LoDashImplicitObjectWrapper>;\n\n /**\n * @see _.invertBy\n */\n invertBy(\n interatee?: W\n ): LoDashImplicitObjectWrapper>;\n }\n\n interface LoDashImplicitObjectWrapper {\n /**\n * @see _.invertBy\n */\n invertBy(\n interatee?: InvertByIterator|string\n ): LoDashImplicitObjectWrapper>;\n\n /**\n * @see _.invertBy\n */\n invertBy(\n interatee?: W\n ): LoDashImplicitObjectWrapper>;\n }\n\n interface LoDashExplicitWrapper {\n /**\n * @see _.invertBy\n */\n invertBy(\n interatee?: InvertByIterator\n ): LoDashExplicitObjectWrapper>;\n }\n\n interface LoDashExplicitArrayWrapper {\n /**\n * @see _.invertBy\n */\n invertBy(\n interatee?: InvertByIterator|string\n ): LoDashExplicitObjectWrapper>;\n\n /**\n * @see _.invertBy\n */\n invertBy(\n interatee?: W\n ): LoDashExplicitObjectWrapper>;\n }\n\n interface LoDashExplicitObjectWrapper {\n /**\n * @see _.invertBy\n */\n invertBy(\n interatee?: InvertByIterator|string\n ): LoDashExplicitObjectWrapper>;\n\n /**\n * @see _.invertBy\n */\n invertBy(\n interatee?: W\n ): LoDashExplicitObjectWrapper>;\n }\n\n //_.keys\n interface LoDashStatic {\n /**\n * Creates an array of the own enumerable property names of object.\n *\n * Note: Non-object values are coerced to objects. See the ES spec for more details.\n *\n * @param object The object to query.\n * @return Returns the array of property names.\n */\n keys(object?: any): string[];\n }\n\n interface LoDashImplicitObjectWrapperBase {\n /**\n * @see _.keys\n */\n keys(): LoDashImplicitArrayWrapper;\n }\n\n interface LoDashExplicitObjectWrapperBase {\n /**\n * @see _.keys\n */\n keys(): LoDashExplicitArrayWrapper;\n }\n\n //_.keysIn\n interface LoDashStatic {\n /**\n * Creates an array of the own and inherited enumerable property names of object.\n *\n * Note: Non-object values are coerced to objects.\n *\n * @param object The object to query.\n * @return An array of property names.\n */\n keysIn(object?: any): string[];\n }\n\n interface LoDashImplicitObjectWrapperBase {\n /**\n * @see _.keysIn\n */\n keysIn(): LoDashImplicitArrayWrapper;\n }\n\n interface LoDashExplicitObjectWrapperBase {\n /**\n * @see _.keysIn\n */\n keysIn(): LoDashExplicitArrayWrapper;\n }\n\n //_.mapKeys\n interface LoDashStatic {\n /**\n * The opposite of _.mapValues; this method creates an object with the same values as object and keys generated\n * by running each own enumerable property of object through iteratee.\n *\n * @param object The object to iterate over.\n * @param iteratee The function invoked per iteration.\n * @param thisArg The this binding of iteratee.\n * @return Returns the new mapped object.\n */\n mapKeys(\n object: List | null | undefined,\n iteratee?: ListIterator\n ): Dictionary;\n\n /**\n * @see _.mapKeys\n */\n mapKeys(\n object: Dictionary | null | undefined,\n iteratee?: DictionaryIterator\n ): Dictionary;\n\n /**\n * @see _.mapKeys\n */\n mapKeys(\n object: List|Dictionary | null | undefined,\n iteratee?: TObject\n ): Dictionary;\n\n /**\n * @see _.mapKeys\n */\n mapKeys(\n object: List|Dictionary | null | undefined,\n iteratee?: string\n ): Dictionary;\n }\n\n interface LoDashImplicitArrayWrapperBase {\n /**\n * @see _.mapKeys\n */\n mapKeys(\n iteratee?: ListIterator\n ): LoDashImplicitObjectWrapper>;\n\n /**\n * @see _.mapKeys\n */\n mapKeys(\n iteratee?: TObject\n ): LoDashImplicitObjectWrapper>;\n\n /**\n * @see _.mapKeys\n */\n mapKeys(\n iteratee?: string\n ): LoDashImplicitObjectWrapper>;\n }\n\n interface LoDashImplicitObjectWrapperBase {\n /**\n * @see _.mapKeys\n */\n mapKeys(\n iteratee?: ListIterator|DictionaryIterator\n ): LoDashImplicitObjectWrapper>;\n\n /**\n * @see _.mapKeys\n */\n mapKeys(\n iteratee?: TObject\n ): LoDashImplicitObjectWrapper>;\n\n /**\n * @see _.mapKeys\n */\n mapKeys(\n iteratee?: string\n ): LoDashImplicitObjectWrapper>;\n }\n\n interface LoDashExplicitArrayWrapperBase {\n /**\n * @see _.mapKeys\n */\n mapKeys(\n iteratee?: ListIterator\n ): LoDashExplicitObjectWrapper>;\n\n /**\n * @see _.mapKeys\n */\n mapKeys(\n iteratee?: TObject\n ): LoDashExplicitObjectWrapper>;\n\n /**\n * @see _.mapKeys\n */\n mapKeys(\n iteratee?: string\n ): LoDashExplicitObjectWrapper>;\n }\n\n interface LoDashExplicitObjectWrapperBase {\n /**\n * @see _.mapKeys\n */\n mapKeys(\n iteratee?: ListIterator|DictionaryIterator\n ): LoDashExplicitObjectWrapper>;\n\n /**\n * @see _.mapKeys\n */\n mapKeys(\n iteratee?: TObject\n ): LoDashExplicitObjectWrapper>;\n\n /**\n * @see _.mapKeys\n */\n mapKeys(\n iteratee?: string\n ): LoDashExplicitObjectWrapper>;\n }\n\n //_.mapValues\n interface LoDashStatic {\n /**\n * Creates an object with the same keys as object and values generated by running each own\n * enumerable property of object through iteratee. The iteratee function is bound to thisArg\n * and invoked with three arguments: (value, key, object).\n *\n * If a property name is provided iteratee the created \"_.property\" style callback returns\n * the property value of the given element.\n *\n * If a value is also provided for thisArg the creted \"_.matchesProperty\" style callback returns\n * true for elements that have a matching property value, else false;.\n *\n * If an object is provided for iteratee the created \"_.matches\" style callback returns true\n * for elements that have the properties of the given object, else false.\n *\n * @param {Object} object The object to iterate over.\n * @param {Function|Object|string} [iteratee=_.identity] The function invoked per iteration.\n * @param {Object} [thisArg] The `this` binding of `iteratee`.\n * @return {Object} Returns the new mapped object.\n */\n mapValues(obj: Dictionary | null | undefined, callback: ObjectIterator): Dictionary;\n mapValues(obj: Dictionary | null | undefined, where: Dictionary): Dictionary;\n mapValues(obj: T | null | undefined, pluck: string): TMapped;\n mapValues(obj: T | null | undefined, callback: ObjectIterator): T;\n }\n\n interface LoDashImplicitObjectWrapperBase {\n /**\n * @see _.mapValues\n * TValue is the type of the property values of T.\n * TResult is the type output by the ObjectIterator function\n */\n mapValues(callback: ObjectIterator): LoDashImplicitObjectWrapper>;\n\n /**\n * @see _.mapValues\n * TResult is the type of the property specified by pluck.\n * T should be a Dictionary>\n */\n mapValues(pluck: string): LoDashImplicitObjectWrapper>;\n\n /**\n * @see _.mapValues\n * TResult is the type of the properties of each object in the values of T\n * T should be a Dictionary>\n */\n mapValues(where: Dictionary): LoDashImplicitArrayWrapper;\n }\n\n interface LoDashExplicitObjectWrapperBase {\n /**\n * @see _.mapValues\n * TValue is the type of the property values of T.\n * TResult is the type output by the ObjectIterator function\n */\n mapValues(callback: ObjectIterator): LoDashExplicitObjectWrapper>;\n\n /**\n * @see _.mapValues\n * TResult is the type of the property specified by pluck.\n * T should be a Dictionary>\n */\n mapValues(pluck: string): LoDashExplicitObjectWrapper>;\n\n /**\n * @see _.mapValues\n * TResult is the type of the properties of each object in the values of T\n * T should be a Dictionary>\n */\n mapValues(where: Dictionary): LoDashExplicitObjectWrapper;\n }\n\n //_.merge\n interface LoDashStatic {\n /**\n * Recursively merges own and inherited enumerable properties of source\n * objects into the destination object, skipping source properties that resolve\n * to `undefined`. Array and plain object properties are merged recursively.\n * Other objects and value types are overridden by assignment. Source objects\n * are applied from left to right. Subsequent sources overwrite property\n * assignments of previous sources.\n *\n * **Note:** This method mutates `object`.\n *\n * @static\n * @memberOf _\n * @category Object\n * @param {Object} object The destination object.\n * @param {...Object} [sources] The source objects.\n * @returns {Object} Returns `object`.\n * @example\n *\n * var users = {\n * 'data': [{ 'user': 'barney' }, { 'user': 'fred' }]\n * };\n *\n * var ages = {\n * 'data': [{ 'age': 36 }, { 'age': 40 }]\n * };\n *\n * _.merge(users, ages);\n * // => { 'data': [{ 'user': 'barney', 'age': 36 }, { 'user': 'fred', 'age': 40 }] }\n */\n merge(\n object: TObject,\n source: TSource\n ): TObject & TSource;\n\n /**\n * @see _.merge\n */\n merge(\n object: TObject,\n source1: TSource1,\n source2: TSource2\n ): TObject & TSource1 & TSource2;\n\n /**\n * @see _.merge\n */\n merge(\n object: TObject,\n source1: TSource1,\n source2: TSource2,\n source3: TSource3\n ): TObject & TSource1 & TSource2 & TSource3;\n\n /**\n * @see _.merge\n */\n merge(\n object: TObject,\n source1: TSource1,\n source2: TSource2,\n source3: TSource3,\n source4: TSource4\n ): TObject & TSource1 & TSource2 & TSource3 & TSource4;\n\n /**\n * @see _.merge\n */\n merge(\n object: any,\n ...otherArgs: any[]\n ): TResult;\n }\n\n interface LoDashImplicitObjectWrapper {\n /**\n * @see _.merge\n */\n merge(\n source: TSource\n ): LoDashImplicitObjectWrapper;\n\n /**\n * @see _.merge\n */\n merge(\n source1: TSource1,\n source2: TSource2\n ): LoDashImplicitObjectWrapper;\n\n /**\n * @see _.merge\n */\n merge(\n source1: TSource1,\n source2: TSource2,\n source3: TSource3\n ): LoDashImplicitObjectWrapper;\n\n /**\n * @see _.merge\n */\n merge(\n source1: TSource1,\n source2: TSource2,\n source3: TSource3,\n source4: TSource4\n ): LoDashImplicitObjectWrapper;\n\n /**\n * @see _.merge\n */\n merge(\n ...otherArgs: any[]\n ): LoDashImplicitObjectWrapper;\n }\n\n interface LoDashExplicitObjectWrapper {\n /**\n * @see _.merge\n */\n merge(\n source: TSource\n ): LoDashExplicitObjectWrapper;\n\n /**\n * @see _.merge\n */\n merge(\n source1: TSource1,\n source2: TSource2\n ): LoDashExplicitObjectWrapper;\n\n /**\n * @see _.merge\n */\n merge(\n source1: TSource1,\n source2: TSource2,\n source3: TSource3\n ): LoDashExplicitObjectWrapper;\n\n /**\n * @see _.merge\n */\n merge(\n ): LoDashExplicitObjectWrapper;\n\n /**\n * @see _.merge\n */\n merge(\n ...otherArgs: any[]\n ): LoDashExplicitObjectWrapper;\n }\n\n //_.mergeWith\n type MergeWithCustomizer = (value: any, srcValue: any, key?: string, object?: Object, source?: Object) => any;\n\n interface LoDashStatic {\n /**\n * This method is like `_.merge` except that it accepts `customizer` which\n * is invoked to produce the merged values of the destination and source\n * properties. If `customizer` returns `undefined` merging is handled by the\n * method instead. The `customizer` is invoked with seven arguments:\n * (objValue, srcValue, key, object, source, stack).\n *\n * @static\n * @memberOf _\n * @category Object\n * @param {Object} object The destination object.\n * @param {...Object} sources The source objects.\n * @param {Function} customizer The function to customize assigned values.\n * @returns {Object} Returns `object`.\n * @example\n *\n * function customizer(objValue, srcValue) {\n * if (_.isArray(objValue)) {\n * return objValue.concat(srcValue);\n * }\n * }\n *\n * var object = {\n * 'fruits': ['apple'],\n * 'vegetables': ['beet']\n * };\n *\n * var other = {\n * 'fruits': ['banana'],\n * 'vegetables': ['carrot']\n * };\n *\n * _.merge(object, other, customizer);\n * // => { 'fruits': ['apple', 'banana'], 'vegetables': ['beet', 'carrot'] }\n */\n mergeWith(\n object: TObject,\n source: TSource,\n customizer: MergeWithCustomizer\n ): TObject & TSource;\n\n /**\n * @see _.mergeWith\n */\n mergeWith(\n object: TObject,\n source1: TSource1,\n source2: TSource2,\n customizer: MergeWithCustomizer\n ): TObject & TSource1 & TSource2;\n\n /**\n * @see _.mergeWith\n */\n mergeWith(\n object: TObject,\n source1: TSource1,\n source2: TSource2,\n source3: TSource3,\n customizer: MergeWithCustomizer\n ): TObject & TSource1 & TSource2 & TSource3;\n\n /**\n * @see _.mergeWith\n */\n mergeWith(\n object: TObject,\n source1: TSource1,\n source2: TSource2,\n source3: TSource3,\n source4: TSource4,\n customizer: MergeWithCustomizer\n ): TObject & TSource1 & TSource2 & TSource3 & TSource4;\n\n /**\n * @see _.mergeWith\n */\n mergeWith(\n object: any,\n ...otherArgs: any[]\n ): TResult;\n }\n\n interface LoDashImplicitObjectWrapper {\n /**\n * @see _.mergeWith\n */\n mergeWith(\n source: TSource,\n customizer: MergeWithCustomizer\n ): LoDashImplicitObjectWrapper;\n\n /**\n * @see _.mergeWith\n */\n mergeWith(\n source1: TSource1,\n source2: TSource2,\n customizer: MergeWithCustomizer\n ): LoDashImplicitObjectWrapper;\n\n /**\n * @see _.mergeWith\n */\n mergeWith(\n source1: TSource1,\n source2: TSource2,\n source3: TSource3,\n customizer: MergeWithCustomizer\n ): LoDashImplicitObjectWrapper;\n\n /**\n * @see _.mergeWith\n */\n mergeWith(\n source1: TSource1,\n source2: TSource2,\n source3: TSource3,\n source4: TSource4,\n customizer: MergeWithCustomizer\n ): LoDashImplicitObjectWrapper;\n\n /**\n * @see _.mergeWith\n */\n mergeWith(\n ...otherArgs: any[]\n ): LoDashImplicitObjectWrapper;\n }\n\n //_.omit\n interface LoDashStatic {\n /**\n * The opposite of `_.pick`; this method creates an object composed of the\n * own and inherited enumerable properties of `object` that are not omitted.\n *\n * @static\n * @memberOf _\n * @category Object\n * @param {Object} object The source object.\n * @param {...(string|string[])} [props] The property names to omit, specified\n * individually or in arrays..\n * @returns {Object} Returns the new object.\n * @example\n *\n * var object = { 'a': 1, 'b': '2', 'c': 3 };\n *\n * _.omit(object, ['a', 'c']);\n * // => { 'b': '2' }\n */\n\n omit(\n object: T | null | undefined,\n ...predicate: Array>\n ): TResult;\n }\n\n interface LoDashImplicitObjectWrapperBase {\n /**\n * @see _.omit\n */\n omit(\n ...predicate: Array>\n ): LoDashImplicitObjectWrapper;\n }\n\n interface LoDashExplicitObjectWrapperBase {\n /**\n * @see _.omit\n */\n omit(\n ...predicate: Array>\n ): LoDashExplicitObjectWrapper;\n }\n\n //_.omitBy\n interface LoDashStatic {\n /**\n * The opposite of `_.pickBy`; this method creates an object composed of the\n * own and inherited enumerable properties of `object` that `predicate`\n * doesn't return truthy for.\n *\n * @static\n * @memberOf _\n * @category Object\n * @param {Object} object The source object.\n * @param {Function|Object|string} [predicate=_.identity] The function invoked per property.\n * @returns {Object} Returns the new object.\n * @example\n *\n * var object = { 'a': 1, 'b': '2', 'c': 3 };\n *\n * _.omitBy(object, _.isNumber);\n * // => { 'b': '2' }\n */\n omitBy(\n object: T | null | undefined,\n predicate: ObjectIterator\n ): TResult;\n }\n\n interface LoDashImplicitObjectWrapperBase {\n /**\n * @see _.omitBy\n */\n omitBy(\n predicate: ObjectIterator\n ): LoDashImplicitObjectWrapper;\n }\n\n interface LoDashExplicitObjectWrapperBase {\n /**\n * @see _.omitBy\n */\n omitBy(\n predicate: ObjectIterator\n ): LoDashExplicitObjectWrapper;\n }\n\n //_.pick\n interface LoDashStatic {\n /**\n * Creates an object composed of the picked `object` properties.\n *\n * @static\n * @memberOf _\n * @category Object\n * @param {Object} object The source object.\n * @param {...(string|string[])} [props] The property names to pick, specified\n * individually or in arrays.\n * @returns {Object} Returns the new object.\n * @example\n *\n * var object = { 'a': 1, 'b': '2', 'c': 3 };\n *\n * _.pick(object, ['a', 'c']);\n * // => { 'a': 1, 'c': 3 }\n */\n pick(\n object: T | null | undefined,\n ...predicate: Array>\n ): TResult;\n }\n\n interface LoDashImplicitObjectWrapperBase {\n /**\n * @see _.pick\n */\n pick(\n ...predicate: Array>\n ): LoDashImplicitObjectWrapper;\n }\n\n interface LoDashExplicitObjectWrapperBase {\n /**\n * @see _.pick\n */\n pick(\n ...predicate: Array>\n ): LoDashExplicitObjectWrapper;\n }\n\n //_.pickBy\n interface LoDashStatic {\n /**\n * Creates an object composed of the `object` properties `predicate` returns\n * truthy for. The predicate is invoked with one argument: (value).\n *\n * @static\n * @memberOf _\n * @category Object\n * @param {Object} object The source object.\n * @param {Function|Object|string} [predicate=_.identity] The function invoked per property.\n * @returns {Object} Returns the new object.\n * @example\n *\n * var object = { 'a': 1, 'b': '2', 'c': 3 };\n *\n * _.pickBy(object, _.isNumber);\n * // => { 'a': 1, 'c': 3 }\n */\n pickBy(\n object: T | null | undefined,\n predicate?: ObjectIterator\n ): TResult;\n }\n\n interface LoDashImplicitObjectWrapperBase {\n /**\n * @see _.pickBy\n */\n pickBy(\n predicate?: ObjectIterator\n ): LoDashImplicitObjectWrapper;\n }\n\n interface LoDashExplicitObjectWrapperBase {\n /**\n * @see _.pickBy\n */\n pickBy(\n predicate?: ObjectIterator\n ): LoDashExplicitObjectWrapper;\n }\n\n //_.result\n interface LoDashStatic {\n /**\n * This method is like _.get except that if the resolved value is a function it’s invoked with the this binding\n * of its parent object and its result is returned.\n *\n * @param object The object to query.\n * @param path The path of the property to resolve.\n * @param defaultValue The value returned if the resolved value is undefined.\n * @return Returns the resolved value.\n */\n result(\n object: TObject,\n path: Many,\n defaultValue?: TResult|((...args: any[]) => TResult)\n ): TResult;\n\n /**\n * @see _.result\n */\n result(\n object: any,\n path: Many,\n defaultValue?: TResult|((...args: any[]) => TResult)\n ): TResult;\n }\n\n interface LoDashImplicitWrapper {\n /**\n * @see _.result\n */\n result(\n path: Many,\n defaultValue?: TResult|((...args: any[]) => TResult)\n ): TResult;\n }\n\n interface LoDashImplicitArrayWrapperBase {\n /**\n * @see _.result\n */\n result(\n path: Many,\n defaultValue?: TResult|((...args: any[]) => TResult)\n ): TResult;\n }\n\n interface LoDashImplicitObjectWrapperBase {\n /**\n * @see _.result\n */\n result(\n path: Many,\n defaultValue?: TResult|((...args: any[]) => TResult)\n ): TResult;\n }\n\n interface LoDashExplicitWrapper {\n /**\n * @see _.result\n */\n result(\n path: Many,\n defaultValue?: any\n ): TResultWrapper;\n }\n\n interface LoDashExplicitArrayWrapperBase {\n /**\n * @see _.result\n */\n result(\n path: Many,\n defaultValue?: any\n ): TResultWrapper;\n }\n\n interface LoDashExplicitObjectWrapper {\n /**\n * @see _.result\n */\n result(\n path: Many,\n defaultValue?: any\n ): TResultWrapper;\n }\n\n //_.set\n interface LoDashStatic {\n /**\n * Sets the value at path of object. If a portion of path doesn’t exist it’s created. Arrays are created for\n * missing index properties while objects are created for all other missing properties. Use _.setWith to\n * customize path creation.\n *\n * @param object The object to modify.\n * @param path The path of the property to set.\n * @param value The value to set.\n * @return Returns object.\n */\n set(\n object: Object,\n path: Many,\n value: any\n ): TResult;\n\n /**\n * @see _.set\n */\n set(\n object: Object,\n path: Many,\n value: V\n ): TResult;\n\n /**\n * @see _.set\n */\n set(\n object: O,\n path: Many,\n value: V\n ): TResult;\n }\n\n interface LoDashImplicitObjectWrapper {\n /**\n * @see _.set\n */\n set(\n path: Many,\n value: any\n ): LoDashImplicitObjectWrapper;\n\n /**\n * @see _.set\n */\n set(\n path: Many,\n value: V\n ): LoDashImplicitObjectWrapper;\n }\n\n interface LoDashExplicitObjectWrapper {\n /**\n * @see _.set\n */\n set(\n path: Many,\n value: any\n ): LoDashExplicitObjectWrapper;\n\n /**\n * @see _.set\n */\n set(\n path: Many,\n value: V\n ): LoDashExplicitObjectWrapper;\n }\n\n //_.setWith\n type SetWithCustomizer = (nsValue: any, key: string, nsObject: T) => any;\n\n interface LoDashStatic {\n /**\n * This method is like _.set except that it accepts customizer which is invoked to produce the objects of\n * path. If customizer returns undefined path creation is handled by the method instead. The customizer is\n * invoked with three arguments: (nsValue, key, nsObject).\n *\n * @param object The object to modify.\n * @param path The path of the property to set.\n * @param value The value to set.\n * @parem customizer The function to customize assigned values.\n * @return Returns object.\n */\n setWith(\n object: Object,\n path: Many,\n value: any,\n customizer?: SetWithCustomizer\n ): TResult;\n\n /**\n * @see _.setWith\n */\n setWith(\n object: Object,\n path: Many,\n value: V,\n customizer?: SetWithCustomizer\n ): TResult;\n\n /**\n * @see _.setWith\n */\n setWith(\n object: O,\n path: Many,\n value: V,\n customizer?: SetWithCustomizer\n ): TResult;\n }\n\n interface LoDashImplicitObjectWrapper {\n /**\n * @see _.setWith\n */\n setWith(\n path: Many,\n value: any,\n customizer?: SetWithCustomizer\n ): LoDashImplicitObjectWrapper;\n\n /**\n * @see _.setWith\n */\n setWith(\n path: Many,\n value: V,\n customizer?: SetWithCustomizer\n ): LoDashImplicitObjectWrapper;\n }\n\n interface LoDashExplicitObjectWrapper {\n /**\n * @see _.setWith\n */\n setWith(\n path: Many,\n value: any,\n customizer?: SetWithCustomizer\n ): LoDashExplicitObjectWrapper;\n\n /**\n * @see _.setWith\n */\n setWith(\n path: Many,\n value: V,\n customizer?: SetWithCustomizer\n ): LoDashExplicitObjectWrapper;\n }\n\n //_.toPairs\n interface LoDashStatic {\n /**\n * Creates an array of own enumerable key-value pairs for object.\n *\n * @param object The object to query.\n * @return Returns the new array of key-value pairs.\n */\n toPairs(object?: T): [string, any][];\n\n toPairs(object?: T): [string, TResult][];\n }\n\n interface LoDashImplicitObjectWrapper {\n /**\n * @see _.toPairs\n */\n toPairs(): LoDashImplicitArrayWrapper<[string, TResult]>;\n }\n\n interface LoDashExplicitObjectWrapper {\n /**\n * @see _.toPairs\n */\n toPairs(): LoDashExplicitArrayWrapper<[string, TResult]>;\n }\n\n //_.toPairsIn\n interface LoDashStatic {\n /**\n * Creates an array of own and inherited enumerable key-value pairs for object.\n *\n * @param object The object to query.\n * @return Returns the new array of key-value pairs.\n */\n toPairsIn(object?: T): [string, any][];\n\n toPairsIn(object?: T): [string, TResult][];\n }\n\n interface LoDashImplicitObjectWrapper {\n /**\n * @see _.toPairsIn\n */\n toPairsIn(): LoDashImplicitArrayWrapper<[string, TResult]>;\n }\n\n interface LoDashExplicitObjectWrapper {\n /**\n * @see _.toPairsIn\n */\n toPairsIn(): LoDashExplicitArrayWrapper<[string, TResult]>;\n }\n\n //_.transform\n interface LoDashStatic {\n /**\n * An alternative to _.reduce; this method transforms object to a new accumulator object which is the result of\n * running each of its own enumerable properties through iteratee, with each invocation potentially mutating\n * the accumulator object. The iteratee is bound to thisArg and invoked with four arguments: (accumulator,\n * value, key, object). Iteratee functions may exit iteration early by explicitly returning false.\n *\n * @param object The object to iterate over.\n * @param iteratee The function invoked per iteration.\n * @param accumulator The custom accumulator value.\n * @param thisArg The this binding of iteratee.\n * @return Returns the accumulated value.\n */\n transform(\n object: T[],\n iteratee?: MemoVoidArrayIterator,\n accumulator?: TResult[]\n ): TResult[];\n\n /**\n * @see _.transform\n */\n transform(\n object: T[],\n iteratee?: MemoVoidArrayIterator>,\n accumulator?: Dictionary\n ): Dictionary;\n\n /**\n * @see _.transform\n */\n transform(\n object: Dictionary,\n iteratee?: MemoVoidDictionaryIterator>,\n accumulator?: Dictionary\n ): Dictionary;\n\n /**\n * @see _.transform\n */\n transform(\n object: Dictionary,\n iteratee?: MemoVoidDictionaryIterator,\n accumulator?: TResult[]\n ): TResult[];\n }\n\n interface LoDashImplicitArrayWrapper {\n /**\n * @see _.transform\n */\n transform(\n iteratee?: MemoVoidArrayIterator,\n accumulator?: TResult[]\n ): LoDashImplicitArrayWrapper;\n\n /**\n * @see _.transform\n */\n transform(\n iteratee?: MemoVoidArrayIterator>,\n accumulator?: Dictionary\n ): LoDashImplicitObjectWrapper>;\n }\n\n interface LoDashImplicitObjectWrapper {\n /**\n * @see _.transform\n */\n transform(\n iteratee?: MemoVoidDictionaryIterator>,\n accumulator?: Dictionary\n ): LoDashImplicitObjectWrapper>;\n\n /**\n * @see _.transform\n */\n transform(\n iteratee?: MemoVoidDictionaryIterator,\n accumulator?: TResult[]\n ): LoDashImplicitArrayWrapper;\n }\n\n //_.unset\n interface LoDashStatic {\n /**\n * Removes the property at path of object.\n *\n * Note: This method mutates object.\n *\n * @param object The object to modify.\n * @param path The path of the property to unset.\n * @return Returns true if the property is deleted, else false.\n */\n unset(\n object: T,\n path: Many\n ): boolean;\n }\n\n interface LoDashImplicitObjectWrapper {\n /**\n * @see _.unset\n */\n unset(path: Many): LoDashImplicitWrapper;\n }\n\n interface LoDashExplicitObjectWrapper {\n /**\n * @see _.unset\n */\n unset(path: Many): LoDashExplicitWrapper;\n }\n\n //_.update\n interface LoDashStatic {\n /**\n * This method is like _.set except that accepts updater to produce the value to set. Use _.updateWith to\n * customize path creation. The updater is invoked with one argument: (value).\n *\n * @param object The object to modify.\n * @param path The path of the property to set.\n * @param updater The function to produce the updated value.\n * @return Returns object.\n */\n update(\n object: Object,\n path: Many,\n updater: Function\n ): TResult;\n\n /**\n * @see _.update\n */\n update(\n object: Object,\n path: Many,\n updater: U\n ): TResult;\n\n /**\n * @see _.update\n */\n update(\n object: O,\n path: Many,\n updater: Function\n ): TResult;\n\n /**\n * @see _.update\n */\n update(\n object: O,\n path: Many,\n updater: U\n ): TResult;\n }\n\n interface LoDashImplicitObjectWrapper {\n /**\n * @see _.update\n */\n update(\n path: Many,\n updater: any\n ): LoDashImplicitObjectWrapper;\n\n /**\n * @see _.update\n */\n update(\n path: Many,\n updater: U\n ): LoDashImplicitObjectWrapper;\n }\n\n interface LoDashExplicitObjectWrapper {\n /**\n * @see _.update\n */\n update(\n path: Many,\n updater: any\n ): LoDashExplicitObjectWrapper;\n\n /**\n * @see _.update\n */\n update(\n path: Many,\n updater: U\n ): LoDashExplicitObjectWrapper;\n }\n\n //_.values\n interface LoDashStatic {\n /**\n * Creates an array of the own enumerable property values of object.\n *\n * @param object The object to query.\n * @return Returns an array of property values.\n */\n values(object?: Dictionary|NumericDictionary|List | null | undefined): T[];\n\n /**\n * @see _.values\n */\n values(object?: any): T[];\n }\n\n interface LoDashImplicitStringWrapper {\n /**\n * @see _.values\n */\n values(): LoDashImplicitArrayWrapper;\n }\n\n interface LoDashImplicitWrapper {\n /**\n * @see _.values\n */\n values(): LoDashImplicitArrayWrapper;\n }\n\n interface LoDashImplicitArrayWrapperBase {\n /**\n * @see _.values\n */\n values(): LoDashImplicitArrayWrapper;\n }\n\n interface LoDashImplicitObjectWrapperBase {\n /**\n * @see _.values\n */\n values(): LoDashImplicitArrayWrapper;\n }\n\n interface LoDashExplicitWrapper {\n /**\n * @see _.values\n */\n values(): LoDashExplicitArrayWrapper;\n }\n\n interface LoDashExplicitArrayWrapperBase {\n /**\n * @see _.values\n */\n values(): LoDashExplicitArrayWrapper;\n }\n\n interface LoDashExplicitObjectWrapperBase {\n /**\n * @see _.values\n */\n values(): LoDashExplicitArrayWrapper;\n }\n\n //_.valuesIn\n interface LoDashStatic {\n /**\n * Creates an array of the own and inherited enumerable property values of object.\n *\n * @param object The object to query.\n * @return Returns the array of property values.\n */\n valuesIn(object?: Dictionary): T[];\n\n /**\n * @see _.valuesIn\n */\n valuesIn(object?: any): T[];\n }\n\n interface LoDashImplicitObjectWrapperBase {\n /**\n * @see _.valuesIn\n */\n valuesIn(): LoDashImplicitArrayWrapper;\n }\n\n interface LoDashExplicitObjectWrapperBase {\n /**\n * @see _.valuesIn\n */\n valuesIn(): LoDashExplicitArrayWrapper;\n }\n\n /**********\n * String *\n **********/\n\n //_.camelCase\n interface LoDashStatic {\n /**\n * Converts string to camel case.\n *\n * @param string The string to convert.\n * @return Returns the camel cased string.\n */\n camelCase(string?: string): string;\n }\n\n interface LoDashImplicitWrapper {\n /**\n * @see _.camelCase\n */\n camelCase(): string;\n }\n\n interface LoDashExplicitWrapper {\n /**\n * @see _.camelCase\n */\n camelCase(): LoDashExplicitWrapper;\n }\n\n //_.capitalize\n interface LoDashStatic {\n /**\n * Converts the first character of string to upper case and the remaining to lower case.\n *\n * @param string The string to capitalize.\n * @return Returns the capitalized string.\n */\n capitalize(string?: string): string;\n }\n\n interface LoDashImplicitWrapper {\n /**\n * @see _.capitalize\n */\n capitalize(): string;\n }\n\n interface LoDashExplicitWrapper {\n /**\n * @see _.capitalize\n */\n capitalize(): LoDashExplicitWrapper;\n }\n\n //_.deburr\n interface LoDashStatic {\n /**\n * Deburrs string by converting latin-1 supplementary letters to basic latin letters and removing combining\n * diacritical marks.\n *\n * @param string The string to deburr.\n * @return Returns the deburred string.\n */\n deburr(string?: string): string;\n }\n\n interface LoDashImplicitWrapper {\n /**\n * @see _.deburr\n */\n deburr(): string;\n }\n\n interface LoDashExplicitWrapper {\n /**\n * @see _.deburr\n */\n deburr(): LoDashExplicitWrapper;\n }\n\n //_.endsWith\n interface LoDashStatic {\n /**\n * Checks if string ends with the given target string.\n *\n * @param string The string to search.\n * @param target The string to search for.\n * @param position The position to search from.\n * @return Returns true if string ends with target, else false.\n */\n endsWith(\n string?: string,\n target?: string,\n position?: number\n ): boolean;\n }\n\n interface LoDashImplicitWrapper {\n /**\n * @see _.endsWith\n */\n endsWith(\n target?: string,\n position?: number\n ): boolean;\n }\n\n interface LoDashExplicitWrapper {\n /**\n * @see _.endsWith\n */\n endsWith(\n target?: string,\n position?: number\n ): LoDashExplicitWrapper;\n }\n\n // _.escape\n interface LoDashStatic {\n /**\n * Converts the characters \"&\", \"<\", \">\", '\"', \"'\", and \"`\" in string to their corresponding HTML entities.\n *\n * Note: No other characters are escaped. To escape additional characters use a third-party library like he.\n *\n * hough the \">\" character is escaped for symmetry, characters like \">\" and \"/\" don’t need escaping in HTML\n * and have no special meaning unless they're part of a tag or unquoted attribute value. See Mathias Bynens’s\n * article (under \"semi-related fun fact\") for more details.\n *\n * Backticks are escaped because in IE < 9, they can break out of attribute values or HTML comments. See #59,\n * #102, #108, and #133 of the HTML5 Security Cheatsheet for more details.\n *\n * When working with HTML you should always quote attribute values to reduce XSS vectors.\n *\n * @param string The string to escape.\n * @return Returns the escaped string.\n */\n escape(string?: string): string;\n }\n\n interface LoDashImplicitWrapper {\n /**\n * @see _.escape\n */\n escape(): string;\n }\n\n interface LoDashExplicitWrapper {\n /**\n * @see _.escape\n */\n escape(): LoDashExplicitWrapper;\n }\n\n // _.escapeRegExp\n interface LoDashStatic {\n /**\n * Escapes the RegExp special characters \"^\", \"$\", \"\\\", \".\", \"*\", \"+\", \"?\", \"(\", \")\", \"[\", \"]\",\n * \"{\", \"}\", and \"|\" in string.\n *\n * @param string The string to escape.\n * @return Returns the escaped string.\n */\n escapeRegExp(string?: string): string;\n }\n\n interface LoDashImplicitWrapper {\n /**\n * @see _.escapeRegExp\n */\n escapeRegExp(): string;\n }\n\n interface LoDashExplicitWrapper {\n /**\n * @see _.escapeRegExp\n */\n escapeRegExp(): LoDashExplicitWrapper;\n }\n\n //_.kebabCase\n interface LoDashStatic {\n /**\n * Converts string to kebab case.\n *\n * @param string The string to convert.\n * @return Returns the kebab cased string.\n */\n kebabCase(string?: string): string;\n }\n\n interface LoDashImplicitWrapper {\n /**\n * @see _.kebabCase\n */\n kebabCase(): string;\n }\n\n interface LoDashExplicitWrapper {\n /**\n * @see _.kebabCase\n */\n kebabCase(): LoDashExplicitWrapper;\n }\n\n //_.lowerCase\n interface LoDashStatic {\n /**\n * Converts `string`, as space separated words, to lower case.\n *\n * @param string The string to convert.\n * @return Returns the lower cased string.\n */\n lowerCase(string?: string): string;\n }\n\n interface LoDashImplicitWrapper {\n /**\n * @see _.lowerCase\n */\n lowerCase(): string;\n }\n\n interface LoDashExplicitWrapper {\n /**\n * @see _.lowerCase\n */\n lowerCase(): LoDashExplicitWrapper;\n }\n\n //_.lowerFirst\n interface LoDashStatic {\n /**\n * Converts the first character of `string` to lower case.\n *\n * @param string The string to convert.\n * @return Returns the converted string.\n */\n lowerFirst(string?: string): string;\n }\n\n interface LoDashImplicitWrapper {\n /**\n * @see _.lowerFirst\n */\n lowerFirst(): string;\n }\n\n interface LoDashExplicitWrapper {\n /**\n * @see _.lowerFirst\n */\n lowerFirst(): LoDashExplicitWrapper;\n }\n\n //_.pad\n interface LoDashStatic {\n /**\n * Pads string on the left and right sides if it’s shorter than length. Padding characters are truncated if\n * they can’t be evenly divided by length.\n *\n * @param string The string to pad.\n * @param length The padding length.\n * @param chars The string used as padding.\n * @return Returns the padded string.\n */\n pad(\n string?: string,\n length?: number,\n chars?: string\n ): string;\n }\n\n interface LoDashImplicitWrapper {\n /**\n * @see _.pad\n */\n pad(\n length?: number,\n chars?: string\n ): string;\n }\n\n interface LoDashExplicitWrapper {\n /**\n * @see _.pad\n */\n pad(\n length?: number,\n chars?: string\n ): LoDashExplicitWrapper;\n }\n\n //_.padEnd\n interface LoDashStatic {\n /**\n * Pads string on the right side if it’s shorter than length. Padding characters are truncated if they exceed\n * length.\n *\n * @param string The string to pad.\n * @param length The padding length.\n * @param chars The string used as padding.\n * @return Returns the padded string.\n */\n padEnd(\n string?: string,\n length?: number,\n chars?: string\n ): string;\n }\n\n interface LoDashImplicitWrapper {\n /**\n * @see _.padEnd\n */\n padEnd(\n length?: number,\n chars?: string\n ): string;\n }\n\n interface LoDashExplicitWrapper {\n /**\n * @see _.padEnd\n */\n padEnd(\n length?: number,\n chars?: string\n ): LoDashExplicitWrapper;\n }\n\n //_.padStart\n interface LoDashStatic {\n /**\n * Pads string on the left side if it’s shorter than length. Padding characters are truncated if they exceed\n * length.\n *\n * @param string The string to pad.\n * @param length The padding length.\n * @param chars The string used as padding.\n * @return Returns the padded string.\n */\n padStart(\n string?: string,\n length?: number,\n chars?: string\n ): string;\n }\n\n interface LoDashImplicitWrapper {\n /**\n * @see _.padStart\n */\n padStart(\n length?: number,\n chars?: string\n ): string;\n }\n\n interface LoDashExplicitWrapper {\n /**\n * @see _.padStart\n */\n padStart(\n length?: number,\n chars?: string\n ): LoDashExplicitWrapper;\n }\n\n //_.parseInt\n interface LoDashStatic {\n /**\n * Converts string to an integer of the specified radix. If radix is undefined or 0, a radix of 10 is used\n * unless value is a hexadecimal, in which case a radix of 16 is used.\n *\n * Note: This method aligns with the ES5 implementation of parseInt.\n *\n * @param string The string to convert.\n * @param radix The radix to interpret value by.\n * @return Returns the converted integer.\n */\n parseInt(\n string: string,\n radix?: number\n ): number;\n }\n\n interface LoDashImplicitWrapper {\n /**\n * @see _.parseInt\n */\n parseInt(radix?: number): number;\n }\n\n interface LoDashExplicitWrapper {\n /**\n * @see _.parseInt\n */\n parseInt(radix?: number): LoDashExplicitWrapper;\n }\n\n //_.repeat\n interface LoDashStatic {\n /**\n * Repeats the given string n times.\n *\n * @param string The string to repeat.\n * @param n The number of times to repeat the string.\n * @return Returns the repeated string.\n */\n repeat(\n string?: string,\n n?: number\n ): string;\n }\n\n interface LoDashImplicitWrapper {\n /**\n * @see _.repeat\n */\n repeat(n?: number): string;\n }\n\n interface LoDashExplicitWrapper {\n /**\n * @see _.repeat\n */\n repeat(n?: number): LoDashExplicitWrapper;\n }\n\n //_.replace\n interface LoDashStatic {\n /**\n * Replaces matches for pattern in string with replacement.\n *\n * Note: This method is based on String#replace.\n *\n * @param string\n * @param pattern\n * @param replacement\n * @return Returns the modified string.\n */\n replace(\n string: string,\n pattern: RegExp|string,\n replacement: Function|string\n ): string;\n\n /**\n * @see _.replace\n */\n replace(\n pattern?: RegExp|string,\n replacement?: Function|string\n ): string;\n }\n\n interface LoDashImplicitWrapper {\n /**\n * @see _.replace\n */\n replace(\n pattern?: RegExp|string,\n replacement?: Function|string\n ): string;\n\n /**\n * @see _.replace\n */\n replace(\n replacement?: Function|string\n ): string;\n }\n\n interface LoDashImplicitObjectWrapper {\n /**\n * @see _.replace\n */\n replace(\n pattern?: RegExp|string,\n replacement?: Function|string\n ): string;\n\n /**\n * @see _.replace\n */\n replace(\n replacement?: Function|string\n ): string;\n }\n\n interface LoDashExplicitWrapper {\n /**\n * @see _.replace\n */\n replace(\n pattern?: RegExp|string,\n replacement?: Function|string\n ): LoDashExplicitWrapper;\n\n /**\n * @see _.replace\n */\n replace(\n replacement?: Function|string\n ): LoDashExplicitWrapper;\n }\n\n interface LoDashExplicitObjectWrapper {\n /**\n * @see _.replace\n */\n replace(\n pattern?: RegExp|string,\n replacement?: Function|string\n ): LoDashExplicitWrapper;\n\n /**\n * @see _.replace\n */\n replace(\n replacement?: Function|string\n ): LoDashExplicitWrapper;\n }\n\n //_.snakeCase\n interface LoDashStatic {\n /**\n * Converts string to snake case.\n *\n * @param string The string to convert.\n * @return Returns the snake cased string.\n */\n snakeCase(string?: string): string;\n }\n\n interface LoDashImplicitWrapper {\n /**\n * @see _.snakeCase\n */\n snakeCase(): string;\n }\n\n interface LoDashExplicitWrapper {\n /**\n * @see _.snakeCase\n */\n snakeCase(): LoDashExplicitWrapper;\n }\n\n //_.split\n interface LoDashStatic {\n /**\n * Splits string by separator.\n *\n * Note: This method is based on String#split.\n *\n * @param string\n * @param separator\n * @param limit\n * @return Returns the new array of string segments.\n */\n split(\n string: string,\n separator?: RegExp|string,\n limit?: number\n ): string[];\n }\n\n interface LoDashImplicitWrapper {\n /**\n * @see _.split\n */\n split(\n separator?: RegExp|string,\n limit?: number\n ): LoDashImplicitArrayWrapper;\n }\n\n interface LoDashExplicitWrapper {\n /**\n * @see _.split\n */\n split(\n separator?: RegExp|string,\n limit?: number\n ): LoDashExplicitArrayWrapper;\n }\n\n //_.startCase\n interface LoDashStatic {\n /**\n * Converts string to start case.\n *\n * @param string The string to convert.\n * @return Returns the start cased string.\n */\n startCase(string?: string): string;\n }\n\n interface LoDashImplicitWrapper {\n /**\n * @see _.startCase\n */\n startCase(): string;\n }\n\n interface LoDashExplicitWrapper {\n /**\n * @see _.startCase\n */\n startCase(): LoDashExplicitWrapper;\n }\n\n //_.startsWith\n interface LoDashStatic {\n /**\n * Checks if string starts with the given target string.\n *\n * @param string The string to search.\n * @param target The string to search for.\n * @param position The position to search from.\n * @return Returns true if string starts with target, else false.\n */\n startsWith(\n string?: string,\n target?: string,\n position?: number\n ): boolean;\n }\n\n interface LoDashImplicitWrapper {\n /**\n * @see _.startsWith\n */\n startsWith(\n target?: string,\n position?: number\n ): boolean;\n }\n\n interface LoDashExplicitWrapper {\n /**\n * @see _.startsWith\n */\n startsWith(\n target?: string,\n position?: number\n ): LoDashExplicitWrapper;\n }\n\n //_.template\n interface TemplateOptions extends TemplateSettings {\n /**\n * The sourceURL of the template's compiled source.\n */\n sourceURL?: string;\n }\n\n interface TemplateExecutor {\n (data?: Object): string;\n source: string;\n }\n\n interface LoDashStatic {\n /**\n * Creates a compiled template function that can interpolate data properties in \"interpolate\" delimiters,\n * HTML-escape interpolated data properties in \"escape\" delimiters, and execute JavaScript in \"evaluate\"\n * delimiters. Data properties may be accessed as free variables in the template. If a setting object is\n * provided it takes precedence over _.templateSettings values.\n *\n * Note: In the development build _.template utilizes\n * [sourceURLs](http://www.html5rocks.com/en/tutorials/developertools/sourcemaps/#toc-sourceurl) for easier\n * debugging.\n *\n * For more information on precompiling templates see\n * [lodash's custom builds documentation](https://lodash.com/custom-builds).\n *\n * For more information on Chrome extension sandboxes see\n * [Chrome's extensions documentation](https://developer.chrome.com/extensions/sandboxingEval).\n *\n * @param string The template string.\n * @param options The options object.\n * @param options.escape The HTML \"escape\" delimiter.\n * @param options.evaluate The \"evaluate\" delimiter.\n * @param options.imports An object to import into the template as free variables.\n * @param options.interpolate The \"interpolate\" delimiter.\n * @param options.sourceURL The sourceURL of the template's compiled source.\n * @param options.variable The data object variable name.\n * @return Returns the compiled template function.\n */\n template(\n string: string,\n options?: TemplateOptions\n ): TemplateExecutor;\n }\n\n interface LoDashImplicitWrapper {\n /**\n * @see _.template\n */\n template(options?: TemplateOptions): TemplateExecutor;\n }\n\n interface LoDashExplicitWrapper {\n /**\n * @see _.template\n */\n template(options?: TemplateOptions): LoDashExplicitObjectWrapper;\n }\n\n //_.toLower\n interface LoDashStatic {\n /**\n * Converts `string`, as a whole, to lower case.\n *\n * @param string The string to convert.\n * @return Returns the lower cased string.\n */\n toLower(string?: string): string;\n }\n\n interface LoDashImplicitWrapper {\n /**\n * @see _.toLower\n */\n toLower(): string;\n }\n\n interface LoDashExplicitWrapper {\n /**\n * @see _.toLower\n */\n toLower(): LoDashExplicitWrapper;\n }\n\n //_.toUpper\n interface LoDashStatic {\n /**\n * Converts `string`, as a whole, to upper case.\n *\n * @param string The string to convert.\n * @return Returns the upper cased string.\n */\n toUpper(string?: string): string;\n }\n\n interface LoDashImplicitWrapper {\n /**\n * @see _.toUpper\n */\n toUpper(): string;\n }\n\n interface LoDashExplicitWrapper {\n /**\n * @see _.toUpper\n */\n toUpper(): LoDashExplicitWrapper;\n }\n\n //_.trim\n interface LoDashStatic {\n /**\n * Removes leading and trailing whitespace or specified characters from string.\n *\n * @param string The string to trim.\n * @param chars The characters to trim.\n * @return Returns the trimmed string.\n */\n trim(\n string?: string,\n chars?: string\n ): string;\n }\n\n interface LoDashImplicitWrapper {\n /**\n * @see _.trim\n */\n trim(chars?: string): string;\n }\n\n interface LoDashExplicitWrapper {\n /**\n * @see _.trim\n */\n trim(chars?: string): LoDashExplicitWrapper;\n }\n\n //_.trimEnd\n interface LoDashStatic {\n /**\n * Removes trailing whitespace or specified characters from string.\n *\n * @param string The string to trim.\n * @param chars The characters to trim.\n * @return Returns the trimmed string.\n */\n trimEnd(\n string?: string,\n chars?: string\n ): string;\n }\n\n interface LoDashImplicitWrapper {\n /**\n * @see _.trimEnd\n */\n trimEnd(chars?: string): string;\n }\n\n interface LoDashExplicitWrapper {\n /**\n * @see _.trimEnd\n */\n trimEnd(chars?: string): LoDashExplicitWrapper;\n }\n\n //_.trimStart\n interface LoDashStatic {\n /**\n * Removes leading whitespace or specified characters from string.\n *\n * @param string The string to trim.\n * @param chars The characters to trim.\n * @return Returns the trimmed string.\n */\n trimStart(\n string?: string,\n chars?: string\n ): string;\n }\n\n interface LoDashImplicitWrapper {\n /**\n * @see _.trimStart\n */\n trimStart(chars?: string): string;\n }\n\n interface LoDashExplicitWrapper {\n /**\n * @see _.trimStart\n */\n trimStart(chars?: string): LoDashExplicitWrapper;\n }\n\n //_.truncate\n interface TruncateOptions {\n /** The maximum string length. */\n length?: number;\n /** The string to indicate text is omitted. */\n omission?: string;\n /** The separator pattern to truncate to. */\n separator?: string|RegExp;\n }\n\n interface LoDashStatic {\n /**\n * Truncates string if it’s longer than the given maximum string length. The last characters of the truncated\n * string are replaced with the omission string which defaults to \"…\".\n *\n * @param string The string to truncate.\n * @param options The options object or maximum string length.\n * @return Returns the truncated string.\n */\n truncate(\n string?: string,\n options?: TruncateOptions\n ): string;\n }\n\n interface LoDashImplicitWrapper {\n /**\n * @see _.truncate\n */\n truncate(options?: TruncateOptions): string;\n }\n\n interface LoDashExplicitWrapper {\n /**\n * @see _.truncate\n */\n truncate(options?: TruncateOptions): LoDashExplicitWrapper;\n }\n\n //_.unescape\n interface LoDashStatic {\n /**\n * The inverse of _.escape; this method converts the HTML entities &, <, >, ", ', and `\n * in string to their corresponding characters.\n *\n * Note: No other HTML entities are unescaped. To unescape additional HTML entities use a third-party library\n * like he.\n *\n * @param string The string to unescape.\n * @return Returns the unescaped string.\n */\n unescape(string?: string): string;\n }\n\n interface LoDashImplicitWrapper {\n /**\n * @see _.unescape\n */\n unescape(): string;\n }\n\n interface LoDashExplicitWrapper {\n /**\n * @see _.unescape\n */\n unescape(): LoDashExplicitWrapper;\n }\n\n //_.upperCase\n interface LoDashStatic {\n /**\n * Converts `string`, as space separated words, to upper case.\n *\n * @param string The string to convert.\n * @return Returns the upper cased string.\n */\n upperCase(string?: string): string;\n }\n\n interface LoDashImplicitWrapper {\n /**\n * @see _.upperCase\n */\n upperCase(): string;\n }\n\n interface LoDashExplicitWrapper {\n /**\n * @see _.upperCase\n */\n upperCase(): LoDashExplicitWrapper;\n }\n\n //_.upperFirst\n interface LoDashStatic {\n /**\n * Converts the first character of `string` to upper case.\n *\n * @param string The string to convert.\n * @return Returns the converted string.\n */\n upperFirst(string?: string): string;\n }\n\n interface LoDashImplicitWrapper {\n /**\n * @see _.upperFirst\n */\n upperFirst(): string;\n }\n\n interface LoDashExplicitWrapper {\n /**\n * @see _.upperFirst\n */\n upperFirst(): LoDashExplicitWrapper;\n }\n\n //_.words\n interface LoDashStatic {\n /**\n * Splits `string` into an array of its words.\n *\n * @param string The string to inspect.\n * @param pattern The pattern to match words.\n * @return Returns the words of `string`.\n */\n words(\n string?: string,\n pattern?: string|RegExp\n ): string[];\n }\n\n interface LoDashImplicitWrapper {\n /**\n * @see _.words\n */\n words(pattern?: string|RegExp): string[];\n }\n\n interface LoDashExplicitWrapper {\n /**\n * @see _.words\n */\n words(pattern?: string|RegExp): LoDashExplicitArrayWrapper;\n }\n\n /***********\n * Utility *\n ***********/\n\n //_.attempt\n interface LoDashStatic {\n /**\n * Attempts to invoke func, returning either the result or the caught error object. Any additional arguments\n * are provided to func when it’s invoked.\n *\n * @param func The function to attempt.\n * @return Returns the func result or error object.\n */\n attempt(func: (...args: any[]) => TResult, ...args: any[]): TResult|Error;\n }\n\n interface LoDashImplicitObjectWrapper {\n /**\n * @see _.attempt\n */\n attempt(...args: any[]): TResult|Error;\n }\n\n interface LoDashExplicitObjectWrapper {\n /**\n * @see _.attempt\n */\n attempt(...args: any[]): LoDashExplicitObjectWrapper;\n }\n\n //_.constant\n interface LoDashStatic {\n /**\n * Creates a function that returns value.\n *\n * @param value The value to return from the new function.\n * @return Returns the new function.\n */\n constant(value: T): () => T;\n }\n\n interface LoDashImplicitWrapperBase {\n /**\n * @see _.constant\n */\n constant(): LoDashImplicitObjectWrapper<() => TResult>;\n }\n\n interface LoDashExplicitWrapperBase {\n /**\n * @see _.constant\n */\n constant(): LoDashExplicitObjectWrapper<() => TResult>;\n }\n\n //_.defaultTo\n interface LoDashStatic {\n /**\n * Checks `value` to determine whether a default value should be returned in\n * its place. The `defaultValue` is returned if `value` is `NaN`, `null`,\n * or `undefined`.\n *\n * @param value The value to check.\n * @param defaultValue The default value.\n * @returns Returns the resolved value.\n */\n defaultTo(value: T | null | undefined, defaultValue: T): T;\n }\n\n interface LoDashImplicitWrapperBase {\n /**\n * @see _.defaultTo\n */\n defaultTo(value: TResult): LoDashImplicitObjectWrapper;\n }\n\n interface LoDashExplicitWrapperBase {\n /**\n * @see _.defaultTo\n */\n defaultTo(value: TResult): LoDashExplicitObjectWrapper;\n }\n\n //_.identity\n interface LoDashStatic {\n /**\n * This method returns the first argument provided to it.\n *\n * @param value Any value.\n * @return Returns value.\n */\n identity(value: T): T;\n\n /**\n * @see _.identity\n */\n identity(): undefined;\n }\n\n interface LoDashImplicitWrapper {\n /**\n * @see _.identity\n */\n identity(): T;\n }\n\n interface LoDashImplicitArrayWrapperBase {\n /**\n * @see _.identity\n */\n identity(): TArray;\n }\n\n interface LoDashImplicitObjectWrapperBase {\n /**\n * @see _.identity\n */\n identity(): TObject;\n }\n\n interface LoDashExplicitWrapper {\n /**\n * @see _.identity\n */\n identity(): LoDashExplicitWrapper;\n }\n\n interface LoDashExplicitArrayWrapperBase {\n /**\n * @see _.identity\n */\n identity(): TWrapper;\n }\n\n interface LoDashExplicitObjectWrapperBase {\n /**\n * @see _.identity\n */\n identity(): TWrapper;\n }\n\n //_.iteratee\n interface LoDashStatic {\n /**\n * Creates a function that invokes `func` with the arguments of the created\n * function. If `func` is a property name the created callback returns the\n * property value for a given element. If `func` is an object the created\n * callback returns `true` for elements that contain the equivalent object properties, otherwise it returns `false`.\n *\n * @static\n * @memberOf _\n * @category Util\n * @param {*} [func=_.identity] The value to convert to a callback.\n * @returns {Function} Returns the callback.\n * @example\n *\n * var users = [\n * { 'user': 'barney', 'age': 36 },\n * { 'user': 'fred', 'age': 40 }\n * ];\n *\n * // create custom iteratee shorthands\n * _.iteratee = _.wrap(_.iteratee, function(callback, func) {\n * var p = /^(\\S+)\\s*([<>])\\s*(\\S+)$/.exec(func);\n * return !p ? callback(func) : function(object) {\n * return (p[2] == '>' ? object[p[1]] > p[3] : object[p[1]] < p[3]);\n * };\n * });\n *\n * _.filter(users, 'age > 36');\n * // => [{ 'user': 'fred', 'age': 40 }]\n */\n iteratee(\n func: TFunction\n ): TFunction;\n\n /**\n * @see _.iteratee\n */\n iteratee(\n func: string\n ): (object: any) => TResult;\n\n /**\n * @see _.iteratee\n */\n iteratee(\n func: Object\n ): (object: any) => boolean;\n\n /**\n * @see _.iteratee\n */\n iteratee(): (value: TResult) => TResult;\n }\n\n interface LoDashImplicitWrapper {\n /**\n * @see _.iteratee\n */\n iteratee(): LoDashImplicitObjectWrapper<(object: any) => TResult>;\n }\n\n interface LoDashImplicitObjectWrapper {\n /**\n * @see _.iteratee\n */\n iteratee(): LoDashImplicitObjectWrapper<(object: any) => boolean>;\n\n /**\n * @see _.iteratee\n */\n iteratee(): LoDashImplicitObjectWrapper<(...args: any[]) => TResult>;\n }\n\n interface LoDashExplicitWrapper {\n /**\n * @see _.iteratee\n */\n iteratee(): LoDashExplicitObjectWrapper<(object: any) => TResult>;\n }\n\n interface LoDashExplicitObjectWrapper {\n /**\n * @see _.iteratee\n */\n iteratee(): LoDashExplicitObjectWrapper<(object: any) => boolean>;\n\n /**\n * @see _.iteratee\n */\n iteratee(): LoDashExplicitObjectWrapper<(...args: any[]) => TResult>;\n }\n\n //_.matches\n interface LoDashStatic {\n /**\n * Creates a function that performs a deep comparison between a given object and source, returning true if the\n * given object has equivalent property values, else false.\n *\n * Note: This method supports comparing arrays, booleans, Date objects, numbers, Object objects, regexes, and\n * strings. Objects are compared by their own, not inherited, enumerable properties. For comparing a single own\n * or inherited property value see _.matchesProperty.\n *\n * @param source The object of property values to match.\n * @return Returns the new function.\n */\n matches(source: T): (value: any) => boolean;\n\n /**\n * @see _.matches\n */\n matches(source: T): (value: V) => boolean;\n }\n\n interface LoDashImplicitWrapperBase {\n /**\n * @see _.matches\n */\n matches(): LoDashImplicitObjectWrapper<(value: V) => boolean>;\n }\n\n interface LoDashExplicitWrapperBase {\n /**\n * @see _.matches\n */\n matches(): LoDashExplicitObjectWrapper<(value: V) => boolean>;\n }\n\n //_.matchesProperty\n interface LoDashStatic {\n /**\n * Creates a function that compares the property value of path on a given object to value.\n *\n * Note: This method supports comparing arrays, booleans, Date objects, numbers, Object objects, regexes, and\n * strings. Objects are compared by their own, not inherited, enumerable properties.\n *\n * @param path The path of the property to get.\n * @param srcValue The value to match.\n * @return Returns the new function.\n */\n matchesProperty(\n path: Many,\n srcValue: T\n ): (value: any) => boolean;\n\n /**\n * @see _.matchesProperty\n */\n matchesProperty(\n path: Many,\n srcValue: T\n ): (value: V) => boolean;\n }\n\n interface LoDashImplicitWrapperBase {\n /**\n * @see _.matchesProperty\n */\n matchesProperty(\n srcValue: SrcValue\n ): LoDashImplicitObjectWrapper<(value: any) => boolean>;\n\n /**\n * @see _.matchesProperty\n */\n matchesProperty(\n srcValue: SrcValue\n ): LoDashImplicitObjectWrapper<(value: Value) => boolean>;\n }\n\n interface LoDashExplicitWrapperBase {\n /**\n * @see _.matchesProperty\n */\n matchesProperty(\n srcValue: SrcValue\n ): LoDashExplicitObjectWrapper<(value: any) => boolean>;\n\n /**\n * @see _.matchesProperty\n */\n matchesProperty(\n srcValue: SrcValue\n ): LoDashExplicitObjectWrapper<(value: Value) => boolean>;\n }\n\n //_.method\n interface LoDashStatic {\n /**\n * Creates a function that invokes the method at path on a given object. Any additional arguments are provided\n * to the invoked method.\n *\n * @param path The path of the method to invoke.\n * @param args The arguments to invoke the method with.\n * @return Returns the new function.\n */\n method(\n path: string|StringRepresentable[],\n ...args: any[]\n ): (object: TObject) => TResult;\n\n /**\n * @see _.method\n */\n method(\n path: string|StringRepresentable[],\n ...args: any[]\n ): (object: any) => TResult;\n }\n\n interface LoDashImplicitWrapper {\n /**\n * @see _.method\n */\n method(...args: any[]): LoDashImplicitObjectWrapper<(object: TObject) => TResult>;\n\n /**\n * @see _.method\n */\n method(...args: any[]): LoDashImplicitObjectWrapper<(object: any) => TResult>;\n }\n\n interface LoDashImplicitArrayWrapper {\n /**\n * @see _.method\n */\n method(...args: any[]): LoDashImplicitObjectWrapper<(object: TObject) => TResult>;\n\n /**\n * @see _.method\n */\n method(...args: any[]): LoDashImplicitObjectWrapper<(object: any) => TResult>;\n }\n\n interface LoDashExplicitWrapper {\n /**\n * @see _.method\n */\n method(...args: any[]): LoDashExplicitObjectWrapper<(object: TObject) => TResult>;\n\n /**\n * @see _.method\n */\n method(...args: any[]): LoDashExplicitObjectWrapper<(object: any) => TResult>;\n }\n\n interface LoDashExplicitArrayWrapper {\n /**\n * @see _.method\n */\n method(...args: any[]): LoDashExplicitObjectWrapper<(object: TObject) => TResult>;\n\n /**\n * @see _.method\n */\n method(...args: any[]): LoDashExplicitObjectWrapper<(object: any) => TResult>;\n }\n\n //_.methodOf\n interface LoDashStatic {\n /**\n * The opposite of _.method; this method creates a function that invokes the method at a given path on object.\n * Any additional arguments are provided to the invoked method.\n *\n * @param object The object to query.\n * @param args The arguments to invoke the method with.\n * @return Returns the new function.\n */\n methodOf(\n object: TObject,\n ...args: any[]\n ): (path: Many) => TResult;\n\n /**\n * @see _.methodOf\n */\n methodOf(\n object: {},\n ...args: any[]\n ): (path: Many) => TResult;\n }\n\n interface LoDashImplicitObjectWrapper {\n /**\n * @see _.methodOf\n */\n methodOf(\n ...args: any[]\n ): LoDashImplicitObjectWrapper<(path: Many) => TResult>;\n }\n\n interface LoDashExplicitObjectWrapper {\n /**\n * @see _.methodOf\n */\n methodOf(\n ...args: any[]\n ): LoDashExplicitObjectWrapper<(path: Many) => TResult>;\n }\n\n //_.mixin\n interface MixinOptions {\n chain?: boolean;\n }\n\n interface LoDashStatic {\n /**\n * Adds all own enumerable function properties of a source object to the destination object. If object is a\n * function then methods are added to its prototype as well.\n *\n * Note: Use _.runInContext to create a pristine lodash function to avoid conflicts caused by modifying\n * the original.\n *\n * @param object The destination object.\n * @param source The object of functions to add.\n * @param options The options object.\n * @param options.chain Specify whether the functions added are chainable.\n * @return Returns object.\n */\n mixin(\n object: TObject,\n source: Dictionary,\n options?: MixinOptions\n ): TResult;\n\n /**\n * @see _.mixin\n */\n mixin(\n source: Dictionary,\n options?: MixinOptions\n ): TResult;\n }\n\n interface LoDashImplicitObjectWrapper {\n /**\n * @see _.mixin\n */\n mixin(\n source: Dictionary,\n options?: MixinOptions\n ): LoDashImplicitObjectWrapper;\n\n /**\n * @see _.mixin\n */\n mixin(\n options?: MixinOptions\n ): LoDashImplicitObjectWrapper;\n }\n\n interface LoDashExplicitObjectWrapper {\n /**\n * @see _.mixin\n */\n mixin(\n source: Dictionary,\n options?: MixinOptions\n ): LoDashExplicitObjectWrapper;\n\n /**\n * @see _.mixin\n */\n mixin(\n options?: MixinOptions\n ): LoDashExplicitObjectWrapper;\n }\n\n //_.noConflict\n interface LoDashStatic {\n /**\n * Reverts the _ variable to its previous value and returns a reference to the lodash function.\n *\n * @return Returns the lodash function.\n */\n noConflict(): typeof _;\n }\n\n interface LoDashImplicitWrapperBase {\n /**\n * @see _.noConflict\n */\n noConflict(): typeof _;\n }\n\n interface LoDashExplicitWrapperBase {\n /**\n * @see _.noConflict\n */\n noConflict(): LoDashExplicitObjectWrapper;\n }\n\n //_.noop\n interface LoDashStatic {\n /**\n * A no-operation function that returns undefined regardless of the arguments it receives.\n *\n * @return undefined\n */\n noop(...args: any[]): void;\n }\n\n interface LoDashImplicitWrapperBase {\n /**\n * @see _.noop\n */\n noop(...args: any[]): void;\n }\n\n interface LoDashExplicitWrapperBase {\n /**\n * @see _.noop\n */\n noop(...args: any[]): _.LoDashExplicitWrapper;\n }\n\n //_.nthArg\n interface LoDashStatic {\n /**\n * Creates a function that returns its nth argument.\n *\n * @param n The index of the argument to return.\n * @return Returns the new function.\n */\n nthArg(n?: number): TResult;\n }\n\n interface LoDashImplicitWrapper {\n /**\n * @see _.nthArg\n */\n nthArg(): LoDashImplicitObjectWrapper;\n }\n\n interface LoDashExplicitWrapper {\n /**\n * @see _.nthArg\n */\n nthArg(): LoDashExplicitObjectWrapper;\n }\n\n //_.over\n interface LoDashStatic {\n /**\n * Creates a function that invokes iteratees with the arguments provided to the created function and returns\n * their results.\n *\n * @param iteratees The iteratees to invoke.\n * @return Returns the new function.\n */\n over(...iteratees: Array>): (...args: any[]) => TResult[];\n }\n\n interface LoDashImplicitArrayWrapper {\n /**\n * @see _.over\n */\n over(...iteratees: Array>): LoDashImplicitObjectWrapper<(...args: any[]) => TResult[]>;\n }\n\n interface LoDashImplicitObjectWrapper {\n /**\n * @see _.over\n */\n over(...iteratees: Array>): LoDashImplicitObjectWrapper<(...args: any[]) => TResult[]>;\n }\n\n interface LoDashExplicitArrayWrapper {\n /**\n * @see _.over\n */\n over(...iteratees: Array>): LoDashExplicitObjectWrapper<(...args: any[]) => TResult[]>;\n }\n\n interface LoDashExplicitObjectWrapper {\n /**\n * @see _.over\n */\n over(...iteratees: Array>): LoDashExplicitObjectWrapper<(...args: any[]) => TResult[]>;\n }\n\n //_.overEvery\n interface LoDashStatic {\n /**\n * Creates a function that checks if all of the predicates return truthy when invoked with the arguments\n * provided to the created function.\n *\n * @param predicates The predicates to check.\n * @return Returns the new function.\n */\n overEvery(...predicates: Array>): (...args: any[]) => boolean;\n }\n\n interface LoDashImplicitArrayWrapper {\n /**\n * @see _.overEvery\n */\n overEvery(...predicates: Array>): LoDashImplicitObjectWrapper<(...args: any[]) => boolean>;\n }\n\n interface LoDashImplicitObjectWrapper {\n /**\n * @see _.overEvery\n */\n overEvery(...predicates: Array>): LoDashImplicitObjectWrapper<(...args: any[]) => boolean>;\n }\n\n interface LoDashExplicitArrayWrapper {\n /**\n * @see _.overEvery\n */\n overEvery(...predicates: Array>): LoDashExplicitObjectWrapper<(...args: any[]) => boolean>;\n }\n\n interface LoDashExplicitObjectWrapper {\n /**\n * @see _.overEvery\n */\n overEvery(...predicates: Array>): LoDashExplicitObjectWrapper<(...args: any[]) => boolean>;\n }\n\n //_.overSome\n interface LoDashStatic {\n /**\n * Creates a function that checks if any of the predicates return truthy when invoked with the arguments\n * provided to the created function.\n *\n * @param predicates The predicates to check.\n * @return Returns the new function.\n */\n overSome(...predicates: Array>): (...args: any[]) => boolean;\n }\n\n interface LoDashImplicitArrayWrapper {\n /**\n * @see _.overSome\n */\n overSome(...predicates: Array>): LoDashImplicitObjectWrapper<(...args: any[]) => boolean>;\n }\n\n interface LoDashImplicitObjectWrapper {\n /**\n * @see _.overSome\n */\n overSome(...predicates: Array>): LoDashImplicitObjectWrapper<(...args: any[]) => boolean>;\n }\n\n interface LoDashExplicitArrayWrapper {\n /**\n * @see _.overSome\n */\n overSome(...predicates: Array>): LoDashExplicitObjectWrapper<(...args: any[]) => boolean>;\n }\n\n interface LoDashExplicitObjectWrapper {\n /**\n * @see _.overSome\n */\n overSome(...predicates: Array>): LoDashExplicitObjectWrapper<(...args: any[]) => boolean>;\n }\n\n //_.property\n interface LoDashStatic {\n /**\n * Creates a function that returns the property value at path on a given object.\n *\n * @param path The path of the property to get.\n * @return Returns the new function.\n */\n property(path: Many): (obj: TObj) => TResult;\n }\n\n interface LoDashImplicitWrapper {\n /**\n * @see _.property\n */\n property(): LoDashImplicitObjectWrapper<(obj: TObj) => TResult>;\n }\n\n interface LoDashImplicitArrayWrapper {\n /**\n * @see _.property\n */\n property(): LoDashImplicitObjectWrapper<(obj: TObj) => TResult>;\n }\n\n interface LoDashExplicitWrapper {\n /**\n * @see _.property\n */\n property(): LoDashExplicitObjectWrapper<(obj: TObj) => TResult>;\n }\n\n interface LoDashExplicitArrayWrapper {\n /**\n * @see _.property\n */\n property(): LoDashExplicitObjectWrapper<(obj: TObj) => TResult>;\n }\n\n //_.propertyOf\n interface LoDashStatic {\n /**\n * The opposite of _.property; this method creates a function that returns the property value at a given path\n * on object.\n *\n * @param object The object to query.\n * @return Returns the new function.\n */\n propertyOf(object: T): (path: Many) => any;\n }\n\n interface LoDashImplicitObjectWrapper {\n /**\n * @see _.propertyOf\n */\n propertyOf(): LoDashImplicitObjectWrapper<(path: Many) => any>;\n }\n\n interface LoDashExplicitObjectWrapper {\n /**\n * @see _.propertyOf\n */\n propertyOf(): LoDashExplicitObjectWrapper<(path: Many) => any>;\n }\n\n //_.range\n interface LoDashStatic {\n /**\n * Creates an array of numbers (positive and/or negative) progressing from start up to, but not including, end.\n * If end is not specified it’s set to start with start then set to 0. If end is less than start a zero-length\n * range is created unless a negative step is specified.\n *\n * @param start The start of the range.\n * @param end The end of the range.\n * @param step The value to increment or decrement by.\n * @return Returns a new range array.\n */\n range(\n start: number,\n end: number,\n step?: number\n ): number[];\n\n /**\n * @see _.range\n */\n range(\n end: number,\n step?: number\n ): number[];\n }\n\n interface LoDashImplicitWrapper {\n /**\n * @see _.range\n */\n range(\n end?: number,\n step?: number\n ): LoDashImplicitArrayWrapper;\n }\n\n interface LoDashExplicitWrapper {\n /**\n * @see _.range\n */\n range(\n end?: number,\n step?: number\n ): LoDashExplicitArrayWrapper;\n }\n\n //_.rangeRight\n interface LoDashStatic {\n /**\n * This method is like `_.range` except that it populates values in\n * descending order.\n *\n * @static\n * @memberOf _\n * @category Util\n * @param {number} [start=0] The start of the range.\n * @param {number} end The end of the range.\n * @param {number} [step=1] The value to increment or decrement by.\n * @returns {Array} Returns the new array of numbers.\n * @example\n *\n * _.rangeRight(4);\n * // => [3, 2, 1, 0]\n *\n * _.rangeRight(-4);\n * // => [-3, -2, -1, 0]\n *\n * _.rangeRight(1, 5);\n * // => [4, 3, 2, 1]\n *\n * _.rangeRight(0, 20, 5);\n * // => [15, 10, 5, 0]\n *\n * _.rangeRight(0, -4, -1);\n * // => [-3, -2, -1, 0]\n *\n * _.rangeRight(1, 4, 0);\n * // => [1, 1, 1]\n *\n * _.rangeRight(0);\n * // => []\n */\n rangeRight(\n start: number,\n end: number,\n step?: number\n ): number[];\n\n /**\n * @see _.rangeRight\n */\n rangeRight(\n end: number,\n step?: number\n ): number[];\n }\n\n interface LoDashImplicitWrapper {\n /**\n * @see _.rangeRight\n */\n rangeRight(\n end?: number,\n step?: number\n ): LoDashImplicitArrayWrapper;\n }\n\n interface LoDashExplicitWrapper {\n /**\n * @see _.rangeRight\n */\n rangeRight(\n end?: number,\n step?: number\n ): LoDashExplicitArrayWrapper;\n }\n\n //_.runInContext\n interface LoDashStatic {\n /**\n * Create a new pristine lodash function using the given context object.\n *\n * @param context The context object.\n * @return Returns a new lodash function.\n */\n runInContext(context?: Object): typeof _;\n }\n\n interface LoDashImplicitObjectWrapper {\n /**\n * @see _.runInContext\n */\n runInContext(): typeof _;\n }\n\n // _.stubArray\n interface LoDashStatic {\n /**\n * This method returns a new empty array.\n *\n * @returns Returns the new empty array.\n */\n stubArray(): any[];\n }\n\n interface LoDashImplicitWrapperBase {\n /**\n * @see _.stubArray\n */\n stubArray(): any[];\n }\n\n interface LoDashExplicitWrapperBase {\n /**\n * @see _.stubArray\n */\n stubArray(): _.LoDashExplicitArrayWrapper;\n }\n\n // _.stubFalse\n interface LoDashStatic {\n /**\n * This method returns `false`.\n *\n * @returns Returns `false`.\n */\n stubFalse(): boolean;\n }\n\n interface LoDashImplicitWrapperBase {\n /**\n * @see _.stubFalse\n */\n stubFalse(): boolean;\n }\n\n interface LoDashExplicitWrapperBase {\n /**\n * @see _.stubFalse\n */\n stubFalse(): _.LoDashExplicitWrapper;\n }\n\n interface LoDashStatic {\n /**\n * This method returns a new empty object.\n *\n * @returns Returns the new empty object.\n */\n stubObject(): Object;\n }\n\n interface LoDashImplicitWrapperBase {\n /**\n * @see _.stubObject\n */\n stubObject(): Object;\n }\n\n interface LoDashExplicitWrapperBase {\n /**\n * @see _.stubObject\n */\n stubObject(): _.LoDashExplicitObjectWrapper;\n }\n\n interface LoDashStatic {\n /**\n * This method returns an empty string.\n *\n * @returns Returns the empty string.\n */\n stubString(): string;\n }\n\n interface LoDashImplicitWrapperBase {\n /**\n * @see _.stubString\n */\n stubString(): string;\n }\n\n interface LoDashExplicitWrapperBase {\n /**\n * @see _.stubString\n */\n stubString(): _.LoDashExplicitWrapper;\n }\n\n interface LoDashStatic {\n /**\n * This method returns `true`.\n *\n * @returns Returns `true`.\n */\n stubTrue(): boolean;\n }\n\n interface LoDashImplicitWrapperBase {\n /**\n * @see _.stubTrue\n */\n stubTrue(): boolean;\n }\n\n interface LoDashExplicitWrapperBase {\n /**\n * @see _.stubTrue\n */\n stubTrue(): _.LoDashExplicitWrapper;\n }\n\n //_.times\n interface LoDashStatic {\n /**\n * Invokes the iteratee function n times, returning an array of the results of each invocation. The iteratee\n * is invoked with one argument; (index).\n *\n * @param n The number of times to invoke iteratee.\n * @param iteratee The function invoked per iteration.\n * @return Returns the array of results.\n */\n times(\n n: number,\n iteratee: (num: number) => TResult\n ): TResult[];\n\n /**\n * @see _.times\n */\n times(n: number): number[];\n }\n\n interface LoDashImplicitWrapper {\n /**\n * @see _.times\n */\n times(\n iteratee: (num: number) => TResult\n ): TResult[];\n\n /**\n * @see _.times\n */\n times(): number[];\n }\n\n interface LoDashExplicitWrapper {\n /**\n * @see _.times\n */\n times(\n iteratee: (num: number) => TResult\n ): LoDashExplicitArrayWrapper;\n\n /**\n * @see _.times\n */\n times(): LoDashExplicitArrayWrapper;\n }\n\n //_.toPath\n interface LoDashStatic {\n /**\n * Converts `value` to a property path array.\n *\n * @static\n * @memberOf _\n * @category Util\n * @param {*} value The value to convert.\n * @returns {Array} Returns the new property path array.\n * @example\n *\n * _.toPath('a.b.c');\n * // => ['a', 'b', 'c']\n *\n * _.toPath('a[0].b.c');\n * // => ['a', '0', 'b', 'c']\n *\n * var path = ['a', 'b', 'c'],\n * newPath = _.toPath(path);\n *\n * console.log(newPath);\n * // => ['a', 'b', 'c']\n *\n * console.log(path === newPath);\n * // => false\n */\n toPath(value: any): string[];\n }\n\n interface LoDashImplicitWrapperBase {\n /**\n * @see _.toPath\n */\n toPath(): LoDashImplicitWrapper;\n }\n\n interface LoDashExplicitWrapperBase {\n /**\n * @see _.toPath\n */\n toPath(): LoDashExplicitWrapper;\n }\n\n //_.uniqueId\n interface LoDashStatic {\n /**\n * Generates a unique ID. If prefix is provided the ID is appended to it.\n *\n * @param prefix The value to prefix the ID with.\n * @return Returns the unique ID.\n */\n uniqueId(prefix?: string): string;\n }\n\n interface LoDashImplicitWrapper {\n /**\n * @see _.uniqueId\n */\n uniqueId(): string;\n }\n\n interface LoDashExplicitWrapper {\n /**\n * @see _.uniqueId\n */\n uniqueId(): LoDashExplicitWrapper;\n }\n\n type ListIterator = (value: T, index: number, collection: List) => TResult;\n\n type ListIteratorTypeGuard = (value: T, index: number, collection: List) => value is S;\n\n type DictionaryIterator = (value: T, key: string, collection: Dictionary) => TResult;\n\n type DictionaryIteratorTypeGuard = (value: T, key: string, collection: Dictionary) => value is S;\n\n type NumericDictionaryIterator = (value: T, key: number, collection: Dictionary) => TResult;\n\n type ObjectIterator = (element: T, key: string, collection: any) => TResult;\n\n type StringIterator = (char: string, index: number, string: string) => TResult;\n\n type MemoVoidIterator = (prev: TResult, curr: T, indexOrKey: any, list: T[]) => void;\n\n type MemoIterator = (prev: TResult, curr: T, indexOrKey: any, list: T[]) => TResult;\n\n type MemoVoidArrayIterator = (acc: TResult, curr: T, index: number, arr: T[]) => void;\n type MemoVoidDictionaryIterator = (acc: TResult, curr: T, key: string, dict: Dictionary) => void;\n\n /** Common interface between Arrays and jQuery objects */\n type List = ArrayLike;\n\n interface Dictionary {\n [index: string]: T;\n }\n\n interface NumericDictionary {\n [index: number]: T;\n }\n\n interface StringRepresentable {\n toString(): string;\n }\n\n interface Cancelable {\n cancel(): void;\n flush(): void;\n }\n}\n\n// Backward compatibility with --target es5\ndeclare global {\n interface Set { }\n interface Map { }\n interface WeakSet { }\n interface WeakMap { }\n}\n" } + ); + verifyAfterPartialOrCompleteNpmInstall(2); + + filesAndFoldersToAdd.push( + { "path": "/a/b/node_modules/.staging/rxjs-22375c61/src/scheduler" }, + { "path": "/a/b/node_modules/.staging/rxjs-22375c61/src/util" }, + { "path": "/a/b/node_modules/.staging/rxjs-22375c61/symbol" }, + { "path": "/a/b/node_modules/.staging/rxjs-22375c61/testing" }, + { "path": "/a/b/node_modules/.staging/rxjs-22375c61/package.json.2252192041", "content": "{\n \"_args\": [\n [\n {\n \"raw\": \"rxjs@^5.4.2\",\n \"scope\": null,\n \"escapedName\": \"rxjs\",\n \"name\": \"rxjs\",\n \"rawSpec\": \"^5.4.2\",\n \"spec\": \">=5.4.2 <6.0.0\",\n \"type\": \"range\"\n },\n \"C:\\\\Users\\\\shkamat\\\\Desktop\\\\app\"\n ]\n ],\n \"_from\": \"rxjs@>=5.4.2 <6.0.0\",\n \"_id\": \"rxjs@5.4.3\",\n \"_inCache\": true,\n \"_location\": \"/rxjs\",\n \"_nodeVersion\": \"7.7.2\",\n \"_npmOperationalInternal\": {\n \"host\": \"s3://npm-registry-packages\",\n \"tmp\": \"tmp/rxjs-5.4.3.tgz_1502407898166_0.6800217325799167\"\n },\n \"_npmUser\": {\n \"name\": \"blesh\",\n \"email\": \"ben@benlesh.com\"\n },\n \"_npmVersion\": \"5.3.0\",\n \"_phantomChildren\": {},\n \"_requested\": {\n \"raw\": \"rxjs@^5.4.2\",\n \"scope\": null,\n \"escapedName\": \"rxjs\",\n \"name\": \"rxjs\",\n \"rawSpec\": \"^5.4.2\",\n \"spec\": \">=5.4.2 <6.0.0\",\n \"type\": \"range\"\n },\n \"_requiredBy\": [\n \"/\"\n ],\n \"_resolved\": \"https://registry.npmjs.org/rxjs/-/rxjs-5.4.3.tgz\",\n \"_shasum\": \"0758cddee6033d68e0fd53676f0f3596ce3d483f\",\n \"_shrinkwrap\": null,\n \"_spec\": \"rxjs@^5.4.2\",\n \"_where\": \"C:\\\\Users\\\\shkamat\\\\Desktop\\\\app\",\n \"author\": {\n \"name\": \"Ben Lesh\",\n \"email\": \"ben@benlesh.com\"\n },\n \"bugs\": {\n \"url\": \"https://github.com/ReactiveX/RxJS/issues\"\n },\n \"config\": {\n \"commitizen\": {\n \"path\": \"cz-conventional-changelog\"\n }\n },\n \"contributors\": [\n {\n \"name\": \"Ben Lesh\",\n \"email\": \"ben@benlesh.com\"\n },\n {\n \"name\": \"Paul Taylor\",\n \"email\": \"paul.e.taylor@me.com\"\n },\n {\n \"name\": \"Jeff Cross\",\n \"email\": \"crossj@google.com\"\n },\n {\n \"name\": \"Matthew Podwysocki\",\n \"email\": \"matthewp@microsoft.com\"\n },\n {\n \"name\": \"OJ Kwon\",\n \"email\": \"kwon.ohjoong@gmail.com\"\n },\n {\n \"name\": \"Andre Staltz\",\n \"email\": \"andre@staltz.com\"\n }\n ],\n \"dependencies\": {\n \"symbol-observable\": \"^1.0.1\"\n },\n \"description\": \"Reactive Extensions for modern JavaScript\",\n \"devDependencies\": {\n \"babel-polyfill\": \"^6.23.0\",\n \"benchmark\": \"^2.1.0\",\n \"benchpress\": \"2.0.0-beta.1\",\n \"chai\": \"^3.5.0\",\n \"color\": \"^0.11.1\",\n \"colors\": \"1.1.2\",\n \"commitizen\": \"^2.8.6\",\n \"coveralls\": \"^2.11.13\",\n \"cz-conventional-changelog\": \"^1.2.0\",\n \"danger\": \"^1.1.0\",\n \"doctoc\": \"^1.0.0\",\n \"escape-string-regexp\": \"^1.0.5 \",\n \"esdoc\": \"^0.4.7\",\n \"eslint\": \"^3.8.0\",\n \"fs-extra\": \"^2.1.2\",\n \"get-folder-size\": \"^1.0.0\",\n \"glob\": \"^7.0.3\",\n \"gm\": \"^1.22.0\",\n \"google-closure-compiler-js\": \"^20170218.0.0\",\n \"gzip-size\": \"^3.0.0\",\n \"http-server\": \"^0.9.0\",\n \"husky\": \"^0.13.3\",\n \"lint-staged\": \"3.2.5\",\n \"lodash\": \"^4.15.0\",\n \"madge\": \"^1.4.3\",\n \"markdown-doctest\": \"^0.9.1\",\n \"minimist\": \"^1.2.0\",\n \"mkdirp\": \"^0.5.1\",\n \"mocha\": \"^3.0.2\",\n \"mocha-in-sauce\": \"0.0.1\",\n \"npm-run-all\": \"^4.0.2\",\n \"npm-scripts-info\": \"^0.3.4\",\n \"nyc\": \"^10.2.0\",\n \"opn-cli\": \"^3.1.0\",\n \"platform\": \"^1.3.1\",\n \"promise\": \"^7.1.1\",\n \"protractor\": \"^3.1.1\",\n \"rollup\": \"0.36.3\",\n \"rollup-plugin-inject\": \"^2.0.0\",\n \"rollup-plugin-node-resolve\": \"^2.0.0\",\n \"rx\": \"latest\",\n \"rxjs\": \"latest\",\n \"shx\": \"^0.2.2\",\n \"sinon\": \"^2.1.0\",\n \"sinon-chai\": \"^2.9.0\",\n \"source-map-support\": \"^0.4.0\",\n \"tslib\": \"^1.5.0\",\n \"tslint\": \"^4.4.2\",\n \"typescript\": \"~2.0.6\",\n \"typings\": \"^2.0.0\",\n \"validate-commit-msg\": \"^2.14.0\",\n \"watch\": \"^1.0.1\",\n \"webpack\": \"^1.13.1\",\n \"xmlhttprequest\": \"1.8.0\"\n },\n \"directories\": {},\n \"dist\": {\n \"integrity\": \"sha512-fSNi+y+P9ss+EZuV0GcIIqPUK07DEaMRUtLJvdcvMyFjc9dizuDjere+A4V7JrLGnm9iCc+nagV/4QdMTkqC4A==\",\n \"shasum\": \"0758cddee6033d68e0fd53676f0f3596ce3d483f\",\n \"tarball\": \"https://registry.npmjs.org/rxjs/-/rxjs-5.4.3.tgz\"\n },\n \"engines\": {\n \"npm\": \">=2.0.0\"\n },\n \"homepage\": \"https://github.com/ReactiveX/RxJS\",\n \"keywords\": [\n \"Rx\",\n \"RxJS\",\n \"ReactiveX\",\n \"ReactiveExtensions\",\n \"Streams\",\n \"Observables\",\n \"Observable\",\n \"Stream\",\n \"ES6\",\n \"ES2015\"\n ],\n \"license\": \"Apache-2.0\",\n \"lint-staged\": {\n \"*.@(js)\": [\n \"eslint --fix\",\n \"git add\"\n ],\n \"*.@(ts)\": [\n \"tslint --fix\",\n \"git add\"\n ]\n },\n \"main\": \"Rx.js\",\n \"maintainers\": [\n {\n \"name\": \"blesh\",\n \"email\": \"ben@benlesh.com\"\n }\n ],\n \"name\": \"rxjs\",\n \"optionalDependencies\": {},\n \"readme\": \"ERROR: No README data found!\",\n \"repository\": {\n \"type\": \"git\",\n \"url\": \"git+ssh://git@github.com/ReactiveX/RxJS.git\"\n },\n \"scripts-info\": {\n \"info\": \"List available script\",\n \"build_all\": \"Build all packages (ES6, CJS, UMD) and generate packages\",\n \"build_cjs\": \"Build CJS package with clean up existing build, copy source into dist\",\n \"build_es6\": \"Build ES6 package with clean up existing build, copy source into dist\",\n \"build_closure_core\": \"Minify Global core build using closure compiler\",\n \"build_global\": \"Build Global package, then minify build\",\n \"build_perf\": \"Build CJS & Global build, run macro performance test\",\n \"build_test\": \"Build CJS package & test spec, execute mocha test runner\",\n \"build_cover\": \"Run lint to current code, build CJS & test spec, execute test coverage\",\n \"build_docs\": \"Build ES6 & global package, create documentation using it\",\n \"build_spec\": \"Build test specs\",\n \"check_circular_dependencies\": \"Check codebase has circular dependencies\",\n \"clean_spec\": \"Clean up existing test spec build output\",\n \"clean_dist_cjs\": \"Clean up existing CJS package output\",\n \"clean_dist_es6\": \"Clean up existing ES6 package output\",\n \"clean_dist_global\": \"Clean up existing Global package output\",\n \"commit\": \"Run git commit wizard\",\n \"compile_dist_cjs\": \"Compile codebase into CJS module\",\n \"compile_module_es6\": \"Compile codebase into ES6\",\n \"cover\": \"Execute test coverage\",\n \"lint_perf\": \"Run lint against performance test suite\",\n \"lint_spec\": \"Run lint against test spec\",\n \"lint_src\": \"Run lint against source\",\n \"lint\": \"Run lint against everything\",\n \"perf\": \"Run macro performance benchmark\",\n \"perf_micro\": \"Run micro performance benchmark\",\n \"test_mocha\": \"Execute mocha test runner against existing test spec build\",\n \"test_browser\": \"Execute mocha test runner on browser against existing test spec build\",\n \"test\": \"Clean up existing test spec build, build test spec and execute mocha test runner\",\n \"tests2png\": \"Generate marble diagram image from test spec\",\n \"watch\": \"Watch codebase, trigger compile when source code changes\"\n },\n \"typings\": \"Rx.d.ts\",\n \"version\": \"5.4.3\"\n}\n" } + ); + verifyAfterPartialOrCompleteNpmInstall(0); + + // remove /a/b/node_modules/.staging/rxjs-22375c61/package.json.2252192041 + filesAndFoldersToAdd.length--; + // and add few more folders/files + filesAndFoldersToAdd.push( + { "path": "/a/b/node_modules/symbol-observable" }, + { "path": "/a/b/node_modules/@types" }, + { "path": "/a/b/node_modules/@types/lodash" }, + { "path": "/a/b/node_modules/lodash" }, + { "path": "/a/b/node_modules/rxjs" }, + { "path": "/a/b/node_modules/typescript" }, + { "path": "/a/b/node_modules/.bin" } + ); + verifyAfterPartialOrCompleteNpmInstall(0); + + forEach(filesAndFoldersToAdd, f => { + f.path = f.path + .replace("/a/b/node_modules/.staging", "/a/b/node_modules") + .replace(/[\-\.][\d\w][\d\w][\d\w][\d\w][\d\w][\d\w][\d\w][\d\w]/g, ""); + }); + + const lodashIndexPath = "/a/b/node_modules/@types/lodash/index.d.ts"; + projectFiles.push(find(filesAndFoldersToAdd, f => f.path === lodashIndexPath)); + watchedModuleLocations.length = watchedModuleLocations.indexOf(lodashIndexPath); + // npm installation complete, timeout after reload fs + timeoutAfterReloadFs = true; + verifyAfterPartialOrCompleteNpmInstall(2); + + function verifyAfterPartialOrCompleteNpmInstall(timeoutQueueLengthWhenRunningTimeouts: number) { + host.reloadFS(projectFiles.concat(otherFiles, filesAndFoldersToAdd)); + if (timeoutAfterReloadFs) { + host.checkTimeoutQueueLengthAndRun(timeoutQueueLengthWhenRunningTimeouts); + } + else { + host.checkTimeoutQueueLength(2); + } + verifyProject(); + } + + function verifyProject() { + checkNumberOfConfiguredProjects(projectService, 1); + + const project = projectService.configuredProjects.get(tsconfigJson.path); + const projectFilePaths = map(projectFiles, f => f.path); + checkProjectActualFiles(project, projectFilePaths); + + const filesWatched = filter(projectFilePaths, p => p !== app.path).concat(watchedModuleLocations); + checkWatchedFiles(host, filesWatched); + checkWatchedDirectories(host, [appFolder], /*recursive*/ true); + checkWatchedDirectories(host, [], /*recursive*/ false); + } + + function getNodeModulesWatchedDirectories(path: string, module: string): string[] { + const nodeModulesDir = combinePaths(path, "node_modules/"); + const parentDir = getDirectoryPath(path); + const parentNodeModules = parentDir !== path ? getNodeModulesWatchedDirectories(parentDir, module) : []; + return [ + `${nodeModulesDir}${module}.ts`, + `${nodeModulesDir}${module}.tsx`, + `${nodeModulesDir}${module}.d.ts`, + `${nodeModulesDir}${module}/index.ts`, + `${nodeModulesDir}${module}/index.tsx`, + `${nodeModulesDir}${module}/index.d.ts`, + `${nodeModulesDir}@types/${module}.d.ts`, + `${nodeModulesDir}@types/${module}/index.d.ts`, + `${nodeModulesDir}@types/${module}/package.json`, + `${nodeModulesDir}${module}.js`, + `${nodeModulesDir}${module}.jsx`, + `${nodeModulesDir}${module}/package.json`, + `${nodeModulesDir}${module}/index.js`, + `${nodeModulesDir}${module}/index.jsx`, + ].concat(parentNodeModules); + } + } + + it("timeouts occur inbetween installation", () => { + verifyNpmInstall(/*timeoutDuringPartialInstallation*/ true); + }); + + it("timeout occurs after installation", () => { + verifyNpmInstall(/*timeoutDuringPartialInstallation*/ false); + }); }); }); } From e71123857c1a4088f34a8a6d236b256ef2ffdc86 Mon Sep 17 00:00:00 2001 From: Sheetal Nandi Date: Tue, 15 Aug 2017 14:50:52 -0700 Subject: [PATCH 074/109] Add api in builder to get changed files and use it to send project changed event --- src/compiler/builder.ts | 224 ++++++++++-------- .../unittests/tsserverProjectSystem.ts | 4 +- src/server/editorServices.ts | 46 ++-- src/server/project.ts | 15 +- src/server/protocol.ts | 23 ++ src/server/session.ts | 58 +++-- 6 files changed, 226 insertions(+), 144 deletions(-) diff --git a/src/compiler/builder.ts b/src/compiler/builder.ts index 04d520cb2286c..d96c3a547c0c8 100644 --- a/src/compiler/builder.ts +++ b/src/compiler/builder.ts @@ -18,6 +18,13 @@ namespace ts { text: string; } + export interface ChangedProgramFiles { + /** Minimal set of list of files that require emit */ + readonly filesToEmit: ReadonlyArray; + /** File paths of source files changed/added/removed or affected by changed files */ + readonly changedFiles: ReadonlyArray; + } + export interface Builder { /** * This is the callback when file infos in the builder are updated @@ -25,8 +32,15 @@ namespace ts { onProgramUpdateGraph(program: Program, hasInvalidatedResolution: HasInvalidatedResolution): void; getFilesAffectedBy(program: Program, path: Path): string[]; emitFile(program: Program, path: Path): EmitOutput; + + /** Emit the changed files and clear the cache of the changed files */ emitChangedFiles(program: Program): EmitOutputDetailed[]; + /** Get the changed files since last query and then clear the cache of changed files */ + getChangedProgramFiles(program: Program): ChangedProgramFiles; + /** When called gets the semantic diagnostics for the program. It also caches the diagnostics and manage them */ getSemanticDiagnostics(program: Program, cancellationToken?: CancellationToken): Diagnostic[]; + + /** Called to reset the status of the builder */ clear(): void; } @@ -73,16 +87,18 @@ namespace ts { ): Builder { let isModuleEmit: boolean | undefined; // Last checked shape signature for the file info - type FileInfo = { version: string; signature: string; }; - let fileInfos: Map; + type FileInfo = { fileName: string; version: string; signature: string; }; + const fileInfos = createMap(); const semanticDiagnosticsPerFile = createMap(); - let changedFilesSinceLastEmit: Map; + /** The map has key by source file's path that has been changed */ + const changedFileNames = createMap(); let emitHandler: EmitHandler; return { onProgramUpdateGraph, getFilesAffectedBy, emitFile, emitChangedFiles, + getChangedProgramFiles, getSemanticDiagnostics, clear }; @@ -92,13 +108,11 @@ namespace ts { if (isModuleEmit !== currentIsModuleEmit) { isModuleEmit = currentIsModuleEmit; emitHandler = isModuleEmit ? getModuleEmitHandler() : getNonModuleEmitHandler(); - fileInfos = undefined; + fileInfos.clear(); semanticDiagnosticsPerFile.clear(); } - - changedFilesSinceLastEmit = changedFilesSinceLastEmit || createMap(); mutateMap( - fileInfos || (fileInfos = createMap()), + fileInfos, arrayToMap(program.getSourceFiles(), sourceFile => sourceFile.path), { // Add new file info @@ -111,27 +125,26 @@ namespace ts { ); } - function registerChangedFile(path: Path) { - changedFilesSinceLastEmit.set(path, true); + function registerChangedFile(path: Path, fileName: string) { + changedFileNames.set(path, fileName); // All changed files need to re-evaluate its semantic diagnostics semanticDiagnosticsPerFile.delete(path); } function addNewFileInfo(program: Program, sourceFile: SourceFile): FileInfo { - registerChangedFile(sourceFile.path); + registerChangedFile(sourceFile.path, sourceFile.fileName); emitHandler.addScriptInfo(program, sourceFile); - return { version: sourceFile.version, signature: undefined }; + return { fileName: sourceFile.fileName, version: sourceFile.version, signature: undefined }; } - function removeExistingFileInfo(path: Path, _existingFileInfo: FileInfo) { - registerChangedFile(path); + function removeExistingFileInfo(path: Path, existingFileInfo: FileInfo) { + registerChangedFile(path, existingFileInfo.fileName); emitHandler.removeScriptInfo(path); } function updateExistingFileInfo(program: Program, existingInfo: FileInfo, sourceFile: SourceFile, hasInvalidatedResolution: HasInvalidatedResolution) { if (existingInfo.version !== sourceFile.version || hasInvalidatedResolution(sourceFile.path)) { - registerChangedFile(sourceFile.path); - semanticDiagnosticsPerFile.delete(sourceFile.path); + registerChangedFile(sourceFile.path, sourceFile.fileName); existingInfo.version = sourceFile.version; emitHandler.updateScriptInfo(program, sourceFile); } @@ -154,7 +167,7 @@ namespace ts { const sourceFile = program.getSourceFile(path); const singleFileResult = sourceFile && shouldEmitFile(sourceFile) ? [sourceFile.fileName] : []; - const info = fileInfos && fileInfos.get(path); + const info = fileInfos.get(path); if (!info || !updateShapeSignature(program, sourceFile, info)) { return singleFileResult; } @@ -165,44 +178,61 @@ namespace ts { function emitFile(program: Program, path: Path) { ensureProgramGraph(program); - if (!fileInfos || !fileInfos.has(path)) { + if (!fileInfos.has(path)) { return { outputFiles: [], emitSkipped: true }; } return getEmitOutput(program, program.getSourceFileByPath(path), /*emitOnlyDtsFiles*/ false, /*isDetailed*/ false); } - function emitChangedFiles(program: Program): EmitOutputDetailed[] { - ensureProgramGraph(program); - const result: EmitOutputDetailed[] = []; - if (changedFilesSinceLastEmit) { - const seenFiles = createMap(); - changedFilesSinceLastEmit.forEach((__value, path: Path) => { - const affectedFiles = getFilesAffectedBy(program, path); - for (const file of affectedFiles) { - if (!seenFiles.has(file)) { - const sourceFile = program.getSourceFile(file); - seenFiles.set(file, sourceFile); - if (sourceFile) { - // Any affected file shouldnt have the cached diagnostics - semanticDiagnosticsPerFile.delete(sourceFile.path); - - const emitOutput = getEmitOutput(program, sourceFile, /*emitOnlyDtsFiles*/ false, /*isDetailed*/ true) as EmitOutputDetailed; - result.push(emitOutput); - - // mark all the emitted source files as seen - if (emitOutput.emittedSourceFiles) { - for (const file of emitOutput.emittedSourceFiles) { - seenFiles.set(file.fileName, file); - } - } + function enumerateChangedFilesSet( + program: Program, + onChangedFile: (fileName: string) => void, + onAffectedFile: (fileName: string, sourceFile: SourceFile) => void + ) { + changedFileNames.forEach((fileName, path) => { + onChangedFile(fileName); + const affectedFiles = getFilesAffectedBy(program, path as Path); + for (const file of affectedFiles) { + onAffectedFile(file, program.getSourceFile(file)); + } + }); + } + + function enumerateChangedFilesEmitOutput( + program: Program, + emitOnlyDtsFiles: boolean, + onChangedFile: (fileName: string) => void, + onEmitOutput: (emitOutput: EmitOutputDetailed, sourceFile: SourceFile) => void + ) { + const seenFiles = createMap(); + enumerateChangedFilesSet(program, onChangedFile, (fileName, sourceFile) => { + if (!seenFiles.has(fileName)) { + seenFiles.set(fileName, sourceFile); + if (sourceFile) { + // Any affected file shouldnt have the cached diagnostics + semanticDiagnosticsPerFile.delete(sourceFile.path); + + const emitOutput = getEmitOutput(program, sourceFile, emitOnlyDtsFiles, /*isDetailed*/ true) as EmitOutputDetailed; + onEmitOutput(emitOutput, sourceFile); + + // mark all the emitted source files as seen + if (emitOutput.emittedSourceFiles) { + for (const file of emitOutput.emittedSourceFiles) { + seenFiles.set(file.fileName, file); } } } - }); + } + }); + } - changedFilesSinceLastEmit = undefined; - } + function emitChangedFiles(program: Program): EmitOutputDetailed[] { + ensureProgramGraph(program); + const result: EmitOutputDetailed[] = []; + enumerateChangedFilesEmitOutput(program, /*emitOnlyDtsFiles*/ false, + /*onChangedFile*/ noop, emitOutput => result.push(emitOutput)); + changedFileNames.clear(); return result; } @@ -210,17 +240,11 @@ namespace ts { ensureProgramGraph(program); // Ensure that changed files have cleared their respective - if (changedFilesSinceLastEmit) { - changedFilesSinceLastEmit.forEach((__value, path: Path) => { - const affectedFiles = getFilesAffectedBy(program, path); - for (const file of affectedFiles) { - const sourceFile = program.getSourceFile(file); - if (sourceFile) { - semanticDiagnosticsPerFile.delete(sourceFile.path); - } - } - }); - } + enumerateChangedFilesSet(program, /*onChangedFile*/ noop, (_affectedFileName, sourceFile) => { + if (sourceFile) { + semanticDiagnosticsPerFile.delete(sourceFile.path); + } + }); let diagnostics: Diagnostic[]; for (const sourceFile of program.getSourceFiles()) { @@ -240,11 +264,36 @@ namespace ts { return diagnostics || emptyArray; } + function getChangedProgramFiles(program: Program): ChangedProgramFiles { + ensureProgramGraph(program); + + let filesToEmit: string[]; + let changedFiles: string[]; + enumerateChangedFilesEmitOutput(program, /*emitOnlyDtsFiles*/ true, + // All the changed files are required to get diagnostics + changedFileName => addFileForDiagnostics(changedFileName), + // Emitted file is for emit as well as diagnostic + (_emitOutput, sourceFile) => { + (filesToEmit || (filesToEmit = [])).push(sourceFile.fileName); + addFileForDiagnostics(sourceFile.fileName); + }); + changedFileNames.clear(); + return { + filesToEmit: filesToEmit || emptyArray, + changedFiles: changedFiles || emptyArray + }; + + function addFileForDiagnostics(fileName: string) { + (changedFiles || (changedFiles = [])).push(fileName); + } + } + function clear() { isModuleEmit = undefined; emitHandler = undefined; - fileInfos = undefined; + fileInfos.clear(); semanticDiagnosticsPerFile.clear(); + changedFileNames.clear(); } /** @@ -287,9 +336,7 @@ namespace ts { } /** - * Gets the referenced files for a file from the program - * @param program - * @param path + * Gets the referenced files for a file from the program with values for the keys as referenced file's path to be true */ function getReferencedFiles(program: Program, sourceFile: SourceFile): Map { const referencedFiles = createMap(); @@ -371,55 +418,36 @@ namespace ts { function getModuleEmitHandler(): EmitHandler { const references = createMap>(); - const referencedBy = createMultiMap(); return { - addScriptInfo: (program, sourceFile) => { - const refs = createMap(); - references.set(sourceFile.path, refs); - setReferences(program, sourceFile, refs); - }, + addScriptInfo: setReferences, removeScriptInfo, - updateScriptInfo: (program, sourceFile) => setReferences(program, sourceFile, references.get(sourceFile.path)), + updateScriptInfo: setReferences, getFilesAffectedByUpdatedShape }; - function setReferences(program: Program, sourceFile: SourceFile, existingReferences: Map) { - const path = sourceFile.path; - mutateMap( - // Existing references - existingReferences, - // Updated references - getReferencedFiles(program, sourceFile), - { - // Creating new Reference: as sourceFile references file with path 'key' - // in other words source file (path) is referenced by 'key' - createNewValue: (key): true => { referencedBy.add(key, path); return true; }, - // Remove existing reference by entry: source file doesnt reference file 'key' any more - // in other words source file (path) is not referenced by 'key' - onDeleteValue: (key, _existingValue) => { referencedBy.remove(key, path); } - } - ); + function setReferences(program: Program, sourceFile: SourceFile) { + references.set(sourceFile.path, getReferencedFiles(program, sourceFile)); } - function removeScriptInfo(path: Path) { + function removeScriptInfo(removedFilePath: Path) { // Remove existing references - references.forEach((_value, key) => { - referencedBy.remove(key, path); - }); - references.delete(path); - - // Delete the entry and add files referencing this file, as chagned files too - const referencedByPaths = referencedBy.get(path); - if (referencedByPaths) { - for (const path of referencedByPaths) { - registerChangedFile(path); + references.forEach((referencesInFile, filePath) => { + if (referencesInFile.has(removedFilePath)) { + // add files referencing the removedFilePath, as changed files too + const referencedByInfo = fileInfos.get(filePath); + if (referencedByInfo) { + registerChangedFile(filePath as Path, referencedByInfo.fileName); + } } - referencedBy.delete(path); - } + }); + // Delete the entry for the removed file path + references.delete(removedFilePath); } - function getReferencedByPaths(path: Path) { - return referencedBy.get(path) || []; + function getReferencedByPaths(referencedFilePath: Path) { + return mapDefinedIter(references.entries(), ([filePath, referencesInFile]) => + referencesInFile.has(referencedFilePath) ? filePath as Path : undefined + ); } function getFilesAffectedByUpdatedShape(program: Program, sourceFile: SourceFile, singleFileResult: string[]): string[] { @@ -444,7 +472,7 @@ namespace ts { // Start with the paths this file was referenced by const path = sourceFile.path; setSeenFileName(path, sourceFile); - const queue = getReferencedByPaths(path).slice(); + const queue = getReferencedByPaths(path); while (queue.length > 0) { const currentPath = queue.pop(); if (!seenFileNamesMap.has(currentPath)) { diff --git a/src/harness/unittests/tsserverProjectSystem.ts b/src/harness/unittests/tsserverProjectSystem.ts index 58c2b3a614d7f..3833089743b2f 100644 --- a/src/harness/unittests/tsserverProjectSystem.ts +++ b/src/harness/unittests/tsserverProjectSystem.ts @@ -125,7 +125,7 @@ namespace ts.projectSystem { this.events.push(event); } - checkEventCountOfType(eventType: "context" | "configFileDiag", expectedCount: number) { + checkEventCountOfType(eventType: "configFileDiag", expectedCount: number) { const eventsOfType = filter(this.events, e => e.eventName === eventType); assert.equal(eventsOfType.length, expectedCount, `The actual event counts of type ${eventType} is ${eventsOfType.length}, while expected ${expectedCount}`); } @@ -2001,7 +2001,7 @@ namespace ts.projectSystem { const session = createSession(host, { canUseEvents: true, eventHandler: e => { - if (e.eventName === server.ConfigFileDiagEvent || e.eventName === server.ContextEvent || e.eventName === server.ProjectInfoTelemetryEvent) { + if (e.eventName === server.ConfigFileDiagEvent || e.eventName === server.ProjectChangedEvent || e.eventName === server.ProjectInfoTelemetryEvent) { return; } assert.equal(e.eventName, server.ProjectLanguageServiceStateEvent); diff --git a/src/server/editorServices.ts b/src/server/editorServices.ts index 884ee518a30b9..385ade0b81644 100644 --- a/src/server/editorServices.ts +++ b/src/server/editorServices.ts @@ -10,14 +10,14 @@ namespace ts.server { export const maxProgramSizeForNonTsFiles = 20 * 1024 * 1024; - export const ContextEvent = "context"; + export const ProjectChangedEvent = "projectChanged"; export const ConfigFileDiagEvent = "configFileDiag"; export const ProjectLanguageServiceStateEvent = "projectLanguageServiceState"; export const ProjectInfoTelemetryEvent = "projectInfo"; - export interface ContextEvent { - eventName: typeof ContextEvent; - data: { project: Project; fileName: NormalizedPath }; + export interface ProjectChangedEvent { + eventName: typeof ProjectChangedEvent; + data: { project: Project; filesToEmit: string[]; changedFiles: string[] }; } export interface ConfigFileDiagEvent { @@ -77,7 +77,7 @@ namespace ts.server { readonly dts: number; } - export type ProjectServiceEvent = ContextEvent | ConfigFileDiagEvent | ProjectLanguageServiceStateEvent | ProjectInfoTelemetryEvent; + export type ProjectServiceEvent = ProjectChangedEvent | ConfigFileDiagEvent | ProjectLanguageServiceStateEvent | ProjectInfoTelemetryEvent; export interface ProjectServiceEventHandler { (event: ProjectServiceEvent): void; @@ -492,9 +492,32 @@ namespace ts.server { if (this.pendingProjectUpdates.delete(projectName)) { project.updateGraph(); } + // Send the update event to notify about the project changes + this.sendProjectChangedEvent(project); }); } + private sendProjectChangedEvent(project: Project) { + if (project.isClosed() || !this.eventHandler || !project.languageServiceEnabled) { + return; + } + + const { filesToEmit, changedFiles } = project.getChangedFiles(); + if (changedFiles.length === 0) { + return; + } + + const event: ProjectChangedEvent = { + eventName: ProjectChangedEvent, + data: { + project, + filesToEmit: filesToEmit as string[], + changedFiles: changedFiles as string[] + } + }; + this.eventHandler(event); + } + /* @internal */ delayUpdateProjectGraphAndInferredProjectsRefresh(project: Project) { this.delayUpdateProjectGraph(project); @@ -678,19 +701,6 @@ namespace ts.server { // update projects to make sure that set of referenced files is correct this.delayUpdateProjectGraphs(containingProjects); - - // TODO: (sheetalkamat) Someway to send this event so that error checks are updated? - // if (!this.eventHandler) { - // return; - // } - - // for (const openFile of this.openFiles) { - // const event: ContextEvent = { - // eventName: ContextEvent, - // data: { project: openFile.getDefaultProject(), fileName: openFile.fileName } - // }; - // this.eventHandler(event); - // } } } diff --git a/src/server/project.ts b/src/server/project.ts index b35298b293867..cce0fc5b5103a 100644 --- a/src/server/project.ts +++ b/src/server/project.ts @@ -133,7 +133,7 @@ namespace ts.server { /*@internal*/ lsHost: LSHost; - builder: Builder; + private builder: Builder; /** * Set of files names that were updated since the last call to getChangesSinceVersion. */ @@ -307,6 +307,12 @@ namespace ts.server { return !emitSkipped; } + getChangedFiles() { + Debug.assert(this.languageServiceEnabled); + this.ensureBuilder(); + return this.builder.getChangedProgramFiles(this.program); + } + getProjectVersion() { return this.projectStateVersion.toString(); } @@ -392,6 +398,10 @@ namespace ts.server { this.languageService = undefined; } + isClosed() { + return this.lsHost === undefined; + } + getCompilerOptions() { return this.compilerOptions; } @@ -635,8 +645,9 @@ namespace ts.server { // update builder only if language service is enabled // otherwise tell it to drop its internal state + // Note we are retaining builder so we can send events for project change if (this.builder) { - if (this.languageServiceEnabled && this.compileOnSaveEnabled) { + if (this.languageServiceEnabled) { this.builder.onProgramUpdateGraph(this.program, this.lsHost.hasInvalidatedResolution); } else { diff --git a/src/server/protocol.ts b/src/server/protocol.ts index f50c873112671..3cc8555032fb1 100644 --- a/src/server/protocol.ts +++ b/src/server/protocol.ts @@ -2040,6 +2040,29 @@ namespace ts.server.protocol { languageServiceEnabled: boolean; } + export type ProjectChangedEventName = "projectChanged"; + export interface ProjectStructureChangedEvent extends Event { + event: ProjectChangedEventName; + body: ProjectChangedEventBody; + } + + export interface ProjectChangedEventBody { + /** + * Project name that has changes + */ + projectName: string; + + /** + * Minimum set of file names to emit + */ + fileNamesToEmit: string[]; + + /** + * List of files that have changed/added/removed or could have been affected by the changed files + */ + changedFiles: string[]; + } + /** * Arguments for reload request. */ diff --git a/src/server/session.ts b/src/server/session.ts index d4fc22425a065..4b42aaba96a2d 100644 --- a/src/server/session.ts +++ b/src/server/session.ts @@ -332,14 +332,18 @@ namespace ts.server { private defaultEventHandler(event: ProjectServiceEvent) { switch (event.eventName) { - case ContextEvent: - const { project, fileName } = event.data; - this.projectService.logger.info(`got context event, updating diagnostics for ${fileName}`); - this.errorCheck.startNew(next => this.updateErrorCheck(next, [{ fileName, project }], 100)); + case ProjectChangedEvent: + const { project, filesToEmit, changedFiles } = event.data; + this.projectChangedEvent(project, filesToEmit, changedFiles); break; case ConfigFileDiagEvent: - const { triggerFile, configFileName, diagnostics } = event.data; - this.configFileDiagnosticEvent(triggerFile, configFileName, diagnostics); + const { triggerFile, configFileName: configFile, diagnostics } = event.data; + const bakedDiags = map(diagnostics, diagnostic => formatConfigFileDiag(diagnostic, /*includeFileName*/ true)); + this.event({ + triggerFile, + configFile, + diagnostics: bakedDiags + }, "configFileDiag"); break; case ProjectLanguageServiceStateEvent: { const eventName: protocol.ProjectLanguageServiceStateEventName = "projectLanguageServiceState"; @@ -360,6 +364,24 @@ namespace ts.server { } } + private projectChangedEvent(project: Project, fileNamesToEmit: string[], changedFiles: string[]): void { + this.projectService.logger.info(`got project changed event, updating diagnostics for ${changedFiles}`); + if (changedFiles.length) { + const checkList = this.createCheckList(changedFiles, project); + + // For now only queue error checking for open files. We can change this to include non open files as well + this.errorCheck.startNew(next => this.updateErrorCheck(next, checkList, 100, /*requireOpen*/ true)); + + + // Send project changed event + this.event({ + projectName: project.getProjectName(), + changedFiles, + fileNamesToEmit + }, "projectChanged"); + } + } + public logError(err: Error, cmd: string) { let msg = "Exception on executing command " + cmd; if (err.message) { @@ -381,21 +403,6 @@ namespace ts.server { this.host.write(formatMessage(msg, this.logger, this.byteLength, this.host.newLine)); } - public configFileDiagnosticEvent(triggerFile: string, configFile: string, diagnostics: ReadonlyArray) { - const bakedDiags = map(diagnostics, diagnostic => formatConfigFileDiag(diagnostic, /*includeFileName*/ true)); - const ev: protocol.ConfigFileDiagnosticEvent = { - seq: 0, - type: "event", - event: "configFileDiag", - body: { - triggerFile, - configFile, - diagnostics: bakedDiags - } - }; - this.send(ev); - } - public event(info: T, eventName: string) { const ev: protocol.Event = { seq: 0, @@ -1257,13 +1264,16 @@ namespace ts.server { } } - private getDiagnostics(next: NextStep, delay: number, fileNames: string[]): void { - const checkList = mapDefined(fileNames, uncheckedFileName => { + private createCheckList(fileNames: string[], defaultProject?: Project): PendingErrorCheck[] { + return mapDefined(fileNames, uncheckedFileName => { const fileName = toNormalizedPath(uncheckedFileName); - const project = this.projectService.getDefaultProjectForFile(fileName, /*refreshInferredProjects*/ true); + const project = defaultProject || this.projectService.getDefaultProjectForFile(fileName, /*refreshInferredProjects*/ true); return project && { fileName, project }; }); + } + private getDiagnostics(next: NextStep, delay: number, fileNames: string[]): void { + const checkList = this.createCheckList(fileNames); if (checkList.length > 0) { this.updateErrorCheck(next, checkList, delay); } From 3b85f3fbe215b401cc9eed0998107e1b9bcee8d5 Mon Sep 17 00:00:00 2001 From: Sheetal Nandi Date: Tue, 22 Aug 2017 16:03:55 -0700 Subject: [PATCH 075/109] Add tests to verify project changed event sent --- src/compiler/builder.ts | 18 +- .../unittests/tsserverProjectSystem.ts | 533 ++++++++++++++++++ 2 files changed, 542 insertions(+), 9 deletions(-) diff --git a/src/compiler/builder.ts b/src/compiler/builder.ts index d96c3a547c0c8..4fb90ca7143f5 100644 --- a/src/compiler/builder.ts +++ b/src/compiler/builder.ts @@ -187,11 +187,11 @@ namespace ts { function enumerateChangedFilesSet( program: Program, - onChangedFile: (fileName: string) => void, + onChangedFile: (fileName: string, path: Path) => void, onAffectedFile: (fileName: string, sourceFile: SourceFile) => void ) { changedFileNames.forEach((fileName, path) => { - onChangedFile(fileName); + onChangedFile(fileName, path as Path); const affectedFiles = getFilesAffectedBy(program, path as Path); for (const file of affectedFiles) { onAffectedFile(file, program.getSourceFile(file)); @@ -202,7 +202,7 @@ namespace ts { function enumerateChangedFilesEmitOutput( program: Program, emitOnlyDtsFiles: boolean, - onChangedFile: (fileName: string) => void, + onChangedFile: (fileName: string, path: Path) => void, onEmitOutput: (emitOutput: EmitOutputDetailed, sourceFile: SourceFile) => void ) { const seenFiles = createMap(); @@ -268,23 +268,23 @@ namespace ts { ensureProgramGraph(program); let filesToEmit: string[]; - let changedFiles: string[]; + const changedFiles = createMap(); enumerateChangedFilesEmitOutput(program, /*emitOnlyDtsFiles*/ true, // All the changed files are required to get diagnostics - changedFileName => addFileForDiagnostics(changedFileName), + (changedFileName, changedFilePath) => addFileForDiagnostics(changedFileName, changedFilePath), // Emitted file is for emit as well as diagnostic (_emitOutput, sourceFile) => { (filesToEmit || (filesToEmit = [])).push(sourceFile.fileName); - addFileForDiagnostics(sourceFile.fileName); + addFileForDiagnostics(sourceFile.fileName, sourceFile.path); }); changedFileNames.clear(); return { filesToEmit: filesToEmit || emptyArray, - changedFiles: changedFiles || emptyArray + changedFiles: arrayFrom(changedFiles.values()) }; - function addFileForDiagnostics(fileName: string) { - (changedFiles || (changedFiles = [])).push(fileName); + function addFileForDiagnostics(fileName: string, path: Path) { + changedFiles.set(path, fileName); } } diff --git a/src/harness/unittests/tsserverProjectSystem.ts b/src/harness/unittests/tsserverProjectSystem.ts index 3833089743b2f..3c394da4e3167 100644 --- a/src/harness/unittests/tsserverProjectSystem.ts +++ b/src/harness/unittests/tsserverProjectSystem.ts @@ -4467,4 +4467,537 @@ namespace ts.projectSystem { }); }); }); + + describe("ProjectChangedEvent", () => { + function verifyFiles(caption: string, actual: ReadonlyArray, expected: ReadonlyArray) { + assert.equal(actual.length, expected.length, `Incorrect number of ${caption}. Actual: ${actual} Expected: ${expected}`); + const seen = createMap(); + forEach(actual, f => { + assert.isFalse(seen.has(f), `${caption}: Found duplicate ${f}. Actual: ${actual} Expected: ${expected}`); + seen.set(f, true); + assert.isTrue(contains(expected, f), `${caption}: Expected not to contain ${f}. Actual: ${actual} Expected: ${expected}`); + }); + } + + function createVerifyInitialOpen(session: TestSession, verifyProjectChangedEventHandler: (events: server.ProjectChangedEvent[]) => void) { + return (file: FileOrFolder) => { + session.executeCommandSeq({ + command: server.CommandNames.Open, + arguments: { + file: file.path + } + }); + verifyProjectChangedEventHandler([]); + }; + } + + interface ProjectChangeEventVerifier { + session: TestSession; + verifyProjectChangedEventHandler(events: server.ProjectChangedEvent[]): void; + verifyInitialOpen(file: FileOrFolder): void; + } + + function verifyProjectChangedEvent(createSession: (host: TestServerHost) => ProjectChangeEventVerifier) { + it("when adding new file", () => { + const commonFile1: FileOrFolder = { + path: "/a/b/file1.ts", + content: "export var x = 10;" + }; + const commonFile2: FileOrFolder = { + path: "/a/b/file2.ts", + content: "export var y = 10;" + }; + const commonFile3: FileOrFolder = { + path: "/a/b/file3.ts", + content: "export var z = 10;" + }; + const configFile: FileOrFolder = { + path: "/a/b/tsconfig.json", + content: `{}` + }; + const host = createServerHost([commonFile1, libFile, configFile]); + const { session, verifyProjectChangedEventHandler, verifyInitialOpen } = createSession(host, ); + const projectService = session.getProjectService(); + verifyInitialOpen(commonFile1); + + host.reloadFS([commonFile1, libFile, configFile, commonFile2]); + host.runQueuedTimeoutCallbacks(); + // Since this is first event + const project = projectService.configuredProjects.get(configFile.path); + verifyProjectChangedEventHandler([{ + eventName: server.ProjectChangedEvent, + data: { + project, + changedFiles: [libFile.path, commonFile1.path, commonFile2.path], + filesToEmit: [commonFile1.path, commonFile2.path] + } + }]); + + host.reloadFS([commonFile1, commonFile2, libFile, configFile, commonFile3]); + host.runQueuedTimeoutCallbacks(); + verifyProjectChangedEventHandler([{ + eventName: server.ProjectChangedEvent, + data: { + project, + changedFiles: [commonFile3.path], + filesToEmit: [commonFile3.path] + } + }]); + }); + + describe("with --out or --outFile setting", () => { + function verifyEventWithOutSettings(compilerOptions: CompilerOptions = {}) { + const config: FileOrFolder = { + path: "/a/tsconfig.json", + content: JSON.stringify({ + compilerOptions + }) + }; + + const f1: FileOrFolder = { + path: "/a/a.ts", + content: "export let x = 1" + }; + const f2: FileOrFolder = { + path: "/a/b.ts", + content: "export let y = 1" + }; + + const files = [f1, config, libFile]; + const host = createServerHost(files); + const { session, verifyInitialOpen, verifyProjectChangedEventHandler } = createSession(host); + const projectService = session.getProjectService(); + verifyInitialOpen(f1); + + files.push(f2); + host.reloadFS(files); + host.runQueuedTimeoutCallbacks(); + + // Since this is first event + const project = projectService.configuredProjects.get(config.path); + verifyProjectChangedEventHandler([{ + eventName: server.ProjectChangedEvent, + data: { + project, + changedFiles: [libFile.path, f1.path, f2.path], + filesToEmit: [f1.path, f2.path] + } + }]); + + f2.content = "export let x = 11"; + host.reloadFS(files); + host.runQueuedTimeoutCallbacks(); + verifyProjectChangedEventHandler([{ + eventName: server.ProjectChangedEvent, + data: { + project, + changedFiles: [f2.path], + filesToEmit: [f2.path] + } + }]); + } + + it("when both options are not set", () => { + verifyEventWithOutSettings(); + }); + + it("when --out is set", () => { + const outJs = "/a/out.js"; + verifyEventWithOutSettings({ out: outJs }); + }); + + it("when --outFile is set", () => { + const outJs = "/a/out.js"; + verifyEventWithOutSettings({ outFile: outJs }); + }); + }); + + describe("with modules and configured project", () => { + const file1Consumer1Path = "/a/b/file1Consumer1.ts"; + const moduleFile1Path = "/a/b/moduleFile1.ts"; + const configFilePath = "/a/b/tsconfig.json"; + type InitialStateParams = { + /** custom config file options */ + configObj?: any; + /** list of files emitted/changed on first update graph */ + firstCompilationEmitFiles?: string[]; + /** Additional files and folders to add */ + getAdditionalFileOrFolder?(): FileOrFolder[]; + /** initial list of files to reload in fs and first file in this list being the file to open */ + firstReloadFileList?: string[]; + }; + function getInitialState({ configObj = {}, getAdditionalFileOrFolder, firstReloadFileList, firstCompilationEmitFiles }: InitialStateParams = {}) { + const moduleFile1: FileOrFolder = { + path: moduleFile1Path, + content: "export function Foo() { };", + }; + + const file1Consumer1: FileOrFolder = { + path: file1Consumer1Path, + content: `import {Foo} from "./moduleFile1"; export var y = 10;`, + }; + + const file1Consumer2: FileOrFolder = { + path: "/a/b/file1Consumer2.ts", + content: `import {Foo} from "./moduleFile1"; let z = 10;`, + }; + + const moduleFile2: FileOrFolder = { + path: "/a/b/moduleFile2.ts", + content: `export var Foo4 = 10;`, + }; + + const globalFile3: FileOrFolder = { + path: "/a/b/globalFile3.ts", + content: `interface GlobalFoo { age: number }` + }; + + const additionalFiles = getAdditionalFileOrFolder ? getAdditionalFileOrFolder() : []; + const configFile = { + path: configFilePath, + content: JSON.stringify(configObj || { compilerOptions: {} }) + }; + + const files = [file1Consumer1, moduleFile1, file1Consumer2, moduleFile2, ...additionalFiles, globalFile3, libFile, configFile]; + + const filesToReload = firstReloadFileList && getFiles(firstReloadFileList) || files; + const host = createServerHost([filesToReload[0], configFile]); + + // Initial project creation + const { session, verifyProjectChangedEventHandler, verifyInitialOpen } = createSession(host); + const projectService = session.getProjectService(); + verifyInitialOpen(filesToReload[0]); + + // Since this is first event, it will have all the files + const firstFilesExpected = firstCompilationEmitFiles && getFiles(firstCompilationEmitFiles) || filesToReload; + verifyProjectChangedEvent(firstFilesExpected, filesToReload); + + return { + moduleFile1, file1Consumer1, file1Consumer2, moduleFile2, globalFile3, configFile, + files, + updateContentOfOpenFile, + verifyProjectChangedEvent, + verifyAffectedAllFiles, + }; + + function getFiles(filelist: string[]) { + return map(filelist, getFile); + } + + function getFile(fileName: string) { + return find(files, file => file.path === fileName); + } + + function verifyAffectedAllFiles() { + verifyProjectChangedEvent(filter(files, f => f !== libFile)); + } + + function verifyProjectChangedEvent(filesToEmit: FileOrFolder[], filesToReload?: FileOrFolder[], additionalChangedFiles?: FileOrFolder[]) { + const changedFiles = mapDefined(additionalChangedFiles ? filesToEmit.concat(additionalChangedFiles) : filesToEmit, f => f !== configFile ? f.path : undefined); + host.reloadFS(filesToReload || files); + host.runQueuedTimeoutCallbacks(); + const project = projectService.configuredProjects.get(configFile.path); + verifyProjectChangedEventHandler([{ + eventName: server.ProjectChangedEvent, + data: { + project, + changedFiles, + filesToEmit: mapDefined(filesToEmit, f => f !== libFile && f !== configFile ? f.path : undefined) + } + }]); + } + + function updateContentOfOpenFile(file: FileOrFolder, newContent: string) { + session.executeCommandSeq({ + command: server.CommandNames.Change, + arguments: { + file: file.path, + insertString: newContent, + endLine: 1, + endOffset: file.content.length, + line: 1, + offset: 1 + } + }); + file.content = newContent; + } + } + + it("should contains only itself if a module file's shape didn't change, and all files referencing it if its shape changed", () => { + const { moduleFile1, file1Consumer1, file1Consumer2, verifyProjectChangedEvent } = getInitialState(); + + // Change the content of moduleFile1 to `export var T: number;export function Foo() { };` + moduleFile1.content = `export var T: number;export function Foo() { };`; + verifyProjectChangedEvent([moduleFile1, file1Consumer1, file1Consumer2]); + + // Change the content of moduleFile1 to `export var T: number;export function Foo() { console.log('hi'); };` + moduleFile1.content = `export var T: number;export function Foo() { console.log('hi'); };`; + verifyProjectChangedEvent([moduleFile1]); + }); + + it("should be up-to-date with the reference map changes", () => { + const { moduleFile1, file1Consumer1, file1Consumer2, updateContentOfOpenFile, verifyProjectChangedEvent } = getInitialState(); + + // Change file1Consumer1 content to `export let y = Foo();` + updateContentOfOpenFile(file1Consumer1, "export let y = Foo();"); + verifyProjectChangedEvent([file1Consumer1]); + + // Change the content of moduleFile1 to `export var T: number;export function Foo() { };` + moduleFile1.content = `export var T: number;export function Foo() { };`; + verifyProjectChangedEvent([moduleFile1, file1Consumer2]); + + // Add the import statements back to file1Consumer1 + updateContentOfOpenFile(file1Consumer1, `import {Foo} from "./moduleFile1";let y = Foo();`); + verifyProjectChangedEvent([file1Consumer1]); + + // Change the content of moduleFile1 to `export var T: number;export var T2: string;export function Foo() { };` + moduleFile1.content = `export var T: number;export var T2: string;export function Foo() { };`; + verifyProjectChangedEvent([moduleFile1, file1Consumer2, file1Consumer1]); + + // Multiple file edits in one go: + + // Change file1Consumer1 content to `export let y = Foo();` + // Change the content of moduleFile1 to `export var T: number;export function Foo() { };` + updateContentOfOpenFile(file1Consumer1, `export let y = Foo();`); + moduleFile1.content = `export var T: number;export function Foo() { };`; + verifyProjectChangedEvent([moduleFile1, file1Consumer1, file1Consumer2]); + }); + + it("should be up-to-date with deleted files", () => { + const { moduleFile1, file1Consumer1, file1Consumer2, files, verifyProjectChangedEvent } = getInitialState(); + + // Change the content of moduleFile1 to `export var T: number;export function Foo() { };` + moduleFile1.content = `export var T: number;export function Foo() { };`; + + // Delete file1Consumer2 + const filesToLoad = filter(files, file => file !== file1Consumer2); + verifyProjectChangedEvent([moduleFile1, file1Consumer1], filesToLoad, [file1Consumer2]); + }); + + it("should be up-to-date with newly created files", () => { + const { moduleFile1, file1Consumer1, file1Consumer2, files, verifyProjectChangedEvent, } = getInitialState(); + + const file1Consumer3: FileOrFolder = { + path: "/a/b/file1Consumer3.ts", + content: `import {Foo} from "./moduleFile1"; let y = Foo();` + }; + moduleFile1.content = `export var T: number;export function Foo() { };`; + verifyProjectChangedEvent([moduleFile1, file1Consumer1, file1Consumer3, file1Consumer2], files.concat(file1Consumer3)); + }); + + it("should detect changes in non-root files", () => { + const { moduleFile1, file1Consumer1, verifyProjectChangedEvent } = getInitialState({ + configObj: { files: [file1Consumer1Path] }, + firstCompilationEmitFiles: [file1Consumer1Path, moduleFile1Path, libFile.path] + }); + + moduleFile1.content = `export var T: number;export function Foo() { };`; + verifyProjectChangedEvent([moduleFile1, file1Consumer1]); + + // change file1 internal, and verify only file1 is affected + moduleFile1.content += "var T1: number;"; + verifyProjectChangedEvent([moduleFile1]); + }); + + it("should return all files if a global file changed shape", () => { + const { globalFile3, verifyAffectedAllFiles } = getInitialState(); + + globalFile3.content += "var T2: string;"; + verifyAffectedAllFiles(); + }); + + it("should always return the file itself if '--isolatedModules' is specified", () => { + const { moduleFile1, verifyProjectChangedEvent } = getInitialState({ + configObj: { compilerOptions: { isolatedModules: true } } + }); + + moduleFile1.content = `export var T: number;export function Foo() { };`; + verifyProjectChangedEvent([moduleFile1]); + }); + + it("should always return the file itself if '--out' or '--outFile' is specified", () => { + const outFilePath = "/a/b/out.js"; + const { moduleFile1, verifyProjectChangedEvent } = getInitialState({ + configObj: { compilerOptions: { module: "system", outFile: outFilePath } } + }); + + moduleFile1.content = `export var T: number;export function Foo() { };`; + verifyProjectChangedEvent([moduleFile1]); + }); + + it("should return cascaded affected file list", () => { + const file1Consumer1Consumer1: FileOrFolder = { + path: "/a/b/file1Consumer1Consumer1.ts", + content: `import {y} from "./file1Consumer1";` + }; + const { moduleFile1, file1Consumer1, file1Consumer2, updateContentOfOpenFile, verifyProjectChangedEvent } = getInitialState({ + getAdditionalFileOrFolder: () => [file1Consumer1Consumer1] + }); + + updateContentOfOpenFile(file1Consumer1, file1Consumer1.content + "export var T: number;"); + verifyProjectChangedEvent([file1Consumer1, file1Consumer1Consumer1]); + + // Doesnt change the shape of file1Consumer1 + moduleFile1.content = `export var T: number;export function Foo() { };`; + verifyProjectChangedEvent([moduleFile1, file1Consumer1, file1Consumer2]); + + // Change both files before the timeout + updateContentOfOpenFile(file1Consumer1, file1Consumer1.content + "export var T2: number;"); + moduleFile1.content = `export var T2: number;export function Foo() { };`; + verifyProjectChangedEvent([moduleFile1, file1Consumer1, file1Consumer2, file1Consumer1Consumer1]); + }); + + it("should work fine for files with circular references", () => { + const file1: FileOrFolder = { + path: "/a/b/file1.ts", + content: ` + /// + export var t1 = 10;` + }; + const file2: FileOrFolder = { + path: "/a/b/file2.ts", + content: ` + /// + export var t2 = 10;` + }; + const { configFile, verifyProjectChangedEvent, updateContentOfOpenFile } = getInitialState({ + getAdditionalFileOrFolder: () => [file1, file2], + firstReloadFileList: [file1.path, libFile.path, file2.path, configFilePath] + }); + + updateContentOfOpenFile(file1, file1.content + "export var t3 = 10;"); + verifyProjectChangedEvent([file1, file2], [file1, file2, libFile, configFile]); + }); + + it("should detect removed code file", () => { + const referenceFile1: FileOrFolder = { + path: "/a/b/referenceFile1.ts", + content: ` + /// + export var x = Foo();` + }; + const { configFile, verifyProjectChangedEvent, moduleFile1 } = getInitialState({ + getAdditionalFileOrFolder: () => [referenceFile1], + firstReloadFileList: [referenceFile1.path, libFile.path, moduleFile1Path, configFilePath] + }); + + verifyProjectChangedEvent([referenceFile1], [libFile, referenceFile1, configFile], [moduleFile1]); + }); + + it("should detect non-existing code file", () => { + const referenceFile1: FileOrFolder = { + path: "/a/b/referenceFile1.ts", + content: ` + /// + export var x = Foo();` + }; + const { configFile, moduleFile2, updateContentOfOpenFile, verifyProjectChangedEvent } = getInitialState({ + getAdditionalFileOrFolder: () => [referenceFile1], + firstReloadFileList: [referenceFile1.path, libFile.path, configFilePath] + }); + + updateContentOfOpenFile(referenceFile1, referenceFile1.content + "export var yy = Foo();"); + verifyProjectChangedEvent([referenceFile1], [libFile, referenceFile1, configFile]); + + // Create module File2 and see both files are saved + verifyProjectChangedEvent([referenceFile1, moduleFile2], [libFile, moduleFile2, referenceFile1, configFile]); + }); + }); + } + + describe("when event handler is set in the session", () => { + verifyProjectChangedEvent(createSessionWithProjectChangedEventHandler); + + function createSessionWithProjectChangedEventHandler(host: TestServerHost): ProjectChangeEventVerifier { + const projectChangedEvents: server.ProjectChangedEvent[] = []; + const session = createSession(host, { + eventHandler: e => { + if (e.eventName === server.ProjectChangedEvent) { + projectChangedEvents.push(e); + } + } + }); + + return { + session, + verifyProjectChangedEventHandler, + verifyInitialOpen: createVerifyInitialOpen(session, verifyProjectChangedEventHandler) + }; + + function eventToString(event: server.ProjectChangedEvent) { + const eventToModify = event && { + eventName: event.eventName, + data: { + project: event.data.project.getProjectName(), + changedFiles: event.data.changedFiles, + filesToEmit: event.data.filesToEmit + } + }; + return JSON.stringify(eventToModify); + } + + function eventsToString(events: ReadonlyArray) { + return "[" + map(events, eventToString).join(",") + "]"; + } + + function verifyProjectChangedEventHandler(expectedEvents: ReadonlyArray) { + assert.equal(projectChangedEvents.length, expectedEvents.length, `Incorrect number of events Actual: ${eventsToString(projectChangedEvents)} Expected: ${eventsToString(expectedEvents)}`); + forEach(projectChangedEvents, (actualEvent, i) => { + const expectedEvent = expectedEvents[i]; + assert.strictEqual(actualEvent.eventName, expectedEvent.eventName); + assert.strictEqual(actualEvent.data.project, expectedEvent.data.project); + verifyFiles("changedFiles", actualEvent.data.changedFiles, expectedEvent.data.changedFiles); + verifyFiles("filesToEmit", actualEvent.data.filesToEmit, expectedEvent.data.filesToEmit); + }); + + // Verified the events, reset them + projectChangedEvents.length = 0; + } + } + }); + + describe("when event handler is not set but session is created with canUseEvents = true", () => { + verifyProjectChangedEvent(createSessionThatUsesEvents); + + function createSessionThatUsesEvents(host: TestServerHost): ProjectChangeEventVerifier { + const session = createSession(host, { canUseEvents: true }); + + return { + session, + verifyProjectChangedEventHandler, + verifyInitialOpen: createVerifyInitialOpen(session, verifyProjectChangedEventHandler) + }; + + function verifyProjectChangedEventHandler(expected: ReadonlyArray) { + const expectedEvents: protocol.ProjectChangedEventBody[] = map(expected, e => { + return { + projectName: e.data.project.getProjectName(), + changedFiles: e.data.changedFiles, + fileNamesToEmit: e.data.filesToEmit + }; + }); + const outputEventRegex = /Content\-Length: [\d]+\r\n\r\n/; + const events: protocol.ProjectStructureChangedEvent[] = filter( + map( + host.getOutput(), s => convertToObject( + ts.parseJsonText("json.json", s.replace(outputEventRegex, "")), + [] + ) + ), + e => e.event === server.ProjectChangedEvent + ); + assert.equal(events.length, expectedEvents.length, `Incorrect number of events Actual: ${map(events, e => e.body)} Expected: ${expectedEvents}`); + forEach(events, (actualEvent, i) => { + const expectedEvent = expectedEvents[i]; + assert.strictEqual(actualEvent.body.projectName, expectedEvent.projectName); + verifyFiles("changedFiles", actualEvent.body.changedFiles, expectedEvent.changedFiles); + verifyFiles("fileNamesToEmit", actualEvent.body.fileNamesToEmit, expectedEvent.fileNamesToEmit); + }); + + // Verified the events, reset them + host.clearOutput(); + } + } + }); + }); } From 4c79033894896fc57786aa1628e137329167e868 Mon Sep 17 00:00:00 2001 From: Sheetal Nandi Date: Wed, 23 Aug 2017 13:34:41 -0700 Subject: [PATCH 076/109] Refactoring to watches and caching of system such that we minimize function expressions Also unified watcher info logging --- src/compiler/builder.ts | 4 +- src/compiler/core.ts | 13 +- src/compiler/program.ts | 15 +-- src/compiler/resolutionCache.ts | 43 +++---- src/compiler/sys.ts | 7 +- src/compiler/utilities.ts | 71 +++++++++-- src/compiler/watchedProgram.ts | 158 ++++++------------------- src/server/editorServices.ts | 203 ++++++++++++++------------------ src/server/lsHost.ts | 125 ++------------------ src/server/project.ts | 121 +++++++++---------- src/server/scriptInfo.ts | 2 +- 11 files changed, 301 insertions(+), 461 deletions(-) diff --git a/src/compiler/builder.ts b/src/compiler/builder.ts index 4fb90ca7143f5..68fb36b9144f8 100644 --- a/src/compiler/builder.ts +++ b/src/compiler/builder.ts @@ -120,7 +120,7 @@ namespace ts { // Remove existing file info onDeleteValue: removeExistingFileInfo, // We will update in place instead of deleting existing value and adding new one - onExistingValue: (_key, existingInfo, sourceFile) => updateExistingFileInfo(program, existingInfo, sourceFile, hasInvalidatedResolution) + onExistingValue: (existingInfo, sourceFile) => updateExistingFileInfo(program, existingInfo, sourceFile, hasInvalidatedResolution) } ); } @@ -137,7 +137,7 @@ namespace ts { return { fileName: sourceFile.fileName, version: sourceFile.version, signature: undefined }; } - function removeExistingFileInfo(path: Path, existingFileInfo: FileInfo) { + function removeExistingFileInfo(existingFileInfo: FileInfo, path: Path) { registerChangedFile(path, existingFileInfo.fileName); emitHandler.removeScriptInfo(path); } diff --git a/src/compiler/core.ts b/src/compiler/core.ts index 00e8065a9f903..bb4cf903a981f 100644 --- a/src/compiler/core.ts +++ b/src/compiler/core.ts @@ -2631,10 +2631,6 @@ namespace ts { return sourceFile.checkJsDirective ? sourceFile.checkJsDirective.enabled : compilerOptions.checkJs; } - export interface HostForCaching extends PartialSystem { - useCaseSensitiveFileNames: boolean; - } - export interface CachedHost { addOrDeleteFileOrFolder(fileOrFolder: string, fileOrFolderPath: Path): void; addOrDeleteFile(fileName: string, filePath: Path, eventKind: FileWatcherEventKind): void; @@ -2649,11 +2645,15 @@ namespace ts { readonly directories: string[]; } - export function createCachedPartialSystem(host: HostForCaching): CachedPartialSystem { + export function createCachedPartialSystem(host: PartialSystem): CachedPartialSystem { const cachedReadDirectoryResult = createMap(); const getCurrentDirectory = memoize(() => host.getCurrentDirectory()); const getCanonicalFileName = createGetCanonicalFileName(host.useCaseSensitiveFileNames); return { + useCaseSensitiveFileNames: host.useCaseSensitiveFileNames, + newLine: host.newLine, + readFile: (path, encoding) => host.readFile(path, encoding), + write: s => host.write(s), writeFile, fileExists, directoryExists, @@ -2663,7 +2663,8 @@ namespace ts { readDirectory, addOrDeleteFileOrFolder, addOrDeleteFile, - clearCache + clearCache, + exit: code => host.exit(code) }; function toPath(fileName: string) { diff --git a/src/compiler/program.ts b/src/compiler/program.ts index c3b83e1ba7c61..22459d36cb0f8 100644 --- a/src/compiler/program.ts +++ b/src/compiler/program.ts @@ -461,7 +461,6 @@ namespace ts { program: Program, missingFileWatches: Map, createMissingFileWatch: (missingFilePath: Path) => FileWatcher, - closeExistingMissingFilePathFileWatcher: (missingFilePath: Path, fileWatcher: FileWatcher) => void ) { const missingFilePaths = program.getMissingFilePaths(); const newMissingFilePathMap = arrayToSet(missingFilePaths); @@ -474,7 +473,7 @@ namespace ts { createNewValue: createMissingFileWatch, // Files that are no longer missing (e.g. because they are no longer required) // should no longer be watched. - onDeleteValue: closeExistingMissingFilePathFileWatcher + onDeleteValue: closeFileWatcher } ); } @@ -493,8 +492,7 @@ namespace ts { export function updateWatchingWildcardDirectories( existingWatchedForWildcards: Map, wildcardDirectories: Map, - watchDirectory: (directory: string, flags: WatchDirectoryFlags) => FileWatcher, - closeDirectoryWatcher: (directory: string, wildcardDirectoryWatcher: WildcardDirectoryWatcher, flagsChanged: boolean) => void + watchDirectory: (directory: string, flags: WatchDirectoryFlags) => FileWatcher ) { mutateMap( existingWatchedForWildcards, @@ -503,8 +501,7 @@ namespace ts { // Create new watch and recursive info createNewValue: createWildcardDirectoryWatcher, // Close existing watch thats not needed any more - onDeleteValue: (directory, wildcardDirectoryWatcher) => - closeDirectoryWatcher(directory, wildcardDirectoryWatcher, /*flagsChanged*/ false), + onDeleteValue: closeFileWatcherOf, // Close existing watch that doesnt match in the flags onExistingValue: updateWildcardDirectoryWatcher } @@ -518,13 +515,13 @@ namespace ts { }; } - function updateWildcardDirectoryWatcher(directory: string, wildcardDirectoryWatcher: WildcardDirectoryWatcher, flags: WatchDirectoryFlags) { + function updateWildcardDirectoryWatcher(existingWatcher: WildcardDirectoryWatcher, flags: WatchDirectoryFlags, directory: string) { // Watcher needs to be updated if the recursive flags dont match - if (wildcardDirectoryWatcher.flags === flags) { + if (existingWatcher.flags === flags) { return; } - closeDirectoryWatcher(directory, wildcardDirectoryWatcher, /*flagsChanged*/ true); + existingWatcher.watcher.close(); existingWatchedForWildcards.set(directory, createWildcardDirectoryWatcher(directory, flags)); } } diff --git a/src/compiler/resolutionCache.ts b/src/compiler/resolutionCache.ts index 6d7c1f6c0e4f6..8001dd81bd974 100644 --- a/src/compiler/resolutionCache.ts +++ b/src/compiler/resolutionCache.ts @@ -27,14 +27,14 @@ namespace ts { } interface FailedLookupLocationsWatcher { - fileWatcher: FileWatcher; + watcher: FileWatcher; refCount: number; } export function createResolutionCache( toPath: (fileName: string) => Path, getCompilerOptions: () => CompilerOptions, - watchForFailedLookupLocation: (failedLookupLocation: string, failedLookupLocationPath: Path, containingFile: string, name: string) => FileWatcher, + watchForFailedLookupLocation: (failedLookupLocation: string, failedLookupLocationPath: Path) => FileWatcher, log: (s: string) => void, projectName?: string, getGlobalCache?: () => string | undefined): ResolutionCache { @@ -69,10 +69,7 @@ namespace ts { function clear() { // Close all the watches for failed lookup locations, irrespective of refcounts for them since this is to clear the cache - clearMap(failedLookupLocationsWatches, (failedLookupLocationPath, failedLookupLocationWatcher) => { - log(`Watcher: FailedLookupLocations: Status: ForceClose: LocationPath: ${failedLookupLocationPath}, refCount: ${failedLookupLocationWatcher.refCount}`); - failedLookupLocationWatcher.fileWatcher.close(); - }); + clearMap(failedLookupLocationsWatches, closeFileWatcherOf); resolvedModuleNames.clear(); resolvedTypeReferenceDirectives.clear(); } @@ -142,7 +139,7 @@ namespace ts { } else { resolution = loader(name, containingFile, compilerOptions, host); - updateFailedLookupLocationWatches(containingFile, name, existingResolution && existingResolution.failedLookupLocations, resolution.failedLookupLocations); + updateFailedLookupLocationWatches(resolution.failedLookupLocations, existingResolution && existingResolution.failedLookupLocations); } newResolutions.set(name, resolution); if (logChanges && filesWithChangedSetOfUnresolvedImports && !resolutionIsEqualTo(existingResolution, resolution)) { @@ -205,46 +202,44 @@ namespace ts { m => m.resolvedModule, r => r.resolvedFileName, logChanges); } - function watchFailedLookupLocation(failedLookupLocation: string, failedLookupLocationPath: Path, containingFile: string, name: string) { + function watchFailedLookupLocation(failedLookupLocation: string, failedLookupLocationPath: Path) { const failedLookupLocationWatcher = failedLookupLocationsWatches.get(failedLookupLocationPath); if (failedLookupLocationWatcher) { failedLookupLocationWatcher.refCount++; - log(`Watcher: FailedLookupLocations: Status: Using existing watcher: Location: ${failedLookupLocation}, containingFile: ${containingFile}, name: ${name} refCount: ${failedLookupLocationWatcher.refCount}`); + log(`Watcher: FailedLookupLocations: Status: Using existing watcher: Location: ${failedLookupLocation}`); } else { - log(`Watcher: FailedLookupLocations: Status: new watch: Location: ${failedLookupLocation}, containingFile: ${containingFile}, name: ${name}`); - const fileWatcher = watchForFailedLookupLocation(failedLookupLocation, failedLookupLocationPath, containingFile, name); - failedLookupLocationsWatches.set(failedLookupLocationPath, { fileWatcher, refCount: 1 }); + const watcher = watchForFailedLookupLocation(failedLookupLocation, failedLookupLocationPath); + failedLookupLocationsWatches.set(failedLookupLocationPath, { watcher, refCount: 1 }); } } - function closeFailedLookupLocationWatcher(failedLookupLocation: string, failedLookupLocationPath: Path, containingFile: string, name: string) { + function closeFailedLookupLocationWatcher(failedLookupLocation: string, failedLookupLocationPath: Path) { const failedLookupLocationWatcher = failedLookupLocationsWatches.get(failedLookupLocationPath); Debug.assert(!!failedLookupLocationWatcher); failedLookupLocationWatcher.refCount--; if (failedLookupLocationWatcher.refCount) { - log(`Watcher: FailedLookupLocations: Status: Removing existing watcher: Location: ${failedLookupLocation}, containingFile: ${containingFile}, name: ${name}: refCount: ${failedLookupLocationWatcher.refCount}`); + log(`Watcher: FailedLookupLocations: Status: Removing existing watcher: Location: ${failedLookupLocation}`); } else { - log(`Watcher: FailedLookupLocations: Status: Closing the file watcher: Location: ${failedLookupLocation}, containingFile: ${containingFile}, name: ${name}`); - failedLookupLocationWatcher.fileWatcher.close(); + failedLookupLocationWatcher.watcher.close(); failedLookupLocationsWatches.delete(failedLookupLocationPath); } } - type FailedLookupLocationAction = (failedLookupLocation: string, failedLookupLocationPath: Path, containingFile: string, name: string) => void; - function withFailedLookupLocations(failedLookupLocations: ReadonlyArray, containingFile: string, name: string, fn: FailedLookupLocationAction) { + type FailedLookupLocationAction = (failedLookupLocation: string, failedLookupLocationPath: Path) => void; + function withFailedLookupLocations(failedLookupLocations: ReadonlyArray | undefined, fn: FailedLookupLocationAction) { forEach(failedLookupLocations, failedLookupLocation => { - fn(failedLookupLocation, toPath(failedLookupLocation), containingFile, name); + fn(failedLookupLocation, toPath(failedLookupLocation)); }); } - function updateFailedLookupLocationWatches(containingFile: string, name: string, existingFailedLookupLocations: ReadonlyArray | undefined, failedLookupLocations: ReadonlyArray) { + function updateFailedLookupLocationWatches(failedLookupLocations: ReadonlyArray | undefined, existingFailedLookupLocations: ReadonlyArray | undefined) { // Watch all the failed lookup locations - withFailedLookupLocations(failedLookupLocations, containingFile, name, watchFailedLookupLocation); + withFailedLookupLocations(failedLookupLocations, watchFailedLookupLocation); // Close existing watches for the failed locations - withFailedLookupLocations(existingFailedLookupLocations, containingFile, name, closeFailedLookupLocationWatcher); + withFailedLookupLocations(existingFailedLookupLocations, closeFailedLookupLocationWatcher); } function invalidateResolutionCacheOfDeletedFile( @@ -255,8 +250,8 @@ namespace ts { cache.forEach((value, path) => { if (path === deletedFilePath) { cache.delete(path); - value.forEach((resolution, name) => { - withFailedLookupLocations(resolution.failedLookupLocations, path, name, closeFailedLookupLocationWatcher); + value.forEach(resolution => { + withFailedLookupLocations(resolution.failedLookupLocations, closeFailedLookupLocationWatcher); }); } else if (value) { diff --git a/src/compiler/sys.ts b/src/compiler/sys.ts index 982d679381924..7a419be7c2d02 100644 --- a/src/compiler/sys.ts +++ b/src/compiler/sys.ts @@ -34,6 +34,10 @@ namespace ts { * Partial interface of the System thats needed to support the caching of directory structure */ export interface PartialSystem { + newLine: string; + useCaseSensitiveFileNames: boolean; + write(s: string): void; + readFile(path: string, encoding?: string): string | undefined; writeFile(path: string, data: string, writeByteOrderMark?: boolean): void; fileExists(path: string): boolean; directoryExists(path: string): boolean; @@ -41,6 +45,7 @@ namespace ts { getCurrentDirectory(): string; getDirectories(path: string): string[]; readDirectory(path: string, extensions?: ReadonlyArray, exclude?: ReadonlyArray, include?: ReadonlyArray, depth?: number): string[]; + exit(exitCode?: number): void; } export interface System extends PartialSystem { @@ -48,7 +53,6 @@ namespace ts { newLine: string; useCaseSensitiveFileNames: boolean; write(s: string): void; - readFile(path: string, encoding?: string): string | undefined; getFileSize?(path: string): number; /** * @pollingInterval - this parameter is used in polling-based watchers and ignored in watchers that @@ -65,7 +69,6 @@ namespace ts { */ createHash?(data: string): string; getMemoryUsage?(): number; - exit(exitCode?: number): void; realpath?(path: string): string; /*@internal*/ getEnvironmentVariable(name: string): string; /*@internal*/ tryEnableSourceMapsForHost?(): void; diff --git a/src/compiler/utilities.ts b/src/compiler/utilities.ts index f09ce44649179..7e43d793f0bc9 100644 --- a/src/compiler/utilities.ts +++ b/src/compiler/utilities.ts @@ -3490,17 +3490,15 @@ namespace ts { /** * clears already present map by calling onDeleteExistingValue callback before deleting that key/value */ - export function clearMap(map: Map, onDeleteValue: (key: string, existingValue: T) => void) { + export function clearMap(map: Map, onDeleteValue: (existingValue: T, key: string) => void) { // Remove all - map.forEach((existingValue, key) => { - onDeleteValue(key, existingValue); - }); + map.forEach(onDeleteValue); map.clear(); } export interface MutateMapOptions { createNewValue(key: string, valueInNewMap: U): T; - onDeleteValue(key: string, existingValue: T): void; + onDeleteValue(existingValue: T, key: string): void; /** * If present this is called with the key when there is value for that key both in new map as well as existing map provided @@ -3508,7 +3506,7 @@ namespace ts { * If the key is removed, caller will get callback of createNewValue for that key. * If this callback is not provided, the value of such keys is not updated. */ - onExistingValue?(key: string, existingValue: T, valueInNewMap: U): void; + onExistingValue?(existingValue: T, valueInNewMap: U, key: string): void; } /** @@ -3524,11 +3522,11 @@ namespace ts { // Not present any more in new map, remove it if (valueInNewMap === undefined) { map.delete(key); - onDeleteValue(key, existingValue); + onDeleteValue(existingValue, key); } // If present notify about existing values else if (onExistingValue) { - onExistingValue(key, existingValue, valueInNewMap); + onExistingValue(existingValue, valueInNewMap, key); } }); @@ -3544,6 +3542,63 @@ namespace ts { clearMap(map, options.onDeleteValue); } } + + export function addFileWatcher(host: System, file: string, cb: FileWatcherCallback): FileWatcher { + return host.watchFile(file, cb); + } + + export function addFileWatcherWithLogging(host: System, file: string, cb: FileWatcherCallback, log: (s: string) => void): FileWatcher { + const watcherCaption = `FileWatcher:: `; + return createWatcherWithLogging(addFileWatcher, watcherCaption, log, host, file, cb); + } + + export type FilePathWatcherCallback = (fileName: string, eventKind: FileWatcherEventKind, filePath: Path) => void; + export function addFilePathWatcher(host: System, file: string, cb: FilePathWatcherCallback, path: Path): FileWatcher { + return host.watchFile(file, (fileName, eventKind) => cb(fileName, eventKind, path)); + } + + export function addFilePathWatcherWithLogging(host: System, file: string, cb: FilePathWatcherCallback, path: Path, log: (s: string) => void): FileWatcher { + const watcherCaption = `FileWatcher:: `; + return createWatcherWithLogging(addFileWatcher, watcherCaption, log, host, file, cb, path); + } + + export function addDirectoryWatcher(host: System, directory: string, cb: DirectoryWatcherCallback, flags: WatchDirectoryFlags): FileWatcher { + const recursive = (flags & WatchDirectoryFlags.Recursive) !== 0; + return host.watchDirectory(directory, fileName => { + cb(getNormalizedAbsolutePath(fileName, directory)); + }, recursive); + } + + export function addDirectoryWatcherWithLogging(host: System, directory: string, cb: DirectoryWatcherCallback, flags: WatchDirectoryFlags, log: (s: string) => void): FileWatcher { + const watcherCaption = `DirectoryWatcher ${(flags & WatchDirectoryFlags.Recursive) !== 0 ? "recursive" : ""}:: `; + return createWatcherWithLogging(addDirectoryWatcher, watcherCaption, log, host, directory, cb, flags); + } + + type WatchCallback = (fileName: string, cbOptional1?: T, optional?: U) => void; + type AddWatch = (host: System, file: string, cb: WatchCallback, optional?: U) => FileWatcher; + function createWatcherWithLogging(addWatch: AddWatch, watcherCaption: string, log: (s: string) => void, host: System, file: string, cb: WatchCallback, optional?: U): FileWatcher { + const info = `PathInfo: ${file}`; + log(`${watcherCaption}Added: ${info}`); + const watcher = addWatch(host, file, (fileName, cbOptional1?) => { + const optionalInfo = cbOptional1 !== undefined ? ` ${cbOptional1}` : ""; + log(`${watcherCaption}Trigger: ${fileName}${optionalInfo} ${info}`); + cb(fileName, cbOptional1, optional); + }, optional); + return { + close: () => { + log(`${watcherCaption}Close: ${info}`); + watcher.close(); + } + }; + } + + export function closeFileWatcher(watcher: FileWatcher) { + watcher.close(); + } + + export function closeFileWatcherOf(objWithWatcher: T) { + objWithWatcher.watcher.close(); + } } namespace ts { diff --git a/src/compiler/watchedProgram.ts b/src/compiler/watchedProgram.ts index 084c707cea8fa..2a23e66763b3d 100644 --- a/src/compiler/watchedProgram.ts +++ b/src/compiler/watchedProgram.ts @@ -4,8 +4,7 @@ namespace ts { export type DiagnosticReporter = (diagnostic: Diagnostic) => void; - export type DiagnosticWorker = (diagnostic: Diagnostic, host: FormatDiagnosticsHost, system: System) => void; - export type ParseConfigFile = (configFileName: string, optionsToExtend: CompilerOptions, system: System, reportDiagnostic: DiagnosticReporter, reportWatchDiagnostic: DiagnosticReporter) => ParsedCommandLine; + export type ParseConfigFile = (configFileName: string, optionsToExtend: CompilerOptions, system: PartialSystem, reportDiagnostic: DiagnosticReporter, reportWatchDiagnostic: DiagnosticReporter) => ParsedCommandLine; export interface WatchingSystemHost { // FS system to use system: System; @@ -19,7 +18,7 @@ namespace ts { // Callbacks to do custom action before creating program and after creating program beforeCompile(compilerOptions: CompilerOptions): void; - afterCompile(host: System, program: Program, builder: Builder): void; + afterCompile(host: PartialSystem, program: Program, builder: Builder): void; } const defaultFormatDiagnosticsHost: FormatDiagnosticsHost = sys ? { @@ -62,7 +61,7 @@ namespace ts { system.write(ts.formatDiagnosticsWithColorAndContext([diagnostic], host) + host.getNewLine()); } - export function parseConfigFile(configFileName: string, optionsToExtend: CompilerOptions, system: System, reportDiagnostic: DiagnosticReporter, reportWatchDiagnostic: DiagnosticReporter): ParsedCommandLine { + export function parseConfigFile(configFileName: string, optionsToExtend: CompilerOptions, system: PartialSystem, reportDiagnostic: DiagnosticReporter, reportWatchDiagnostic: DiagnosticReporter): ParsedCommandLine { let configFileText: string; try { configFileText = system.readFile(configFileName); @@ -90,7 +89,7 @@ namespace ts { return configParseResult; } - function reportEmittedFiles(files: string[], system: System): void { + function reportEmittedFiles(files: string[], system: PartialSystem): void { if (!files || files.length === 0) { return; } @@ -101,7 +100,7 @@ namespace ts { } } - export function handleEmitOutputAndReportErrors(system: System, program: Program, + export function handleEmitOutputAndReportErrors(system: PartialSystem, program: Program, emittedFiles: string[], emitSkipped: boolean, diagnostics: Diagnostic[], reportDiagnostic: DiagnosticReporter ): ExitStatus { @@ -142,7 +141,7 @@ namespace ts { afterCompile: compileWatchedProgram, }; - function compileWatchedProgram(host: System, program: Program, builder: Builder) { + function compileWatchedProgram(host: PartialSystem, program: Program, builder: Builder) { // First get and report any syntactic errors. let diagnostics = program.getSyntacticDiagnostics(); let reportSemanticDiagnostics = false; @@ -249,26 +248,28 @@ namespace ts { let hasInvalidatedResolution: HasInvalidatedResolution; // Passed along to see if source file has invalidated resolutions let hasChangedCompilerOptions = false; // True if the compiler options have changed between compilations + const loggingEnabled = compilerOptions.diagnostics || compilerOptions.extendedDiagnostics; + const writeLog: (s: string) => void = loggingEnabled ? s => system.write(s) : noop; + const watchFile = loggingEnabled ? ts.addFileWatcherWithLogging : ts.addFileWatcher; + const watchFilePath = loggingEnabled ? ts.addFilePathWatcherWithLogging : ts.addFilePathWatcher; + const watchDirectory = loggingEnabled ? ts.addDirectoryWatcherWithLogging : ts.addDirectoryWatcher; + watchingHost = watchingHost || createWatchingSystemHost(compilerOptions.pretty); const { system, parseConfigFile, reportDiagnostic, reportWatchDiagnostic, beforeCompile, afterCompile } = watchingHost; - let host: System; + const host = configFileName ? createCachedPartialSystem(system) : system; if (configFileName) { - host = createCachedSystem(system); - configFileWatcher = system.watchFile(configFileName, onConfigFileChanged); - } - else { - host = system; + configFileWatcher = watchFile(system, configFileName, scheduleProgramReload, writeLog); } const currentDirectory = host.getCurrentDirectory(); - const getCanonicalFileName = createGetCanonicalFileName(host.useCaseSensitiveFileNames); + const getCanonicalFileName = createGetCanonicalFileName(system.useCaseSensitiveFileNames); // Cache for the module resolution const resolutionCache = createResolutionCache( fileName => toPath(fileName), () => compilerOptions, watchFailedLookupLocation, - s => writeLog(s) + writeLog ); // There is no extra check needed since we can just rely on the program to decide emit @@ -303,7 +304,7 @@ namespace ts { builder.onProgramUpdateGraph(program, hasInvalidatedResolution); // Update watches - updateMissingFilePathsWatch(program, missingFilesMap || (missingFilesMap = createMap()), watchMissingFilePath, closeMissingFilePathWatcher); + updateMissingFilePathsWatch(program, missingFilesMap || (missingFilesMap = createMap()), watchMissingFilePath); if (missingFilePathsRequestedForRelease) { // These are the paths that program creater told us as not in use any more but were missing on the disk. // We didnt remove the entry for them from sourceFiles cache so that we dont have to do File IO, @@ -324,7 +325,7 @@ namespace ts { function createWatchedCompilerHost(options: CompilerOptions): CompilerHost { const newLine = getNewLineCharacter(options, system); - const realpath = host.realpath && ((path: string) => host.realpath(path)); + const realpath = system.realpath && ((path: string) => system.realpath(path)); return { getSourceFile: getVersionedSourceFile, @@ -333,14 +334,14 @@ namespace ts { getDefaultLibFileName: options => combinePaths(getDefaultLibLocation(), getDefaultLibFileName(options)), writeFile: (_fileName, _data, _writeByteOrderMark, _onError?, _sourceFiles?) => { }, getCurrentDirectory: memoize(() => host.getCurrentDirectory()), - useCaseSensitiveFileNames: () => host.useCaseSensitiveFileNames, + useCaseSensitiveFileNames: () => system.useCaseSensitiveFileNames, getCanonicalFileName, getNewLine: () => newLine, fileExists, - readFile: fileName => host.readFile(fileName), - trace: (s: string) => host.write(s + newLine), + readFile: fileName => system.readFile(fileName), + trace: (s: string) => system.write(s + newLine), directoryExists: directoryName => host.directoryExists(directoryName), - getEnvironmentVariable: name => host.getEnvironmentVariable ? host.getEnvironmentVariable(name) : "", + getEnvironmentVariable: name => system.getEnvironmentVariable ? system.getEnvironmentVariable(name) : "", getDirectories: (path: string) => host.getDirectories(path), realpath, resolveTypeReferenceDirectives: (typeDirectiveNames, containingFile) => resolutionCache.resolveTypeReferenceDirectives(typeDirectiveNames, containingFile), @@ -365,7 +366,7 @@ namespace ts { } function getDefaultLibLocation(): string { - return getDirectoryPath(normalizePath(host.getExecutingFilePath())); + return getDirectoryPath(normalizePath(system.getExecutingFilePath())); } function getVersionedSourceFile(fileName: string, languageVersion: ScriptTarget, onError?: (message: string) => void, shouldCreateNewSourceFile?: boolean): SourceFile { @@ -390,7 +391,7 @@ namespace ts { hostSourceFile.sourceFile = sourceFile; sourceFile.version = hostSourceFile.version.toString(); if (!hostSourceFile.fileWatcher) { - hostSourceFile.fileWatcher = watchSourceFileForChanges(path); + hostSourceFile.fileWatcher = watchFilePath(system, fileName, onSourceFileChange, path, writeLog); } } else { @@ -403,7 +404,7 @@ namespace ts { let fileWatcher: FileWatcher; if (sourceFile) { sourceFile.version = "0"; - fileWatcher = watchSourceFileForChanges(path); + fileWatcher = watchFilePath(system, fileName, onSourceFileChange, path, writeLog); sourceFilesCache.set(path, { sourceFile, version: 0, fileWatcher }); } else { @@ -418,7 +419,7 @@ namespace ts { let text: string; try { performance.mark("beforeIORead"); - text = host.readFile(fileName, compilerOptions.charset); + text = system.readFile(fileName, compilerOptions.charset); performance.mark("afterIORead"); performance.measure("I/O Read", "beforeIORead", "afterIORead"); } @@ -502,7 +503,7 @@ namespace ts { writeLog(`Reloading config file: ${configFileName}`); needsReload = false; - const cachedHost = host as CachedSystem; + const cachedHost = host as CachedPartialSystem; cachedHost.clearCache(); const configParseResult = parseConfigFile(configFileName, optionsToExtendForConfigFile, cachedHost, reportDiagnostic, reportWatchDiagnostic); rootFileNames = configParseResult.fileNames; @@ -517,13 +518,7 @@ namespace ts { watchConfigFileWildCardDirectories(); } - function watchSourceFileForChanges(path: Path) { - return host.watchFile(path, (fileName, eventKind) => onSourceFileChange(fileName, path, eventKind)); - } - - function onSourceFileChange(fileName: string, path: Path, eventKind: FileWatcherEventKind) { - writeLog(`Source file path : ${path} changed: ${FileWatcherEventKind[eventKind]}, fileName: ${fileName}`); - + function onSourceFileChange(fileName: string, eventKind: FileWatcherEventKind, path: Path) { updateCachedSystemWithFile(fileName, path, eventKind); const hostSourceFile = sourceFilesCache.get(path); if (hostSourceFile) { @@ -553,35 +548,29 @@ namespace ts { function updateCachedSystemWithFile(fileName: string, path: Path, eventKind: FileWatcherEventKind) { if (configFileName) { - (host as CachedSystem).addOrDeleteFile(fileName, path, eventKind); + (host as CachedPartialSystem).addOrDeleteFile(fileName, path, eventKind); } } - function watchFailedLookupLocation(failedLookupLocation: string, failedLookupLocationPath: Path, containingFile: string, name: string) { - return host.watchFile(failedLookupLocation, (fileName, eventKind) => onFailedLookupLocationChange(fileName, eventKind, failedLookupLocation, failedLookupLocationPath, containingFile, name)); + function watchFailedLookupLocation(failedLookupLocation: string, failedLookupLocationPath: Path) { + return watchFilePath(system, failedLookupLocation, onFailedLookupLocationChange, failedLookupLocationPath, writeLog); } - function onFailedLookupLocationChange(fileName: string, eventKind: FileWatcherEventKind, failedLookupLocation: string, failedLookupLocationPath: Path, containingFile: string, name: string) { - writeLog(`Failed lookup location : ${failedLookupLocation} changed: ${FileWatcherEventKind[eventKind]}, fileName: ${fileName} containingFile: ${containingFile}, name: ${name}`); + function onFailedLookupLocationChange(fileName: string, eventKind: FileWatcherEventKind, failedLookupLocationPath: Path) { updateCachedSystemWithFile(fileName, failedLookupLocationPath, eventKind); resolutionCache.invalidateResolutionOfChangedFailedLookupLocation(failedLookupLocationPath); scheduleProgramUpdate(); } function watchMissingFilePath(missingFilePath: Path) { - return host.watchFile(missingFilePath, (fileName, eventKind) => onMissingFileChange(fileName, missingFilePath, eventKind)); + return watchFilePath(system, missingFilePath, onMissingFileChange, missingFilePath, writeLog); } - function closeMissingFilePathWatcher(_missingFilePath: Path, fileWatcher: FileWatcher) { - fileWatcher.close(); - } - - function onMissingFileChange(fileName: string, missingFilePath: Path, eventKind: FileWatcherEventKind) { - writeLog(`Missing file path : ${missingFilePath} changed: ${FileWatcherEventKind[eventKind]}, fileName: ${fileName}`); + function onMissingFileChange(fileName: string, eventKind: FileWatcherEventKind, missingFilePath: Path) { updateCachedSystemWithFile(fileName, missingFilePath, eventKind); if (eventKind === FileWatcherEventKind.Created && missingFilesMap.has(missingFilePath)) { - closeMissingFilePathWatcher(missingFilePath, missingFilesMap.get(missingFilePath)); + missingFilesMap.get(missingFilePath).close(); missingFilesMap.delete(missingFilePath); // Delete the entry in the source files cache so that new source file is created @@ -596,19 +585,12 @@ namespace ts { updateWatchingWildcardDirectories( watchedWildcardDirectories || (watchedWildcardDirectories = createMap()), createMapFromTemplate(configFileWildCardDirectories), - watchWildCardDirectory, - stopWatchingWildCardDirectory + watchWildCardDirectory ); } function watchWildCardDirectory(directory: string, flags: WatchDirectoryFlags) { - return host.watchDirectory(directory, fileOrFolder => - onFileAddOrRemoveInWatchedDirectory(getNormalizedAbsolutePath(fileOrFolder, directory)), - (flags & WatchDirectoryFlags.Recursive) !== 0); - } - - function stopWatchingWildCardDirectory(_directory: string, { watcher }: WildcardDirectoryWatcher, _recursiveChanged: boolean) { - watcher.close(); + return watchDirectory(system, directory, onFileAddOrRemoveInWatchedDirectory, flags, writeLog); } function onFileAddOrRemoveInWatchedDirectory(fileOrFolder: string) { @@ -617,7 +599,7 @@ namespace ts { const fileOrFolderPath = toPath(fileOrFolder); // Since the file existance changed, update the sourceFiles cache - (host as CachedSystem).addOrDeleteFileOrFolder(fileOrFolder, fileOrFolderPath); + (host as CachedPartialSystem).addOrDeleteFileOrFolder(fileOrFolder, fileOrFolderPath); removeSourceFile(fileOrFolderPath); // If a change was made inside "folder/file", node will trigger the callback twice: @@ -628,8 +610,6 @@ namespace ts { return; } - writeLog(`Project: ${configFileName} Detected file add/remove of supported extension: ${fileOrFolder}`); - // Reload is pending, do the reload if (!needsReload) { const result = getFileNamesFromConfigSpecs(configFileSpecs, getDirectoryPath(configFileName), compilerOptions, host); @@ -643,70 +623,8 @@ namespace ts { } } - function onConfigFileChanged(fileName: string, eventKind: FileWatcherEventKind) { - writeLog(`Config file : ${configFileName} changed: ${FileWatcherEventKind[eventKind]}, fileName: ${fileName}`); - scheduleProgramReload(); - } - - function writeLog(s: string) { - const hasDiagnostics = compilerOptions.diagnostics || compilerOptions.extendedDiagnostics; - if (hasDiagnostics) { - host.write(s); - } - } - function computeHash(data: string) { return system.createHash ? system.createHash(data) : data; } } - - interface CachedSystem extends System, CachedHost { - } - - function createCachedSystem(host: System): CachedSystem { - const getFileSize = host.getFileSize ? (path: string) => host.getFileSize(path) : undefined; - const watchFile = host.watchFile ? (path: string, callback: FileWatcherCallback, pollingInterval?: number) => host.watchFile(path, callback, pollingInterval) : undefined; - const watchDirectory = host.watchDirectory ? (path: string, callback: DirectoryWatcherCallback, recursive?: boolean) => host.watchDirectory(path, callback, recursive) : undefined; - const getModifiedTime = host.getModifiedTime ? (path: string) => host.getModifiedTime(path) : undefined; - const createHash = host.createHash ? (data: string) => host.createHash(data) : undefined; - const getMemoryUsage = host.getMemoryUsage ? () => host.getMemoryUsage() : undefined; - const realpath = host.realpath ? (path: string) => host.realpath(path) : undefined; - const tryEnableSourceMapsForHost = host.tryEnableSourceMapsForHost ? () => host.tryEnableSourceMapsForHost() : undefined; - const setTimeout = host.setTimeout ? (callback: (...args: any[]) => void, ms: number, ...args: any[]) => host.setTimeout(callback, ms, ...args) : undefined; - const clearTimeout = host.clearTimeout ? (timeoutId: any) => host.clearTimeout(timeoutId) : undefined; - - const cachedPartialSystem = createCachedPartialSystem(host); - return { - args: host.args, - newLine: host.newLine, - useCaseSensitiveFileNames: host.useCaseSensitiveFileNames, - write: s => host.write(s), - readFile: (path, encoding?) => host.readFile(path, encoding), - getFileSize, - writeFile: (fileName, data, writeByteOrderMark?) => cachedPartialSystem.writeFile(fileName, data, writeByteOrderMark), - watchFile, - watchDirectory, - resolvePath: path => host.resolvePath(path), - fileExists: fileName => cachedPartialSystem.fileExists(fileName), - directoryExists: dir => cachedPartialSystem.directoryExists(dir), - createDirectory: dir => cachedPartialSystem.createDirectory(dir), - getExecutingFilePath: () => host.getExecutingFilePath(), - getCurrentDirectory: () => cachedPartialSystem.getCurrentDirectory(), - getDirectories: dir => cachedPartialSystem.getDirectories(dir), - readDirectory: (path, extensions, excludes, includes, depth) => cachedPartialSystem.readDirectory(path, extensions, excludes, includes, depth), - getModifiedTime, - createHash, - getMemoryUsage, - exit: exitCode => host.exit(exitCode), - realpath, - getEnvironmentVariable: name => host.getEnvironmentVariable(name), - tryEnableSourceMapsForHost, - debugMode: host.debugMode, - setTimeout, - clearTimeout, - addOrDeleteFileOrFolder: (fileOrFolder, fileOrFolderPath) => cachedPartialSystem.addOrDeleteFileOrFolder(fileOrFolder, fileOrFolderPath), - addOrDeleteFile: (file, filePath, eventKind) => cachedPartialSystem.addOrDeleteFile(file, filePath, eventKind), - clearCache: () => cachedPartialSystem.clearCache() - }; - } } diff --git a/src/server/editorServices.ts b/src/server/editorServices.ts index 385ade0b81644..292f6e3b90a86 100644 --- a/src/server/editorServices.ts +++ b/src/server/editorServices.ts @@ -248,22 +248,7 @@ namespace ts.server { TypeRoot = "Type root of the project", ClosedScriptInfo = "Closed Script info", ConfigFileForInferredRoot = "Config file for the inferred project root", - FailedLookupLocation = "Failed lookup locations in module resolution" - } - - /* @internal */ - export const enum WatcherCloseReason { - ProjectClose = "Project close", - NotNeeded = "After project update isnt required any more", - FileCreated = "File got created", - RecursiveChanged = "Recursive changed for the watch", - ProjectReloadHitMaxSize = "Project reloaded and hit the max file size capacity", - OrphanScriptInfoWithChange = "Orphan script info, Detected change in file thats not needed any more", - OrphanScriptInfo = "Removing Orphan script info as part of cleanup", - FileDeleted = "File was deleted", - FileOpened = "File opened", - ConfigProjectCreated = "Config file project created", - FileClosed = "File is closed" + FailedLookupLocation = "Directory of Failed lookup locations in module resolution" } const enum ConfigFileWatcherStatus { @@ -276,9 +261,6 @@ namespace ts.server { RootOfInferredProjectFalse = "Open file was set as not inferred root", } - /* @internal */ - export type ServerDirectoryWatcherCallback = (path: NormalizedPath) => void; - interface ConfigFileExistenceInfo { /** * Cached value of existence of config file @@ -318,6 +300,10 @@ namespace ts.server { allowLocalPluginLoads?: boolean; } + type WatchFile = (host: ServerHost, file: string, cb: FileWatcherCallback, watchType: WatchType, project?: Project) => FileWatcher; + type WatchFilePath = (host: ServerHost, file: string, cb: FilePathWatcherCallback, path: Path, watchType: WatchType, project?: Project) => FileWatcher; + type WatchDirectory = (host: ServerHost, directory: string, cb: DirectoryWatcherCallback, flags: WatchDirectoryFlags, watchType: WatchType, project?: Project) => FileWatcher; + export class ProjectService { public readonly typingsCache: TypingsCache; @@ -392,6 +378,13 @@ namespace ts.server { /** Tracks projects that we have already sent telemetry for. */ private readonly seenProjects = createMap(); + /*@internal*/ + readonly watchFile: WatchFile; + /*@internal*/ + readonly watchFilePath: WatchFilePath; + /*@internal*/ + readonly watchDirectory: WatchDirectory; + constructor(opts: ProjectServiceOptions) { this.host = opts.host; this.logger = opts.logger; @@ -422,6 +415,21 @@ namespace ts.server { }; this.documentRegistry = createDocumentRegistry(this.host.useCaseSensitiveFileNames, this.currentDirectory); + if (this.logger.hasLevel(LogLevel.verbose)) { + this.watchFile = (host, file, cb, watchType, project) => ts.addFileWatcherWithLogging(host, file, cb, this.createWatcherLog(watchType, project)); + this.watchFilePath = (host, file, cb, path, watchType, project) => ts.addFilePathWatcherWithLogging(host, file, cb, path, this.createWatcherLog(watchType, project)); + this.watchDirectory = (host, dir, cb, flags, watchType, project) => ts.addDirectoryWatcherWithLogging(host, dir, cb, flags, this.createWatcherLog(watchType, project)); + } + else { + this.watchFile = ts.addFileWatcher; + this.watchFilePath = ts.addFilePathWatcher; + this.watchDirectory = ts.addDirectoryWatcher; + } + } + + private createWatcherLog(watchType: WatchType, project: Project | undefined): (s: string) => void { + const detailedInfo = ` Project: ${project ? project.getProjectName() : ""} WatchType: ${watchType}`; + return s => this.logger.info(s + detailedInfo); } toPath(fileName: string, basePath = this.currentDirectory) { @@ -674,7 +682,7 @@ namespace ts.server { else if (!info.isScriptOpen()) { if (info.containingProjects.length === 0) { // Orphan script info, remove it as we can always reload it on next open file request - this.stopWatchingScriptInfo(info, WatcherCloseReason.OrphanScriptInfoWithChange); + this.stopWatchingScriptInfo(info); this.filenameToScriptInfo.delete(info.path); } else { @@ -687,7 +695,7 @@ namespace ts.server { } private handleDeletedFile(info: ScriptInfo) { - this.stopWatchingScriptInfo(info, WatcherCloseReason.FileDeleted); + this.stopWatchingScriptInfo(info); // TODO: handle isOpen = true case @@ -705,8 +713,8 @@ namespace ts.server { } /* @internal */ - onTypeRootFileChanged(project: ConfiguredProject, fileOrFolder: NormalizedPath) { - project.getCachedServerHost().addOrDeleteFileOrFolder(fileOrFolder, this.toPath(fileOrFolder)); + onTypeRootFileChanged(project: ConfiguredProject, fileOrFolder: string) { + project.getCachedPartialSystem().addOrDeleteFileOrFolder(fileOrFolder, this.toPath(fileOrFolder)); project.updateTypes(); this.delayUpdateProjectGraphAndInferredProjectsRefresh(project); } @@ -717,8 +725,8 @@ namespace ts.server { * @param fileName the absolute file name that changed in watched directory */ /* @internal */ - onFileAddOrRemoveInWatchedDirectoryOfProject(project: ConfiguredProject, fileOrFolder: NormalizedPath) { - project.getCachedServerHost().addOrDeleteFileOrFolder(fileOrFolder, this.toPath(fileOrFolder)); + onFileAddOrRemoveInWatchedDirectoryOfProject(project: ConfiguredProject, fileOrFolder: string) { + project.getCachedPartialSystem().addOrDeleteFileOrFolder(fileOrFolder, this.toPath(fileOrFolder)); const configFilename = project.getConfigFilePath(); // If a change was made inside "folder/file", node will trigger the callback twice: @@ -730,7 +738,7 @@ namespace ts.server { } const configFileSpecs = project.configFileSpecs; - const result = getFileNamesFromConfigSpecs(configFileSpecs, getDirectoryPath(configFilename), project.getCompilerOptions(), project.getCachedServerHost(), this.hostConfiguration.extraFileExtensions); + const result = getFileNamesFromConfigSpecs(configFileSpecs, getDirectoryPath(configFilename), project.getCompilerOptions(), project.getCachedPartialSystem(), this.hostConfiguration.extraFileExtensions); project.updateErrorOnNoInputFiles(result.fileNames.length !== 0); this.updateNonInferredProjectFiles(project, result.fileNames, fileNamePropertyReader, /*clientFileName*/ undefined); this.delayUpdateProjectGraphAndInferredProjectsRefresh(project); @@ -923,7 +931,7 @@ namespace ts.server { this.filenameToScriptInfo.forEach(info => { if (!info.isScriptOpen() && info.isOrphan()) { // if there are not projects that include this script info - delete it - this.stopWatchingScriptInfo(info, WatcherCloseReason.OrphanScriptInfo); + this.stopWatchingScriptInfo(info); this.filenameToScriptInfo.delete(info.path); } }); @@ -967,10 +975,7 @@ namespace ts.server { // close existing watcher if (configFileExistenceInfo.configFileWatcherForRootOfInferredProject) { const configFileName = project.getConfigFilePath(); - this.closeFileWatcher( - WatchType.ConfigFileForInferredRoot, /*project*/ undefined, configFileName, - configFileExistenceInfo.configFileWatcherForRootOfInferredProject, WatcherCloseReason.ConfigProjectCreated - ); + configFileExistenceInfo.configFileWatcherForRootOfInferredProject.close(); configFileExistenceInfo.configFileWatcherForRootOfInferredProject = undefined; this.logConfigFileWatchUpdate(configFileName, project.canonicalConfigFilePath, configFileExistenceInfo, ConfigFileWatcherStatus.UpdatedCallback); } @@ -1002,11 +1007,7 @@ namespace ts.server { // created when any of the script infos are added as root of inferred project if (this.configFileExistenceImpactsRootOfInferredProject(configFileExistenceInfo)) { Debug.assert(!configFileExistenceInfo.configFileWatcherForRootOfInferredProject); - configFileExistenceInfo.configFileWatcherForRootOfInferredProject = this.addFileWatcher( - WatchType.ConfigFileForInferredRoot, /*project*/ undefined, configFileName, - (_filename, eventKind) => this.onConfigFileChangeForOpenScriptInfo(configFileName, eventKind) - ); - this.logConfigFileWatchUpdate(configFileName, closedProject.canonicalConfigFilePath, configFileExistenceInfo, ConfigFileWatcherStatus.UpdatedCallback); + this.createConfigFileWatcherOfConfigFileExistence(configFileName, closedProject.canonicalConfigFilePath, configFileExistenceInfo); } } else { @@ -1016,7 +1017,7 @@ namespace ts.server { } private logConfigFileWatchUpdate(configFileName: NormalizedPath, canonicalConfigFilePath: string, configFileExistenceInfo: ConfigFileExistenceInfo, status: ConfigFileWatcherStatus) { - if (!this.logger.loggingEnabled()) { + if (!this.logger.hasLevel(LogLevel.verbose)) { return; } const inferredRoots: string[] = []; @@ -1036,21 +1037,32 @@ namespace ts.server { this.logger.info(`ConfigFilePresence:: Current Watches: ${watches}:: File: ${configFileName} Currently impacted open files: RootsOfInferredProjects: ${inferredRoots} OtherOpenFiles: ${otherFiles} Status: ${status}`); } + /** + * Create the watcher for the configFileExistenceInfo + */ + private createConfigFileWatcherOfConfigFileExistence( + configFileName: NormalizedPath, + canonicalConfigFilePath: string, + configFileExistenceInfo: ConfigFileExistenceInfo + ) { + configFileExistenceInfo.configFileWatcherForRootOfInferredProject = this.watchFile( + this.host, + configFileName, + (_filename, eventKind) => this.onConfigFileChangeForOpenScriptInfo(configFileName, eventKind), + WatchType.ConfigFileForInferredRoot + ); + this.logConfigFileWatchUpdate(configFileName, canonicalConfigFilePath, configFileExistenceInfo, ConfigFileWatcherStatus.UpdatedCallback); + } + /** * Close the config file watcher in the cached ConfigFileExistenceInfo * if there arent any open files that are root of inferred project */ - private closeConfigFileWatcherOfConfigFileExistenceInfo( - configFileName: NormalizedPath, configFileExistenceInfo: ConfigFileExistenceInfo, - reason: WatcherCloseReason - ) { + private closeConfigFileWatcherOfConfigFileExistenceInfo(configFileExistenceInfo: ConfigFileExistenceInfo) { // Close the config file watcher if there are no more open files that are root of inferred project if (configFileExistenceInfo.configFileWatcherForRootOfInferredProject && !this.configFileExistenceImpactsRootOfInferredProject(configFileExistenceInfo)) { - this.closeFileWatcher( - WatchType.ConfigFileForInferredRoot, /*project*/ undefined, configFileName, - configFileExistenceInfo.configFileWatcherForRootOfInferredProject, reason - ); + configFileExistenceInfo.configFileWatcherForRootOfInferredProject.close(); configFileExistenceInfo.configFileWatcherForRootOfInferredProject = undefined; } } @@ -1074,9 +1086,7 @@ namespace ts.server { if (infoIsRootOfInferredProject) { // But if it is a root, it could be the last script info that is root of inferred project // and hence we would need to close the config file watcher - this.closeConfigFileWatcherOfConfigFileExistenceInfo( - configFileName, configFileExistenceInfo, WatcherCloseReason.FileClosed - ); + this.closeConfigFileWatcherOfConfigFileExistenceInfo(configFileExistenceInfo); } // If there are no open files that are impacted by configFileExistenceInfo after closing this script info @@ -1097,27 +1107,24 @@ namespace ts.server { startWatchingConfigFilesForInferredProjectRoot(info: ScriptInfo) { Debug.assert(info.isScriptOpen()); this.forEachConfigFileLocation(info, (configFileName, canonicalConfigFilePath) => { - let configFilePresenceInfo = this.configFileExistenceInfoCache.get(canonicalConfigFilePath); - if (!configFilePresenceInfo) { + let configFileExistenceInfo = this.configFileExistenceInfoCache.get(canonicalConfigFilePath); + if (!configFileExistenceInfo) { // Create the cache - configFilePresenceInfo = { + configFileExistenceInfo = { exists: this.host.fileExists(configFileName), openFilesImpactedByConfigFile: createMap() }; - this.configFileExistenceInfoCache.set(canonicalConfigFilePath, configFilePresenceInfo); + this.configFileExistenceInfoCache.set(canonicalConfigFilePath, configFileExistenceInfo); } // Set this file as the root of inferred project - configFilePresenceInfo.openFilesImpactedByConfigFile.set(info.path, true); - this.logConfigFileWatchUpdate(configFileName, canonicalConfigFilePath, configFilePresenceInfo, ConfigFileWatcherStatus.RootOfInferredProjectTrue); + configFileExistenceInfo.openFilesImpactedByConfigFile.set(info.path, true); + this.logConfigFileWatchUpdate(configFileName, canonicalConfigFilePath, configFileExistenceInfo, ConfigFileWatcherStatus.RootOfInferredProjectTrue); // If there is no configured project for this config file, add the file watcher - if (!configFilePresenceInfo.configFileWatcherForRootOfInferredProject && + if (!configFileExistenceInfo.configFileWatcherForRootOfInferredProject && !this.getConfiguredProjectByCanonicalConfigFilePath(canonicalConfigFilePath)) { - configFilePresenceInfo.configFileWatcherForRootOfInferredProject = this.addFileWatcher(WatchType.ConfigFileForInferredRoot, /*project*/ undefined, configFileName, - (_fileName, eventKind) => this.onConfigFileChangeForOpenScriptInfo(configFileName, eventKind) - ); - this.logConfigFileWatchUpdate(configFileName, canonicalConfigFilePath, configFilePresenceInfo, ConfigFileWatcherStatus.UpdatedCallback); + this.createConfigFileWatcherOfConfigFileExistence(configFileName, canonicalConfigFilePath, configFileExistenceInfo); } }); } @@ -1126,7 +1133,7 @@ namespace ts.server { * This is called by inferred project whenever root script info is removed from it */ /* @internal */ - stopWatchingConfigFilesForInferredProjectRoot(info: ScriptInfo, reason: WatcherCloseReason) { + stopWatchingConfigFilesForInferredProjectRoot(info: ScriptInfo) { this.forEachConfigFileLocation(info, (configFileName, canonicalConfigFilePath) => { const configFileExistenceInfo = this.configFileExistenceInfoCache.get(canonicalConfigFilePath); if (configFileExistenceInfo && configFileExistenceInfo.openFilesImpactedByConfigFile.has(info.path)) { @@ -1137,9 +1144,7 @@ namespace ts.server { this.logConfigFileWatchUpdate(configFileName, canonicalConfigFilePath, configFileExistenceInfo, ConfigFileWatcherStatus.RootOfInferredProjectFalse); // Close the config file watcher - this.closeConfigFileWatcherOfConfigFileExistenceInfo( - configFileName, configFileExistenceInfo, reason - ); + this.closeConfigFileWatcherOfConfigFileExistenceInfo(configFileExistenceInfo); } }); } @@ -1248,7 +1253,7 @@ namespace ts.server { return findProjectByName(projectFileName, this.externalProjects); } - private convertConfigFileContentToProjectOptions(configFilename: string, cachedServerHost: CachedServerHost) { + private convertConfigFileContentToProjectOptions(configFilename: string, cachedServerHost: PartialSystem) { configFilename = normalizePath(configFilename); const configFileContent = this.host.readFile(configFilename); @@ -1385,8 +1390,8 @@ namespace ts.server { } private createConfiguredProject(configFileName: NormalizedPath, clientFileName?: string) { - const cachedServerHost = new CachedServerHost(this.host); - const { projectOptions, configFileErrors, configFileSpecs } = this.convertConfigFileContentToProjectOptions(configFileName, cachedServerHost); + const cachedPartialSystem = createCachedPartialSystem(this.host); + const { projectOptions, configFileErrors, configFileSpecs } = this.convertConfigFileContentToProjectOptions(configFileName, cachedPartialSystem); this.logger.info(`Opened configuration file ${configFileName}`); const languageServiceEnabled = !this.exceededTotalSizeLimitForNonTsFiles(configFileName, projectOptions.compilerOptions, projectOptions.files, fileNamePropertyReader); const project = new ConfiguredProject( @@ -1397,12 +1402,16 @@ namespace ts.server { projectOptions.compilerOptions, languageServiceEnabled, projectOptions.compileOnSave === undefined ? false : projectOptions.compileOnSave, - cachedServerHost); + cachedPartialSystem); project.configFileSpecs = configFileSpecs; // TODO: We probably should also watch the configFiles that are extended - project.configFileWatcher = this.addFileWatcher(WatchType.ConfigFilePath, project, - configFileName, (_fileName, eventKind) => this.onConfigChangedForConfiguredProject(project, eventKind) + project.configFileWatcher = this.watchFile( + this.host, + configFileName, + (_fileName, eventKind) => this.onConfigChangedForConfiguredProject(project, eventKind), + WatchType.ConfigFilePath, + project ); if (languageServiceEnabled) { project.watchWildcards(projectOptions.wildcardDirectories); @@ -1490,7 +1499,7 @@ namespace ts.server { /* @internal */ reloadConfiguredProject(project: ConfiguredProject) { // At this point, there is no reason to not have configFile in the host - const host = project.getCachedServerHost(); + const host = project.getCachedPartialSystem(); // Clear the cache since we are reloading the project from disk host.clearCache(); @@ -1505,8 +1514,8 @@ namespace ts.server { project.setProjectErrors(configFileErrors); if (this.exceededTotalSizeLimitForNonTsFiles(project.canonicalConfigFilePath, projectOptions.compilerOptions, projectOptions.files, fileNamePropertyReader)) { project.disableLanguageService(); - project.stopWatchingWildCards(WatcherCloseReason.ProjectReloadHitMaxSize); - project.stopWatchingTypeRoots(WatcherCloseReason.ProjectReloadHitMaxSize); + project.stopWatchingWildCards(); + project.stopWatchingTypeRoots(); } else { project.enableLanguageService(); @@ -1594,7 +1603,7 @@ namespace ts.server { * @param fileContent is a known version of the file content that is more up to date than the one on disk */ /*@internal*/ - getOrCreateScriptInfo(uncheckedFileName: string, openedByClient: boolean, hostToQueryFileExistsOn: ServerHost) { + getOrCreateScriptInfo(uncheckedFileName: string, openedByClient: boolean, hostToQueryFileExistsOn: PartialSystem) { return this.getOrCreateScriptInfoForNormalizedPath( toNormalizedPath(uncheckedFileName), openedByClient, /*fileContent*/ undefined, /*scriptKind*/ undefined, /*hasMixedContent*/ undefined, hostToQueryFileExistsOn @@ -1610,20 +1619,23 @@ namespace ts.server { // do not watch files with mixed content - server doesn't know how to interpret it if (!info.hasMixedContent) { const { fileName } = info; - info.fileWatcher = this.addFileWatcher(WatchType.ClosedScriptInfo, /*project*/ undefined, fileName, - (_fileName, eventKind) => this.onSourceFileChanged(fileName, eventKind) + info.fileWatcher = this.watchFile( + this.host, + fileName, + (_fileName, eventKind) => this.onSourceFileChanged(fileName, eventKind), + WatchType.ClosedScriptInfo ); } } - private stopWatchingScriptInfo(info: ScriptInfo, reason: WatcherCloseReason) { + private stopWatchingScriptInfo(info: ScriptInfo) { if (info.fileWatcher) { - this.closeFileWatcher(WatchType.ClosedScriptInfo, /*project*/ undefined, info.fileName, info.fileWatcher, reason); + info.fileWatcher.close(); info.fileWatcher = undefined; } } - getOrCreateScriptInfoForNormalizedPath(fileName: NormalizedPath, openedByClient: boolean, fileContent?: string, scriptKind?: ScriptKind, hasMixedContent?: boolean, hostToQueryFileExistsOn?: ServerHost) { + getOrCreateScriptInfoForNormalizedPath(fileName: NormalizedPath, openedByClient: boolean, fileContent?: string, scriptKind?: ScriptKind, hasMixedContent?: boolean, hostToQueryFileExistsOn?: PartialSystem) { const path = normalizedPathToPath(fileName, this.currentDirectory, this.toCanonicalFileName); let info = this.getScriptInfoForPath(path); if (!info) { @@ -1645,7 +1657,7 @@ namespace ts.server { } if (info) { if (openedByClient && !info.isScriptOpen()) { - this.stopWatchingScriptInfo(info, WatcherCloseReason.FileOpened); + this.stopWatchingScriptInfo(info); info.open(fileContent); if (hasMixedContent) { info.registerFileUpdate(); @@ -1693,39 +1705,6 @@ namespace ts.server { } } - /* @internal */ - closeFileWatcher(watchType: WatchType, project: Project, file: string, watcher: FileWatcher, reason: WatcherCloseReason) { - this.logger.info(`FileWatcher:: Close: ${file} Project: ${project ? project.getProjectName() : ""} WatchType: ${watchType} Reason: ${reason}`); - watcher.close(); - } - - /* @internal */ - addFileWatcher(watchType: WatchType, project: Project, file: string, cb: FileWatcherCallback) { - this.logger.info(`FileWatcher:: Added: ${file} Project: ${project ? project.getProjectName() : ""} WatchType: ${watchType}`); - return this.host.watchFile(file, (fileName, eventKind) => { - this.logger.info(`FileWatcher:: File ${FileWatcherEventKind[eventKind]}: ${file} Project: ${project ? project.getProjectName() : ""} WatchType: ${watchType}`); - cb(fileName, eventKind); - }); - } - - /* @internal */ - closeDirectoryWatcher(watchType: WatchType, project: Project, directory: string, watcher: FileWatcher, flags: WatchDirectoryFlags, reason: WatcherCloseReason) { - const recursive = (flags & WatchDirectoryFlags.Recursive) !== 0; - this.logger.info(`DirectoryWatcher ${recursive ? "recursive" : ""}:: Close: ${directory} Project: ${project.getProjectName()} WatchType: ${watchType} Reason: ${reason}`); - watcher.close(); - } - - /* @internal */ - addDirectoryWatcher(watchType: WatchType, project: Project, directory: string, cb: ServerDirectoryWatcherCallback, flags: WatchDirectoryFlags) { - const recursive = (flags & WatchDirectoryFlags.Recursive) !== 0; - this.logger.info(`DirectoryWatcher ${recursive ? "recursive" : ""}:: Added: ${directory} Project: ${project.getProjectName()} WatchType: ${watchType}`); - return this.host.watchDirectory(directory, fileName => { - const path = toNormalizedPath(getNormalizedAbsolutePath(fileName, directory)); - this.logger.info(`DirectoryWatcher:: EventOn: ${directory} Trigger: ${fileName} Path: ${path} Project: ${project.getProjectName()} WatchType: ${watchType}`); - cb(path); - }, recursive); - } - closeLog() { this.logger.close(); } diff --git a/src/server/lsHost.ts b/src/server/lsHost.ts index 44b9893bc3d42..b758cd796bf8d 100644 --- a/src/server/lsHost.ts +++ b/src/server/lsHost.ts @@ -4,110 +4,6 @@ /// namespace ts.server { - /*@internal*/ - export class CachedServerHost implements ServerHost { - args: string[]; - newLine: string; - useCaseSensitiveFileNames: boolean; - - private readonly cachedPartialSystem: CachedPartialSystem; - - readonly trace: (s: string) => void; - readonly realpath?: (path: string) => string; - - constructor(private readonly host: ServerHost) { - this.args = host.args; - this.newLine = host.newLine; - this.useCaseSensitiveFileNames = host.useCaseSensitiveFileNames; - if (host.trace) { - this.trace = s => host.trace(s); - } - if (this.host.realpath) { - this.realpath = path => this.host.realpath(path); - } - this.cachedPartialSystem = createCachedPartialSystem(host); - } - - write(s: string) { - return this.host.write(s); - } - - writeFile(fileName: string, data: string, writeByteOrderMark?: boolean) { - this.cachedPartialSystem.writeFile(fileName, data, writeByteOrderMark); - } - - resolvePath(path: string) { - return this.host.resolvePath(path); - } - - createDirectory(path: string) { - Debug.fail(`Why is createDirectory called on the cached server for ${path}`); - } - - getExecutingFilePath() { - return this.host.getExecutingFilePath(); - } - - getCurrentDirectory() { - return this.cachedPartialSystem.getCurrentDirectory(); - } - - exit(exitCode?: number) { - Debug.fail(`Why is exit called on the cached server: ${exitCode}`); - } - - getEnvironmentVariable(name: string) { - Debug.fail(`Why is getEnvironmentVariable called on the cached server: ${name}`); - return this.host.getEnvironmentVariable(name); - } - - getDirectories(rootDir: string) { - return this.cachedPartialSystem.getDirectories(rootDir); - } - - readDirectory(rootDir: string, extensions: string[], excludes: string[], includes: string[], depth: number): string[] { - return this.cachedPartialSystem.readDirectory(rootDir, extensions, excludes, includes, depth); - } - - fileExists(fileName: string): boolean { - return this.cachedPartialSystem.fileExists(fileName); - } - - directoryExists(dirPath: string) { - return this.cachedPartialSystem.directoryExists(dirPath); - } - - readFile(path: string, encoding?: string): string { - return this.host.readFile(path, encoding); - } - - addOrDeleteFileOrFolder(fileOrFolder: NormalizedPath, fileOrFolderPath: Path) { - return this.cachedPartialSystem.addOrDeleteFileOrFolder(fileOrFolder, fileOrFolderPath); - } - - addOrDeleteFile(file: string, path: Path, eventKind: FileWatcherEventKind) { - return this.cachedPartialSystem.addOrDeleteFile(file, path, eventKind); - } - - clearCache() { - return this.cachedPartialSystem.clearCache(); - } - - setTimeout(callback: (...args: any[]) => void, ms: number, ...args: any[]) { - return this.host.setTimeout(callback, ms, ...args); - } - clearTimeout(timeoutId: any) { - return this.host.clearTimeout(timeoutId); - } - setImmediate(callback: (...args: any[]) => void, ...args: any[]) { - this.host.setImmediate(callback, ...args); - } - clearImmediate(timeoutId: any) { - this.host.clearImmediate(timeoutId); - } - - } - export class LSHost implements LanguageServiceHost, ModuleResolutionHost { /*@internal*/ compilationSettings: CompilerOptions; @@ -124,21 +20,26 @@ namespace ts.server { * file system entries as we would anyways be watching files in the project (so safe to cache) */ /*@internal*/ - host: ServerHost; + host: PartialSystem; - constructor(host: ServerHost, private project: Project, private readonly cancellationToken: HostCancellationToken) { + constructor(host: PartialSystem, private project: Project, private readonly cancellationToken: HostCancellationToken) { this.host = host; this.cancellationToken = new ThrottledCancellationToken(cancellationToken, project.projectService.throttleWaitMilliseconds); - if (host.trace) { - this.trace = s => host.trace(s); + const serverHost = this.getServerHost(); + if (serverHost.trace) { + this.trace = s => serverHost.trace(s); } - if (this.host.realpath) { - this.realpath = path => this.host.realpath(path); + if (serverHost.realpath) { + this.realpath = path => serverHost.realpath(path); } } + private getServerHost() { + return this.project.projectService.host; + } + dispose() { this.project = undefined; this.host = undefined; @@ -173,7 +74,7 @@ namespace ts.server { } getDefaultLibFileName() { - const nodeModuleBinDir = getDirectoryPath(normalizePath(this.host.getExecutingFilePath())); + const nodeModuleBinDir = getDirectoryPath(normalizePath(this.getServerHost().getExecutingFilePath())); return combinePaths(nodeModuleBinDir, getDefaultLibFileName(this.compilationSettings)); } @@ -207,7 +108,7 @@ namespace ts.server { } resolvePath(path: string): string { - return this.host.resolvePath(path); + return this.getServerHost().resolvePath(path); } fileExists(file: string): boolean { diff --git a/src/server/project.ts b/src/server/project.ts index cce0fc5b5103a..ec58e4cb88d6d 100644 --- a/src/server/project.ts +++ b/src/server/project.ts @@ -198,7 +198,7 @@ namespace ts.server { languageServiceEnabled: boolean, private compilerOptions: CompilerOptions, public compileOnSaveEnabled: boolean, - host: ServerHost) { + host: PartialSystem) { if (!this.compilerOptions) { this.compilerOptions = getDefaultCompilerOptions(); @@ -216,7 +216,7 @@ namespace ts.server { this.resolutionCache = createResolutionCache( fileName => this.projectService.toPath(fileName), () => this.compilerOptions, - (failedLookupLocation, failedLookupLocationPath, containingFile, name) => this.watchFailedLookupLocation(failedLookupLocation, failedLookupLocationPath, containingFile, name), + (failedLookupLocation, failedLookupLocationPath) => this.watchFailedLookupLocation(failedLookupLocation, failedLookupLocationPath), s => this.projectService.logger.info(s), this.getProjectName(), () => this.getTypeAcquisition().enable ? this.projectService.typingsInstaller.globalTypingsCacheLocation : undefined @@ -233,17 +233,24 @@ namespace ts.server { this.markAsDirty(); } - private watchFailedLookupLocation(failedLookupLocation: string, failedLookupLocationPath: Path, containingFile: string, name: string) { + private watchFailedLookupLocation(failedLookupLocation: string, failedLookupLocationPath: Path) { + return this.projectService.watchFile( + this.projectService.host, + failedLookupLocation, + (fileName, eventKind) => this.onFailedLookupLocationChanged(fileName, eventKind, failedLookupLocationPath), + WatchType.FailedLookupLocation, + this + ); + } + + private onFailedLookupLocationChanged(fileName: string, eventKind: FileWatcherEventKind, failedLookupLocationPath: Path) { // There is some kind of change in the failed lookup location, update the program - return this.projectService.addFileWatcher(WatchType.FailedLookupLocation, this, failedLookupLocation, (fileName, eventKind) => { - this.projectService.logger.info(`Watcher: FailedLookupLocations: Status: ${FileWatcherEventKind[eventKind]}: Location: ${failedLookupLocation}, containingFile: ${containingFile}, name: ${name}`); - if (this.projectKind === ProjectKind.Configured) { - (this.lsHost.host as CachedServerHost).addOrDeleteFile(fileName, failedLookupLocationPath, eventKind); - } - this.resolutionCache.invalidateResolutionOfChangedFailedLookupLocation(failedLookupLocationPath); - this.markAsDirty(); - this.projectService.delayUpdateProjectGraphAndInferredProjectsRefresh(this); - }); + if (this.projectKind === ProjectKind.Configured) { + (this.lsHost.host as CachedPartialSystem).addOrDeleteFile(fileName, failedLookupLocationPath, eventKind); + } + this.resolutionCache.invalidateResolutionOfChangedFailedLookupLocation(failedLookupLocationPath); + this.markAsDirty(); + this.projectService.delayUpdateProjectGraphAndInferredProjectsRefresh(this); } private setInternalCompilerOptionsForEmittingJsFiles() { @@ -387,9 +394,7 @@ namespace ts.server { // Clean up file watchers waiting for missing files if (this.missingFilesMap) { - clearMap(this.missingFilesMap, (missingFilePath, fileWatcher) => { - this.projectService.closeFileWatcher(WatchType.MissingFilePath, this, missingFilePath, fileWatcher, WatcherCloseReason.ProjectClose); - }); + clearMap(this.missingFilesMap, closeFileWatcher); this.missingFilesMap = undefined; } @@ -698,10 +703,7 @@ namespace ts.server { this.program, this.missingFilesMap || (this.missingFilesMap = createMap()), // Watch the missing files - missingFilePath => this.addMissingFileWatcher(missingFilePath), - // Files that are no longer missing (e.g. because they are no longer required) - // should no longer be watched. - (missingFilePath, fileWatcher) => this.closeMissingFileWatcher(missingFilePath, fileWatcher, WatcherCloseReason.NotNeeded) + missingFilePath => this.addMissingFileWatcher(missingFilePath) ); } @@ -726,30 +728,29 @@ namespace ts.server { } private addMissingFileWatcher(missingFilePath: Path) { - const fileWatcher = this.projectService.addFileWatcher( - WatchType.MissingFilePath, this, missingFilePath, + const fileWatcher = this.projectService.watchFile( + this.projectService.host, + missingFilePath, (fileName, eventKind) => { if (this.projectKind === ProjectKind.Configured) { - (this.lsHost.host as CachedServerHost).addOrDeleteFile(fileName, missingFilePath, eventKind); + (this.lsHost.host as CachedPartialSystem).addOrDeleteFile(fileName, missingFilePath, eventKind); } if (eventKind === FileWatcherEventKind.Created && this.missingFilesMap.has(missingFilePath)) { this.missingFilesMap.delete(missingFilePath); - this.closeMissingFileWatcher(missingFilePath, fileWatcher, WatcherCloseReason.FileCreated); + fileWatcher.close(); // When a missing file is created, we should update the graph. this.markAsDirty(); this.projectService.delayUpdateProjectGraphAndInferredProjectsRefresh(this); } - } + }, + WatchType.MissingFilePath, + this ); return fileWatcher; } - private closeMissingFileWatcher(missingFilePath: Path, fileWatcher: FileWatcher, reason: WatcherCloseReason) { - this.projectService.closeFileWatcher(WatchType.MissingFilePath, this, missingFilePath, fileWatcher, reason); - } - isWatchedMissingFile(path: Path) { return this.missingFilesMap && this.missingFilesMap.has(path); } @@ -942,7 +943,7 @@ namespace ts.server { } removeRoot(info: ScriptInfo) { - this.projectService.stopWatchingConfigFilesForInferredProjectRoot(info, WatcherCloseReason.NotNeeded); + this.projectService.stopWatchingConfigFilesForInferredProjectRoot(info); super.removeRoot(info); if (this._isJsInferredProject && info.isJavaScript()) { if (every(this.getRootScriptInfos(), rootInfo => !rootInfo.isJavaScript())) { @@ -968,7 +969,7 @@ namespace ts.server { } close() { - forEach(this.getRootScriptInfos(), info => this.projectService.stopWatchingConfigFilesForInferredProjectRoot(info, WatcherCloseReason.ProjectClose)); + forEach(this.getRootScriptInfos(), info => this.projectService.stopWatchingConfigFilesForInferredProjectRoot(info)); super.close(); } @@ -1014,8 +1015,8 @@ namespace ts.server { compilerOptions: CompilerOptions, languageServiceEnabled: boolean, public compileOnSaveEnabled: boolean, - cachedServerHost: CachedServerHost) { - super(configFileName, ProjectKind.Configured, projectService, documentRegistry, hasExplicitListOfFiles, languageServiceEnabled, compilerOptions, compileOnSaveEnabled, cachedServerHost); + cachedPartialSystem: PartialSystem) { + super(configFileName, ProjectKind.Configured, projectService, documentRegistry, hasExplicitListOfFiles, languageServiceEnabled, compilerOptions, compileOnSaveEnabled, cachedPartialSystem); this.canonicalConfigFilePath = asNormalizedPath(projectService.toCanonicalFileName(configFileName)); this.enablePlugins(); } @@ -1034,8 +1035,8 @@ namespace ts.server { } /*@internal*/ - getCachedServerHost() { - return this.lsHost.host as CachedServerHost; + getCachedPartialSystem() { + return this.lsHost.host as CachedPartialSystem; } getConfigFilePath() { @@ -1167,29 +1168,21 @@ namespace ts.server { this.directoriesWatchedForWildcards || (this.directoriesWatchedForWildcards = createMap()), wildcardDirectories, // Create new directory watcher - (directory, flags) => this.projectService.addDirectoryWatcher( - WatchType.WildcardDirectories, this, directory, + (directory, flags) => this.projectService.watchDirectory( + this.projectService.host, + directory, path => this.projectService.onFileAddOrRemoveInWatchedDirectoryOfProject(this, path), - flags - ), - // Close directory watcher - (directory, wildcardDirectoryWatcher, flagsChanged) => this.closeWildcardDirectoryWatcher( - directory, wildcardDirectoryWatcher, flagsChanged ? WatcherCloseReason.RecursiveChanged : WatcherCloseReason.NotNeeded + flags, + WatchType.WildcardDirectories, + this ) ); } - private closeWildcardDirectoryWatcher(directory: string, { watcher, flags }: WildcardDirectoryWatcher, closeReason: WatcherCloseReason) { - this.projectService.closeDirectoryWatcher(WatchType.WildcardDirectories, this, directory, watcher, flags, closeReason); - } - /*@internal*/ - stopWatchingWildCards(reason: WatcherCloseReason) { + stopWatchingWildCards() { if (this.directoriesWatchedForWildcards) { - clearMap( - this.directoriesWatchedForWildcards, - (directory, wildcardDirectoryWatcher) => this.closeWildcardDirectoryWatcher(directory, wildcardDirectoryWatcher, reason) - ); + clearMap(this.directoriesWatchedForWildcards, closeFileWatcherOf); this.directoriesWatchedForWildcards = undefined; } } @@ -1202,26 +1195,24 @@ namespace ts.server { newTypeRoots, { // Create new watch - createNewValue: root => this.projectService.addDirectoryWatcher(WatchType.TypeRoot, this, root, - path => this.projectService.onTypeRootFileChanged(this, path), WatchDirectoryFlags.None + createNewValue: root => this.projectService.watchDirectory( + this.projectService.host, + root, + path => this.projectService.onTypeRootFileChanged(this, path), + WatchDirectoryFlags.None, + WatchType.TypeRoot, + this ), // Close existing watch thats not needed any more - onDeleteValue: (directory, watcher) => this.projectService.closeDirectoryWatcher( - WatchType.TypeRoot, this, directory, watcher, WatchDirectoryFlags.None, WatcherCloseReason.NotNeeded - ) + onDeleteValue: closeFileWatcher } ); } /*@internal*/ - stopWatchingTypeRoots(reason: WatcherCloseReason) { + stopWatchingTypeRoots() { if (this.typeRootsWatchers) { - clearMap( - this.typeRootsWatchers, - (directory, watcher) => - this.projectService.closeDirectoryWatcher(WatchType.TypeRoot, this, - directory, watcher, WatchDirectoryFlags.None, reason) - ); + clearMap(this.typeRootsWatchers, closeFileWatcher); this.typeRootsWatchers = undefined; } } @@ -1230,12 +1221,12 @@ namespace ts.server { super.close(); if (this.configFileWatcher) { - this.projectService.closeFileWatcher(WatchType.ConfigFilePath, this, this.getConfigFilePath(), this.configFileWatcher, WatcherCloseReason.ProjectClose); + this.configFileWatcher.close(); this.configFileWatcher = undefined; } - this.stopWatchingTypeRoots(WatcherCloseReason.ProjectClose); - this.stopWatchingWildCards(WatcherCloseReason.ProjectClose); + this.stopWatchingTypeRoots(); + this.stopWatchingWildCards(); } addOpenRef() { diff --git a/src/server/scriptInfo.ts b/src/server/scriptInfo.ts index 93781df95c093..c35f26e3d3c27 100644 --- a/src/server/scriptInfo.ts +++ b/src/server/scriptInfo.ts @@ -237,7 +237,7 @@ namespace ts.server { detachAllProjects() { for (const p of this.containingProjects) { if (p.projectKind === ProjectKind.Configured) { - (p.lsHost.host as CachedServerHost).addOrDeleteFile(this.fileName, this.path, FileWatcherEventKind.Deleted); + (p.lsHost.host as CachedPartialSystem).addOrDeleteFile(this.fileName, this.path, FileWatcherEventKind.Deleted); } const isInfoRoot = p.isRoot(this); // detach is unnecessary since we'll clean the list of containing projects anyways From 5aafd3f06c2cd8919d0fa4b168bd6f695666a1df Mon Sep 17 00:00:00 2001 From: Sheetal Nandi Date: Wed, 23 Aug 2017 13:34:41 -0700 Subject: [PATCH 077/109] Reduce number of watches for failed lookup locations as part of module resolution --- src/compiler/resolutionCache.ts | 78 ++++++++++++++++++++------------- src/compiler/utilities.ts | 12 +++++ src/compiler/watchedProgram.ts | 22 +++++++--- src/server/editorServices.ts | 4 +- src/server/project.ts | 26 ++++++----- 5 files changed, 92 insertions(+), 50 deletions(-) diff --git a/src/compiler/resolutionCache.ts b/src/compiler/resolutionCache.ts index 8001dd81bd974..dd03a055804e6 100644 --- a/src/compiler/resolutionCache.ts +++ b/src/compiler/resolutionCache.ts @@ -14,7 +14,7 @@ namespace ts { resolveTypeReferenceDirectives(typeDirectiveNames: string[], containingFile: string): ResolvedTypeReferenceDirective[]; invalidateResolutionOfFile(filePath: Path): void; - invalidateResolutionOfChangedFailedLookupLocation(failedLookupLocationPath: Path): void; + onFileAddOrRemoveInDirectoryOfFailedLookup(fileOrFolder: Path): boolean; createHasInvalidatedResolution(): HasInvalidatedResolution; @@ -26,15 +26,17 @@ namespace ts { isInvalidated?: boolean; } - interface FailedLookupLocationsWatcher { + interface DirectoryWatchesOfFailedLookup { + /** watcher for the directory of failed lookup */ watcher: FileWatcher; - refCount: number; + /** map with key being the failed lookup location path and value being the actual location */ + mapLocations: MultiMap; } export function createResolutionCache( toPath: (fileName: string) => Path, getCompilerOptions: () => CompilerOptions, - watchForFailedLookupLocation: (failedLookupLocation: string, failedLookupLocationPath: Path) => FileWatcher, + watchDirectoryOfFailedLookupLocation: (directory: string) => FileWatcher, log: (s: string) => void, projectName?: string, getGlobalCache?: () => string | undefined): ResolutionCache { @@ -49,7 +51,7 @@ namespace ts { const resolvedModuleNames = createMap>(); const resolvedTypeReferenceDirectives = createMap>(); - const failedLookupLocationsWatches = createMap(); + const directoryWatchesOfFailedLookups = createMap(); return { setModuleResolutionHost, @@ -58,7 +60,7 @@ namespace ts { resolveModuleNames, resolveTypeReferenceDirectives, invalidateResolutionOfFile, - invalidateResolutionOfChangedFailedLookupLocation, + onFileAddOrRemoveInDirectoryOfFailedLookup, createHasInvalidatedResolution, clear }; @@ -69,7 +71,7 @@ namespace ts { function clear() { // Close all the watches for failed lookup locations, irrespective of refcounts for them since this is to clear the cache - clearMap(failedLookupLocationsWatches, closeFileWatcherOf); + clearMap(directoryWatchesOfFailedLookups, closeFileWatcherOf); resolvedModuleNames.clear(); resolvedTypeReferenceDirectives.clear(); } @@ -203,43 +205,51 @@ namespace ts { } function watchFailedLookupLocation(failedLookupLocation: string, failedLookupLocationPath: Path) { - const failedLookupLocationWatcher = failedLookupLocationsWatches.get(failedLookupLocationPath); - if (failedLookupLocationWatcher) { - failedLookupLocationWatcher.refCount++; - log(`Watcher: FailedLookupLocations: Status: Using existing watcher: Location: ${failedLookupLocation}`); + const dirPath = getDirectoryPath(failedLookupLocationPath); + const watches = directoryWatchesOfFailedLookups.get(dirPath); + if (watches) { + watches.mapLocations.add(failedLookupLocationPath, failedLookupLocation); } else { - const watcher = watchForFailedLookupLocation(failedLookupLocation, failedLookupLocationPath); - failedLookupLocationsWatches.set(failedLookupLocationPath, { watcher, refCount: 1 }); + const mapLocations = createMultiMap(); + mapLocations.add(failedLookupLocationPath, failedLookupLocation); + directoryWatchesOfFailedLookups.set(dirPath, { + watcher: watchDirectoryOfFailedLookupLocation(getDirectoryPath(failedLookupLocation)), + mapLocations + }); } } function closeFailedLookupLocationWatcher(failedLookupLocation: string, failedLookupLocationPath: Path) { - const failedLookupLocationWatcher = failedLookupLocationsWatches.get(failedLookupLocationPath); - Debug.assert(!!failedLookupLocationWatcher); - failedLookupLocationWatcher.refCount--; - if (failedLookupLocationWatcher.refCount) { - log(`Watcher: FailedLookupLocations: Status: Removing existing watcher: Location: ${failedLookupLocation}`); - } - else { - failedLookupLocationWatcher.watcher.close(); - failedLookupLocationsWatches.delete(failedLookupLocationPath); + const dirPath = getDirectoryPath(failedLookupLocationPath); + const watches = directoryWatchesOfFailedLookups.get(dirPath); + watches.mapLocations.remove(failedLookupLocationPath, failedLookupLocation); + if (watches.mapLocations.size === 0) { + watches.watcher.close(); + directoryWatchesOfFailedLookups.delete(dirPath); } } type FailedLookupLocationAction = (failedLookupLocation: string, failedLookupLocationPath: Path) => void; - function withFailedLookupLocations(failedLookupLocations: ReadonlyArray | undefined, fn: FailedLookupLocationAction) { - forEach(failedLookupLocations, failedLookupLocation => { - fn(failedLookupLocation, toPath(failedLookupLocation)); - }); + function withFailedLookupLocations(failedLookupLocations: ReadonlyArray | undefined, fn: FailedLookupLocationAction, startIndex?: number) { + if (failedLookupLocations) { + for (let i = startIndex || 0; i < failedLookupLocations.length; i++) { + fn(failedLookupLocations[i], toPath(failedLookupLocations[i])); + } + } } function updateFailedLookupLocationWatches(failedLookupLocations: ReadonlyArray | undefined, existingFailedLookupLocations: ReadonlyArray | undefined) { + log(`Resolution cache: Updating...`); + const index = existingFailedLookupLocations && failedLookupLocations ? + findDiffIndex(failedLookupLocations, existingFailedLookupLocations) : + 0; + // Watch all the failed lookup locations - withFailedLookupLocations(failedLookupLocations, watchFailedLookupLocation); + withFailedLookupLocations(failedLookupLocations, watchFailedLookupLocation, index); // Close existing watches for the failed locations - withFailedLookupLocations(existingFailedLookupLocations, closeFailedLookupLocationWatcher); + withFailedLookupLocations(existingFailedLookupLocations, closeFailedLookupLocationWatcher, index); } function invalidateResolutionCacheOfDeletedFile( @@ -291,9 +301,15 @@ namespace ts { invalidateResolutionCacheOfDeletedFile(filePath, resolvedTypeReferenceDirectives, m => m.resolvedTypeReferenceDirective, r => r.resolvedFileName); } - function invalidateResolutionOfChangedFailedLookupLocation(failedLookupLocationPath: Path) { - invalidateResolutionCacheOfChangedFailedLookupLocation(failedLookupLocationPath, resolvedModuleNames); - invalidateResolutionCacheOfChangedFailedLookupLocation(failedLookupLocationPath, resolvedTypeReferenceDirectives); + function onFileAddOrRemoveInDirectoryOfFailedLookup(fileOrFolder: Path) { + const dirPath = getDirectoryPath(fileOrFolder); + const watches = directoryWatchesOfFailedLookups.get(dirPath); + const isFailedLookupFile = watches.mapLocations.has(fileOrFolder); + if (isFailedLookupFile) { + invalidateResolutionCacheOfChangedFailedLookupLocation(fileOrFolder, resolvedModuleNames); + invalidateResolutionCacheOfChangedFailedLookupLocation(fileOrFolder, resolvedTypeReferenceDirectives); + } + return isFailedLookupFile; } } } diff --git a/src/compiler/utilities.ts b/src/compiler/utilities.ts index 7e43d793f0bc9..641caa0ce572f 100644 --- a/src/compiler/utilities.ts +++ b/src/compiler/utilities.ts @@ -3543,6 +3543,18 @@ namespace ts { } } + /** + * Find the index where arrayA and arrayB differ + */ + export function findDiffIndex(arrayA: ReadonlyArray, arrayB: ReadonlyArray) { + for (let i = 0; i < arrayA.length; i++) { + if (i === arrayB.length || arrayA[i] !== arrayB[i]) { + return i; + } + } + return arrayA.length; + } + export function addFileWatcher(host: System, file: string, cb: FileWatcherCallback): FileWatcher { return host.watchFile(file, cb); } diff --git a/src/compiler/watchedProgram.ts b/src/compiler/watchedProgram.ts index 2a23e66763b3d..77c67325ef9db 100644 --- a/src/compiler/watchedProgram.ts +++ b/src/compiler/watchedProgram.ts @@ -268,7 +268,7 @@ namespace ts { const resolutionCache = createResolutionCache( fileName => toPath(fileName), () => compilerOptions, - watchFailedLookupLocation, + watchDirectoryOfFailedLookupLocation, writeLog ); @@ -552,14 +552,22 @@ namespace ts { } } - function watchFailedLookupLocation(failedLookupLocation: string, failedLookupLocationPath: Path) { - return watchFilePath(system, failedLookupLocation, onFailedLookupLocationChange, failedLookupLocationPath, writeLog); + function watchDirectoryOfFailedLookupLocation(directory: string) { + return watchDirectory(system, directory, onFileAddOrRemoveInDirectoryOfFailedLookup, WatchDirectoryFlags.None, writeLog); } - function onFailedLookupLocationChange(fileName: string, eventKind: FileWatcherEventKind, failedLookupLocationPath: Path) { - updateCachedSystemWithFile(fileName, failedLookupLocationPath, eventKind); - resolutionCache.invalidateResolutionOfChangedFailedLookupLocation(failedLookupLocationPath); - scheduleProgramUpdate(); + function onFileAddOrRemoveInDirectoryOfFailedLookup(fileOrFolder: string) { + const fileOrFolderPath = toPath(fileOrFolder); + + if (configFileName) { + // Since the file existance changed, update the sourceFiles cache + (host as CachedPartialSystem).addOrDeleteFileOrFolder(fileOrFolder, fileOrFolderPath); + } + + // If the location results in update to failed lookup, schedule program update + if (resolutionCache.onFileAddOrRemoveInDirectoryOfFailedLookup(fileOrFolderPath)) { + scheduleProgramUpdate(); + } } function watchMissingFilePath(missingFilePath: Path) { diff --git a/src/server/editorServices.ts b/src/server/editorServices.ts index 292f6e3b90a86..73912b3bd919a 100644 --- a/src/server/editorServices.ts +++ b/src/server/editorServices.ts @@ -432,8 +432,8 @@ namespace ts.server { return s => this.logger.info(s + detailedInfo); } - toPath(fileName: string, basePath = this.currentDirectory) { - return toPath(fileName, basePath, this.toCanonicalFileName); + toPath(fileName: string) { + return toPath(fileName, this.currentDirectory, this.toCanonicalFileName); } /* @internal */ diff --git a/src/server/project.ts b/src/server/project.ts index ec58e4cb88d6d..420d080b9f23f 100644 --- a/src/server/project.ts +++ b/src/server/project.ts @@ -216,7 +216,7 @@ namespace ts.server { this.resolutionCache = createResolutionCache( fileName => this.projectService.toPath(fileName), () => this.compilerOptions, - (failedLookupLocation, failedLookupLocationPath) => this.watchFailedLookupLocation(failedLookupLocation, failedLookupLocationPath), + directory => this.watchDirectoryOfFailedLookup(directory), s => this.projectService.logger.info(s), this.getProjectName(), () => this.getTypeAcquisition().enable ? this.projectService.typingsInstaller.globalTypingsCacheLocation : undefined @@ -233,24 +233,30 @@ namespace ts.server { this.markAsDirty(); } - private watchFailedLookupLocation(failedLookupLocation: string, failedLookupLocationPath: Path) { - return this.projectService.watchFile( + private watchDirectoryOfFailedLookup(directory: string) { + return this.projectService.watchDirectory( this.projectService.host, - failedLookupLocation, - (fileName, eventKind) => this.onFailedLookupLocationChanged(fileName, eventKind, failedLookupLocationPath), + directory, + fileOrFolder => this.onFileAddOrRemoveInDirectoryOfFailedLookup(fileOrFolder), + WatchDirectoryFlags.None, WatchType.FailedLookupLocation, this ); } - private onFailedLookupLocationChanged(fileName: string, eventKind: FileWatcherEventKind, failedLookupLocationPath: Path) { + private onFileAddOrRemoveInDirectoryOfFailedLookup(fileOrFolder: string) { + const fileOrFolderPath = this.projectService.toPath(fileOrFolder); + // There is some kind of change in the failed lookup location, update the program if (this.projectKind === ProjectKind.Configured) { - (this.lsHost.host as CachedPartialSystem).addOrDeleteFile(fileName, failedLookupLocationPath, eventKind); + (this.lsHost.host as CachedPartialSystem).addOrDeleteFileOrFolder(fileOrFolder, fileOrFolderPath); + } + + // If the location results in update to failed lookup, schedule program update + if (this.resolutionCache.onFileAddOrRemoveInDirectoryOfFailedLookup(fileOrFolderPath)) { + this.markAsDirty(); + this.projectService.delayUpdateProjectGraphAndInferredProjectsRefresh(this); } - this.resolutionCache.invalidateResolutionOfChangedFailedLookupLocation(failedLookupLocationPath); - this.markAsDirty(); - this.projectService.delayUpdateProjectGraphAndInferredProjectsRefresh(this); } private setInternalCompilerOptionsForEmittingJsFiles() { From 17565d84079cec98e86fbf36b35265cfaa020bd3 Mon Sep 17 00:00:00 2001 From: Sheetal Nandi Date: Thu, 24 Aug 2017 11:50:27 -0700 Subject: [PATCH 078/109] Handle watches of missing directories and make project the module resolution host --- Jakefile.js | 1 - src/compiler/resolutionCache.ts | 85 +++--- src/compiler/sys.ts | 140 +++++---- src/compiler/utilities.ts | 4 +- src/compiler/watchedProgram.ts | 197 +++++++------ .../unittests/cachingInServerLSHost.ts | 2 +- .../unittests/tsserverProjectSystem.ts | 116 +++++--- src/harness/virtualFileSystemWithWatch.ts | 61 +++- src/server/editorServices.ts | 85 +++--- src/server/lsHost.ts | 137 --------- src/server/project.ts | 275 +++++++++++------- src/server/scriptInfo.ts | 2 +- src/server/session.ts | 2 +- src/server/tsconfig.json | 1 - src/server/tsconfig.library.json | 1 - src/server/typingsCache.ts | 6 +- src/server/utilities.ts | 2 +- 17 files changed, 586 insertions(+), 531 deletions(-) delete mode 100644 src/server/lsHost.ts diff --git a/Jakefile.js b/Jakefile.js index 2dcba9f97e7a3..46a744445d361 100644 --- a/Jakefile.js +++ b/Jakefile.js @@ -149,7 +149,6 @@ var harnessSources = harnessCoreSources.concat([ "utilities.ts", "scriptVersionCache.ts", "scriptInfo.ts", - "lsHost.ts", "project.ts", "typingsCache.ts", "editorServices.ts", diff --git a/src/compiler/resolutionCache.ts b/src/compiler/resolutionCache.ts index dd03a055804e6..d1bd285019f92 100644 --- a/src/compiler/resolutionCache.ts +++ b/src/compiler/resolutionCache.ts @@ -5,8 +5,6 @@ namespace ts { /** This is the cache of module/typedirectives resolution that can be retained across program */ export interface ResolutionCache { - setModuleResolutionHost(host: ModuleResolutionHost): void; - startRecordingFilesWithChangedResolutions(): void; finishRecordingFilesWithChangedResolutions(): Path[]; @@ -14,8 +12,6 @@ namespace ts { resolveTypeReferenceDirectives(typeDirectiveNames: string[], containingFile: string): ResolvedTypeReferenceDirective[]; invalidateResolutionOfFile(filePath: Path): void; - onFileAddOrRemoveInDirectoryOfFailedLookup(fileOrFolder: Path): boolean; - createHasInvalidatedResolution(): HasInvalidatedResolution; clear(): void; @@ -33,15 +29,17 @@ namespace ts { mapLocations: MultiMap; } - export function createResolutionCache( - toPath: (fileName: string) => Path, - getCompilerOptions: () => CompilerOptions, - watchDirectoryOfFailedLookupLocation: (directory: string) => FileWatcher, - log: (s: string) => void, - projectName?: string, - getGlobalCache?: () => string | undefined): ResolutionCache { + export interface ResolutionCacheHost extends ModuleResolutionHost { + toPath(fileName: string): Path; + getCompilationSettings(): CompilerOptions; + watchDirectoryOfFailedLookupLocation(directory: string, cb: DirectoryWatcherCallback): FileWatcher; + onInvalidatedResolution(): void; + getCachedPartialSystem?(): CachedPartialSystem; + projectName?: string; + getGlobalCache?(): string | undefined; + } - let host: ModuleResolutionHost; + export function createResolutionCache(resolutionHost: ResolutionCacheHost): ResolutionCache { let filesWithChangedSetOfUnresolvedImports: Path[] | undefined; let filesWithInvalidatedResolutions: Map | undefined; @@ -52,23 +50,16 @@ namespace ts { const resolvedTypeReferenceDirectives = createMap>(); const directoryWatchesOfFailedLookups = createMap(); - return { - setModuleResolutionHost, startRecordingFilesWithChangedResolutions, finishRecordingFilesWithChangedResolutions, resolveModuleNames, resolveTypeReferenceDirectives, invalidateResolutionOfFile, - onFileAddOrRemoveInDirectoryOfFailedLookup, createHasInvalidatedResolution, clear }; - function setModuleResolutionHost(updatedHost: ModuleResolutionHost) { - host = updatedHost; - } - function clear() { // Close all the watches for failed lookup locations, irrespective of refcounts for them since this is to clear the cache clearMap(directoryWatchesOfFailedLookups, closeFileWatcherOf); @@ -95,16 +86,16 @@ namespace ts { function resolveModuleName(moduleName: string, containingFile: string, compilerOptions: CompilerOptions, host: ModuleResolutionHost): ResolvedModuleWithFailedLookupLocations { const primaryResult = ts.resolveModuleName(moduleName, containingFile, compilerOptions, host); // return result immediately only if global cache support is not enabled or if it is .ts, .tsx or .d.ts - if (!getGlobalCache) { + if (!resolutionHost.getGlobalCache) { return primaryResult; } // otherwise try to load typings from @types - const globalCache = getGlobalCache(); + const globalCache = resolutionHost.getGlobalCache(); if (globalCache !== undefined && !isExternalModuleNameRelative(moduleName) && !(primaryResult.resolvedModule && extensionIsTypeScript(primaryResult.resolvedModule.extension))) { // create different collection of failed lookup locations for second pass // if it will fail and we've already found something during the first pass - we don't want to pollute its results - const { resolvedModule, failedLookupLocations } = loadModuleFromGlobalCache(moduleName, projectName, compilerOptions, host, globalCache); + const { resolvedModule, failedLookupLocations } = loadModuleFromGlobalCache(moduleName, resolutionHost.projectName, compilerOptions, host, globalCache); if (resolvedModule) { return { resolvedModule, failedLookupLocations: addRange(primaryResult.failedLookupLocations as Array, failedLookupLocations) }; } @@ -123,12 +114,12 @@ namespace ts { getResultFileName: (result: R) => string | undefined, logChanges: boolean): R[] { - const path = toPath(containingFile); + const path = resolutionHost.toPath(containingFile); const currentResolutionsInFile = cache.get(path); const newResolutions: Map = createMap(); const resolvedModules: R[] = []; - const compilerOptions = getCompilerOptions(); + const compilerOptions = resolutionHost.getCompilationSettings(); for (const name of names) { // check if this is a duplicate entry in the list @@ -140,7 +131,7 @@ namespace ts { resolution = existingResolution; } else { - resolution = loader(name, containingFile, compilerOptions, host); + resolution = loader(name, containingFile, compilerOptions, resolutionHost); updateFailedLookupLocationWatches(resolution.failedLookupLocations, existingResolution && existingResolution.failedLookupLocations); } newResolutions.set(name, resolution); @@ -214,12 +205,31 @@ namespace ts { const mapLocations = createMultiMap(); mapLocations.add(failedLookupLocationPath, failedLookupLocation); directoryWatchesOfFailedLookups.set(dirPath, { - watcher: watchDirectoryOfFailedLookupLocation(getDirectoryPath(failedLookupLocation)), + watcher: createDirectoryWatcher(getDirectoryPath(failedLookupLocation), dirPath), mapLocations }); } } + function createDirectoryWatcher(directory: string, dirPath: Path) { + return resolutionHost.watchDirectoryOfFailedLookupLocation(directory, fileOrFolder => { + const fileOrFolderPath = resolutionHost.toPath(fileOrFolder); + if (resolutionHost.getCachedPartialSystem) { + // Since the file existance changed, update the sourceFiles cache + resolutionHost.getCachedPartialSystem().addOrDeleteFileOrFolder(fileOrFolder, fileOrFolderPath); + } + + // If the location results in update to failed lookup, schedule program update + if (dirPath === fileOrFolderPath) { + onAddOrRemoveDirectoryOfFailedLookup(dirPath); + resolutionHost.onInvalidatedResolution(); + } + else if (onFileAddOrRemoveInDirectoryOfFailedLookup(fileOrFolderPath)) { + resolutionHost.onInvalidatedResolution(); + } + }); + } + function closeFailedLookupLocationWatcher(failedLookupLocation: string, failedLookupLocationPath: Path) { const dirPath = getDirectoryPath(failedLookupLocationPath); const watches = directoryWatchesOfFailedLookups.get(dirPath); @@ -234,13 +244,12 @@ namespace ts { function withFailedLookupLocations(failedLookupLocations: ReadonlyArray | undefined, fn: FailedLookupLocationAction, startIndex?: number) { if (failedLookupLocations) { for (let i = startIndex || 0; i < failedLookupLocations.length; i++) { - fn(failedLookupLocations[i], toPath(failedLookupLocations[i])); + fn(failedLookupLocations[i], resolutionHost.toPath(failedLookupLocations[i])); } } } function updateFailedLookupLocationWatches(failedLookupLocations: ReadonlyArray | undefined, existingFailedLookupLocations: ReadonlyArray | undefined) { - log(`Resolution cache: Updating...`); const index = existingFailedLookupLocations && failedLookupLocations ? findDiffIndex(failedLookupLocations, existingFailedLookupLocations) : 0; @@ -269,7 +278,7 @@ namespace ts { if (resolution && !resolution.isInvalidated) { const result = getResult(resolution); if (result) { - if (toPath(getResultFileName(result)) === deletedFilePath) { + if (resolutionHost.toPath(getResultFileName(result)) === deletedFilePath) { resolution.isInvalidated = true; (filesWithInvalidatedResolutions || (filesWithInvalidatedResolutions = createMap())).set(path, true); } @@ -281,12 +290,13 @@ namespace ts { } function invalidateResolutionCacheOfChangedFailedLookupLocation( - failedLookupLocationPath: Path, - cache: Map>) { + cache: Map>, + isChangedFailedLookupLocation: (location: string) => boolean + ) { cache.forEach((value, containingFile) => { if (value) { value.forEach(resolution => { - if (resolution && !resolution.isInvalidated && some(resolution.failedLookupLocations, location => toPath(location) === failedLookupLocationPath)) { + if (resolution && !resolution.isInvalidated && some(resolution.failedLookupLocations, isChangedFailedLookupLocation)) { // Mark the file as needing re-evaluation of module resolution instead of using it blindly. resolution.isInvalidated = true; (filesWithInvalidatedResolutions || (filesWithInvalidatedResolutions = createMap())).set(containingFile, true); @@ -306,10 +316,17 @@ namespace ts { const watches = directoryWatchesOfFailedLookups.get(dirPath); const isFailedLookupFile = watches.mapLocations.has(fileOrFolder); if (isFailedLookupFile) { - invalidateResolutionCacheOfChangedFailedLookupLocation(fileOrFolder, resolvedModuleNames); - invalidateResolutionCacheOfChangedFailedLookupLocation(fileOrFolder, resolvedTypeReferenceDirectives); + const isFileOrFolder: (location: string) => boolean = location => resolutionHost.toPath(location) === fileOrFolder; + invalidateResolutionCacheOfChangedFailedLookupLocation(resolvedModuleNames, isFileOrFolder); + invalidateResolutionCacheOfChangedFailedLookupLocation(resolvedTypeReferenceDirectives, isFileOrFolder); } return isFailedLookupFile; } + + function onAddOrRemoveDirectoryOfFailedLookup(dirPath: Path) { + const isInDirPath: (location: string) => boolean = location => getDirectoryPath(resolutionHost.toPath(location)) === dirPath; + invalidateResolutionCacheOfChangedFailedLookupLocation(resolvedModuleNames, isInDirPath); + invalidateResolutionCacheOfChangedFailedLookupLocation(resolvedTypeReferenceDirectives, isInDirPath); + } } } diff --git a/src/compiler/sys.ts b/src/compiler/sys.ts index 7a419be7c2d02..5f3b8bd06d167 100644 --- a/src/compiler/sys.ts +++ b/src/compiler/sys.ts @@ -161,11 +161,10 @@ namespace ts { watcher.referenceCount += 1; return; } - watcher = _fs.watch( + watcher = fsWatchDirectory( dirPath || ".", - { persistent: true }, (eventName: string, relativeFileName: string) => fileEventHandler(eventName, relativeFileName, dirPath) - ); + ) as DirectoryWatcher; watcher.referenceCount = 1; dirWatchers.set(dirPath, watcher); return; @@ -232,6 +231,83 @@ namespace ts { const platform: string = _os.platform(); const useCaseSensitiveFileNames = isFileSystemCaseSensitive(); + function fsWatchFile(fileName: string, callback: FileWatcherCallback, pollingInterval?: number): FileWatcher { + _fs.watchFile(fileName, { persistent: true, interval: pollingInterval || 250 }, fileChanged); + return { + close: () => _fs.unwatchFile(fileName, fileChanged) + }; + + function fileChanged(curr: any, prev: any) { + const isCurrZero = +curr.mtime === 0; + const isPrevZero = +prev.mtime === 0; + const created = !isCurrZero && isPrevZero; + const deleted = isCurrZero && !isPrevZero; + + const eventKind = created + ? FileWatcherEventKind.Created + : deleted + ? FileWatcherEventKind.Deleted + : FileWatcherEventKind.Changed; + + if (eventKind === FileWatcherEventKind.Changed && +curr.mtime <= +prev.mtime) { + return; + } + + callback(fileName, eventKind); + } + } + + function fsWatchDirectory(directoryName: string, callback: (eventName: string, relativeFileName: string) => void, recursive?: boolean): FileWatcher { + // Node 4.0 `fs.watch` function supports the "recursive" option on both OSX and Windows + // (ref: https://github.com/nodejs/node/pull/2649 and https://github.com/Microsoft/TypeScript/issues/4643) + let options: any; + let watcher = !directoryExists(directoryName) ? + watchMissingDirectory() : + watchPresentDirectory(); + return { + close: () => { + watcher.close(); + } + }; + + function watchPresentDirectory(): FileWatcher { + if (options === undefined) { + if (isNode4OrLater && (process.platform === "win32" || process.platform === "darwin")) { + options = { persistent: true, recursive: !!recursive }; + } + else { + options = { persistent: true }; + } + } + + const dirWatcher = _fs.watch( + directoryName, + options, + callback + ); + dirWatcher.on("error", () => { + // Deleting file + watcher = watchMissingDirectory(); + // Call the callback for current directory + callback("rename", ""); + }); + return dirWatcher; + } + + function watchMissingDirectory(): FileWatcher { + return fsWatchFile(directoryName, (_fileName, eventKind) => { + if (eventKind === FileWatcherEventKind.Created && directoryExists(directoryName)) { + watcher.close(); + watcher = watchPresentDirectory(); + // Call the callback for current directory + // For now it could be callback for the inner directory creation, + // but just return current directory, better than current no-op + callback("rename", ""); + } + }); + } + } + function readFile(fileName: string, _encoding?: string): string | undefined { if (!fileExists(fileName)) { return undefined; @@ -349,7 +425,6 @@ namespace ts { return filter(_fs.readdirSync(path), dir => fileSystemEntryExists(combinePaths(path, dir), FileSystemEntryKind.Directory)); } - const noOpFileWatcher: FileWatcher = { close: noop }; const nodeSystem: System = { args: process.argv.slice(2), newLine: _os.EOL, @@ -367,60 +442,21 @@ namespace ts { }; } else { - _fs.watchFile(fileName, { persistent: true, interval: pollingInterval || 250 }, fileChanged); - return { - close: () => _fs.unwatchFile(fileName, fileChanged) - }; - } - - function fileChanged(curr: any, prev: any) { - const isCurrZero = +curr.mtime === 0; - const isPrevZero = +prev.mtime === 0; - const created = !isCurrZero && isPrevZero; - const deleted = isCurrZero && !isPrevZero; - - const eventKind = created - ? FileWatcherEventKind.Created - : deleted - ? FileWatcherEventKind.Deleted - : FileWatcherEventKind.Changed; - - if (eventKind === FileWatcherEventKind.Changed && +curr.mtime <= +prev.mtime) { - return; - } - - callback(fileName, eventKind); + return fsWatchFile(fileName, callback, pollingInterval); } }, watchDirectory: (directoryName, callback, recursive) => { // Node 4.0 `fs.watch` function supports the "recursive" option on both OSX and Windows // (ref: https://github.com/nodejs/node/pull/2649 and https://github.com/Microsoft/TypeScript/issues/4643) - let options: any; - if (!directoryExists(directoryName)) { - // do nothing if target folder does not exist - return noOpFileWatcher; - } - - if (isNode4OrLater && (process.platform === "win32" || process.platform === "darwin")) { - options = { persistent: true, recursive: !!recursive }; - } - else { - options = { persistent: true }; - } - - return _fs.watch( - directoryName, - options, - (eventName: string, relativeFileName: string) => { - // In watchDirectory we only care about adding and removing files (when event name is - // "rename"); changes made within files are handled by corresponding fileWatchers (when - // event name is "change") - if (eventName === "rename") { - // When deleting a file, the passed baseFileName is null - callback(!relativeFileName ? relativeFileName : normalizePath(combinePaths(directoryName, relativeFileName))); - } + return fsWatchDirectory(directoryName, (eventName, relativeFileName) => { + // In watchDirectory we only care about adding and removing files (when event name is + // "rename"); changes made within files are handled by corresponding fileWatchers (when + // event name is "change") + if (eventName === "rename") { + // When deleting a file, the passed baseFileName is null + callback(!relativeFileName ? relativeFileName : normalizePath(combinePaths(directoryName, relativeFileName))); } - ); + }, recursive); }, resolvePath: path => _path.resolve(path), fileExists, diff --git a/src/compiler/utilities.ts b/src/compiler/utilities.ts index 641caa0ce572f..19748bfe0dee3 100644 --- a/src/compiler/utilities.ts +++ b/src/compiler/utilities.ts @@ -3576,9 +3576,7 @@ namespace ts { export function addDirectoryWatcher(host: System, directory: string, cb: DirectoryWatcherCallback, flags: WatchDirectoryFlags): FileWatcher { const recursive = (flags & WatchDirectoryFlags.Recursive) !== 0; - return host.watchDirectory(directory, fileName => { - cb(getNormalizedAbsolutePath(fileName, directory)); - }, recursive); + return host.watchDirectory(directory, cb, recursive); } export function addDirectoryWatcherWithLogging(host: System, directory: string, cb: DirectoryWatcherCallback, flags: WatchDirectoryFlags, log: (s: string) => void): FileWatcher { diff --git a/src/compiler/watchedProgram.ts b/src/compiler/watchedProgram.ts index 77c67325ef9db..9058588ade11f 100644 --- a/src/compiler/watchedProgram.ts +++ b/src/compiler/watchedProgram.ts @@ -245,7 +245,6 @@ namespace ts { const sourceFilesCache = createMap(); // 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 let hasChangedCompilerOptions = false; // True if the compiler options have changed between compilations const loggingEnabled = compilerOptions.diagnostics || compilerOptions.extendedDiagnostics; @@ -257,20 +256,47 @@ namespace ts { watchingHost = watchingHost || createWatchingSystemHost(compilerOptions.pretty); const { system, parseConfigFile, reportDiagnostic, reportWatchDiagnostic, beforeCompile, afterCompile } = watchingHost; - const host = configFileName ? createCachedPartialSystem(system) : system; + const partialSystem = configFileName ? createCachedPartialSystem(system) : system; if (configFileName) { configFileWatcher = watchFile(system, configFileName, scheduleProgramReload, writeLog); } - const currentDirectory = host.getCurrentDirectory(); - const getCanonicalFileName = createGetCanonicalFileName(system.useCaseSensitiveFileNames); - // Cache for the module resolution - const resolutionCache = createResolutionCache( - fileName => toPath(fileName), - () => compilerOptions, + const getCurrentDirectory = memoize(() => partialSystem.getCurrentDirectory()); + const realpath = system.realpath && ((path: string) => system.realpath(path)); + const getCachedPartialSystem = configFileName && (() => partialSystem as CachedPartialSystem); + const getCanonicalFileName = createGetCanonicalFileName(system.useCaseSensitiveFileNames); + let newLine = getNewLineCharacter(compilerOptions, system); + + const compilerHost: CompilerHost & ResolutionCacheHost = { + // Members for CompilerHost + getSourceFile: getVersionedSourceFile, + getSourceFileByPath: getVersionedSourceFileByPath, + getDefaultLibLocation, + getDefaultLibFileName: options => combinePaths(getDefaultLibLocation(), getDefaultLibFileName(options)), + writeFile: (_fileName, _data, _writeByteOrderMark, _onError?, _sourceFiles?) => { }, + getCurrentDirectory, + useCaseSensitiveFileNames: () => system.useCaseSensitiveFileNames, + getCanonicalFileName, + getNewLine: () => newLine, + fileExists, + readFile, + trace, + directoryExists, + getEnvironmentVariable: name => system.getEnvironmentVariable ? system.getEnvironmentVariable(name) : "", + getDirectories, + realpath, + resolveTypeReferenceDirectives, + resolveModuleNames, + onReleaseOldSourceFile, + // Members for ResolutionCacheHost + toPath, + getCompilationSettings: () => compilerOptions, watchDirectoryOfFailedLookupLocation, - writeLog - ); + getCachedPartialSystem, + onInvalidatedResolution: scheduleProgramUpdate + }; + // Cache for the module resolution + const resolutionCache = createResolutionCache(compilerHost); // There is no extra check needed since we can just rely on the program to decide emit const builder = createBuilder(getCanonicalFileName, getFileEmitOutput, computeHash, _sourceFile => true); @@ -285,14 +311,15 @@ namespace ts { function synchronizeProgram() { writeLog(`Synchronizing program`); - hasInvalidatedResolution = resolutionCache.createHasInvalidatedResolution(); + if (hasChangedCompilerOptions) { + newLine = getNewLineCharacter(compilerOptions, system); + } + + const hasInvalidatedResolution = resolutionCache.createHasInvalidatedResolution(); if (isProgramUptoDate(program, rootFileNames, compilerOptions, getSourceVersion, fileExists, hasInvalidatedResolution)) { return; } - // Create the compiler host - const compilerHost = createWatchedCompilerHost(compilerOptions); - resolutionCache.setModuleResolutionHost(compilerHost); if (hasChangedCompilerOptions && changesAffectModuleResolution(program && program.getCompilerOptions(), compilerOptions)) { resolutionCache.clear(); } @@ -300,6 +327,7 @@ namespace ts { beforeCompile(compilerOptions); // Compile the program + compilerHost.hasInvalidatedResolution = hasInvalidatedResolution; program = createProgram(rootFileNames, compilerOptions, compilerHost, program); builder.onProgramUpdateGraph(program, hasInvalidatedResolution); @@ -309,7 +337,7 @@ namespace ts { // These are the paths that program creater told us as not in use any more but were missing on the disk. // We didnt remove the entry for them from sourceFiles cache so that we dont have to do File IO, // if there is already watcher for it (for missing files) - // At that point our watches were updated, hence now we know that these paths are not tracked and need to be removed + // At this point our watches were updated, hence now we know that these paths are not tracked and need to be removed // so that at later time we have correct result of their presence for (const missingFilePath of missingFilePathsRequestedForRelease) { if (!missingFilesMap.has(missingFilePath)) { @@ -319,40 +347,12 @@ namespace ts { missingFilePathsRequestedForRelease = undefined; } - afterCompile(host, program, builder); + afterCompile(partialSystem, program, builder); reportWatchDiagnostic(createCompilerDiagnostic(Diagnostics.Compilation_complete_Watching_for_file_changes)); } - function createWatchedCompilerHost(options: CompilerOptions): CompilerHost { - const newLine = getNewLineCharacter(options, system); - const realpath = system.realpath && ((path: string) => system.realpath(path)); - - return { - getSourceFile: getVersionedSourceFile, - getSourceFileByPath: getVersionedSourceFileByPath, - getDefaultLibLocation, - getDefaultLibFileName: options => combinePaths(getDefaultLibLocation(), getDefaultLibFileName(options)), - writeFile: (_fileName, _data, _writeByteOrderMark, _onError?, _sourceFiles?) => { }, - getCurrentDirectory: memoize(() => host.getCurrentDirectory()), - useCaseSensitiveFileNames: () => system.useCaseSensitiveFileNames, - getCanonicalFileName, - getNewLine: () => newLine, - fileExists, - readFile: fileName => system.readFile(fileName), - trace: (s: string) => system.write(s + newLine), - directoryExists: directoryName => host.directoryExists(directoryName), - getEnvironmentVariable: name => system.getEnvironmentVariable ? system.getEnvironmentVariable(name) : "", - getDirectories: (path: string) => host.getDirectories(path), - realpath, - resolveTypeReferenceDirectives: (typeDirectiveNames, containingFile) => resolutionCache.resolveTypeReferenceDirectives(typeDirectiveNames, containingFile), - resolveModuleNames: (moduleNames, containingFile) => resolutionCache.resolveModuleNames(moduleNames, containingFile, /*logChanges*/ false), - onReleaseOldSourceFile, - hasInvalidatedResolution - }; - } - function toPath(fileName: string) { - return ts.toPath(fileName, currentDirectory, getCanonicalFileName); + return ts.toPath(fileName, getCurrentDirectory(), getCanonicalFileName); } function fileExists(fileName: string) { @@ -362,7 +362,31 @@ namespace ts { return !isString(hostSourceFileInfo); } - return host.fileExists(fileName); + return partialSystem.fileExists(fileName); + } + + function directoryExists(directoryName: string) { + return partialSystem.directoryExists(directoryName); + } + + function readFile(fileName: string) { + return system.readFile(fileName); + } + + function trace(s: string) { + return system.write(s + newLine); + } + + function getDirectories(path: string) { + return partialSystem.getDirectories(path); + } + + function resolveModuleNames(moduleNames: string[], containingFile: string) { + return resolutionCache.resolveModuleNames(moduleNames, containingFile, /*logChanges*/ false); + } + + function resolveTypeReferenceDirectives(typeDirectiveNames: string[], containingFile: string) { + return resolutionCache.resolveTypeReferenceDirectives(typeDirectiveNames, containingFile); } function getDefaultLibLocation(): string { @@ -503,7 +527,7 @@ namespace ts { writeLog(`Reloading config file: ${configFileName}`); needsReload = false; - const cachedHost = host as CachedPartialSystem; + const cachedHost = partialSystem as CachedPartialSystem; cachedHost.clearCache(); const configParseResult = parseConfigFile(configFileName, optionsToExtendForConfigFile, cachedHost, reportDiagnostic, reportWatchDiagnostic); rootFileNames = configParseResult.fileNames; @@ -548,26 +572,12 @@ namespace ts { function updateCachedSystemWithFile(fileName: string, path: Path, eventKind: FileWatcherEventKind) { if (configFileName) { - (host as CachedPartialSystem).addOrDeleteFile(fileName, path, eventKind); + (partialSystem as CachedPartialSystem).addOrDeleteFile(fileName, path, eventKind); } } - function watchDirectoryOfFailedLookupLocation(directory: string) { - return watchDirectory(system, directory, onFileAddOrRemoveInDirectoryOfFailedLookup, WatchDirectoryFlags.None, writeLog); - } - - function onFileAddOrRemoveInDirectoryOfFailedLookup(fileOrFolder: string) { - const fileOrFolderPath = toPath(fileOrFolder); - - if (configFileName) { - // Since the file existance changed, update the sourceFiles cache - (host as CachedPartialSystem).addOrDeleteFileOrFolder(fileOrFolder, fileOrFolderPath); - } - - // If the location results in update to failed lookup, schedule program update - if (resolutionCache.onFileAddOrRemoveInDirectoryOfFailedLookup(fileOrFolderPath)) { - scheduleProgramUpdate(); - } + function watchDirectoryOfFailedLookupLocation(directory: string, cb: DirectoryWatcherCallback) { + return watchDirectory(system, directory, cb, WatchDirectoryFlags.None, writeLog); } function watchMissingFilePath(missingFilePath: Path) { @@ -593,42 +603,45 @@ namespace ts { updateWatchingWildcardDirectories( watchedWildcardDirectories || (watchedWildcardDirectories = createMap()), createMapFromTemplate(configFileWildCardDirectories), - watchWildCardDirectory + watchWildcardDirectory ); } - function watchWildCardDirectory(directory: string, flags: WatchDirectoryFlags) { - return watchDirectory(system, directory, onFileAddOrRemoveInWatchedDirectory, flags, writeLog); - } - - function onFileAddOrRemoveInWatchedDirectory(fileOrFolder: string) { - Debug.assert(!!configFileName); + function watchWildcardDirectory(directory: string, flags: WatchDirectoryFlags) { + return watchDirectory( + system, + directory, + fileOrFolder => { + Debug.assert(!!configFileName); - const fileOrFolderPath = toPath(fileOrFolder); + const fileOrFolderPath = toPath(fileOrFolder); - // Since the file existance changed, update the sourceFiles cache - (host as CachedPartialSystem).addOrDeleteFileOrFolder(fileOrFolder, fileOrFolderPath); - removeSourceFile(fileOrFolderPath); + // Since the file existance changed, update the sourceFiles cache + (partialSystem as CachedPartialSystem).addOrDeleteFileOrFolder(fileOrFolder, fileOrFolderPath); + removeSourceFile(fileOrFolderPath); - // If a change was made inside "folder/file", node will trigger the callback twice: - // one with the fileName being "folder/file", and the other one with "folder". - // We don't respond to the second one. - if (fileOrFolder && !isSupportedSourceFileName(fileOrFolder, compilerOptions)) { - writeLog(`Project: ${configFileName} Detected file add/remove of non supported extension: ${fileOrFolder}`); - return; - } + // If the the added or created file or folder is not supported file name, ignore the file + // But when watched directory is added/removed, we need to reload the file list + if (fileOrFolderPath !== directory && !isSupportedSourceFileName(fileOrFolder, compilerOptions)) { + writeLog(`Project: ${configFileName} Detected file add/remove of non supported extension: ${fileOrFolder}`); + return; + } - // Reload is pending, do the reload - if (!needsReload) { - const result = getFileNamesFromConfigSpecs(configFileSpecs, getDirectoryPath(configFileName), compilerOptions, host); - if (!configFileSpecs.filesSpecs && result.fileNames.length === 0) { - reportDiagnostic(getErrorForNoInputFiles(configFileSpecs, configFileName)); - } - rootFileNames = result.fileNames; + // Reload is pending, do the reload + if (!needsReload) { + const result = getFileNamesFromConfigSpecs(configFileSpecs, getDirectoryPath(configFileName), compilerOptions, partialSystem); + if (!configFileSpecs.filesSpecs && result.fileNames.length === 0) { + reportDiagnostic(getErrorForNoInputFiles(configFileSpecs, configFileName)); + } + rootFileNames = result.fileNames; - // Schedule Update the program - scheduleProgramUpdate(); - } + // Schedule Update the program + scheduleProgramUpdate(); + } + }, + flags, + writeLog + ); } function computeHash(data: string) { diff --git a/src/harness/unittests/cachingInServerLSHost.ts b/src/harness/unittests/cachingInServerLSHost.ts index b7b382e5a1d0e..1d22dc0417e59 100644 --- a/src/harness/unittests/cachingInServerLSHost.ts +++ b/src/harness/unittests/cachingInServerLSHost.ts @@ -147,7 +147,7 @@ namespace ts { // setting compiler options discards module resolution cache fileExistsCalled = false; - const compilerOptions = ts.cloneCompilerOptions(project.getCompilerOptions()); + const compilerOptions = ts.cloneCompilerOptions(project.getCompilationSettings()); compilerOptions.target = ts.ScriptTarget.ES5; project.setCompilerOptions(compilerOptions); diff --git a/src/harness/unittests/tsserverProjectSystem.ts b/src/harness/unittests/tsserverProjectSystem.ts index 3c394da4e3167..9068afa98cfc6 100644 --- a/src/harness/unittests/tsserverProjectSystem.ts +++ b/src/harness/unittests/tsserverProjectSystem.ts @@ -321,6 +321,34 @@ namespace ts.projectSystem { verifyDiagnostics(actual, []); } + function getPathsForTypesOrModules(base: string, rootPaths: string[], typesOrModules: string[], map: Map, rootDir?: string) { + while (1) { + forEach(rootPaths, r => { + const rp = combinePaths(base, r); + forEach(typesOrModules, tm => { + map.set(tm === "" ? rp : combinePaths(rp, tm), true); + }); + }); + const parentDir = getDirectoryPath(base); + if (base === rootDir || parentDir === base) { + break; + } + base = parentDir; + } + return map; + } + + function getNodeModulesWatchedDirectories(path: string, modules: string[], map = createMap()) { + forEach(modules, module => { + getPathsForTypesOrModules(path, ["node_modules"], ["", module, "@types", `@types/${module}`], map); + }); + return map; + } + + function getTypesWatchedDirectories(path: string, typeRoots: string[], types: string[], map = createMap()) { + return getPathsForTypesOrModules(path, typeRoots, types.concat(""), map, path); + } + describe("tsserverProjectSystem", () => { const commonFile1: FileOrFolder = { path: "/a/b/commonFile1.ts", @@ -357,8 +385,9 @@ namespace ts.projectSystem { checkFileNames("inferred project", project.getFileNames(), [appFile.path, libFile.path, moduleFile.path]); const configFileLocations = ["/a/b/c/", "/a/b/", "/a/", "/"]; const configFiles = flatMap(configFileLocations, location => [location + "tsconfig.json", location + "jsconfig.json"]); - const moduleLookupLocations = ["/a/b/c/module.ts", "/a/b/c/module.tsx"]; - checkWatchedFiles(host, configFiles.concat(libFile.path, moduleFile.path, ...moduleLookupLocations)); + checkWatchedFiles(host, configFiles.concat(libFile.path, moduleFile.path)); + checkWatchedDirectories(host, ["/a/b/c"], /*recursive*/ false); + checkWatchedDirectories(host, [], /*recursive*/ true); }); it("can handle tsconfig file name with difference casing", () => { @@ -3403,9 +3432,9 @@ namespace ts.projectSystem { checkProjectActualFiles(projectService.inferredProjects[0], [file4.path]); checkProjectActualFiles(projectService.inferredProjects[1], [file1.path, file2.path]); checkProjectActualFiles(projectService.inferredProjects[2], [file3.path]); - assert.equal(projectService.inferredProjects[0].getCompilerOptions().target, ScriptTarget.ESNext); - assert.equal(projectService.inferredProjects[1].getCompilerOptions().target, ScriptTarget.ESNext); - assert.equal(projectService.inferredProjects[2].getCompilerOptions().target, ScriptTarget.ES2015); + assert.equal(projectService.inferredProjects[0].getCompilationSettings().target, ScriptTarget.ESNext); + assert.equal(projectService.inferredProjects[1].getCompilationSettings().target, ScriptTarget.ESNext); + assert.equal(projectService.inferredProjects[2].getCompilationSettings().target, ScriptTarget.ES2015); }); }); @@ -3895,13 +3924,13 @@ namespace ts.projectSystem { projectService.openClientFile(file1.path); let project = projectService.inferredProjects[0]; - let options = project.getCompilerOptions(); + let options = project.getCompilationSettings(); assert.isTrue(options.maxNodeModuleJsDepth === 2); // Assert the option sticks projectService.setCompilerOptionsForInferredProjects({ target: ScriptTarget.ES2016 }); project = projectService.inferredProjects[0]; - options = project.getCompilerOptions(); + options = project.getCompilationSettings(); assert.isTrue(options.maxNodeModuleJsDepth === 2); }); @@ -3921,15 +3950,15 @@ namespace ts.projectSystem { projectService.openClientFile(file1.path); checkNumberOfInferredProjects(projectService, 1); let project = projectService.inferredProjects[0]; - assert.isUndefined(project.getCompilerOptions().maxNodeModuleJsDepth); + assert.isUndefined(project.getCompilationSettings().maxNodeModuleJsDepth); projectService.openClientFile(file2.path); project = projectService.inferredProjects[0]; - assert.isTrue(project.getCompilerOptions().maxNodeModuleJsDepth === 2); + assert.isTrue(project.getCompilationSettings().maxNodeModuleJsDepth === 2); projectService.closeClientFile(file2.path); project = projectService.inferredProjects[0]; - assert.isUndefined(project.getCompilerOptions().maxNodeModuleJsDepth); + assert.isUndefined(project.getCompilationSettings().maxNodeModuleJsDepth); }); }); @@ -4164,7 +4193,8 @@ namespace ts.projectSystem { describe("WatchDirectories for config file with", () => { function verifyWatchDirectoriesCaseSensitivity(useCaseSensitiveFileNames: boolean) { const frontendDir = "/Users/someuser/work/applications/frontend"; - const canonicalFrontendDir = useCaseSensitiveFileNames ? frontendDir : frontendDir.toLowerCase(); + const toCanonical: (s: string) => Path = useCaseSensitiveFileNames ? s => s as Path : s => s.toLowerCase() as Path; + const canonicalFrontendDir = toCanonical(frontendDir); const file1: FileOrFolder = { path: `${frontendDir}/src/app/utils/Analytic.ts`, content: "export class SomeClass { };" @@ -4181,6 +4211,8 @@ namespace ts.projectSystem { path: "/a/lib/lib.es2016.full.d.ts", content: libFile.content }; + const typeRoots = ["types", "node_modules/@types"]; + const types = ["node", "jest"]; const tsconfigFile: FileOrFolder = { path: `${frontendDir}/tsconfig.json`, content: JSON.stringify({ @@ -4194,16 +4226,10 @@ namespace ts.projectSystem { "noEmitOnError": true, "experimentalDecorators": true, "emitDecoratorMetadata": true, - "types": [ - "node", - "jest" - ], + types, "noUnusedLocals": true, "outDir": "./compiled", - "typeRoots": [ - "types", - "node_modules/@types" - ], + typeRoots, "baseUrl": ".", "paths": { "*": [ @@ -4223,10 +4249,21 @@ namespace ts.projectSystem { const projectFiles = [file1, file2, es2016LibFile, tsconfigFile]; const host = createServerHost(projectFiles, { useCaseSensitiveFileNames }); const projectService = createProjectService(host); - const canonicalConfigPath = useCaseSensitiveFileNames ? tsconfigFile.path : tsconfigFile.path.toLowerCase(); + const canonicalConfigPath = toCanonical(tsconfigFile.path); const { configFileName } = projectService.openClientFile(file1.path); assert.equal(configFileName, tsconfigFile.path, `should find config`); checkNumberOfConfiguredProjects(projectService, 1); + const watchedModuleDirectories = arrayFrom( + getNodeModulesWatchedDirectories( + canonicalFrontendDir, + types, + getTypesWatchedDirectories( + canonicalFrontendDir, + typeRoots, + types + ) + ).keys() + ); const project = projectService.configuredProjects.get(canonicalConfigPath); verifyProjectAndWatchedDirectories(); @@ -4270,10 +4307,17 @@ namespace ts.projectSystem { verifyProjectAndWatchedDirectories(); callsTrackingHost.verifyNoHostCalls(); + function getFilePathIfOpen(f: FileOrFolder) { + const path = toCanonical(f.path); + const info = projectService.getScriptInfoForPath(toCanonical(f.path)); + return info && info.isScriptOpen() ? undefined : path; + } + function verifyProjectAndWatchedDirectories() { checkProjectActualFiles(project, map(projectFiles, f => f.path)); + checkWatchedFiles(host, mapDefined(projectFiles, getFilePathIfOpen)); checkWatchedDirectories(host, [`${canonicalFrontendDir}/src`], /*recursive*/ true); - checkWatchedDirectories(host, [`${canonicalFrontendDir}/types`, `${canonicalFrontendDir}/node_modules/@types`], /*recursive*/ false); + checkWatchedDirectories(host, watchedModuleDirectories, /*recursive*/ false); } } @@ -4328,7 +4372,7 @@ namespace ts.projectSystem { const projectService = createProjectService(host); const { configFileName } = projectService.openClientFile(app.path); assert.equal(configFileName, tsconfigJson.path, `should find config`); - const watchedModuleLocations = getNodeModulesWatchedDirectories(appFolder, "lodash"); + const watchedModuleLocations = arrayFrom(getNodeModulesWatchedDirectories(appFolder, ["lodash"]).keys()); verifyProject(); let timeoutAfterReloadFs = timeoutDuringPartialInstallation; @@ -4406,7 +4450,7 @@ namespace ts.projectSystem { const lodashIndexPath = "/a/b/node_modules/@types/lodash/index.d.ts"; projectFiles.push(find(filesAndFoldersToAdd, f => f.path === lodashIndexPath)); - watchedModuleLocations.length = watchedModuleLocations.indexOf(lodashIndexPath); + watchedModuleLocations.length = indexOf(watchedModuleLocations, getDirectoryPath(lodashIndexPath)); // npm installation complete, timeout after reload fs timeoutAfterReloadFs = true; verifyAfterPartialOrCompleteNpmInstall(2); @@ -4429,32 +4473,10 @@ namespace ts.projectSystem { const projectFilePaths = map(projectFiles, f => f.path); checkProjectActualFiles(project, projectFilePaths); - const filesWatched = filter(projectFilePaths, p => p !== app.path).concat(watchedModuleLocations); + const filesWatched = filter(projectFilePaths, p => p !== app.path); checkWatchedFiles(host, filesWatched); checkWatchedDirectories(host, [appFolder], /*recursive*/ true); - checkWatchedDirectories(host, [], /*recursive*/ false); - } - - function getNodeModulesWatchedDirectories(path: string, module: string): string[] { - const nodeModulesDir = combinePaths(path, "node_modules/"); - const parentDir = getDirectoryPath(path); - const parentNodeModules = parentDir !== path ? getNodeModulesWatchedDirectories(parentDir, module) : []; - return [ - `${nodeModulesDir}${module}.ts`, - `${nodeModulesDir}${module}.tsx`, - `${nodeModulesDir}${module}.d.ts`, - `${nodeModulesDir}${module}/index.ts`, - `${nodeModulesDir}${module}/index.tsx`, - `${nodeModulesDir}${module}/index.d.ts`, - `${nodeModulesDir}@types/${module}.d.ts`, - `${nodeModulesDir}@types/${module}/index.d.ts`, - `${nodeModulesDir}@types/${module}/package.json`, - `${nodeModulesDir}${module}.js`, - `${nodeModulesDir}${module}.jsx`, - `${nodeModulesDir}${module}/package.json`, - `${nodeModulesDir}${module}/index.js`, - `${nodeModulesDir}${module}/index.jsx`, - ].concat(parentNodeModules); + checkWatchedDirectories(host, watchedModuleLocations, /*recursive*/ false); } } diff --git a/src/harness/virtualFileSystemWithWatch.ts b/src/harness/virtualFileSystemWithWatch.ts index 44beb83922f3d..4b249c78e152b 100644 --- a/src/harness/virtualFileSystemWithWatch.ts +++ b/src/harness/virtualFileSystemWithWatch.ts @@ -95,8 +95,35 @@ namespace ts.TestFSWithWatch { } } + function getDiffInKeys(map: Map, expectedKeys: string[]) { + if (map.size === expectedKeys.length) { + return ""; + } + const notInActual: string[] = []; + const duplicates: string[] = []; + const seen = createMap(); + forEach(expectedKeys, expectedKey => { + if (seen.has(expectedKey)) { + duplicates.push(expectedKey); + return; + } + seen.set(expectedKey, true); + if (!map.has(expectedKey)) { + notInActual.push(expectedKey); + } + }); + const inActualNotExpected: string[] = []; + map.forEach((_value, key) => { + if (!seen.has(key)) { + inActualNotExpected.push(key); + } + seen.set(key, true); + }); + return `\n\nNotInActual: ${notInActual}\nDuplicates: ${duplicates}\nInActualButNotInExpected: ${inActualNotExpected}`; + } + function checkMapKeys(caption: string, map: Map, expectedKeys: string[]) { - assert.equal(map.size, expectedKeys.length, `${caption}: incorrect size of map: Actual keys: ${arrayFrom(map.keys())} Expected: ${expectedKeys}`); + assert.equal(map.size, expectedKeys.length, `${caption}: incorrect size of map: Actual keys: ${arrayFrom(map.keys())} Expected: ${expectedKeys}${getDiffInKeys(map, expectedKeys)}`); for (const name of expectedKeys) { assert.isTrue(map.has(name), `${caption} is expected to contain ${name}, actual keys: ${arrayFrom(map.keys())}`); } @@ -175,6 +202,8 @@ namespace ts.TestFSWithWatch { type TimeOutCallback = () => any; + export type TestFileWatcher = { cb: FileWatcherCallback; fileName: string; }; + export type TestDirectoryWatcher = { cb: DirectoryWatcherCallback; directoryName: string; }; export class TestServerHost implements server.ServerHost { args: string[] = []; @@ -186,9 +215,9 @@ namespace ts.TestFSWithWatch { private timeoutCallbacks = new Callbacks(); private immediateCallbacks = new Callbacks(); - readonly watchedDirectories = createMultiMap(); - readonly watchedDirectoriesRecursive = createMultiMap(); - readonly watchedFiles = createMultiMap(); + readonly watchedDirectories = createMultiMap(); + readonly watchedDirectoriesRecursive = createMultiMap(); + readonly watchedFiles = createMultiMap(); constructor(public withSafeList: boolean, public useCaseSensitiveFileNames: boolean, private executingFilePath: string, private currentDirectory: string, fileOrFolderList: FileOrFolder[], public readonly newLine = "\n") { this.getCanonicalFileName = createGetCanonicalFileName(useCaseSensitiveFileNames); @@ -317,8 +346,8 @@ namespace ts.TestFSWithWatch { // Invoke directory and recursive directory watcher for the folder // Here we arent invoking recursive directory watchers for the base folders // since that is something we would want to do for both file as well as folder we are deleting - invokeWatcherCallbacks(this.watchedDirectories.get(fileOrFolder.path), cb => cb(relativePath)); - invokeWatcherCallbacks(this.watchedDirectoriesRecursive.get(fileOrFolder.path), cb => cb(relativePath)); + invokeWatcherCallbacks(this.watchedDirectories.get(fileOrFolder.path), cb => this.directoryCallback(cb, relativePath)); + invokeWatcherCallbacks(this.watchedDirectoriesRecursive.get(fileOrFolder.path), cb => this.directoryCallback(cb, relativePath)); } if (basePath !== fileOrFolder.path) { @@ -333,8 +362,7 @@ namespace ts.TestFSWithWatch { private invokeFileWatcher(fileFullPath: string, eventKind: FileWatcherEventKind) { const callbacks = this.watchedFiles.get(this.toPath(fileFullPath)); - const fileName = getBaseFileName(fileFullPath); - invokeWatcherCallbacks(callbacks, cb => cb(fileName, eventKind)); + invokeWatcherCallbacks(callbacks, ({ cb, fileName }) => cb(fileName, eventKind)); } private getRelativePathToDirectory(directoryFullPath: string, fileFullPath: string) { @@ -346,16 +374,20 @@ namespace ts.TestFSWithWatch { */ private invokeDirectoryWatcher(folderFullPath: string, fileName: string) { const relativePath = this.getRelativePathToDirectory(folderFullPath, fileName); - invokeWatcherCallbacks(this.watchedDirectories.get(this.toPath(folderFullPath)), cb => cb(relativePath)); + invokeWatcherCallbacks(this.watchedDirectories.get(this.toPath(folderFullPath)), cb => this.directoryCallback(cb, relativePath)); this.invokeRecursiveDirectoryWatcher(folderFullPath, fileName); } + private directoryCallback({ cb, directoryName }: TestDirectoryWatcher, relativePath: string) { + cb(combinePaths(directoryName, relativePath)); + } + /** * This will call the recursive directory watcher for this directory as well as all the base directories */ private invokeRecursiveDirectoryWatcher(fullPath: string, fileName: string) { const relativePath = this.getRelativePathToDirectory(fullPath, fileName); - invokeWatcherCallbacks(this.watchedDirectoriesRecursive.get(this.toPath(fullPath)), cb => cb(relativePath)); + invokeWatcherCallbacks(this.watchedDirectoriesRecursive.get(this.toPath(fullPath)), cb => this.directoryCallback(cb, relativePath)); const basePath = getDirectoryPath(fullPath); if (this.getCanonicalFileName(fullPath) !== this.getCanonicalFileName(basePath)) { this.invokeRecursiveDirectoryWatcher(basePath, fileName); @@ -437,9 +469,13 @@ namespace ts.TestFSWithWatch { }); } - watchDirectory(directoryName: string, callback: DirectoryWatcherCallback, recursive: boolean): DirectoryWatcher { + watchDirectory(directoryName: string, cb: DirectoryWatcherCallback, recursive: boolean): DirectoryWatcher { const path = this.toFullPath(directoryName); const map = recursive ? this.watchedDirectoriesRecursive : this.watchedDirectories; + const callback: TestDirectoryWatcher = { + cb, + directoryName + }; map.add(path, callback); return { referenceCount: 0, @@ -452,8 +488,9 @@ namespace ts.TestFSWithWatch { return Harness.mockHash(s); } - watchFile(fileName: string, callback: FileWatcherCallback) { + watchFile(fileName: string, cb: FileWatcherCallback) { const path = this.toFullPath(fileName); + const callback: TestFileWatcher = { fileName, cb }; this.watchedFiles.add(path, callback); return { close: () => this.watchedFiles.remove(path, callback) }; } diff --git a/src/server/editorServices.ts b/src/server/editorServices.ts index 73912b3bd919a..a85ebebec5eee 100644 --- a/src/server/editorServices.ts +++ b/src/server/editorServices.ts @@ -3,7 +3,6 @@ /// /// /// -/// /// /// @@ -476,7 +475,6 @@ namespace ts.server { this.typingsCache.deleteTypingsForProject(response.projectName); break; } - project.markAsDirty(); this.delayUpdateProjectGraphAndInferredProjectsRefresh(project); } @@ -528,6 +526,7 @@ namespace ts.server { /* @internal */ delayUpdateProjectGraphAndInferredProjectsRefresh(project: Project) { + project.markAsDirty(); this.delayUpdateProjectGraph(project); this.delayInferredProjectsRefresh(); } @@ -712,36 +711,56 @@ namespace ts.server { } } - /* @internal */ - onTypeRootFileChanged(project: ConfiguredProject, fileOrFolder: string) { - project.getCachedPartialSystem().addOrDeleteFileOrFolder(fileOrFolder, this.toPath(fileOrFolder)); - project.updateTypes(); - this.delayUpdateProjectGraphAndInferredProjectsRefresh(project); + /*@internal*/ + watchTypeRootDirectory(root: Path, project: ConfiguredProject) { + // TODO: This is not needed anymore with watches for failed lookup locations? + return this.watchDirectory( + this.host, + root, + fileOrFolder => { + project.getCachedPartialSystem().addOrDeleteFileOrFolder(fileOrFolder, this.toPath(fileOrFolder)); + project.updateTypes(); + this.delayUpdateProjectGraphAndInferredProjectsRefresh(project); + }, + WatchDirectoryFlags.None, + WatchType.TypeRoot, + project + ); } /** - * This is the callback function when a watched directory has added or removed source code files. - * @param project the project that associates with this directory watcher - * @param fileName the absolute file name that changed in watched directory + * This is to watch whenever files are added or removed to the wildcard directories */ - /* @internal */ - onFileAddOrRemoveInWatchedDirectoryOfProject(project: ConfiguredProject, fileOrFolder: string) { - project.getCachedPartialSystem().addOrDeleteFileOrFolder(fileOrFolder, this.toPath(fileOrFolder)); - const configFilename = project.getConfigFilePath(); - - // If a change was made inside "folder/file", node will trigger the callback twice: - // one with the fileName being "folder/file", and the other one with "folder". - // We don't respond to the second one. - if (fileOrFolder && !isSupportedSourceFileName(fileOrFolder, project.getCompilerOptions(), this.hostConfiguration.extraFileExtensions)) { - this.logger.info(`Project: ${configFilename} Detected file add/remove of non supported extension: ${fileOrFolder}`); - return; - } + /*@internal*/ + watchWildcardDirectory(directory: Path, flags: WatchDirectoryFlags, project: ConfiguredProject) { + return this.watchDirectory( + this.host, + directory, + fileOrFolder => { + const fileOrFolderPath = this.toPath(fileOrFolder); + project.getCachedPartialSystem().addOrDeleteFileOrFolder(fileOrFolder, fileOrFolderPath); + const configFilename = project.getConfigFilePath(); + + // If the the added or created file or folder is not supported file name, ignore the file + // But when watched directory is added/removed, we need to reload the file list + if (fileOrFolderPath !== directory && !isSupportedSourceFileName(fileOrFolder, project.getCompilationSettings(), this.hostConfiguration.extraFileExtensions)) { + this.logger.info(`Project: ${configFilename} Detected file add/remove of non supported extension: ${fileOrFolder}`); + return; + } - const configFileSpecs = project.configFileSpecs; - const result = getFileNamesFromConfigSpecs(configFileSpecs, getDirectoryPath(configFilename), project.getCompilerOptions(), project.getCachedPartialSystem(), this.hostConfiguration.extraFileExtensions); - project.updateErrorOnNoInputFiles(result.fileNames.length !== 0); - this.updateNonInferredProjectFiles(project, result.fileNames, fileNamePropertyReader, /*clientFileName*/ undefined); - this.delayUpdateProjectGraphAndInferredProjectsRefresh(project); + // Reload is pending, do the reload + if (!project.pendingReload) { + const configFileSpecs = project.configFileSpecs; + const result = getFileNamesFromConfigSpecs(configFileSpecs, getDirectoryPath(configFilename), project.getCompilationSettings(), project.getCachedPartialSystem(), this.hostConfiguration.extraFileExtensions); + project.updateErrorOnNoInputFiles(result.fileNames.length !== 0); + this.updateNonInferredProjectFiles(project, result.fileNames, fileNamePropertyReader, /*clientFileName*/ undefined); + this.delayUpdateProjectGraphAndInferredProjectsRefresh(project); + } + }, + flags, + WatchType.WildcardDirectories, + project + ); } private onConfigChangedForConfiguredProject(project: ConfiguredProject, eventKind: FileWatcherEventKind) { @@ -1253,7 +1272,7 @@ namespace ts.server { return findProjectByName(projectFileName, this.externalProjects); } - private convertConfigFileContentToProjectOptions(configFilename: string, cachedServerHost: PartialSystem) { + private convertConfigFileContentToProjectOptions(configFilename: string, cachedPartialSystem: CachedPartialSystem) { configFilename = normalizePath(configFilename); const configFileContent = this.host.readFile(configFilename); @@ -1265,7 +1284,7 @@ namespace ts.server { const errors = result.parseDiagnostics; const parsedCommandLine = parseJsonSourceFileConfigFileContent( result, - cachedServerHost, + cachedPartialSystem, getDirectoryPath(configFilename), /*existingOptions*/ {}, configFilename, @@ -1350,7 +1369,7 @@ namespace ts.server { const data: ProjectInfoTelemetryEventData = { projectId: this.host.createHash(projectKey), fileStats: countEachFileTypes(project.getScriptInfos()), - compilerOptions: convertCompilerOptionsForTelemetry(project.getCompilerOptions()), + compilerOptions: convertCompilerOptionsForTelemetry(project.getCompilationSettings()), typeAcquisition: convertTypeAcquisition(project.getTypeAcquisition()), extends: projectOptions && projectOptions.configHasExtendsProperty, files: projectOptions && projectOptions.configHasFilesProperty, @@ -1435,8 +1454,8 @@ namespace ts.server { const normalizedPath = toNormalizedPath(newRootFile); let scriptInfo: ScriptInfo | NormalizedPath; let path: Path; - // Use the project's lsHost so that it can use caching instead of reaching to disk for the query - if (!project.lsHost.fileExists(newRootFile)) { + // Use the project's fileExists so that it can use caching instead of reaching to disk for the query + if (!project.fileExists(newRootFile)) { path = normalizedPathToPath(normalizedPath, this.currentDirectory, this.toCanonicalFileName); const existingValue = projectRootFilesMap.get(path); if (isScriptInfo(existingValue)) { @@ -1448,7 +1467,7 @@ namespace ts.server { else { const scriptKind = propertyReader.getScriptKind(f); const hasMixedContent = propertyReader.hasMixedContent(f, this.hostConfiguration.extraFileExtensions); - scriptInfo = this.getOrCreateScriptInfoForNormalizedPath(normalizedPath, /*openedByClient*/ clientFileName === newRootFile, /*fileContent*/ undefined, scriptKind, hasMixedContent, project.lsHost.host); + scriptInfo = this.getOrCreateScriptInfoForNormalizedPath(normalizedPath, /*openedByClient*/ clientFileName === newRootFile, /*fileContent*/ undefined, scriptKind, hasMixedContent, project.partialSystem); path = scriptInfo.path; // If this script info is not already a root add it if (!project.isRoot(scriptInfo)) { diff --git a/src/server/lsHost.ts b/src/server/lsHost.ts deleted file mode 100644 index b758cd796bf8d..0000000000000 --- a/src/server/lsHost.ts +++ /dev/null @@ -1,137 +0,0 @@ -/// -/// -/// -/// - -namespace ts.server { - export class LSHost implements LanguageServiceHost, ModuleResolutionHost { - /*@internal*/ - compilationSettings: CompilerOptions; - - 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 - * file system entries as we would anyways be watching files in the project (so safe to cache) - */ - /*@internal*/ - host: PartialSystem; - - constructor(host: PartialSystem, private project: Project, private readonly cancellationToken: HostCancellationToken) { - this.host = host; - this.cancellationToken = new ThrottledCancellationToken(cancellationToken, project.projectService.throttleWaitMilliseconds); - - const serverHost = this.getServerHost(); - if (serverHost.trace) { - this.trace = s => serverHost.trace(s); - } - - if (serverHost.realpath) { - this.realpath = path => serverHost.realpath(path); - } - } - - private getServerHost() { - return this.project.projectService.host; - } - - dispose() { - this.project = undefined; - this.host = undefined; - } - - getNewLine() { - return this.host.newLine; - } - - getProjectVersion() { - return this.project.getProjectVersion(); - } - - getCompilationSettings() { - return this.compilationSettings; - } - - useCaseSensitiveFileNames() { - return this.host.useCaseSensitiveFileNames; - } - - getCancellationToken() { - return this.cancellationToken; - } - - resolveTypeReferenceDirectives(typeDirectiveNames: string[], containingFile: string): ResolvedTypeReferenceDirective[] { - return this.project.resolutionCache.resolveTypeReferenceDirectives(typeDirectiveNames, containingFile); - } - - resolveModuleNames(moduleNames: string[], containingFile: string): ResolvedModuleFull[] { - return this.project.resolutionCache.resolveModuleNames(moduleNames, containingFile, /*logChanges*/ true); - } - - getDefaultLibFileName() { - const nodeModuleBinDir = getDirectoryPath(normalizePath(this.getServerHost().getExecutingFilePath())); - return combinePaths(nodeModuleBinDir, getDefaultLibFileName(this.compilationSettings)); - } - - getScriptSnapshot(filename: string): IScriptSnapshot { - const scriptInfo = this.project.getScriptInfoLSHost(filename); - if (scriptInfo) { - return scriptInfo.getSnapshot(); - } - } - - getScriptFileNames() { - return this.project.getRootFilesLSHost(); - } - - getTypeRootsVersion() { - return this.project.typesVersion; - } - - getScriptKind(fileName: string) { - const info = this.project.getScriptInfoLSHost(fileName); - return info && info.scriptKind; - } - - getScriptVersion(filename: string) { - const info = this.project.getScriptInfoLSHost(filename); - return info && info.getLatestVersion(); - } - - getCurrentDirectory(): string { - return this.host.getCurrentDirectory(); - } - - resolvePath(path: string): string { - return this.getServerHost().resolvePath(path); - } - - fileExists(file: string): boolean { - // As an optimization, don't hit the disks for files we already know don't exist - // (because we're watching for their creation). - const path = this.project.projectService.toPath(file); - return !this.project.isWatchedMissingFile(path) && this.host.fileExists(file); - } - - readFile(fileName: string): string | undefined { - return this.host.readFile(fileName); - } - - directoryExists(path: string): boolean { - return this.host.directoryExists(path); - } - - readDirectory(path: string, extensions?: ReadonlyArray, exclude?: ReadonlyArray, include?: ReadonlyArray, depth?: number): string[] { - return this.host.readDirectory(path, extensions, exclude, include, depth); - } - - getDirectories(path: string): string[] { - return this.host.getDirectories(path); - } - } -} diff --git a/src/server/project.ts b/src/server/project.ts index 420d080b9f23f..bf73d4d175c65 100644 --- a/src/server/project.ts +++ b/src/server/project.ts @@ -1,7 +1,7 @@ /// /// /// -/// +/// /// /// @@ -112,7 +112,7 @@ namespace ts.server { return value instanceof ScriptInfo; } - export abstract class Project { + export abstract class Project implements LanguageServiceHost, ModuleResolutionHost { private rootFiles: ScriptInfo[] = []; private rootFilesMap: Map = createMap(); private program: Program; @@ -127,11 +127,14 @@ namespace ts.server { public languageServiceEnabled = true; + readonly trace?: (s: string) => void; + readonly realpath?: (path: string) => string; + /*@internal*/ - resolutionCache: ResolutionCache; + hasInvalidatedResolution: HasInvalidatedResolution; /*@internal*/ - lsHost: LSHost; + resolutionCache: ResolutionCache; private builder: Builder; /** @@ -161,7 +164,7 @@ namespace ts.server { private typingFiles: SortedReadonlyArray; - public typesVersion = 0; + private typesVersion = 0; public isNonTsProject() { this.updateGraph(); @@ -190,7 +193,7 @@ namespace ts.server { } constructor( - private readonly projectName: string, + /*@internal*/readonly projectName: string, readonly projectKind: ProjectKind, readonly projectService: ProjectService, private documentRegistry: DocumentRegistry, @@ -198,7 +201,7 @@ namespace ts.server { languageServiceEnabled: boolean, private compilerOptions: CompilerOptions, public compileOnSaveEnabled: boolean, - host: PartialSystem) { + /*@internal*/public partialSystem: PartialSystem) { if (!this.compilerOptions) { this.compilerOptions = getDefaultCompilerOptions(); @@ -211,52 +214,162 @@ namespace ts.server { } this.setInternalCompilerOptionsForEmittingJsFiles(); + const host = this.projectService.host; + if (host.trace) { + this.trace = s => host.trace(s); + } - this.lsHost = new LSHost(host, this, this.projectService.cancellationToken); - this.resolutionCache = createResolutionCache( - fileName => this.projectService.toPath(fileName), - () => this.compilerOptions, - directory => this.watchDirectoryOfFailedLookup(directory), - s => this.projectService.logger.info(s), - this.getProjectName(), - () => this.getTypeAcquisition().enable ? this.projectService.typingsInstaller.globalTypingsCacheLocation : undefined - ); - this.lsHost.compilationSettings = this.compilerOptions; - this.resolutionCache.setModuleResolutionHost(this.lsHost); - - this.languageService = createLanguageService(this.lsHost, this.documentRegistry); + if (host.realpath) { + this.realpath = path => host.realpath(path); + } + this.languageService = createLanguageService(this, this.documentRegistry); if (!languageServiceEnabled) { this.disableLanguageService(); } - + this.resolutionCache = createResolutionCache(this); this.markAsDirty(); } - private watchDirectoryOfFailedLookup(directory: string) { + getCompilationSettings() { + return this.compilerOptions; + } + + getNewLine() { + return this.partialSystem.newLine; + } + + getProjectVersion() { + return this.projectStateVersion.toString(); + } + + getScriptFileNames() { + const result: string[] = []; + if (this.rootFiles) { + this.rootFilesMap.forEach((value, _path) => { + const f: ScriptInfo = isScriptInfo(value) && value; + if (this.languageServiceEnabled || (f && f.isScriptOpen())) { + // if language service is disabled - process only files that are open + result.push(f ? f.fileName : value as NormalizedPath); + } + }); + if (this.typingFiles) { + for (const f of this.typingFiles) { + result.push(f); + } + } + } + return result; + } + + private getScriptInfoLSHost(fileName: string) { + const scriptInfo = this.projectService.getOrCreateScriptInfo(fileName, /*openedByClient*/ false, this.partialSystem); + if (scriptInfo) { + const existingValue = this.rootFilesMap.get(scriptInfo.path); + if (existingValue !== undefined && existingValue !== scriptInfo) { + // This was missing path earlier but now the file exists. Update the root + this.rootFiles.push(scriptInfo); + this.rootFilesMap.set(scriptInfo.path, scriptInfo); + } + scriptInfo.attachToProject(this); + } + return scriptInfo; + } + + getScriptKind(fileName: string) { + const info = this.getScriptInfoLSHost(fileName); + return info && info.scriptKind; + } + + getScriptVersion(filename: string) { + const info = this.getScriptInfoLSHost(filename); + return info && info.getLatestVersion(); + } + + getScriptSnapshot(filename: string): IScriptSnapshot { + const scriptInfo = this.getScriptInfoLSHost(filename); + if (scriptInfo) { + return scriptInfo.getSnapshot(); + } + } + + getCancellationToken() { + return this.projectService.cancellationToken; + } + + getCurrentDirectory(): string { + return this.partialSystem.getCurrentDirectory(); + } + + getDefaultLibFileName() { + const nodeModuleBinDir = getDirectoryPath(normalizePath(this.projectService.host.getExecutingFilePath())); + return combinePaths(nodeModuleBinDir, getDefaultLibFileName(this.compilerOptions)); + } + + useCaseSensitiveFileNames() { + return this.partialSystem.useCaseSensitiveFileNames; + } + + readDirectory(path: string, extensions?: ReadonlyArray, exclude?: ReadonlyArray, include?: ReadonlyArray, depth?: number): string[] { + return this.partialSystem.readDirectory(path, extensions, exclude, include, depth); + } + + readFile(fileName: string): string | undefined { + return this.partialSystem.readFile(fileName); + } + + fileExists(file: string): boolean { + // As an optimization, don't hit the disks for files we already know don't exist + // (because we're watching for their creation). + const path = this.toPath(file); + return !this.isWatchedMissingFile(path) && this.partialSystem.fileExists(file); + } + + getTypeRootsVersion() { + return this.typesVersion; + } + + resolveModuleNames(moduleNames: string[], containingFile: string): ResolvedModuleFull[] { + return this.resolutionCache.resolveModuleNames(moduleNames, containingFile, /*logChanges*/ true); + } + + resolveTypeReferenceDirectives(typeDirectiveNames: string[], containingFile: string): ResolvedTypeReferenceDirective[] { + return this.resolutionCache.resolveTypeReferenceDirectives(typeDirectiveNames, containingFile); + } + + directoryExists(path: string): boolean { + return this.partialSystem.directoryExists(path); + } + + getDirectories(path: string): string[] { + return this.partialSystem.getDirectories(path); + } + + /*@internal*/ + toPath(fileName: string) { + return this.projectService.toPath(fileName); + } + + /*@internal*/ + watchDirectoryOfFailedLookupLocation(directory: string, cb: DirectoryWatcherCallback) { return this.projectService.watchDirectory( this.projectService.host, directory, - fileOrFolder => this.onFileAddOrRemoveInDirectoryOfFailedLookup(fileOrFolder), + cb, WatchDirectoryFlags.None, WatchType.FailedLookupLocation, this ); } - private onFileAddOrRemoveInDirectoryOfFailedLookup(fileOrFolder: string) { - const fileOrFolderPath = this.projectService.toPath(fileOrFolder); - - // There is some kind of change in the failed lookup location, update the program - if (this.projectKind === ProjectKind.Configured) { - (this.lsHost.host as CachedPartialSystem).addOrDeleteFileOrFolder(fileOrFolder, fileOrFolderPath); - } + /*@internal*/ + onInvalidatedResolution() { + this.projectService.delayUpdateProjectGraphAndInferredProjectsRefresh(this); + } - // If the location results in update to failed lookup, schedule program update - if (this.resolutionCache.onFileAddOrRemoveInDirectoryOfFailedLookup(fileOrFolderPath)) { - this.markAsDirty(); - this.projectService.delayUpdateProjectGraphAndInferredProjectsRefresh(this); - } + /*@internal*/ + getGlobalCache() { + return this.getTypeAcquisition().enable ? this.projectService.typingsInstaller.globalTypingsCacheLocation : undefined; } private setInternalCompilerOptionsForEmittingJsFiles() { @@ -326,10 +439,6 @@ namespace ts.server { return this.builder.getChangedProgramFiles(this.program); } - getProjectVersion() { - return this.projectStateVersion.toString(); - } - enableLanguageService() { if (this.languageServiceEnabled) { return; @@ -366,7 +475,6 @@ namespace ts.server { updateTypes() { this.typesVersion++; - this.markAsDirty(); } close() { @@ -395,8 +503,7 @@ namespace ts.server { this.resolutionCache.clear(); this.resolutionCache = undefined; this.cachedUnresolvedImportsPerFile = undefined; - this.lsHost.dispose(); - this.lsHost = undefined; + this.partialSystem = undefined; // Clean up file watchers waiting for missing files if (this.missingFilesMap) { @@ -410,11 +517,7 @@ namespace ts.server { } isClosed() { - return this.lsHost === undefined; - } - - getCompilerOptions() { - return this.compilerOptions; + return this.rootFiles === undefined; } hasRoots() { @@ -425,25 +528,6 @@ namespace ts.server { return this.rootFiles && this.rootFiles.map(info => info.fileName); } - getRootFilesLSHost() { - const result: string[] = []; - if (this.rootFiles) { - this.rootFilesMap.forEach((value, _path) => { - const f: ScriptInfo = isScriptInfo(value) && value; - if (this.languageServiceEnabled || (f && f.isScriptOpen())) { - // if language service is disabled - process only files that are open - result.push(f ? f.fileName : value as NormalizedPath); - } - }); - if (this.typingFiles) { - for (const f of this.typingFiles) { - result.push(f); - } - } - } - return result; - } - /*@internal*/ getRootFilesMap() { return this.rootFilesMap; @@ -622,7 +706,7 @@ namespace ts.server { */ updateGraph(): boolean { this.resolutionCache.startRecordingFilesWithChangedResolutions(); - this.lsHost.hasInvalidatedResolution = this.resolutionCache.createHasInvalidatedResolution(); + this.hasInvalidatedResolution = this.resolutionCache.createHasInvalidatedResolution(); let hasChanges = this.updateGraphWorker(); @@ -659,7 +743,7 @@ namespace ts.server { // Note we are retaining builder so we can send events for project change if (this.builder) { if (this.languageServiceEnabled) { - this.builder.onProgramUpdateGraph(this.program, this.lsHost.hasInvalidatedResolution); + this.builder.onProgramUpdateGraph(this.program, this.hasInvalidatedResolution); } else { this.builder.clear(); @@ -720,7 +804,7 @@ namespace ts.server { // by the LSHost for files in the program when the program is retrieved above but // the program doesn't contain external files so this must be done explicitly. inserted => { - const scriptInfo = this.projectService.getOrCreateScriptInfo(inserted, /*openedByClient*/ false, this.lsHost.host); + const scriptInfo = this.projectService.getOrCreateScriptInfo(inserted, /*openedByClient*/ false, this.partialSystem); scriptInfo.attachToProject(this); }, removed => { @@ -739,7 +823,7 @@ namespace ts.server { missingFilePath, (fileName, eventKind) => { if (this.projectKind === ProjectKind.Configured) { - (this.lsHost.host as CachedPartialSystem).addOrDeleteFile(fileName, missingFilePath, eventKind); + (this.partialSystem as CachedPartialSystem).addOrDeleteFile(fileName, missingFilePath, eventKind); } if (eventKind === FileWatcherEventKind.Created && this.missingFilesMap.has(missingFilePath)) { @@ -747,7 +831,6 @@ namespace ts.server { fileWatcher.close(); // When a missing file is created, we should update the graph. - this.markAsDirty(); this.projectService.delayUpdateProjectGraphAndInferredProjectsRefresh(this); } }, @@ -757,28 +840,14 @@ namespace ts.server { return fileWatcher; } - isWatchedMissingFile(path: Path) { + private isWatchedMissingFile(path: Path) { return this.missingFilesMap && this.missingFilesMap.has(path); } - getScriptInfoLSHost(fileName: string) { - const scriptInfo = this.projectService.getOrCreateScriptInfo(fileName, /*openedByClient*/ false, this.lsHost.host); - if (scriptInfo) { - const existingValue = this.rootFilesMap.get(scriptInfo.path); - if (existingValue !== undefined && existingValue !== scriptInfo) { - // This was missing path earlier but now the file exists. Update the root - this.rootFiles.push(scriptInfo); - this.rootFilesMap.set(scriptInfo.path, scriptInfo); - } - scriptInfo.attachToProject(this); - } - return scriptInfo; - } - getScriptInfoForNormalizedPath(fileName: NormalizedPath) { const scriptInfo = this.projectService.getOrCreateScriptInfoForNormalizedPath( fileName, /*openedByClient*/ false, /*fileContent*/ undefined, - /*scriptKind*/ undefined, /*hasMixedContent*/ undefined, this.lsHost.host + /*scriptKind*/ undefined, /*hasMixedContent*/ undefined, this.partialSystem ); if (scriptInfo && !scriptInfo.isAttached(this)) { return Errors.ThrowProjectDoesNotContainDocument(fileName, this); @@ -815,8 +884,6 @@ namespace ts.server { if (changesAffectModuleResolution(oldOptions, compilerOptions)) { this.resolutionCache.clear(); } - this.lsHost.compilationSettings = this.compilerOptions; - this.markAsDirty(); } } @@ -839,7 +906,7 @@ namespace ts.server { projectName: this.getProjectName(), version: this.projectStructureVersion, isInferred: this.projectKind === ProjectKind.Inferred, - options: this.getCompilerOptions(), + options: this.getCompilationSettings(), languageServiceDisabled: !this.languageServiceEnabled }; const updatedFileNames = this.updatedFileNames; @@ -913,7 +980,7 @@ namespace ts.server { setCompilerOptions(options?: CompilerOptions) { // Avoid manipulating the given options directly - const newOptions = options ? cloneCompilerOptions(options) : this.getCompilerOptions(); + const newOptions = options ? cloneCompilerOptions(options) : this.getCompilationSettings(); if (!newOptions) { return; } @@ -1042,7 +1109,7 @@ namespace ts.server { /*@internal*/ getCachedPartialSystem() { - return this.lsHost.host as CachedPartialSystem; + return this.partialSystem as CachedPartialSystem; } getConfigFilePath() { @@ -1051,7 +1118,7 @@ namespace ts.server { enablePlugins() { const host = this.projectService.host; - const options = this.getCompilerOptions(); + const options = this.getCompilationSettings(); if (!host.require) { this.projectService.logger.info("Plugins were requested but not running in environment that supports 'require'. Nothing will be loaded"); @@ -1113,7 +1180,7 @@ namespace ts.server { config: configEntry, project: this, languageService: this.languageService, - languageServiceHost: this.lsHost, + languageServiceHost: this, serverHost: this.projectService.host }; @@ -1174,14 +1241,7 @@ namespace ts.server { this.directoriesWatchedForWildcards || (this.directoriesWatchedForWildcards = createMap()), wildcardDirectories, // Create new directory watcher - (directory, flags) => this.projectService.watchDirectory( - this.projectService.host, - directory, - path => this.projectService.onFileAddOrRemoveInWatchedDirectoryOfProject(this, path), - flags, - WatchType.WildcardDirectories, - this - ) + (directory, flags) => this.projectService.watchWildcardDirectory(directory as Path, flags, this), ); } @@ -1201,14 +1261,7 @@ namespace ts.server { newTypeRoots, { // Create new watch - createNewValue: root => this.projectService.watchDirectory( - this.projectService.host, - root, - path => this.projectService.onTypeRootFileChanged(this, path), - WatchDirectoryFlags.None, - WatchType.TypeRoot, - this - ), + createNewValue: root => this.projectService.watchTypeRootDirectory(root as Path, this), // Close existing watch thats not needed any more onDeleteValue: closeFileWatcher } @@ -1245,7 +1298,7 @@ namespace ts.server { } getEffectiveTypeRoots() { - return getEffectiveTypeRoots(this.getCompilerOptions(), this.lsHost.host) || []; + return getEffectiveTypeRoots(this.getCompilationSettings(), this.partialSystem) || []; } /*@internal*/ diff --git a/src/server/scriptInfo.ts b/src/server/scriptInfo.ts index c35f26e3d3c27..d76f930f5ec24 100644 --- a/src/server/scriptInfo.ts +++ b/src/server/scriptInfo.ts @@ -237,7 +237,7 @@ namespace ts.server { detachAllProjects() { for (const p of this.containingProjects) { if (p.projectKind === ProjectKind.Configured) { - (p.lsHost.host as CachedPartialSystem).addOrDeleteFile(this.fileName, this.path, FileWatcherEventKind.Deleted); + (p.partialSystem as CachedPartialSystem).addOrDeleteFile(this.fileName, this.path, FileWatcherEventKind.Deleted); } const isInfoRoot = p.isRoot(this); // detach is unnecessary since we'll clean the list of containing projects anyways diff --git a/src/server/session.ts b/src/server/session.ts index 4b42aaba96a2d..b4f2b46f99c85 100644 --- a/src/server/session.ts +++ b/src/server/session.ts @@ -1218,7 +1218,7 @@ namespace ts.server { result.push({ projectFileName: project.getProjectName(), fileNames: project.getCompileOnSaveAffectedFileList(info), - projectUsesOutFile: !!project.getCompilerOptions().outFile || !!project.getCompilerOptions().out + projectUsesOutFile: !!project.getCompilationSettings().outFile || !!project.getCompilationSettings().out }); } } diff --git a/src/server/tsconfig.json b/src/server/tsconfig.json index b0eb4965ea687..c838a7209d9a5 100644 --- a/src/server/tsconfig.json +++ b/src/server/tsconfig.json @@ -15,7 +15,6 @@ "utilities.ts", "scriptVersionCache.ts", "scriptInfo.ts", - "lsHost.ts", "typingsCache.ts", "project.ts", "editorServices.ts", diff --git a/src/server/tsconfig.library.json b/src/server/tsconfig.library.json index b59395fda275a..aa3148d58a105 100644 --- a/src/server/tsconfig.library.json +++ b/src/server/tsconfig.library.json @@ -16,7 +16,6 @@ }, "files": [ "editorServices.ts", - "lsHost.ts", "project.ts", "protocol.ts", "scriptInfo.ts", diff --git a/src/server/typingsCache.ts b/src/server/typingsCache.ts index 3ef37685da2c9..207824616a980 100644 --- a/src/server/typingsCache.ts +++ b/src/server/typingsCache.ts @@ -89,12 +89,12 @@ namespace ts.server { if (forceRefresh || !entry || typeAcquisitionChanged(typeAcquisition, entry.typeAcquisition) || - compilerOptionsChanged(project.getCompilerOptions(), entry.compilerOptions) || + compilerOptionsChanged(project.getCompilationSettings(), entry.compilerOptions) || unresolvedImportsChanged(unresolvedImports, entry.unresolvedImports)) { // Note: entry is now poisoned since it does not really contain typings for a given combination of compiler options\typings options. // instead it acts as a placeholder to prevent issuing multiple requests this.perProjectCache.set(project.getProjectName(), { - compilerOptions: project.getCompilerOptions(), + compilerOptions: project.getCompilationSettings(), typeAcquisition, typings: result, unresolvedImports, @@ -125,4 +125,4 @@ namespace ts.server { this.installer.onProjectClosed(project); } } -} \ No newline at end of file +} diff --git a/src/server/utilities.ts b/src/server/utilities.ts index 3ae1dd1d71dad..c596176ad8600 100644 --- a/src/server/utilities.ts +++ b/src/server/utilities.ts @@ -50,7 +50,7 @@ namespace ts.server { return { projectName: project.getProjectName(), fileNames: project.getFileNames(/*excludeFilesFromExternalLibraries*/ true, /*excludeConfigFiles*/ true), - compilerOptions: project.getCompilerOptions(), + compilerOptions: project.getCompilationSettings(), typeAcquisition, unresolvedImports, projectRootPath: getProjectRootPath(project), From 10ea5bf4607fd07b0ad23c6b6c410668ddd5d782 Mon Sep 17 00:00:00 2001 From: Sheetal Nandi Date: Fri, 25 Aug 2017 20:20:14 -0700 Subject: [PATCH 079/109] Script infos while opening/closing shouldnt mark project as dirty if the contents dont change --- .../unittests/cachingInServerLSHost.ts | 2 +- src/harness/unittests/textStorage.ts | 12 +- src/server/editorServices.ts | 96 +++++----- src/server/project.ts | 9 +- src/server/scriptInfo.ts | 164 +++++++++++------- src/server/scriptVersionCache.ts | 21 +-- 6 files changed, 169 insertions(+), 135 deletions(-) diff --git a/src/harness/unittests/cachingInServerLSHost.ts b/src/harness/unittests/cachingInServerLSHost.ts index 1d22dc0417e59..a1095e7dc4899 100644 --- a/src/harness/unittests/cachingInServerLSHost.ts +++ b/src/harness/unittests/cachingInServerLSHost.ts @@ -61,7 +61,7 @@ namespace ts { typingsInstaller: undefined }; const projectService = new server.ProjectService(svcOpts); - const rootScriptInfo = projectService.getOrCreateScriptInfo(rootFile, /* openedByClient */ true, serverHost); + const rootScriptInfo = projectService.getOrCreateScriptInfoForNormalizedPath(server.toNormalizedPath(rootFile), /*openedByClient*/ true); const project = projectService.assignOrphanScriptInfoToInferredProject(rootScriptInfo); project.setCompilerOptions({ module: ts.ModuleKind.AMD, noLib: true } ); diff --git a/src/harness/unittests/textStorage.ts b/src/harness/unittests/textStorage.ts index 545d090df9b33..aa8231aa31af4 100644 --- a/src/harness/unittests/textStorage.ts +++ b/src/harness/unittests/textStorage.ts @@ -21,7 +21,7 @@ namespace ts.textStorage { const ts1 = new server.TextStorage(host, server.asNormalizedPath(f.path)); const ts2 = new server.TextStorage(host, server.asNormalizedPath(f.path)); - ts1.useScriptVersionCache(); + ts1.useScriptVersionCache_TestOnly(); ts2.useText(); const lineMap = computeLineStarts(f.content); @@ -55,16 +55,16 @@ namespace ts.textStorage { const ts1 = new server.TextStorage(host, server.asNormalizedPath(f.path)); ts1.getSnapshot(); - assert.isTrue(!ts1.hasScriptVersionCache(), "should not have script version cache - 1"); + assert.isTrue(!ts1.hasScriptVersionCache_TestOnly(), "should not have script version cache - 1"); ts1.edit(0, 5, " "); - assert.isTrue(ts1.hasScriptVersionCache(), "have script version cache - 1"); + assert.isTrue(ts1.hasScriptVersionCache_TestOnly(), "have script version cache - 1"); ts1.useText(); - assert.isTrue(!ts1.hasScriptVersionCache(), "should not have script version cache - 2"); + assert.isTrue(!ts1.hasScriptVersionCache_TestOnly(), "should not have script version cache - 2"); ts1.getLineInfo(0); - assert.isTrue(ts1.hasScriptVersionCache(), "have script version cache - 2"); + assert.isTrue(ts1.hasScriptVersionCache_TestOnly(), "have script version cache - 2"); }); }); -} \ No newline at end of file +} diff --git a/src/server/editorServices.ts b/src/server/editorServices.ts index a85ebebec5eee..9195083bb6882 100644 --- a/src/server/editorServices.ts +++ b/src/server/editorServices.ts @@ -687,7 +687,7 @@ namespace ts.server { else { // file has been changed which might affect the set of referenced files in projects that include // this file and set of inferred projects - info.reloadFromFile(); + info.delayReloadNonMixedContentFile(); this.delayUpdateProjectGraphs(info.containingProjects); } } @@ -753,7 +753,7 @@ namespace ts.server { const configFileSpecs = project.configFileSpecs; const result = getFileNamesFromConfigSpecs(configFileSpecs, getDirectoryPath(configFilename), project.getCompilationSettings(), project.getCachedPartialSystem(), this.hostConfiguration.extraFileExtensions); project.updateErrorOnNoInputFiles(result.fileNames.length !== 0); - this.updateNonInferredProjectFiles(project, result.fileNames, fileNamePropertyReader, /*clientFileName*/ undefined); + this.updateNonInferredProjectFiles(project, result.fileNames, fileNamePropertyReader); this.delayUpdateProjectGraphAndInferredProjectsRefresh(project); } }, @@ -1352,7 +1352,7 @@ namespace ts.server { /*languageServiceEnabled*/ !this.exceededTotalSizeLimitForNonTsFiles(projectFileName, compilerOptions, files, externalFilePropertyReader), options.compileOnSave === undefined ? true : options.compileOnSave); - this.addFilesToNonInferredProjectAndUpdateGraph(project, files, externalFilePropertyReader, /*clientFileName*/ undefined, typeAcquisition); + this.addFilesToNonInferredProjectAndUpdateGraph(project, files, externalFilePropertyReader, typeAcquisition); this.externalProjects.push(project); this.sendProjectTelemetry(project.externalProjectName, project); return project; @@ -1401,14 +1401,14 @@ namespace ts.server { } } - private addFilesToNonInferredProjectAndUpdateGraph(project: ConfiguredProject | ExternalProject, files: T[], propertyReader: FilePropertyReader, clientFileName: string, typeAcquisition: TypeAcquisition): void { - this.updateNonInferredProjectFiles(project, files, propertyReader, clientFileName); + private addFilesToNonInferredProjectAndUpdateGraph(project: ConfiguredProject | ExternalProject, files: T[], propertyReader: FilePropertyReader, typeAcquisition: TypeAcquisition): void { + this.updateNonInferredProjectFiles(project, files, propertyReader); project.setTypeAcquisition(typeAcquisition); // This doesnt need scheduling since its either creation or reload of the project project.updateGraph(); } - private createConfiguredProject(configFileName: NormalizedPath, clientFileName?: string) { + private createConfiguredProject(configFileName: NormalizedPath) { const cachedPartialSystem = createCachedPartialSystem(this.host); const { projectOptions, configFileErrors, configFileSpecs } = this.convertConfigFileContentToProjectOptions(configFileName, cachedPartialSystem); this.logger.info(`Opened configuration file ${configFileName}`); @@ -1438,14 +1438,14 @@ namespace ts.server { } project.setProjectErrors(configFileErrors); - this.addFilesToNonInferredProjectAndUpdateGraph(project, projectOptions.files, fileNamePropertyReader, clientFileName, projectOptions.typeAcquisition); + this.addFilesToNonInferredProjectAndUpdateGraph(project, projectOptions.files, fileNamePropertyReader, projectOptions.typeAcquisition); this.configuredProjects.set(project.canonicalConfigFilePath, project); this.setConfigFileExistenceByNewConfiguredProject(project); this.sendProjectTelemetry(project.getConfigFilePath(), project, projectOptions); return project; } - private updateNonInferredProjectFiles(project: ExternalProject | ConfiguredProject, files: T[], propertyReader: FilePropertyReader, clientFileName?: string) { + private updateNonInferredProjectFiles(project: ExternalProject | ConfiguredProject, files: T[], propertyReader: FilePropertyReader) { const projectRootFilesMap = project.getRootFilesMap(); const newRootScriptInfoMap = createMap(); @@ -1467,7 +1467,7 @@ namespace ts.server { else { const scriptKind = propertyReader.getScriptKind(f); const hasMixedContent = propertyReader.hasMixedContent(f, this.hostConfiguration.extraFileExtensions); - scriptInfo = this.getOrCreateScriptInfoForNormalizedPath(normalizedPath, /*openedByClient*/ clientFileName === newRootFile, /*fileContent*/ undefined, scriptKind, hasMixedContent, project.partialSystem); + scriptInfo = this.getOrCreateScriptInfoNotOpenedByClientForNormalizedPath(normalizedPath, scriptKind, hasMixedContent, project.partialSystem); path = scriptInfo.path; // If this script info is not already a root add it if (!project.isRoot(scriptInfo)) { @@ -1509,7 +1509,7 @@ namespace ts.server { if (compileOnSave !== undefined) { project.compileOnSaveEnabled = compileOnSave; } - this.addFilesToNonInferredProjectAndUpdateGraph(project, newUncheckedFiles, propertyReader, /*clientFileName*/ undefined, newTypeAcquisition); + this.addFilesToNonInferredProjectAndUpdateGraph(project, newUncheckedFiles, propertyReader, newTypeAcquisition); } /** @@ -1617,15 +1617,11 @@ namespace ts.server { return project; } - /** - * @param uncheckedFileName is absolute pathname - * @param fileContent is a known version of the file content that is more up to date than the one on disk - */ /*@internal*/ - getOrCreateScriptInfo(uncheckedFileName: string, openedByClient: boolean, hostToQueryFileExistsOn: PartialSystem) { - return this.getOrCreateScriptInfoForNormalizedPath( - toNormalizedPath(uncheckedFileName), openedByClient, /*fileContent*/ undefined, - /*scriptKind*/ undefined, /*hasMixedContent*/ undefined, hostToQueryFileExistsOn + getOrCreateScriptInfoNotOpenedByClient(uncheckedFileName: string, hostToQueryFileExistsOn: PartialSystem) { + return this.getOrCreateScriptInfoNotOpenedByClientForNormalizedPath( + toNormalizedPath(uncheckedFileName), /*scriptKind*/ undefined, + /*hasMixedContent*/ undefined, hostToQueryFileExistsOn ); } @@ -1654,38 +1650,41 @@ namespace ts.server { } } + getOrCreateScriptInfoNotOpenedByClientForNormalizedPath(fileName: NormalizedPath, scriptKind?: ScriptKind, hasMixedContent?: boolean, hostToQueryFileExistsOn?: PartialSystem) { + return this.getOrCreateScriptInfoForNormalizedPath(fileName, /*openedByClient*/ false, /*fileContent*/ undefined, scriptKind, hasMixedContent, hostToQueryFileExistsOn); + } + + getOrCreateScriptInfoOpenedByClientForNormalizedPath(fileName: NormalizedPath, fileContent?: string, scriptKind?: ScriptKind, hasMixedContent?: boolean, hostToQueryFileExistsOn?: PartialSystem) { + return this.getOrCreateScriptInfoForNormalizedPath(fileName, /*openedByClient*/ true, fileContent, scriptKind, hasMixedContent, hostToQueryFileExistsOn); + } + getOrCreateScriptInfoForNormalizedPath(fileName: NormalizedPath, openedByClient: boolean, fileContent?: string, scriptKind?: ScriptKind, hasMixedContent?: boolean, hostToQueryFileExistsOn?: PartialSystem) { + Debug.assert(fileContent === undefined || openedByClient, "ScriptInfo needs to be opened by client to be able to set its user defined content"); const path = normalizedPathToPath(fileName, this.currentDirectory, this.toCanonicalFileName); let info = this.getScriptInfoForPath(path); if (!info) { - if (openedByClient || (hostToQueryFileExistsOn || this.host).fileExists(fileName)) { - info = new ScriptInfo(this.host, fileName, scriptKind, hasMixedContent, path); - - this.filenameToScriptInfo.set(info.path, info); - - if (openedByClient) { - if (fileContent === undefined) { - // if file is opened by client and its content is not specified - use file text - fileContent = this.host.readFile(fileName) || ""; - } - } - else { - this.watchClosedScriptInfo(info); - } + // If the file is not opened by client and the file doesnot exist on the disk, return + if (!openedByClient && !(hostToQueryFileExistsOn || this.host).fileExists(fileName)) { + return; } - } - if (info) { - if (openedByClient && !info.isScriptOpen()) { - this.stopWatchingScriptInfo(info); - info.open(fileContent); - if (hasMixedContent) { - info.registerFileUpdate(); - } + info = new ScriptInfo(this.host, fileName, scriptKind, hasMixedContent, path); + this.filenameToScriptInfo.set(info.path, info); + if (!openedByClient) { + this.watchClosedScriptInfo(info); } - else if (fileContent !== undefined) { - info.reload(fileContent); + } + if (openedByClient && !info.isScriptOpen()) { + // Opening closed script info + // either it was created just now, or was part of projects but was closed + this.stopWatchingScriptInfo(info); + info.open(fileContent); + if (hasMixedContent) { + info.registerFileUpdate(); } } + else { + Debug.assert(fileContent === undefined); + } return info; } @@ -1730,9 +1729,16 @@ namespace ts.server { /** * This function rebuilds the project for every file opened by the client + * This does not reload contents of open files from disk. But we could do that if needed */ reloadProjects() { this.logger.info("reload projects."); + // If we want this to also reload open files from disk, we could do that, + // but then we need to make sure we arent calling this function + // (and would separate out below reloading of projects to be called when immediate reload is needed) + // as there is no need to load contents of the files from the disk + + // Reload Projects this.reloadConfiguredProjectForFiles(this.openFiles, /*delayReload*/ false); this.refreshInferredProjects(); } @@ -1771,7 +1777,7 @@ namespace ts.server { if (configFileName) { const project = this.findConfiguredProjectByProjectName(configFileName); if (!project) { - this.createConfiguredProject(configFileName, info.fileName); + this.createConfiguredProject(configFileName); updatedProjects.set(configFileName, true); } else if (!updatedProjects.has(configFileName)) { @@ -1862,14 +1868,14 @@ namespace ts.server { let configFileName: NormalizedPath; let configFileErrors: ReadonlyArray; - const info = this.getOrCreateScriptInfoForNormalizedPath(fileName, /*openedByClient*/ true, fileContent, scriptKind, hasMixedContent); + const info = this.getOrCreateScriptInfoOpenedByClientForNormalizedPath(fileName, fileContent, scriptKind, hasMixedContent); let project: ConfiguredProject | ExternalProject = this.findContainingExternalProject(fileName); if (!project) { configFileName = this.getConfigFileNameForFile(info, projectRootPath); if (configFileName) { project = this.findConfiguredProjectByProjectName(configFileName); if (!project) { - project = this.createConfiguredProject(configFileName, fileName); + project = this.createConfiguredProject(configFileName); // even if opening config file was successful, it could still // contain errors that were tolerated. diff --git a/src/server/project.ts b/src/server/project.ts index bf73d4d175c65..1f6b762b6749f 100644 --- a/src/server/project.ts +++ b/src/server/project.ts @@ -263,7 +263,7 @@ namespace ts.server { } private getScriptInfoLSHost(fileName: string) { - const scriptInfo = this.projectService.getOrCreateScriptInfo(fileName, /*openedByClient*/ false, this.partialSystem); + const scriptInfo = this.projectService.getOrCreateScriptInfoNotOpenedByClient(fileName, this.partialSystem); if (scriptInfo) { const existingValue = this.rootFilesMap.get(scriptInfo.path); if (existingValue !== undefined && existingValue !== scriptInfo) { @@ -804,7 +804,7 @@ namespace ts.server { // by the LSHost for files in the program when the program is retrieved above but // the program doesn't contain external files so this must be done explicitly. inserted => { - const scriptInfo = this.projectService.getOrCreateScriptInfo(inserted, /*openedByClient*/ false, this.partialSystem); + const scriptInfo = this.projectService.getOrCreateScriptInfoNotOpenedByClient(inserted, this.partialSystem); scriptInfo.attachToProject(this); }, removed => { @@ -845,9 +845,8 @@ namespace ts.server { } getScriptInfoForNormalizedPath(fileName: NormalizedPath) { - const scriptInfo = this.projectService.getOrCreateScriptInfoForNormalizedPath( - fileName, /*openedByClient*/ false, /*fileContent*/ undefined, - /*scriptKind*/ undefined, /*hasMixedContent*/ undefined, this.partialSystem + const scriptInfo = this.projectService.getOrCreateScriptInfoNotOpenedByClientForNormalizedPath( + fileName, /*scriptKind*/ undefined, /*hasMixedContent*/ undefined, this.partialSystem ); if (scriptInfo && !scriptInfo.isAttached(this)) { return Errors.ThrowProjectDoesNotContainDocument(fileName, this); diff --git a/src/server/scriptInfo.ts b/src/server/scriptInfo.ts index d76f930f5ec24..bc3e11baa0e45 100644 --- a/src/server/scriptInfo.ts +++ b/src/server/scriptInfo.ts @@ -4,13 +4,19 @@ namespace ts.server { /* @internal */ export class TextStorage { + // Generated only on demand and removes the text if any edits private svc: ScriptVersionCache | undefined; private svcVersion = 0; + // Store text when there is no svc or svc has no change, on edit, remove the text private text: string; private lineMap: number[]; private textVersion = 0; + public isOpen: boolean; + private ownFileText: boolean; + private pendingReloadFromDisk: boolean; + constructor(private readonly host: ServerHost, private readonly fileName: NormalizedPath) { } @@ -20,43 +26,75 @@ namespace ts.server { : `Text-${this.textVersion}`; } - public hasScriptVersionCache() { + public hasScriptVersionCache_TestOnly() { return this.svc !== undefined; } - public useScriptVersionCache(newText?: string) { - this.switchToScriptVersionCache(newText); + public useScriptVersionCache_TestOnly() { + this.switchToScriptVersionCache(); } public useText(newText?: string) { this.svc = undefined; - this.setText(newText); + this.text = newText; + this.lineMap = undefined; + this.textVersion++; } public edit(start: number, end: number, newText: string) { this.switchToScriptVersionCache().edit(start, end - start, newText); + this.ownFileText = false; + this.text = undefined; + this.lineMap = undefined; + } + + /** returns true if text changed */ + public reload(newText: string) { + Debug.assert(newText !== undefined); + + // Reload always has fresh content + this.pendingReloadFromDisk = false; + + // If text changed set the text + // This also ensures that if we had switched to version cache, + // we are switching back to text. + // The change to version cache will happen when needed + // Thus avoiding the computation if there are no changes + if (this.text !== newText) { + this.useText(newText); + // We cant guarantee new text is own file text + this.ownFileText = false; + return true; + } } - public reload(text: string) { - if (this.svc) { - this.svc.reload(text); - } - else { - this.setText(text); + /** returns true if text changed */ + public reloadFromDisk() { + let reloaded = false; + if (!this.pendingReloadFromDisk && !this.ownFileText) { + reloaded = this.reload(this.getFileText()); + this.ownFileText = true; } + return reloaded; } - public reloadFromFile(tempFileName?: string) { - if (this.svc || (tempFileName !== this.fileName)) { - this.reload(this.getFileText(tempFileName)); - } - else { - this.setText(undefined); + public delayReloadFromFileIntoText() { + this.pendingReloadFromDisk = true; + } + + /** returns true if text changed */ + public reloadFromFile(tempFileName: string) { + let reloaded = false; + // Reload if different file or we dont know if we are working with own file text + if (tempFileName !== this.fileName || !this.ownFileText) { + reloaded = this.reload(this.getFileText(tempFileName)); + this.ownFileText = !tempFileName || tempFileName === this.fileName; } + return reloaded; } public getSnapshot(): IScriptSnapshot { - return this.svc + return this.useScriptVersionCacheIfValidOrOpen() ? this.svc.getSnapshot() : ScriptSnapshot.fromString(this.getOrLoadText()); } @@ -68,7 +106,7 @@ namespace ts.server { * @param line 0 based index */ lineToTextSpan(line: number): TextSpan { - if (!this.svc) { + if (!this.useScriptVersionCacheIfValidOrOpen()) { const lineMap = this.getLineMap(); const start = lineMap[line]; // -1 since line is 1-based const end = line + 1 < lineMap.length ? lineMap[line + 1] : this.text.length; @@ -82,7 +120,7 @@ namespace ts.server { * @param offset 1 based index */ lineOffsetToPosition(line: number, offset: number): number { - if (!this.svc) { + if (!this.useScriptVersionCacheIfValidOrOpen()) { return computePositionOfLineAndCharacter(this.getLineMap(), line - 1, offset - 1, this.text); } @@ -91,7 +129,7 @@ namespace ts.server { } positionToLineOffset(position: number): protocol.Location { - if (!this.svc) { + if (!this.useScriptVersionCacheIfValidOrOpen()) { const { line, character } = computeLineAndCharacterOfPosition(this.getLineMap(), position); return { line: line + 1, offset: character + 1 }; } @@ -102,43 +140,39 @@ namespace ts.server { return this.host.readFile(tempFileName || this.fileName) || ""; } - private ensureNoScriptVersionCache() { - Debug.assert(!this.svc, "ScriptVersionCache should not be set"); - } - - private switchToScriptVersionCache(newText?: string): ScriptVersionCache { - if (!this.svc) { - this.svc = ScriptVersionCache.fromString(newText !== undefined ? newText : this.getOrLoadText()); + private switchToScriptVersionCache(): ScriptVersionCache { + if (!this.svc || this.pendingReloadFromDisk) { + this.svc = ScriptVersionCache.fromString(this.getOrLoadText()); this.svcVersion++; - this.text = undefined; } return this.svc; } + private useScriptVersionCacheIfValidOrOpen(): ScriptVersionCache | undefined { + // If this is open script, use the cache + if (this.isOpen) { + return this.switchToScriptVersionCache(); + } + + // Else if the svc is uptodate with the text, we are good + return !this.pendingReloadFromDisk && this.svc; + } + private getOrLoadText() { - this.ensureNoScriptVersionCache(); - if (this.text === undefined) { - this.setText(this.getFileText()); + if (this.text === undefined || this.pendingReloadFromDisk) { + Debug.assert(!this.svc || this.pendingReloadFromDisk, "ScriptVersionCache should not be set when reloading from disk"); + this.reload(this.getFileText()); + this.ownFileText = true; } return this.text; } private getLineMap() { - this.ensureNoScriptVersionCache(); + Debug.assert(!this.svc, "ScriptVersionCache should not be set"); return this.lineMap || (this.lineMap = computeLineStarts(this.getOrLoadText())); } - - private setText(newText: string) { - this.ensureNoScriptVersionCache(); - if (newText === undefined || this.text !== newText) { - this.text = newText; - this.lineMap = undefined; - this.textVersion++; - } - } } - export class ScriptInfo { /** * All projects that include this file @@ -150,8 +184,6 @@ namespace ts.server { fileWatcher: FileWatcher; private textStorage: TextStorage; - private isOpen: boolean; - constructor( private readonly host: ServerHost, readonly fileName: NormalizedPath, @@ -169,19 +201,28 @@ namespace ts.server { } public isScriptOpen() { - return this.isOpen; + return this.textStorage.isOpen; } public open(newText: string) { - this.isOpen = true; - this.textStorage.useScriptVersionCache(newText); - this.markContainingProjectsAsDirty(); + this.textStorage.isOpen = true; + if (newText !== undefined && + this.textStorage.reload(newText)) { + // reload new contents only if the existing contents changed + this.markContainingProjectsAsDirty(); + } } public close() { - this.isOpen = false; - this.textStorage.useText(this.hasMixedContent ? "" : undefined); - this.markContainingProjectsAsDirty(); + this.textStorage.isOpen = false; + if (this.hasMixedContent) { + if (this.textStorage.reload("")) { + this.markContainingProjectsAsDirty(); + } + } + else if (this.textStorage.reloadFromDisk()) { + this.markContainingProjectsAsDirty(); + } } public getSnapshot() { @@ -293,26 +334,31 @@ namespace ts.server { return this.textStorage.getVersion(); } - reload(script: string) { - this.textStorage.reload(script); - this.markContainingProjectsAsDirty(); - } - saveTo(fileName: string) { const snap = this.textStorage.getSnapshot(); this.host.writeFile(fileName, snap.getText(0, snap.getLength())); } + /*@internal*/ + delayReloadNonMixedContentFile() { + Debug.assert(!this.hasMixedContent); + this.textStorage.delayReloadFromFileIntoText(); + this.markContainingProjectsAsDirty(); + } + reloadFromFile(tempFileName?: NormalizedPath) { if (this.hasMixedContent) { - this.reload(""); + this.textStorage.reload(""); + this.markContainingProjectsAsDirty(); } else { - this.textStorage.reloadFromFile(tempFileName); - this.markContainingProjectsAsDirty(); + if (this.textStorage.reloadFromFile(tempFileName)) { + this.markContainingProjectsAsDirty(); + } } } + /*@internal*/ getLineInfo(line: number): AbsolutePositionAndLineText { return this.textStorage.getLineInfo(line); } diff --git a/src/server/scriptVersionCache.ts b/src/server/scriptVersionCache.ts index 451536a0caaaf..7c25e4cc3cb79 100644 --- a/src/server/scriptVersionCache.ts +++ b/src/server/scriptVersionCache.ts @@ -2,6 +2,7 @@ /// /// +/*@internal*/ namespace ts.server { const lineCollectionCapacity = 4; @@ -285,24 +286,6 @@ namespace ts.server { } } - // reload whole script, leaving no change history behind reload - reload(script: string) { - this.currentVersion++; - this.changes = []; // history wiped out by reload - const snap = new LineIndexSnapshot(this.currentVersion, this, new LineIndex()); - - // delete all versions - for (let i = 0; i < this.versions.length; i++) { - this.versions[i] = undefined; - } - - this.versions[this.currentVersionToIndex()] = snap; - const lm = LineIndex.linesFromText(script); - snap.index.load(lm.lines); - - this.minVersion = this.currentVersion; - } - getSnapshot(): IScriptSnapshot { return this._getSnapshot(); } private _getSnapshot(): LineIndexSnapshot { @@ -843,4 +826,4 @@ namespace ts.server { return 1; } } -} \ No newline at end of file +} From 254e39306f771430539fbe94dfe8f11c30e024cd Mon Sep 17 00:00:00 2001 From: Sheetal Nandi Date: Mon, 28 Aug 2017 12:34:58 -0700 Subject: [PATCH 080/109] Watch failed lookups recursively to reduce number of directory watches Also we dont need to watch type roots any more --- src/compiler/core.ts | 2 + src/compiler/resolutionCache.ts | 87 +++++++++++++++++-- src/compiler/watchedProgram.ts | 9 +- .../unittests/tsserverProjectSystem.ts | 71 ++++----------- src/harness/virtualFileSystemWithWatch.ts | 2 +- src/server/editorServices.ts | 21 ----- src/server/project.ts | 38 +++----- 7 files changed, 115 insertions(+), 115 deletions(-) diff --git a/src/compiler/core.ts b/src/compiler/core.ts index bb4cf903a981f..9c761b47fa05f 100644 --- a/src/compiler/core.ts +++ b/src/compiler/core.ts @@ -1809,6 +1809,8 @@ namespace ts { * Removes a trailing directory separator from a path. * @param path The path. */ + export function removeTrailingDirectorySeparator(path: Path): Path; + export function removeTrailingDirectorySeparator(path: string): string; export function removeTrailingDirectorySeparator(path: string) { if (path.charAt(path.length - 1) === directorySeparator) { return path.substr(0, path.length - 1); diff --git a/src/compiler/resolutionCache.ts b/src/compiler/resolutionCache.ts index d1bd285019f92..ca82a68f489c6 100644 --- a/src/compiler/resolutionCache.ts +++ b/src/compiler/resolutionCache.ts @@ -14,6 +14,8 @@ namespace ts { invalidateResolutionOfFile(filePath: Path): void; createHasInvalidatedResolution(): HasInvalidatedResolution; + setRootDirectory(dir: string): void; + clear(): void; } @@ -32,13 +34,15 @@ namespace ts { export interface ResolutionCacheHost extends ModuleResolutionHost { toPath(fileName: string): Path; getCompilationSettings(): CompilerOptions; - watchDirectoryOfFailedLookupLocation(directory: string, cb: DirectoryWatcherCallback): FileWatcher; + watchDirectoryOfFailedLookupLocation(directory: string, cb: DirectoryWatcherCallback, flags: WatchDirectoryFlags): FileWatcher; onInvalidatedResolution(): void; getCachedPartialSystem?(): CachedPartialSystem; projectName?: string; getGlobalCache?(): string | undefined; } + const MAX_DIRPATHS_TO_RECURSE = 5; + export function createResolutionCache(resolutionHost: ResolutionCacheHost): ResolutionCache { let filesWithChangedSetOfUnresolvedImports: Path[] | undefined; let filesWithInvalidatedResolutions: Map | undefined; @@ -50,6 +54,9 @@ namespace ts { const resolvedTypeReferenceDirectives = createMap>(); const directoryWatchesOfFailedLookups = createMap(); + const failedLookupLocationToDirPath = createMap(); + let rootDir: string; + let rootPath: Path; return { startRecordingFilesWithChangedResolutions, finishRecordingFilesWithChangedResolutions, @@ -57,12 +64,27 @@ namespace ts { resolveTypeReferenceDirectives, invalidateResolutionOfFile, createHasInvalidatedResolution, + setRootDirectory, clear }; + function setRootDirectory(dir: string) { + Debug.assert(!resolvedModuleNames.size && !resolvedTypeReferenceDirectives.size && !directoryWatchesOfFailedLookups.size); + rootDir = removeTrailingDirectorySeparator(getNormalizedAbsolutePath(dir, resolutionHost.getCurrentDirectory())); + rootPath = resolutionHost.toPath(rootDir); + } + + function isInDirectoryPath(dir: Path, file: Path) { + if (dir === undefined || file.length <= dir.length) { + return false; + } + return startsWith(file, dir) && file[dir.length] === directorySeparator; + } + function clear() { // Close all the watches for failed lookup locations, irrespective of refcounts for them since this is to clear the cache clearMap(directoryWatchesOfFailedLookups, closeFileWatcherOf); + failedLookupLocationToDirPath.clear(); resolvedModuleNames.clear(); resolvedTypeReferenceDirectives.clear(); } @@ -196,16 +218,58 @@ namespace ts { } function watchFailedLookupLocation(failedLookupLocation: string, failedLookupLocationPath: Path) { - const dirPath = getDirectoryPath(failedLookupLocationPath); + const cachedDir = failedLookupLocationToDirPath.get(failedLookupLocationPath); + if (cachedDir) { + watchFailedLookupLocationInDirectory(cachedDir, failedLookupLocation, failedLookupLocationPath, /*dir*/ undefined); + return; + } + + if (isInDirectoryPath(rootPath, failedLookupLocationPath)) { + // Watch in directory of rootPath + watchFailedLookupLocationInDirectory(rootPath, failedLookupLocation, failedLookupLocationPath, rootDir); + return; + } + + let dirPath = getDirectoryPath(failedLookupLocationPath); + let dir = getDirectoryPath(getNormalizedAbsolutePath(failedLookupLocation, resolutionHost.getCurrentDirectory())); + for (let i = 0; i < MAX_DIRPATHS_TO_RECURSE; i++) { + const parentPath = getDirectoryPath(dirPath); + if (directoryWatchesOfFailedLookups.has(dirPath) || parentPath === dirPath) { + watchFailedLookupLocationInDirectory(dirPath, failedLookupLocation, failedLookupLocationPath, dir); + return; + } + dirPath = parentPath; + dir = getDirectoryPath(dir); + } + + // Verify there are no watches in parent directory + const ancestorDirPath = getAncestorDirectoryWithWatches(dirPath); + // We wont need directory if we are using ancestor since its already cached + watchFailedLookupLocationInDirectory(ancestorDirPath || dirPath, failedLookupLocation, failedLookupLocationPath, dir); + } + + function getAncestorDirectoryWithWatches(dirPath: Path) { + for (let parentDirPath = getDirectoryPath(dirPath); parentDirPath !== dirPath; parentDirPath = getDirectoryPath(parentDirPath)) { + if (directoryWatchesOfFailedLookups.has(parentDirPath)) { + return parentDirPath; + } + dirPath = parentDirPath; + } + return undefined; + } + + function watchFailedLookupLocationInDirectory(dirPath: Path, failedLookupLocation: string, failedLookupLocationPath: Path, dir: string | undefined) { + failedLookupLocationToDirPath.set(failedLookupLocationPath, dirPath); const watches = directoryWatchesOfFailedLookups.get(dirPath); if (watches) { watches.mapLocations.add(failedLookupLocationPath, failedLookupLocation); } else { + Debug.assert(dir !== undefined); const mapLocations = createMultiMap(); mapLocations.add(failedLookupLocationPath, failedLookupLocation); directoryWatchesOfFailedLookups.set(dirPath, { - watcher: createDirectoryWatcher(getDirectoryPath(failedLookupLocation), dirPath), + watcher: createDirectoryWatcher(dir, dirPath), mapLocations }); } @@ -224,16 +288,22 @@ namespace ts { onAddOrRemoveDirectoryOfFailedLookup(dirPath); resolutionHost.onInvalidatedResolution(); } - else if (onFileAddOrRemoveInDirectoryOfFailedLookup(fileOrFolderPath)) { + else if (onFileAddOrRemoveInDirectoryOfFailedLookup(dirPath, fileOrFolderPath)) { resolutionHost.onInvalidatedResolution(); } - }); + }, WatchDirectoryFlags.Recursive); } function closeFailedLookupLocationWatcher(failedLookupLocation: string, failedLookupLocationPath: Path) { - const dirPath = getDirectoryPath(failedLookupLocationPath); + const dirPath = failedLookupLocationToDirPath.get(failedLookupLocationPath); const watches = directoryWatchesOfFailedLookups.get(dirPath); watches.mapLocations.remove(failedLookupLocationPath, failedLookupLocation); + // If this was last failed lookup location being tracked by the dir watcher, + // remove the failed lookup location path to dir Path entry + if (!watches.mapLocations.has(failedLookupLocationPath)) { + failedLookupLocationToDirPath.delete(failedLookupLocationPath); + } + // If there are no more files that need this watcher alive, close the watcher if (watches.mapLocations.size === 0) { watches.watcher.close(); directoryWatchesOfFailedLookups.delete(dirPath); @@ -311,8 +381,7 @@ namespace ts { invalidateResolutionCacheOfDeletedFile(filePath, resolvedTypeReferenceDirectives, m => m.resolvedTypeReferenceDirective, r => r.resolvedFileName); } - function onFileAddOrRemoveInDirectoryOfFailedLookup(fileOrFolder: Path) { - const dirPath = getDirectoryPath(fileOrFolder); + function onFileAddOrRemoveInDirectoryOfFailedLookup(dirPath: Path, fileOrFolder: Path) { const watches = directoryWatchesOfFailedLookups.get(dirPath); const isFailedLookupFile = watches.mapLocations.has(fileOrFolder); if (isFailedLookupFile) { @@ -324,7 +393,7 @@ namespace ts { } function onAddOrRemoveDirectoryOfFailedLookup(dirPath: Path) { - const isInDirPath: (location: string) => boolean = location => getDirectoryPath(resolutionHost.toPath(location)) === dirPath; + const isInDirPath: (location: string) => boolean = location => isInDirectoryPath(dirPath, resolutionHost.toPath(location)); invalidateResolutionCacheOfChangedFailedLookupLocation(resolvedModuleNames, isInDirPath); invalidateResolutionCacheOfChangedFailedLookupLocation(resolvedTypeReferenceDirectives, isInDirPath); } diff --git a/src/compiler/watchedProgram.ts b/src/compiler/watchedProgram.ts index 9058588ade11f..07a783c3c473b 100644 --- a/src/compiler/watchedProgram.ts +++ b/src/compiler/watchedProgram.ts @@ -297,7 +297,10 @@ namespace ts { }; // Cache for the module resolution const resolutionCache = createResolutionCache(compilerHost); - + resolutionCache.setRootDirectory(configFileName ? + getDirectoryPath(getNormalizedAbsolutePath(configFileName, getCurrentDirectory())) : + getCurrentDirectory() + ); // There is no extra check needed since we can just rely on the program to decide emit const builder = createBuilder(getCanonicalFileName, getFileEmitOutput, computeHash, _sourceFile => true); @@ -576,8 +579,8 @@ namespace ts { } } - function watchDirectoryOfFailedLookupLocation(directory: string, cb: DirectoryWatcherCallback) { - return watchDirectory(system, directory, cb, WatchDirectoryFlags.None, writeLog); + function watchDirectoryOfFailedLookupLocation(directory: string, cb: DirectoryWatcherCallback, flags: WatchDirectoryFlags) { + return watchDirectory(system, directory, cb, flags, writeLog); } function watchMissingFilePath(missingFilePath: Path) { diff --git a/src/harness/unittests/tsserverProjectSystem.ts b/src/harness/unittests/tsserverProjectSystem.ts index 9068afa98cfc6..f5cd1766c39ba 100644 --- a/src/harness/unittests/tsserverProjectSystem.ts +++ b/src/harness/unittests/tsserverProjectSystem.ts @@ -321,34 +321,6 @@ namespace ts.projectSystem { verifyDiagnostics(actual, []); } - function getPathsForTypesOrModules(base: string, rootPaths: string[], typesOrModules: string[], map: Map, rootDir?: string) { - while (1) { - forEach(rootPaths, r => { - const rp = combinePaths(base, r); - forEach(typesOrModules, tm => { - map.set(tm === "" ? rp : combinePaths(rp, tm), true); - }); - }); - const parentDir = getDirectoryPath(base); - if (base === rootDir || parentDir === base) { - break; - } - base = parentDir; - } - return map; - } - - function getNodeModulesWatchedDirectories(path: string, modules: string[], map = createMap()) { - forEach(modules, module => { - getPathsForTypesOrModules(path, ["node_modules"], ["", module, "@types", `@types/${module}`], map); - }); - return map; - } - - function getTypesWatchedDirectories(path: string, typeRoots: string[], types: string[], map = createMap()) { - return getPathsForTypesOrModules(path, typeRoots, types.concat(""), map, path); - } - describe("tsserverProjectSystem", () => { const commonFile1: FileOrFolder = { path: "/a/b/commonFile1.ts", @@ -386,8 +358,8 @@ namespace ts.projectSystem { const configFileLocations = ["/a/b/c/", "/a/b/", "/a/", "/"]; const configFiles = flatMap(configFileLocations, location => [location + "tsconfig.json", location + "jsconfig.json"]); checkWatchedFiles(host, configFiles.concat(libFile.path, moduleFile.path)); - checkWatchedDirectories(host, ["/a/b/c"], /*recursive*/ false); - checkWatchedDirectories(host, [], /*recursive*/ true); + checkWatchedDirectories(host, [], /*recursive*/ false); + checkWatchedDirectories(host, ["/"], /*recursive*/ true); }); it("can handle tsconfig file name with difference casing", () => { @@ -4040,7 +4012,7 @@ namespace ts.projectSystem { const calledMaps = getCallsTrackingMap(); return { verifyNoCall, - verifyCalledOnEachEntryOnce, + verifyCalledOnEachEntryNTimes, verifyCalledOnEachEntry, verifyNoHostCalls, verifyNoHostCallsExceptFileExistsOnce, @@ -4090,8 +4062,8 @@ namespace ts.projectSystem { }); } - function verifyCalledOnEachEntryOnce(callback: keyof CalledMaps, expectedKeys: string[]) { - return verifyCalledOnEachEntry(callback, zipToMap(expectedKeys, expectedKeys.map(() => 1))); + function verifyCalledOnEachEntryNTimes(callback: keyof CalledMaps, expectedKeys: string[], nTimes: number) { + return verifyCalledOnEachEntry(callback, zipToMap(expectedKeys, expectedKeys.map(() => nTimes))); } function verifyNoHostCalls() { @@ -4101,7 +4073,7 @@ namespace ts.projectSystem { } function verifyNoHostCallsExceptFileExistsOnce(expectedKeys: string[]) { - verifyCalledOnEachEntryOnce("fileExists", expectedKeys); + verifyCalledOnEachEntryNTimes("fileExists", expectedKeys, 1); verifyNoCall("directoryExists"); verifyNoCall("getDirectories"); verifyNoCall("readFile"); @@ -4253,17 +4225,7 @@ namespace ts.projectSystem { const { configFileName } = projectService.openClientFile(file1.path); assert.equal(configFileName, tsconfigFile.path, `should find config`); checkNumberOfConfiguredProjects(projectService, 1); - const watchedModuleDirectories = arrayFrom( - getNodeModulesWatchedDirectories( - canonicalFrontendDir, - types, - getTypesWatchedDirectories( - canonicalFrontendDir, - typeRoots, - types - ) - ).keys() - ); + const watchingRecursiveDirectories = [`${canonicalFrontendDir}/src`, canonicalFrontendDir, "/"]; const project = projectService.configuredProjects.get(canonicalConfigPath); verifyProjectAndWatchedDirectories(); @@ -4276,7 +4238,7 @@ namespace ts.projectSystem { host.runQueuedTimeoutCallbacks(); const canonicalFile3Path = useCaseSensitiveFileNames ? file3.path : file3.path.toLocaleLowerCase(); - callsTrackingHost.verifyCalledOnEachEntryOnce("fileExists", [canonicalFile3Path]); + callsTrackingHost.verifyCalledOnEachEntryNTimes("fileExists", [canonicalFile3Path], watchingRecursiveDirectories.length); // Called for type root resolution const directoryExistsCalled = createMap(); @@ -4286,11 +4248,11 @@ namespace ts.projectSystem { directoryExistsCalled.set(`/node_modules`, 2); directoryExistsCalled.set(`${frontendDir}/types`, 2); directoryExistsCalled.set(`${frontendDir}/node_modules/@types`, 2); - directoryExistsCalled.set(canonicalFile3Path, 1); + directoryExistsCalled.set(canonicalFile3Path, watchingRecursiveDirectories.length); callsTrackingHost.verifyCalledOnEachEntry("directoryExists", directoryExistsCalled); callsTrackingHost.verifyNoCall("getDirectories"); - callsTrackingHost.verifyCalledOnEachEntryOnce("readFile", [file3.path]); + callsTrackingHost.verifyCalledOnEachEntryNTimes("readFile", [file3.path], 1); callsTrackingHost.verifyNoCall("readDirectory"); checkNumberOfConfiguredProjects(projectService, 1); @@ -4316,8 +4278,8 @@ namespace ts.projectSystem { function verifyProjectAndWatchedDirectories() { checkProjectActualFiles(project, map(projectFiles, f => f.path)); checkWatchedFiles(host, mapDefined(projectFiles, getFilePathIfOpen)); - checkWatchedDirectories(host, [`${canonicalFrontendDir}/src`], /*recursive*/ true); - checkWatchedDirectories(host, watchedModuleDirectories, /*recursive*/ false); + checkWatchedDirectories(host, watchingRecursiveDirectories, /*recursive*/ true); + checkWatchedDirectories(host, [], /*recursive*/ false); } } @@ -4372,7 +4334,7 @@ namespace ts.projectSystem { const projectService = createProjectService(host); const { configFileName } = projectService.openClientFile(app.path); assert.equal(configFileName, tsconfigJson.path, `should find config`); - const watchedModuleLocations = arrayFrom(getNodeModulesWatchedDirectories(appFolder, ["lodash"]).keys()); + const recursiveWatchedDirectories: string[] = [appFolder, "/"]; verifyProject(); let timeoutAfterReloadFs = timeoutDuringPartialInstallation; @@ -4450,7 +4412,8 @@ namespace ts.projectSystem { const lodashIndexPath = "/a/b/node_modules/@types/lodash/index.d.ts"; projectFiles.push(find(filesAndFoldersToAdd, f => f.path === lodashIndexPath)); - watchedModuleLocations.length = indexOf(watchedModuleLocations, getDirectoryPath(lodashIndexPath)); + // we would now not have failed lookup in the parent of appFolder since lodash is available + recursiveWatchedDirectories.length = 1; // npm installation complete, timeout after reload fs timeoutAfterReloadFs = true; verifyAfterPartialOrCompleteNpmInstall(2); @@ -4475,8 +4438,8 @@ namespace ts.projectSystem { const filesWatched = filter(projectFilePaths, p => p !== app.path); checkWatchedFiles(host, filesWatched); - checkWatchedDirectories(host, [appFolder], /*recursive*/ true); - checkWatchedDirectories(host, watchedModuleLocations, /*recursive*/ false); + checkWatchedDirectories(host, recursiveWatchedDirectories, /*recursive*/ true); + checkWatchedDirectories(host, [], /*recursive*/ false); } } diff --git a/src/harness/virtualFileSystemWithWatch.ts b/src/harness/virtualFileSystemWithWatch.ts index 4b249c78e152b..724326d3eb63a 100644 --- a/src/harness/virtualFileSystemWithWatch.ts +++ b/src/harness/virtualFileSystemWithWatch.ts @@ -141,7 +141,7 @@ namespace ts.TestFSWithWatch { } export function checkWatchedDirectories(host: TestServerHost, expectedDirectories: string[], recursive = false) { - checkMapKeys("watchedDirectories", recursive ? host.watchedDirectoriesRecursive : host.watchedDirectories, expectedDirectories); + checkMapKeys(`watchedDirectories${recursive ? " recursive" : ""}`, recursive ? host.watchedDirectoriesRecursive : host.watchedDirectories, expectedDirectories); } export function checkOutputContains(host: TestServerHost, expected: ReadonlyArray) { diff --git a/src/server/editorServices.ts b/src/server/editorServices.ts index 9195083bb6882..e6976008c8362 100644 --- a/src/server/editorServices.ts +++ b/src/server/editorServices.ts @@ -244,7 +244,6 @@ namespace ts.server { ConfigFilePath = "Config file for the program", MissingFilePath = "Missing file from program", WildcardDirectories = "Wild card directory", - TypeRoot = "Type root of the project", ClosedScriptInfo = "Closed Script info", ConfigFileForInferredRoot = "Config file for the inferred project root", FailedLookupLocation = "Directory of Failed lookup locations in module resolution" @@ -711,23 +710,6 @@ namespace ts.server { } } - /*@internal*/ - watchTypeRootDirectory(root: Path, project: ConfiguredProject) { - // TODO: This is not needed anymore with watches for failed lookup locations? - return this.watchDirectory( - this.host, - root, - fileOrFolder => { - project.getCachedPartialSystem().addOrDeleteFileOrFolder(fileOrFolder, this.toPath(fileOrFolder)); - project.updateTypes(); - this.delayUpdateProjectGraphAndInferredProjectsRefresh(project); - }, - WatchDirectoryFlags.None, - WatchType.TypeRoot, - project - ); - } - /** * This is to watch whenever files are added or removed to the wildcard directories */ @@ -1434,7 +1416,6 @@ namespace ts.server { ); if (languageServiceEnabled) { project.watchWildcards(projectOptions.wildcardDirectories); - project.watchTypeRoots(); } project.setProjectErrors(configFileErrors); @@ -1534,12 +1515,10 @@ namespace ts.server { if (this.exceededTotalSizeLimitForNonTsFiles(project.canonicalConfigFilePath, projectOptions.compilerOptions, projectOptions.files, fileNamePropertyReader)) { project.disableLanguageService(); project.stopWatchingWildCards(); - project.stopWatchingTypeRoots(); } else { project.enableLanguageService(); project.watchWildcards(projectOptions.wildcardDirectories); - project.watchTypeRoots(); } this.updateNonInferredProject(project, projectOptions.files, fileNamePropertyReader, projectOptions.compilerOptions, projectOptions.typeAcquisition, projectOptions.compileOnSave); diff --git a/src/server/project.ts b/src/server/project.ts index 1f6b762b6749f..8d391c312b19e 100644 --- a/src/server/project.ts +++ b/src/server/project.ts @@ -351,12 +351,12 @@ namespace ts.server { } /*@internal*/ - watchDirectoryOfFailedLookupLocation(directory: string, cb: DirectoryWatcherCallback) { + watchDirectoryOfFailedLookupLocation(directory: string, cb: DirectoryWatcherCallback, flags: WatchDirectoryFlags) { return this.projectService.watchDirectory( this.projectService.host, directory, cb, - WatchDirectoryFlags.None, + flags, WatchType.FailedLookupLocation, this ); @@ -1012,6 +1012,13 @@ namespace ts.server { this.toggleJsInferredProject(/*isJsInferredProject*/ true); } super.addRoot(info); + // For first root set the resolution cache root dir + if (this.getRootFiles.length === 1) { + const rootDirPath = this.getProjectRootPath(); + if (rootDirPath) { + this.resolutionCache.setRootDirectory(rootDirPath); + } + } } removeRoot(info: ScriptInfo) { @@ -1064,7 +1071,6 @@ namespace ts.server { /* @internal */ configFileWatcher: FileWatcher; private directoriesWatchedForWildcards: Map | undefined; - private typeRootsWatchers: Map | undefined; readonly canonicalConfigFilePath: NormalizedPath; /* @internal */ @@ -1091,6 +1097,7 @@ namespace ts.server { super(configFileName, ProjectKind.Configured, projectService, documentRegistry, hasExplicitListOfFiles, languageServiceEnabled, compilerOptions, compileOnSaveEnabled, cachedPartialSystem); this.canonicalConfigFilePath = asNormalizedPath(projectService.toCanonicalFileName(configFileName)); this.enablePlugins(); + this.resolutionCache.setRootDirectory(getDirectoryPath(configFileName)); } /** @@ -1252,29 +1259,6 @@ namespace ts.server { } } - /*@internal*/ - watchTypeRoots() { - const newTypeRoots = arrayToSet(this.getEffectiveTypeRoots(), dir => this.projectService.toCanonicalFileName(dir)); - mutateMap( - this.typeRootsWatchers || (this.typeRootsWatchers = createMap()), - newTypeRoots, - { - // Create new watch - createNewValue: root => this.projectService.watchTypeRootDirectory(root as Path, this), - // Close existing watch thats not needed any more - onDeleteValue: closeFileWatcher - } - ); - } - - /*@internal*/ - stopWatchingTypeRoots() { - if (this.typeRootsWatchers) { - clearMap(this.typeRootsWatchers, closeFileWatcher); - this.typeRootsWatchers = undefined; - } - } - close() { super.close(); @@ -1283,7 +1267,6 @@ namespace ts.server { this.configFileWatcher = undefined; } - this.stopWatchingTypeRoots(); this.stopWatchingWildCards(); } @@ -1325,6 +1308,7 @@ namespace ts.server { public compileOnSaveEnabled: boolean, private readonly projectFilePath?: string) { super(externalProjectName, ProjectKind.External, projectService, documentRegistry, /*hasExplicitListOfFiles*/ true, languageServiceEnabled, compilerOptions, compileOnSaveEnabled, projectService.host); + this.resolutionCache.setRootDirectory(this.getProjectRootPath()); } getProjectRootPath() { From a3b9467d4159ac0d3ff833149ad1517e249598c8 Mon Sep 17 00:00:00 2001 From: Sheetal Nandi Date: Mon, 28 Aug 2017 17:09:07 -0700 Subject: [PATCH 081/109] Resolve only once in the given directory for name --- src/compiler/resolutionCache.ts | 104 +++++++++++++++++++++++++------- src/compiler/watchedProgram.ts | 5 +- src/server/project.ts | 12 ++++ 3 files changed, 97 insertions(+), 24 deletions(-) diff --git a/src/compiler/resolutionCache.ts b/src/compiler/resolutionCache.ts index ca82a68f489c6..7dff3f5515505 100644 --- a/src/compiler/resolutionCache.ts +++ b/src/compiler/resolutionCache.ts @@ -14,6 +14,9 @@ namespace ts { invalidateResolutionOfFile(filePath: Path): void; createHasInvalidatedResolution(): HasInvalidatedResolution; + startCachingPerDirectoryResolution(): void; + finishCachingPerDirectoryResolution(): void; + setRootDirectory(dir: string): void; clear(): void; @@ -28,7 +31,7 @@ namespace ts { /** watcher for the directory of failed lookup */ watcher: FileWatcher; /** map with key being the failed lookup location path and value being the actual location */ - mapLocations: MultiMap; + mapLocations: Map; } export interface ResolutionCacheHost extends ModuleResolutionHost { @@ -39,6 +42,7 @@ namespace ts { getCachedPartialSystem?(): CachedPartialSystem; projectName?: string; getGlobalCache?(): string | undefined; + writeLog(s: string): void; } const MAX_DIRPATHS_TO_RECURSE = 5; @@ -51,7 +55,9 @@ namespace ts { // The key in the map is source file's path. // The values are Map of resolutions with key being name lookedup. const resolvedModuleNames = createMap>(); + const perDirectoryResolvedModuleNames = createMap>(); const resolvedTypeReferenceDirectives = createMap>(); + const perDirectoryResolvedTypeReferenceDirectives = createMap>(); const directoryWatchesOfFailedLookups = createMap(); const failedLookupLocationToDirPath = createMap(); @@ -60,6 +66,8 @@ namespace ts { return { startRecordingFilesWithChangedResolutions, finishRecordingFilesWithChangedResolutions, + startCachingPerDirectoryResolution, + finishCachingPerDirectoryResolution, resolveModuleNames, resolveTypeReferenceDirectives, invalidateResolutionOfFile, @@ -87,6 +95,7 @@ namespace ts { failedLookupLocationToDirPath.clear(); resolvedModuleNames.clear(); resolvedTypeReferenceDirectives.clear(); + Debug.assert(perDirectoryResolvedModuleNames.size === 0 && perDirectoryResolvedTypeReferenceDirectives.size === 0); } function startRecordingFilesWithChangedResolutions() { @@ -105,6 +114,15 @@ namespace ts { return path => collected && collected.has(path); } + function startCachingPerDirectoryResolution() { + Debug.assert(perDirectoryResolvedModuleNames.size === 0 && perDirectoryResolvedTypeReferenceDirectives.size === 0); + } + + function finishCachingPerDirectoryResolution() { + perDirectoryResolvedModuleNames.clear(); + perDirectoryResolvedTypeReferenceDirectives.clear(); + } + function resolveModuleName(moduleName: string, containingFile: string, compilerOptions: CompilerOptions, host: ModuleResolutionHost): ResolvedModuleWithFailedLookupLocations { const primaryResult = ts.resolveModuleName(moduleName, containingFile, compilerOptions, host); // return result immediately only if global cache support is not enabled or if it is .ts, .tsx or .d.ts @@ -131,6 +149,7 @@ namespace ts { names: string[], containingFile: string, cache: Map>, + perDirectoryCache: Map>, loader: (name: string, containingFile: string, options: CompilerOptions, host: ModuleResolutionHost) => T, getResult: (s: T) => R, getResultFileName: (result: R) => string | undefined, @@ -138,6 +157,12 @@ namespace ts { const path = resolutionHost.toPath(containingFile); const currentResolutionsInFile = cache.get(path); + const dirPath = getDirectoryPath(path); + let perDirectoryResolution = perDirectoryCache.get(dirPath); + if (!perDirectoryResolution) { + perDirectoryResolution = createMap(); + perDirectoryCache.set(dirPath, perDirectoryResolution); + } const newResolutions: Map = createMap(); const resolvedModules: R[] = []; @@ -148,12 +173,24 @@ namespace ts { let resolution = newResolutions.get(name); if (!resolution) { const existingResolution = currentResolutionsInFile && currentResolutionsInFile.get(name); + if (existingResolution) { + // Remove from the cache since we would update the resolution in new file ourselves + currentResolutionsInFile.delete(name); + } + if (moduleResolutionIsValid(existingResolution)) { // ok, it is safe to use existing name resolution results resolution = existingResolution; } else { - resolution = loader(name, containingFile, compilerOptions, resolutionHost); + const resolutionInDirectory = perDirectoryResolution && perDirectoryResolution.get(name); + if (resolutionInDirectory) { + resolution = resolutionInDirectory; + } + else { + resolution = loader(name, containingFile, compilerOptions, resolutionHost); + perDirectoryResolution.set(name, resolution); + } updateFailedLookupLocationWatches(resolution.failedLookupLocations, existingResolution && existingResolution.failedLookupLocations); } newResolutions.set(name, resolution); @@ -169,6 +206,11 @@ namespace ts { resolvedModules.push(getResult(resolution)); } + // Close all the file watchers for the names that arent required any more + if (currentResolutionsInFile) { + clearMap(currentResolutionsInFile, resolution => withFailedLookupLocations(resolution.failedLookupLocations, closeFailedLookupLocationWatcher)); + } + // replace old results with a new one cache.set(path, newResolutions); return resolvedModules; @@ -208,25 +250,35 @@ namespace ts { } function resolveTypeReferenceDirectives(typeDirectiveNames: string[], containingFile: string): ResolvedTypeReferenceDirective[] { - return resolveNamesWithLocalCache(typeDirectiveNames, containingFile, resolvedTypeReferenceDirectives, resolveTypeReferenceDirective, - m => m.resolvedTypeReferenceDirective, r => r.resolvedFileName, /*logChanges*/ false); + resolutionHost.writeLog(`resolveTypeReferenceDirectives: ${typeDirectiveNames} in ${containingFile}`); + return resolveNamesWithLocalCache( + typeDirectiveNames, containingFile, + resolvedTypeReferenceDirectives, perDirectoryResolvedTypeReferenceDirectives, + resolveTypeReferenceDirective, m => m.resolvedTypeReferenceDirective, r => r.resolvedFileName, + /*logChanges*/ false + ); } function resolveModuleNames(moduleNames: string[], containingFile: string, logChanges: boolean): ResolvedModuleFull[] { - return resolveNamesWithLocalCache(moduleNames, containingFile, resolvedModuleNames, resolveModuleName, - m => m.resolvedModule, r => r.resolvedFileName, logChanges); + resolutionHost.writeLog(`resolveModuleNames: ${moduleNames} in ${containingFile}`); + return resolveNamesWithLocalCache( + moduleNames, containingFile, + resolvedModuleNames, perDirectoryResolvedModuleNames, + resolveModuleName, m => m.resolvedModule, r => r.resolvedFileName, + logChanges + ); } function watchFailedLookupLocation(failedLookupLocation: string, failedLookupLocationPath: Path) { const cachedDir = failedLookupLocationToDirPath.get(failedLookupLocationPath); if (cachedDir) { - watchFailedLookupLocationInDirectory(cachedDir, failedLookupLocation, failedLookupLocationPath, /*dir*/ undefined); + watchFailedLookupLocationInDirectory(cachedDir, failedLookupLocationPath, /*dir*/ undefined); return; } if (isInDirectoryPath(rootPath, failedLookupLocationPath)) { // Watch in directory of rootPath - watchFailedLookupLocationInDirectory(rootPath, failedLookupLocation, failedLookupLocationPath, rootDir); + watchFailedLookupLocationInDirectory(rootPath, failedLookupLocationPath, rootDir); return; } @@ -235,7 +287,7 @@ namespace ts { for (let i = 0; i < MAX_DIRPATHS_TO_RECURSE; i++) { const parentPath = getDirectoryPath(dirPath); if (directoryWatchesOfFailedLookups.has(dirPath) || parentPath === dirPath) { - watchFailedLookupLocationInDirectory(dirPath, failedLookupLocation, failedLookupLocationPath, dir); + watchFailedLookupLocationInDirectory(dirPath, failedLookupLocationPath, dir); return; } dirPath = parentPath; @@ -245,7 +297,7 @@ namespace ts { // Verify there are no watches in parent directory const ancestorDirPath = getAncestorDirectoryWithWatches(dirPath); // We wont need directory if we are using ancestor since its already cached - watchFailedLookupLocationInDirectory(ancestorDirPath || dirPath, failedLookupLocation, failedLookupLocationPath, dir); + watchFailedLookupLocationInDirectory(ancestorDirPath || dirPath, failedLookupLocationPath, dir); } function getAncestorDirectoryWithWatches(dirPath: Path) { @@ -258,16 +310,17 @@ namespace ts { return undefined; } - function watchFailedLookupLocationInDirectory(dirPath: Path, failedLookupLocation: string, failedLookupLocationPath: Path, dir: string | undefined) { + function watchFailedLookupLocationInDirectory(dirPath: Path, failedLookupLocationPath: Path, dir: string | undefined) { failedLookupLocationToDirPath.set(failedLookupLocationPath, dirPath); const watches = directoryWatchesOfFailedLookups.get(dirPath); if (watches) { - watches.mapLocations.add(failedLookupLocationPath, failedLookupLocation); + const existingCount = watches.mapLocations.get(failedLookupLocationPath) || 0; + watches.mapLocations.set(failedLookupLocationPath, existingCount + 1); } else { Debug.assert(dir !== undefined); - const mapLocations = createMultiMap(); - mapLocations.add(failedLookupLocationPath, failedLookupLocation); + const mapLocations = createMap(); + mapLocations.set(failedLookupLocationPath, 1); directoryWatchesOfFailedLookups.set(dirPath, { watcher: createDirectoryWatcher(dir, dirPath), mapLocations @@ -294,19 +347,24 @@ namespace ts { }, WatchDirectoryFlags.Recursive); } - function closeFailedLookupLocationWatcher(failedLookupLocation: string, failedLookupLocationPath: Path) { + function closeFailedLookupLocationWatcher(_failedLookupLocation: string, failedLookupLocationPath: Path) { const dirPath = failedLookupLocationToDirPath.get(failedLookupLocationPath); const watches = directoryWatchesOfFailedLookups.get(dirPath); - watches.mapLocations.remove(failedLookupLocationPath, failedLookupLocation); - // If this was last failed lookup location being tracked by the dir watcher, - // remove the failed lookup location path to dir Path entry - if (!watches.mapLocations.has(failedLookupLocationPath)) { + const refCount = watches.mapLocations.get(failedLookupLocationPath); + if (refCount === 1) { + // If this was last failed lookup location being tracked by the dir watcher, + // remove the failed lookup location path to dir Path entry + watches.mapLocations.delete(failedLookupLocationPath); failedLookupLocationToDirPath.delete(failedLookupLocationPath); + + // If there are no more files that need this watcher alive, close the watcher + if (watches.mapLocations.size === 0) { + watches.watcher.close(); + directoryWatchesOfFailedLookups.delete(dirPath); + } } - // If there are no more files that need this watcher alive, close the watcher - if (watches.mapLocations.size === 0) { - watches.watcher.close(); - directoryWatchesOfFailedLookups.delete(dirPath); + else { + watches.mapLocations.set(failedLookupLocationPath, refCount - 1); } } diff --git a/src/compiler/watchedProgram.ts b/src/compiler/watchedProgram.ts index 07a783c3c473b..dbac0861aa5a8 100644 --- a/src/compiler/watchedProgram.ts +++ b/src/compiler/watchedProgram.ts @@ -293,7 +293,8 @@ namespace ts { getCompilationSettings: () => compilerOptions, watchDirectoryOfFailedLookupLocation, getCachedPartialSystem, - onInvalidatedResolution: scheduleProgramUpdate + onInvalidatedResolution: scheduleProgramUpdate, + writeLog }; // Cache for the module resolution const resolutionCache = createResolutionCache(compilerHost); @@ -330,8 +331,10 @@ namespace ts { beforeCompile(compilerOptions); // Compile the program + resolutionCache.startCachingPerDirectoryResolution(); compilerHost.hasInvalidatedResolution = hasInvalidatedResolution; program = createProgram(rootFileNames, compilerOptions, compilerHost, program); + resolutionCache.finishCachingPerDirectoryResolution(); builder.onProgramUpdateGraph(program, hasInvalidatedResolution); // Update watches diff --git a/src/server/project.ts b/src/server/project.ts index 8d391c312b19e..78987d398d446 100644 --- a/src/server/project.ts +++ b/src/server/project.ts @@ -372,6 +372,11 @@ namespace ts.server { return this.getTypeAcquisition().enable ? this.projectService.typingsInstaller.globalTypingsCacheLocation : undefined; } + /*@internal*/ + writeLog(s: string) { + this.projectService.logger.info(s); + } + private setInternalCompilerOptionsForEmittingJsFiles() { if (this.projectKind === ProjectKind.Inferred || this.projectKind === ProjectKind.External) { this.compilerOptions.noEmitForJsFiles = true; @@ -767,7 +772,10 @@ namespace ts.server { private updateGraphWorker() { const oldProgram = this.program; + + this.resolutionCache.startCachingPerDirectoryResolution(); this.program = this.languageService.getProgram(); + this.resolutionCache.finishCachingPerDirectoryResolution(); // bump up the version if // - oldProgram is not set - this is a first time updateGraph is called @@ -795,6 +803,10 @@ namespace ts.server { // Watch the missing files missingFilePath => this.addMissingFileWatcher(missingFilePath) ); + + // Update typeRoots watch + // Watch the type locations that would be added to program as part of automatic type resolutions + // TODO } const oldExternalFiles = this.externalFiles || emptyArray as SortedReadonlyArray; From 16cf7c40a8c1367827d82214233644e6c1191a17 Mon Sep 17 00:00:00 2001 From: Sheetal Nandi Date: Wed, 30 Aug 2017 11:49:58 -0700 Subject: [PATCH 082/109] Watch for the automatic types that included as part of type resolution --- src/compiler/core.ts | 3 + src/compiler/program.ts | 20 ++++-- src/compiler/resolutionCache.ts | 68 ++++++++++++++++++- src/compiler/types.ts | 1 + src/compiler/watchedProgram.ts | 32 ++++++--- src/harness/unittests/tscWatchMode.ts | 6 +- .../unittests/tsserverProjectSystem.ts | 30 ++++++-- src/server/editorServices.ts | 3 +- src/server/project.ts | 45 ++++++++---- src/services/services.ts | 9 ++- src/services/types.ts | 1 + 11 files changed, 179 insertions(+), 39 deletions(-) diff --git a/src/compiler/core.ts b/src/compiler/core.ts index 9c761b47fa05f..4f40a5d1ab31f 100644 --- a/src/compiler/core.ts +++ b/src/compiler/core.ts @@ -1224,6 +1224,9 @@ namespace ts { /** Do nothing and return false */ export function returnFalse(): false { return false; } + /** Do nothing and return true */ + export function returnTrue(): true { return true; } + /** Throws an error because a function is not implemented. */ export function notImplemented(): never { throw new Error("Not implemented"); diff --git a/src/compiler/program.ts b/src/compiler/program.ts index 22459d36cb0f8..f74675dbf3126 100644 --- a/src/compiler/program.ts +++ b/src/compiler/program.ts @@ -393,10 +393,17 @@ namespace ts { allDiagnostics?: Diagnostic[]; } - export function isProgramUptoDate(program: Program | undefined, rootFileNames: string[], newOptions: CompilerOptions, - 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) { + export function isProgramUptoDate( + program: Program | undefined, + rootFileNames: string[], + newOptions: CompilerOptions, + getSourceVersion: (path: Path) => string, + fileExists: (fileName: string) => boolean, + hasInvalidatedResolution: HasInvalidatedResolution, + hasChangedAutomaticTypeDirectiveNames: () => boolean, + ): boolean { + // If we haven't create a program yet or has changed automatic type directives, then it is not up-to-date + if (!program || hasChangedAutomaticTypeDirectiveNames()) { return false; } @@ -587,6 +594,7 @@ namespace ts { let moduleResolutionCache: ModuleResolutionCache; let resolveModuleNamesWorker: (moduleNames: string[], containingFile: string) => ResolvedModuleFull[]; const hasInvalidatedResolution = host.hasInvalidatedResolution || returnFalse; + const hasChangedAutomaticTypeDirectiveNames = host.hasChangedAutomaticTypeDirectiveNames && host.hasChangedAutomaticTypeDirectiveNames.bind(host) || returnFalse; 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. @@ -1090,6 +1098,10 @@ namespace ts { return oldProgram.structureIsReused; } + if (hasChangedAutomaticTypeDirectiveNames()) { + return oldProgram.structureIsReused = StructureIsReused.SafeModules; + } + missingFilePaths = oldProgram.getMissingFilePaths(); // update fileName -> file mapping diff --git a/src/compiler/resolutionCache.ts b/src/compiler/resolutionCache.ts index 7dff3f5515505..37272cac69d6c 100644 --- a/src/compiler/resolutionCache.ts +++ b/src/compiler/resolutionCache.ts @@ -19,6 +19,9 @@ namespace ts { setRootDirectory(dir: string): void; + updateTypeRootsWatch(): void; + closeTypeRootsWatch(): void; + clear(): void; } @@ -39,6 +42,8 @@ namespace ts { getCompilationSettings(): CompilerOptions; watchDirectoryOfFailedLookupLocation(directory: string, cb: DirectoryWatcherCallback, flags: WatchDirectoryFlags): FileWatcher; onInvalidatedResolution(): void; + watchTypeRootsDirectory(directory: string, cb: DirectoryWatcherCallback, flags: WatchDirectoryFlags): FileWatcher; + onChangedAutomaticTypeDirectiveNames(): void; getCachedPartialSystem?(): CachedPartialSystem; projectName?: string; getGlobalCache?(): string | undefined; @@ -59,10 +64,16 @@ namespace ts { const resolvedTypeReferenceDirectives = createMap>(); const perDirectoryResolvedTypeReferenceDirectives = createMap>(); + const getCurrentDirectory = memoize(() => resolutionHost.getCurrentDirectory()); + const directoryWatchesOfFailedLookups = createMap(); const failedLookupLocationToDirPath = createMap(); let rootDir: string; let rootPath: Path; + + // TypeRoot watches for the types that get added as part of getAutomaticTypeDirectiveNames + const typeRootsWatches = createMap(); + return { startRecordingFilesWithChangedResolutions, finishRecordingFilesWithChangedResolutions, @@ -73,12 +84,14 @@ namespace ts { invalidateResolutionOfFile, createHasInvalidatedResolution, setRootDirectory, + updateTypeRootsWatch, + closeTypeRootsWatch, clear }; function setRootDirectory(dir: string) { Debug.assert(!resolvedModuleNames.size && !resolvedTypeReferenceDirectives.size && !directoryWatchesOfFailedLookups.size); - rootDir = removeTrailingDirectorySeparator(getNormalizedAbsolutePath(dir, resolutionHost.getCurrentDirectory())); + rootDir = removeTrailingDirectorySeparator(getNormalizedAbsolutePath(dir, getCurrentDirectory())); rootPath = resolutionHost.toPath(rootDir); } @@ -93,6 +106,7 @@ namespace ts { // Close all the watches for failed lookup locations, irrespective of refcounts for them since this is to clear the cache clearMap(directoryWatchesOfFailedLookups, closeFileWatcherOf); failedLookupLocationToDirPath.clear(); + closeTypeRootsWatch(); resolvedModuleNames.clear(); resolvedTypeReferenceDirectives.clear(); Debug.assert(perDirectoryResolvedModuleNames.size === 0 && perDirectoryResolvedTypeReferenceDirectives.size === 0); @@ -283,7 +297,7 @@ namespace ts { } let dirPath = getDirectoryPath(failedLookupLocationPath); - let dir = getDirectoryPath(getNormalizedAbsolutePath(failedLookupLocation, resolutionHost.getCurrentDirectory())); + let dir = getDirectoryPath(getNormalizedAbsolutePath(failedLookupLocation, getCurrentDirectory())); for (let i = 0; i < MAX_DIRPATHS_TO_RECURSE; i++) { const parentPath = getDirectoryPath(dirPath); if (directoryWatchesOfFailedLookups.has(dirPath) || parentPath === dirPath) { @@ -455,5 +469,55 @@ namespace ts { invalidateResolutionCacheOfChangedFailedLookupLocation(resolvedModuleNames, isInDirPath); invalidateResolutionCacheOfChangedFailedLookupLocation(resolvedTypeReferenceDirectives, isInDirPath); } + + function closeTypeRootsWatch() { + clearMap(typeRootsWatches, closeFileWatcher); + } + + function createTypeRootsWatch(_typeRootPath: string, typeRoot: string): FileWatcher { + // Create new watch and recursive info + return resolutionHost.watchTypeRootsDirectory(typeRoot, fileOrFolder => { + const fileOrFolderPath = resolutionHost.toPath(fileOrFolder); + if (resolutionHost.getCachedPartialSystem) { + // Since the file existance changed, update the sourceFiles cache + resolutionHost.getCachedPartialSystem().addOrDeleteFileOrFolder(fileOrFolder, fileOrFolderPath); + } + + // 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 + resolutionHost.onChangedAutomaticTypeDirectiveNames(); + }, WatchDirectoryFlags.Recursive); + } + + /** + * Watches the types that would get added as part of getAutomaticTypeDirectiveNames + * To be called when compiler options change + */ + function updateTypeRootsWatch() { + const options = resolutionHost.getCompilationSettings(); + if (options.types) { + // No need to do any watch since resolution cache is going to handle the failed lookups + // for the types added by this + closeTypeRootsWatch(); + return; + } + + // we need to assume the directories exist to ensure that we can get all the type root directories that get included + const typeRoots = getEffectiveTypeRoots(options, { directoryExists: returnTrue, getCurrentDirectory }); + if (typeRoots) { + mutateMap( + typeRootsWatches, + arrayToMap(typeRoots, tr => resolutionHost.toPath(tr)), + { + createNewValue: createTypeRootsWatch, + onDeleteValue: closeFileWatcher + } + ); + } + else { + closeTypeRootsWatch(); + } + } } } diff --git a/src/compiler/types.ts b/src/compiler/types.ts index 026a8bf8631c4..a105e5fcbfefc 100644 --- a/src/compiler/types.ts +++ b/src/compiler/types.ts @@ -4067,6 +4067,7 @@ namespace ts { getEnvironmentVariable?(name: string): string; onReleaseOldSourceFile?(oldSourceFile: SourceFile, oldOptions: CompilerOptions): void; hasInvalidatedResolution?: HasInvalidatedResolution; + hasChangedAutomaticTypeDirectiveNames?(): boolean; } /* @internal */ diff --git a/src/compiler/watchedProgram.ts b/src/compiler/watchedProgram.ts index dbac0861aa5a8..0dba848eb1c90 100644 --- a/src/compiler/watchedProgram.ts +++ b/src/compiler/watchedProgram.ts @@ -246,12 +246,13 @@ namespace ts { const sourceFilesCache = createMap(); // 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 hasChangedCompilerOptions = false; // True if the compiler options have changed between compilations + let changedAutomaticTypeDirectiveNames = false; // True if the automatic type directives have changed const loggingEnabled = compilerOptions.diagnostics || compilerOptions.extendedDiagnostics; const writeLog: (s: string) => void = loggingEnabled ? s => system.write(s) : noop; const watchFile = loggingEnabled ? ts.addFileWatcherWithLogging : ts.addFileWatcher; const watchFilePath = loggingEnabled ? ts.addFilePathWatcherWithLogging : ts.addFilePathWatcher; - const watchDirectory = loggingEnabled ? ts.addDirectoryWatcherWithLogging : ts.addDirectoryWatcher; + const watchDirectoryWorker = loggingEnabled ? ts.addDirectoryWatcherWithLogging : ts.addDirectoryWatcher; watchingHost = watchingHost || createWatchingSystemHost(compilerOptions.pretty); const { system, parseConfigFile, reportDiagnostic, reportWatchDiagnostic, beforeCompile, afterCompile } = watchingHost; @@ -288,12 +289,15 @@ namespace ts { resolveTypeReferenceDirectives, resolveModuleNames, onReleaseOldSourceFile, + hasChangedAutomaticTypeDirectiveNames, // Members for ResolutionCacheHost toPath, getCompilationSettings: () => compilerOptions, - watchDirectoryOfFailedLookupLocation, + watchDirectoryOfFailedLookupLocation: watchDirectory, + watchTypeRootsDirectory: watchDirectory, getCachedPartialSystem, onInvalidatedResolution: scheduleProgramUpdate, + onChangedAutomaticTypeDirectiveNames, writeLog }; // Cache for the module resolution @@ -320,13 +324,14 @@ namespace ts { } const hasInvalidatedResolution = resolutionCache.createHasInvalidatedResolution(); - if (isProgramUptoDate(program, rootFileNames, compilerOptions, getSourceVersion, fileExists, hasInvalidatedResolution)) { + if (isProgramUptoDate(program, rootFileNames, compilerOptions, getSourceVersion, fileExists, hasInvalidatedResolution, hasChangedAutomaticTypeDirectiveNames)) { return; } if (hasChangedCompilerOptions && changesAffectModuleResolution(program && program.getCompilerOptions(), compilerOptions)) { resolutionCache.clear(); } + const needsUpdateInTypeRootWatch = hasChangedCompilerOptions || !program; hasChangedCompilerOptions = false; beforeCompile(compilerOptions); @@ -339,6 +344,10 @@ namespace ts { // Update watches updateMissingFilePathsWatch(program, missingFilesMap || (missingFilesMap = createMap()), watchMissingFilePath); + if (needsUpdateInTypeRootWatch) { + resolutionCache.updateTypeRootsWatch(); + } + if (missingFilePathsRequestedForRelease) { // These are the paths that program creater told us as not in use any more but were missing on the disk. // We didnt remove the entry for them from sourceFiles cache so that we dont have to do File IO, @@ -582,8 +591,17 @@ namespace ts { } } - function watchDirectoryOfFailedLookupLocation(directory: string, cb: DirectoryWatcherCallback, flags: WatchDirectoryFlags) { - return watchDirectory(system, directory, cb, flags, writeLog); + function watchDirectory(directory: string, cb: DirectoryWatcherCallback, flags: WatchDirectoryFlags) { + return watchDirectoryWorker(system, directory, cb, flags, writeLog); + } + + function onChangedAutomaticTypeDirectiveNames() { + changedAutomaticTypeDirectiveNames = true; + scheduleProgramUpdate(); + } + + function hasChangedAutomaticTypeDirectiveNames() { + return changedAutomaticTypeDirectiveNames; } function watchMissingFilePath(missingFilePath: Path) { @@ -615,7 +633,6 @@ namespace ts { function watchWildcardDirectory(directory: string, flags: WatchDirectoryFlags) { return watchDirectory( - system, directory, fileOrFolder => { Debug.assert(!!configFileName); @@ -645,8 +662,7 @@ namespace ts { scheduleProgramUpdate(); } }, - flags, - writeLog + flags ); } diff --git a/src/harness/unittests/tscWatchMode.ts b/src/harness/unittests/tscWatchMode.ts index f5ed08b9b872c..66e58a502d67c 100644 --- a/src/harness/unittests/tscWatchMode.ts +++ b/src/harness/unittests/tscWatchMode.ts @@ -167,7 +167,8 @@ namespace ts.tscWatch { checkProgramActualFiles(watch(), [file1.path, libFile.path, file2.path]); checkProgramRootFiles(watch(), [file1.path, file2.path]); checkWatchedFiles(host, [configFile.path, file1.path, file2.path, libFile.path]); - checkWatchedDirectories(host, [getDirectoryPath(configFile.path)], /*recursive*/ true); + const configDir = getDirectoryPath(configFile.path); + checkWatchedDirectories(host, projectSystem.getTypeRootsFromLocation(configDir).concat(configDir), /*recursive*/ true); }); // TODO: if watching for config file creation @@ -181,7 +182,8 @@ namespace ts.tscWatch { }; const host = createWatchedSystem([commonFile1, libFile, configFile]); const watch = createWatchModeWithConfigFile(configFile.path, host); - checkWatchedDirectories(host, ["/a/b"], /*recursive*/ true); + const configDir = getDirectoryPath(configFile.path); + checkWatchedDirectories(host, projectSystem.getTypeRootsFromLocation(configDir).concat(configDir), /*recursive*/ true); checkProgramRootFiles(watch(), [commonFile1.path]); diff --git a/src/harness/unittests/tsserverProjectSystem.ts b/src/harness/unittests/tsserverProjectSystem.ts index f5cd1766c39ba..22623ad1c8b7c 100644 --- a/src/harness/unittests/tsserverProjectSystem.ts +++ b/src/harness/unittests/tsserverProjectSystem.ts @@ -321,6 +321,22 @@ namespace ts.projectSystem { verifyDiagnostics(actual, []); } + const typeRootFromTsserverLocation = "/node_modules/@types"; + + export function getTypeRootsFromLocation(currentDirectory: string) { + currentDirectory = normalizePath(currentDirectory); + const result: string[] = []; + while (true) { + result.push(combinePaths(currentDirectory, "node_modules/@types")); + const parentDirectory = getDirectoryPath(currentDirectory); + if (parentDirectory === currentDirectory) { + break; + } + currentDirectory = parentDirectory; + } + return result; + } + describe("tsserverProjectSystem", () => { const commonFile1: FileOrFolder = { path: "/a/b/commonFile1.ts", @@ -359,7 +375,7 @@ namespace ts.projectSystem { const configFiles = flatMap(configFileLocations, location => [location + "tsconfig.json", location + "jsconfig.json"]); checkWatchedFiles(host, configFiles.concat(libFile.path, moduleFile.path)); checkWatchedDirectories(host, [], /*recursive*/ false); - checkWatchedDirectories(host, ["/"], /*recursive*/ true); + checkWatchedDirectories(host, ["/", typeRootFromTsserverLocation], /*recursive*/ true); }); it("can handle tsconfig file name with difference casing", () => { @@ -430,7 +446,8 @@ namespace ts.projectSystem { checkProjectRootFiles(project, [file1.path, file2.path]); // watching all files except one that was open checkWatchedFiles(host, [configFile.path, file2.path, libFile.path]); - checkWatchedDirectories(host, [getDirectoryPath(configFile.path)], /*recursive*/ true); + const configFileDirectory = getDirectoryPath(configFile.path); + checkWatchedDirectories(host, getTypeRootsFromLocation(configFileDirectory).concat(configFileDirectory), /*recursive*/ true); }); it("create configured project with the file list", () => { @@ -518,7 +535,8 @@ namespace ts.projectSystem { const host = createServerHost([commonFile1, libFile, configFile]); const projectService = createProjectService(host); projectService.openClientFile(commonFile1.path); - checkWatchedDirectories(host, ["/a/b"], /*recursive*/ true); + const configFileDir = getDirectoryPath(configFile.path); + checkWatchedDirectories(host, getTypeRootsFromLocation(configFileDir).concat(configFileDir), /*recursive*/ true); checkNumberOfConfiguredProjects(projectService, 1); const project = configuredProjectAt(projectService, 0); @@ -4329,6 +4347,7 @@ namespace ts.projectSystem { }; const appFolder = getDirectoryPath(app.path); const projectFiles = [app, libFile, tsconfigJson]; + const typeRootDirectories = getTypeRootsFromLocation(getDirectoryPath(tsconfigJson.path)); const otherFiles = [packageJson]; const host = createServerHost(projectFiles.concat(otherFiles)); const projectService = createProjectService(host); @@ -4402,7 +4421,8 @@ namespace ts.projectSystem { { "path": "/a/b/node_modules/typescript" }, { "path": "/a/b/node_modules/.bin" } ); - verifyAfterPartialOrCompleteNpmInstall(0); + // From the type root update + verifyAfterPartialOrCompleteNpmInstall(2); forEach(filesAndFoldersToAdd, f => { f.path = f.path @@ -4438,7 +4458,7 @@ namespace ts.projectSystem { const filesWatched = filter(projectFilePaths, p => p !== app.path); checkWatchedFiles(host, filesWatched); - checkWatchedDirectories(host, recursiveWatchedDirectories, /*recursive*/ true); + checkWatchedDirectories(host, typeRootDirectories.concat(recursiveWatchedDirectories), /*recursive*/ true); checkWatchedDirectories(host, [], /*recursive*/ false); } } diff --git a/src/server/editorServices.ts b/src/server/editorServices.ts index e6976008c8362..e6784942c6664 100644 --- a/src/server/editorServices.ts +++ b/src/server/editorServices.ts @@ -246,7 +246,8 @@ namespace ts.server { WildcardDirectories = "Wild card directory", ClosedScriptInfo = "Closed Script info", ConfigFileForInferredRoot = "Config file for the inferred project root", - FailedLookupLocation = "Directory of Failed lookup locations in module resolution" + FailedLookupLocation = "Directory of Failed lookup locations in module resolution", + TypeRoots = "Type root directory" } const enum ConfigFileWatcherStatus { diff --git a/src/server/project.ts b/src/server/project.ts index 78987d398d446..7dc236e8edf53 100644 --- a/src/server/project.ts +++ b/src/server/project.ts @@ -162,9 +162,9 @@ namespace ts.server { */ private projectStateVersion = 0; - private typingFiles: SortedReadonlyArray; + private changedAutomaticTypeDirectiveNames = false; - private typesVersion = 0; + private typingFiles: SortedReadonlyArray; public isNonTsProject() { this.updateGraph(); @@ -224,10 +224,10 @@ namespace ts.server { } this.languageService = createLanguageService(this, this.documentRegistry); + this.resolutionCache = createResolutionCache(this); if (!languageServiceEnabled) { this.disableLanguageService(); } - this.resolutionCache = createResolutionCache(this); this.markAsDirty(); } @@ -325,10 +325,6 @@ namespace ts.server { return !this.isWatchedMissingFile(path) && this.partialSystem.fileExists(file); } - getTypeRootsVersion() { - return this.typesVersion; - } - resolveModuleNames(moduleNames: string[], containingFile: string): ResolvedModuleFull[] { return this.resolutionCache.resolveModuleNames(moduleNames, containingFile, /*logChanges*/ true); } @@ -367,6 +363,29 @@ namespace ts.server { this.projectService.delayUpdateProjectGraphAndInferredProjectsRefresh(this); } + /*@internal*/ + watchTypeRootsDirectory(directory: string, cb: DirectoryWatcherCallback, flags: WatchDirectoryFlags) { + return this.projectService.watchDirectory( + this.projectService.host, + directory, + cb, + flags, + WatchType.TypeRoots, + this + ); + } + + /*@internal*/ + onChangedAutomaticTypeDirectiveNames() { + this.changedAutomaticTypeDirectiveNames = true; + this.projectService.delayUpdateProjectGraphAndInferredProjectsRefresh(this); + } + + /*@internal*/ + hasChangedAutomaticTypeDirectiveNames() { + return this.changedAutomaticTypeDirectiveNames; + } + /*@internal*/ getGlobalCache() { return this.getTypeAcquisition().enable ? this.projectService.typingsInstaller.globalTypingsCacheLocation : undefined; @@ -458,6 +477,7 @@ namespace ts.server { } this.languageService.cleanupSemanticCache(); this.languageServiceEnabled = false; + this.resolutionCache.closeTypeRootsWatch(); this.projectService.onUpdateLanguageServiceStateForProject(this, /*languageServiceEnabled*/ false); } @@ -478,10 +498,6 @@ namespace ts.server { return this.program.getSourceFileByPath(path); } - updateTypes() { - this.typesVersion++; - } - close() { if (this.program) { // if we have a program - release all files that are enlisted in program @@ -781,7 +797,7 @@ 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 hasChanges = !oldProgram || (this.program !== oldProgram && !(oldProgram.structureIsReused & StructureIsReused.Completely)); - + this.changedAutomaticTypeDirectiveNames = false; if (hasChanges) { if (oldProgram) { for (const f of oldProgram.getSourceFiles()) { @@ -804,9 +820,10 @@ namespace ts.server { missingFilePath => this.addMissingFileWatcher(missingFilePath) ); - // Update typeRoots watch // Watch the type locations that would be added to program as part of automatic type resolutions - // TODO + if (this.languageServiceEnabled) { + this.resolutionCache.updateTypeRootsWatch(); + } } const oldExternalFiles = this.externalFiles || emptyArray as SortedReadonlyArray; diff --git a/src/services/services.ts b/src/services/services.ts index 1939133b6b0fa..087ffb425157a 100644 --- a/src/services/services.ts +++ b/src/services/services.ts @@ -1095,11 +1095,13 @@ namespace ts { } function synchronizeHostData(): void { + const hasChangedAutomaticTypeDirectiveNames = host.hasChangedAutomaticTypeDirectiveNames && host.hasChangedAutomaticTypeDirectiveNames.bind(host) || returnFalse; + // perform fast check if host supports it if (host.getProjectVersion) { const hostProjectVersion = host.getProjectVersion(); if (hostProjectVersion) { - if (lastProjectVersion === hostProjectVersion) { + if (lastProjectVersion === hostProjectVersion && !hasChangedAutomaticTypeDirectiveNames()) { return; } @@ -1121,7 +1123,7 @@ namespace ts { const hasInvalidatedResolution: HasInvalidatedResolution = host.hasInvalidatedResolution || returnFalse; // If the program is already up-to-date, we can reuse it - if (isProgramUptoDate(program, rootFileNames, hostCache.compilationSettings(), path => hostCache.getVersion(path), fileExists, hasInvalidatedResolution)) { + if (isProgramUptoDate(program, rootFileNames, hostCache.compilationSettings(), path => hostCache.getVersion(path), fileExists, hasInvalidatedResolution, hasChangedAutomaticTypeDirectiveNames)) { return; } @@ -1161,7 +1163,8 @@ namespace ts { return host.getDirectories ? host.getDirectories(path) : []; }, onReleaseOldSourceFile, - hasInvalidatedResolution + hasInvalidatedResolution, + hasChangedAutomaticTypeDirectiveNames }; if (host.trace) { compilerHost.trace = message => host.trace(message); diff --git a/src/services/types.ts b/src/services/types.ts index 05715f018bdd8..b26b015049f61 100644 --- a/src/services/types.ts +++ b/src/services/types.ts @@ -186,6 +186,7 @@ namespace ts { resolveModuleNames?(moduleNames: string[], containingFile: string): ResolvedModule[]; resolveTypeReferenceDirectives?(typeDirectiveNames: string[], containingFile: string): ResolvedTypeReferenceDirective[]; hasInvalidatedResolution?: HasInvalidatedResolution; + hasChangedAutomaticTypeDirectiveNames?(): boolean; directoryExists?(directoryName: string): boolean; /* From d7ce95df254033f2c6e1c6bc959c29a83cfed448 Mon Sep 17 00:00:00 2001 From: Sheetal Nandi Date: Wed, 30 Aug 2017 17:15:40 -0700 Subject: [PATCH 083/109] Watch node_modules if possible --- src/compiler/resolutionCache.ts | 103 +++++++++++++++++++------------- 1 file changed, 62 insertions(+), 41 deletions(-) diff --git a/src/compiler/resolutionCache.ts b/src/compiler/resolutionCache.ts index 37272cac69d6c..ff34a4b9a0783 100644 --- a/src/compiler/resolutionCache.ts +++ b/src/compiler/resolutionCache.ts @@ -50,8 +50,6 @@ namespace ts { writeLog(s: string): void; } - const MAX_DIRPATHS_TO_RECURSE = 5; - export function createResolutionCache(resolutionHost: ResolutionCacheHost): ResolutionCache { let filesWithChangedSetOfUnresolvedImports: Path[] | undefined; let filesWithInvalidatedResolutions: Map | undefined; @@ -264,7 +262,6 @@ namespace ts { } function resolveTypeReferenceDirectives(typeDirectiveNames: string[], containingFile: string): ResolvedTypeReferenceDirective[] { - resolutionHost.writeLog(`resolveTypeReferenceDirectives: ${typeDirectiveNames} in ${containingFile}`); return resolveNamesWithLocalCache( typeDirectiveNames, containingFile, resolvedTypeReferenceDirectives, perDirectoryResolvedTypeReferenceDirectives, @@ -274,7 +271,6 @@ namespace ts { } function resolveModuleNames(moduleNames: string[], containingFile: string, logChanges: boolean): ResolvedModuleFull[] { - resolutionHost.writeLog(`resolveModuleNames: ${moduleNames} in ${containingFile}`); return resolveNamesWithLocalCache( moduleNames, containingFile, resolvedModuleNames, perDirectoryResolvedModuleNames, @@ -284,48 +280,64 @@ namespace ts { } function watchFailedLookupLocation(failedLookupLocation: string, failedLookupLocationPath: Path) { - const cachedDir = failedLookupLocationToDirPath.get(failedLookupLocationPath); - if (cachedDir) { - watchFailedLookupLocationInDirectory(cachedDir, failedLookupLocationPath, /*dir*/ undefined); + const failedLocationDirPath = getDirectoryPath(failedLookupLocationPath); + const cachedDirPath = failedLookupLocationToDirPath.get(failedLocationDirPath); + if (cachedDirPath) { + watchFailedLookupLocationInDirectory(cachedDirPath, failedLookupLocationPath, /*dir*/ undefined); return; } if (isInDirectoryPath(rootPath, failedLookupLocationPath)) { // Watch in directory of rootPath + failedLookupLocationToDirPath.set(failedLocationDirPath, rootPath); watchFailedLookupLocationInDirectory(rootPath, failedLookupLocationPath, rootDir); return; } - let dirPath = getDirectoryPath(failedLookupLocationPath); - let dir = getDirectoryPath(getNormalizedAbsolutePath(failedLookupLocation, getCurrentDirectory())); - for (let i = 0; i < MAX_DIRPATHS_TO_RECURSE; i++) { - const parentPath = getDirectoryPath(dirPath); - if (directoryWatchesOfFailedLookups.has(dirPath) || parentPath === dirPath) { - watchFailedLookupLocationInDirectory(dirPath, failedLookupLocationPath, dir); - return; - } - dirPath = parentPath; - dir = getDirectoryPath(dir); - } + const { dir, dirPath } = getDirectoryToWatchWhenNotInRootPath( + getDirectoryPath(getNormalizedAbsolutePath(failedLookupLocation, getCurrentDirectory())), + getDirectoryPath(failedLookupLocationPath) + ); + failedLookupLocationToDirPath.set(failedLocationDirPath, dirPath); + watchFailedLookupLocationInDirectory(dirPath, failedLookupLocationPath, dir); + } - // Verify there are no watches in parent directory - const ancestorDirPath = getAncestorDirectoryWithWatches(dirPath); - // We wont need directory if we are using ancestor since its already cached - watchFailedLookupLocationInDirectory(ancestorDirPath || dirPath, failedLookupLocationPath, dir); + function isNodeModulesDirectory(dirPath: Path) { + const nodemodules = "/node_modules"; + return endsWith(dirPath, nodemodules); } - function getAncestorDirectoryWithWatches(dirPath: Path) { - for (let parentDirPath = getDirectoryPath(dirPath); parentDirPath !== dirPath; parentDirPath = getDirectoryPath(parentDirPath)) { - if (directoryWatchesOfFailedLookups.has(parentDirPath)) { - return parentDirPath; + function getDirectoryToWatchWhenNotInRootPath(dir: string, dirPath: Path): { dir: string, dirPath: Path } { + // If the directory is node_modules use it to watch + if (isNodeModulesDirectory(dirPath)) { + return { dir, dirPath }; + } + + // If directory path contains node module, get the node_modules directory for watching + if (dirPath.indexOf("/node_modules/") !== -1) { + while (!isNodeModulesDirectory(dirPath)) { + dir = getDirectoryPath(dir); + dirPath = getDirectoryPath(dirPath); + } + return { dir, dirPath }; + } + + // Use some ancestor of the root directory + if (rootPath !== undefined) { + while (!isInDirectoryPath(dirPath, rootPath)) { + const parentPath = getDirectoryPath(dirPath); + if (parentPath === dirPath) { + break; + } + dirPath = parentPath; + dir = getDirectoryPath(dir); } - dirPath = parentDirPath; } - return undefined; + + return { dir, dirPath }; } function watchFailedLookupLocationInDirectory(dirPath: Path, failedLookupLocationPath: Path, dir: string | undefined) { - failedLookupLocationToDirPath.set(failedLookupLocationPath, dirPath); const watches = directoryWatchesOfFailedLookups.get(dirPath); if (watches) { const existingCount = watches.mapLocations.get(failedLookupLocationPath) || 0; @@ -350,31 +362,37 @@ namespace ts { resolutionHost.getCachedPartialSystem().addOrDeleteFileOrFolder(fileOrFolder, fileOrFolderPath); } - // If the location results in update to failed lookup, schedule program update - if (dirPath === fileOrFolderPath) { - onAddOrRemoveDirectoryOfFailedLookup(dirPath); - resolutionHost.onInvalidatedResolution(); - } - else if (onFileAddOrRemoveInDirectoryOfFailedLookup(dirPath, fileOrFolderPath)) { - resolutionHost.onInvalidatedResolution(); + // If the files are added to project root or node_modules directory, always run through the invalidation process + // Otherwise run through invalidation only if adding to the immediate directory + if (dirPath === rootPath || isNodeModulesDirectory(dirPath) || getDirectoryPath(fileOrFolderPath) === dirPath) { + // If the location results in update to failed lookup, schedule program update + if (dirPath === fileOrFolderPath) { + if (onAddOrRemoveDirectoryOfFailedLookup(dirPath)) { + resolutionHost.onInvalidatedResolution(); + } + } + else if (onFileAddOrRemoveInDirectoryOfFailedLookup(dirPath, fileOrFolderPath)) { + resolutionHost.onInvalidatedResolution(); + } } }, WatchDirectoryFlags.Recursive); } function closeFailedLookupLocationWatcher(_failedLookupLocation: string, failedLookupLocationPath: Path) { - const dirPath = failedLookupLocationToDirPath.get(failedLookupLocationPath); + const failedLookupDirectory = getDirectoryPath(failedLookupLocationPath); + const dirPath = failedLookupLocationToDirPath.get(failedLookupDirectory); const watches = directoryWatchesOfFailedLookups.get(dirPath); const refCount = watches.mapLocations.get(failedLookupLocationPath); if (refCount === 1) { // If this was last failed lookup location being tracked by the dir watcher, // remove the failed lookup location path to dir Path entry watches.mapLocations.delete(failedLookupLocationPath); - failedLookupLocationToDirPath.delete(failedLookupLocationPath); // If there are no more files that need this watcher alive, close the watcher if (watches.mapLocations.size === 0) { watches.watcher.close(); directoryWatchesOfFailedLookups.delete(dirPath); + failedLookupLocationToDirPath.delete(failedLookupDirectory); } } else { @@ -455,19 +473,22 @@ namespace ts { function onFileAddOrRemoveInDirectoryOfFailedLookup(dirPath: Path, fileOrFolder: Path) { const watches = directoryWatchesOfFailedLookups.get(dirPath); - const isFailedLookupFile = watches.mapLocations.has(fileOrFolder); - if (isFailedLookupFile) { + if (watches.mapLocations.has(fileOrFolder)) { + const invalidatedFiles = filesWithInvalidatedResolutions && filesWithInvalidatedResolutions.size; const isFileOrFolder: (location: string) => boolean = location => resolutionHost.toPath(location) === fileOrFolder; invalidateResolutionCacheOfChangedFailedLookupLocation(resolvedModuleNames, isFileOrFolder); invalidateResolutionCacheOfChangedFailedLookupLocation(resolvedTypeReferenceDirectives, isFileOrFolder); + return filesWithInvalidatedResolutions && filesWithInvalidatedResolutions.size !== invalidatedFiles; } - return isFailedLookupFile; + return false; } function onAddOrRemoveDirectoryOfFailedLookup(dirPath: Path) { + const invalidatedFiles = filesWithInvalidatedResolutions && filesWithInvalidatedResolutions.size; const isInDirPath: (location: string) => boolean = location => isInDirectoryPath(dirPath, resolutionHost.toPath(location)); invalidateResolutionCacheOfChangedFailedLookupLocation(resolvedModuleNames, isInDirPath); invalidateResolutionCacheOfChangedFailedLookupLocation(resolvedTypeReferenceDirectives, isInDirPath); + filesWithInvalidatedResolutions && filesWithInvalidatedResolutions.size !== invalidatedFiles; } function closeTypeRootsWatch() { From 345f36d2eac921a39bda7fb0060d27e133110ad3 Mon Sep 17 00:00:00 2001 From: Sheetal Nandi Date: Thu, 31 Aug 2017 11:35:42 -0700 Subject: [PATCH 084/109] Update tests --- .../unittests/tsserverProjectSystem.ts | 28 +++++++++++++++---- 1 file changed, 23 insertions(+), 5 deletions(-) diff --git a/src/harness/unittests/tsserverProjectSystem.ts b/src/harness/unittests/tsserverProjectSystem.ts index 22623ad1c8b7c..ae227bb683c86 100644 --- a/src/harness/unittests/tsserverProjectSystem.ts +++ b/src/harness/unittests/tsserverProjectSystem.ts @@ -247,6 +247,23 @@ namespace ts.projectSystem { checkFileNames(`${server.ProjectKind[project.projectKind]} project, rootFileNames`, project.getRootFiles(), expectedFiles); } + function getNodeModuleDirectories(dir: string) { + const result: string[] = []; + while (true) { + result.push(combinePaths(dir, "node_modules")); + const parentDir = getDirectoryPath(dir); + if (parentDir === dir) { + break; + } + dir = parentDir; + } + return result; + } + + function getNumberOfWatchesInvokedForRecursiveWatches(recursiveWatchedDirs: string[], file: string) { + return countWhere(recursiveWatchedDirs, dir => file.length > dir.length && startsWith(file, dir) && file[dir.length] === directorySeparator); + } + /** * Test server cancellation token used to mock host token cancellation requests. * The cancelAfterRequest constructor param specifies how many isCancellationRequested() calls @@ -375,7 +392,7 @@ namespace ts.projectSystem { const configFiles = flatMap(configFileLocations, location => [location + "tsconfig.json", location + "jsconfig.json"]); checkWatchedFiles(host, configFiles.concat(libFile.path, moduleFile.path)); checkWatchedDirectories(host, [], /*recursive*/ false); - checkWatchedDirectories(host, ["/", typeRootFromTsserverLocation], /*recursive*/ true); + checkWatchedDirectories(host, ["/a/b/c", typeRootFromTsserverLocation], /*recursive*/ true); }); it("can handle tsconfig file name with difference casing", () => { @@ -4243,7 +4260,7 @@ namespace ts.projectSystem { const { configFileName } = projectService.openClientFile(file1.path); assert.equal(configFileName, tsconfigFile.path, `should find config`); checkNumberOfConfiguredProjects(projectService, 1); - const watchingRecursiveDirectories = [`${canonicalFrontendDir}/src`, canonicalFrontendDir, "/"]; + const watchingRecursiveDirectories = [`${canonicalFrontendDir}/src`, canonicalFrontendDir].concat(getNodeModuleDirectories(getDirectoryPath(canonicalFrontendDir))); const project = projectService.configuredProjects.get(canonicalConfigPath); verifyProjectAndWatchedDirectories(); @@ -4256,7 +4273,8 @@ namespace ts.projectSystem { host.runQueuedTimeoutCallbacks(); const canonicalFile3Path = useCaseSensitiveFileNames ? file3.path : file3.path.toLocaleLowerCase(); - callsTrackingHost.verifyCalledOnEachEntryNTimes("fileExists", [canonicalFile3Path], watchingRecursiveDirectories.length); + const numberOfTimesWatchInvoked = getNumberOfWatchesInvokedForRecursiveWatches(watchingRecursiveDirectories, canonicalFile3Path); + callsTrackingHost.verifyCalledOnEachEntryNTimes("fileExists", [canonicalFile3Path], numberOfTimesWatchInvoked); // Called for type root resolution const directoryExistsCalled = createMap(); @@ -4266,7 +4284,7 @@ namespace ts.projectSystem { directoryExistsCalled.set(`/node_modules`, 2); directoryExistsCalled.set(`${frontendDir}/types`, 2); directoryExistsCalled.set(`${frontendDir}/node_modules/@types`, 2); - directoryExistsCalled.set(canonicalFile3Path, watchingRecursiveDirectories.length); + directoryExistsCalled.set(canonicalFile3Path, numberOfTimesWatchInvoked); callsTrackingHost.verifyCalledOnEachEntry("directoryExists", directoryExistsCalled); callsTrackingHost.verifyNoCall("getDirectories"); @@ -4353,7 +4371,7 @@ namespace ts.projectSystem { const projectService = createProjectService(host); const { configFileName } = projectService.openClientFile(app.path); assert.equal(configFileName, tsconfigJson.path, `should find config`); - const recursiveWatchedDirectories: string[] = [appFolder, "/"]; + const recursiveWatchedDirectories: string[] = [appFolder].concat(getNodeModuleDirectories(getDirectoryPath(appFolder))); verifyProject(); let timeoutAfterReloadFs = timeoutDuringPartialInstallation; From 2b97b2c04defa521fd15a587950eeed1f1736399 Mon Sep 17 00:00:00 2001 From: Sheetal Nandi Date: Thu, 31 Aug 2017 11:35:55 -0700 Subject: [PATCH 085/109] Print number of files in the project when printing project --- src/server/project.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/server/project.ts b/src/server/project.ts index 7dc236e8edf53..3fb2d74795fe9 100644 --- a/src/server/project.ts +++ b/src/server/project.ts @@ -889,10 +889,11 @@ namespace ts.server { filesToString() { if (!this.program) { - return ""; + return "\tFiles (0)\n"; } - let strBuilder = ""; - for (const file of this.program.getSourceFiles()) { + const sourceFiles = this.program.getSourceFiles(); + let strBuilder = `\tFiles (${sourceFiles.length})\n`; + for (const file of sourceFiles) { strBuilder += `\t${file.fileName}\n`; } return strBuilder; From 8d5d4c2a0e01264f842b0608ccf062247338ca97 Mon Sep 17 00:00:00 2001 From: Sheetal Nandi Date: Thu, 31 Aug 2017 15:18:54 -0700 Subject: [PATCH 086/109] Reduce storage of maps/sets for failed lookups --- src/compiler/resolutionCache.ts | 198 ++++++++++---------------------- src/server/project.ts | 2 + 2 files changed, 65 insertions(+), 135 deletions(-) diff --git a/src/compiler/resolutionCache.ts b/src/compiler/resolutionCache.ts index ff34a4b9a0783..1782127dc9f26 100644 --- a/src/compiler/resolutionCache.ts +++ b/src/compiler/resolutionCache.ts @@ -30,13 +30,6 @@ namespace ts { isInvalidated?: boolean; } - interface DirectoryWatchesOfFailedLookup { - /** watcher for the directory of failed lookup */ - watcher: FileWatcher; - /** map with key being the failed lookup location path and value being the actual location */ - mapLocations: Map; - } - export interface ResolutionCacheHost extends ModuleResolutionHost { toPath(fileName: string): Path; getCompilationSettings(): CompilerOptions; @@ -61,11 +54,10 @@ namespace ts { const perDirectoryResolvedModuleNames = createMap>(); const resolvedTypeReferenceDirectives = createMap>(); const perDirectoryResolvedTypeReferenceDirectives = createMap>(); - const getCurrentDirectory = memoize(() => resolutionHost.getCurrentDirectory()); - const directoryWatchesOfFailedLookups = createMap(); - const failedLookupLocationToDirPath = createMap(); + const directoryWatchesOfFailedLookups = createMap(); + let hasChangesInFailedLookupLocations = false; let rootDir: string; let rootPath: Path; @@ -101,9 +93,8 @@ namespace ts { } function clear() { - // Close all the watches for failed lookup locations, irrespective of refcounts for them since this is to clear the cache - clearMap(directoryWatchesOfFailedLookups, closeFileWatcherOf); - failedLookupLocationToDirPath.clear(); + clearMap(directoryWatchesOfFailedLookups, closeFileWatcher); + hasChangesInFailedLookupLocations = false; closeTypeRootsWatch(); resolvedModuleNames.clear(); resolvedTypeReferenceDirectives.clear(); @@ -131,6 +122,18 @@ namespace ts { } function finishCachingPerDirectoryResolution() { + if (hasChangesInFailedLookupLocations) { + const seenDirectories = createMap(); + watchFailedLookupLocationForCache(perDirectoryResolvedModuleNames, seenDirectories); + watchFailedLookupLocationForCache(perDirectoryResolvedTypeReferenceDirectives, seenDirectories); + directoryWatchesOfFailedLookups.forEach((watcher, path) => { + if (!seenDirectories.has(path)) { + watcher.close(); + } + }); + hasChangesInFailedLookupLocations = false; + } + perDirectoryResolvedModuleNames.clear(); perDirectoryResolvedTypeReferenceDirectives.clear(); } @@ -202,8 +205,9 @@ namespace ts { else { resolution = loader(name, containingFile, compilerOptions, resolutionHost); perDirectoryResolution.set(name, resolution); + hasChangesInFailedLookupLocations = hasChangesInFailedLookupLocations || + resolution.failedLookupLocations !== (existingResolution && existingResolution.failedLookupLocations); } - updateFailedLookupLocationWatches(resolution.failedLookupLocations, existingResolution && existingResolution.failedLookupLocations); } newResolutions.set(name, resolution); if (logChanges && filesWithChangedSetOfUnresolvedImports && !resolutionIsEqualTo(existingResolution, resolution)) { @@ -218,11 +222,6 @@ namespace ts { resolvedModules.push(getResult(resolution)); } - // Close all the file watchers for the names that arent required any more - if (currentResolutionsInFile) { - clearMap(currentResolutionsInFile, resolution => withFailedLookupLocations(resolution.failedLookupLocations, closeFailedLookupLocationWatcher)); - } - // replace old results with a new one cache.set(path, newResolutions); return resolvedModules; @@ -262,6 +261,7 @@ namespace ts { } function resolveTypeReferenceDirectives(typeDirectiveNames: string[], containingFile: string): ResolvedTypeReferenceDirective[] { + resolutionHost.writeLog(`resolveTypeReferenceDirectives[${resolvedTypeReferenceDirectives.size}"]: " + ${containingFile}`); return resolveNamesWithLocalCache( typeDirectiveNames, containingFile, resolvedTypeReferenceDirectives, perDirectoryResolvedTypeReferenceDirectives, @@ -271,6 +271,7 @@ namespace ts { } function resolveModuleNames(moduleNames: string[], containingFile: string, logChanges: boolean): ResolvedModuleFull[] { + resolutionHost.writeLog(`resolveModuleNames[${resolvedModuleNames.size}"]: " + ${containingFile}`); return resolveNamesWithLocalCache( moduleNames, containingFile, resolvedModuleNames, perDirectoryResolvedModuleNames, @@ -279,35 +280,19 @@ namespace ts { ); } - function watchFailedLookupLocation(failedLookupLocation: string, failedLookupLocationPath: Path) { - const failedLocationDirPath = getDirectoryPath(failedLookupLocationPath); - const cachedDirPath = failedLookupLocationToDirPath.get(failedLocationDirPath); - if (cachedDirPath) { - watchFailedLookupLocationInDirectory(cachedDirPath, failedLookupLocationPath, /*dir*/ undefined); - return; - } + function isNodeModulesDirectory(dirPath: Path) { + return endsWith(dirPath, "/node_modules"); + } + type DirectoryOfFailedLookupWatch = { dir: string; dirPath: Path; }; + function getDirectoryToWatchFailedLookupLocation(failedLookupLocation: string, failedLookupLocationPath: Path): DirectoryOfFailedLookupWatch { if (isInDirectoryPath(rootPath, failedLookupLocationPath)) { - // Watch in directory of rootPath - failedLookupLocationToDirPath.set(failedLocationDirPath, rootPath); - watchFailedLookupLocationInDirectory(rootPath, failedLookupLocationPath, rootDir); - return; + return { dir: rootDir, dirPath: rootPath }; } - const { dir, dirPath } = getDirectoryToWatchWhenNotInRootPath( - getDirectoryPath(getNormalizedAbsolutePath(failedLookupLocation, getCurrentDirectory())), - getDirectoryPath(failedLookupLocationPath) - ); - failedLookupLocationToDirPath.set(failedLocationDirPath, dirPath); - watchFailedLookupLocationInDirectory(dirPath, failedLookupLocationPath, dir); - } - - function isNodeModulesDirectory(dirPath: Path) { - const nodemodules = "/node_modules"; - return endsWith(dirPath, nodemodules); - } + let dir = getDirectoryPath(getNormalizedAbsolutePath(failedLookupLocation, getCurrentDirectory())); + let dirPath = getDirectoryPath(failedLookupLocationPath); - function getDirectoryToWatchWhenNotInRootPath(dir: string, dirPath: Path): { dir: string, dirPath: Path } { // If the directory is node_modules use it to watch if (isNodeModulesDirectory(dirPath)) { return { dir, dirPath }; @@ -337,21 +322,22 @@ namespace ts { return { dir, dirPath }; } - function watchFailedLookupLocationInDirectory(dirPath: Path, failedLookupLocationPath: Path, dir: string | undefined) { - const watches = directoryWatchesOfFailedLookups.get(dirPath); - if (watches) { - const existingCount = watches.mapLocations.get(failedLookupLocationPath) || 0; - watches.mapLocations.set(failedLookupLocationPath, existingCount + 1); - } - else { - Debug.assert(dir !== undefined); - const mapLocations = createMap(); - mapLocations.set(failedLookupLocationPath, 1); - directoryWatchesOfFailedLookups.set(dirPath, { - watcher: createDirectoryWatcher(dir, dirPath), - mapLocations - }); - } + function watchFailedLookupLocationForCache( + cache: Map>, seenDirectories: Map + ) { + cache.forEach(value => value && value.forEach( + resolution => forEach(resolution && resolution.failedLookupLocations, + failedLookupLocation => { + const { dir, dirPath } = getDirectoryToWatchFailedLookupLocation(failedLookupLocation, resolutionHost.toPath(failedLookupLocation)); + if (!seenDirectories.has(dirPath)) { + if (!directoryWatchesOfFailedLookups.has(dirPath)) { + directoryWatchesOfFailedLookups.set(dirPath, createDirectoryWatcher(dir, dirPath)); + } + seenDirectories.set(dirPath, true); + } + } + ) + )); } function createDirectoryWatcher(directory: string, dirPath: Path) { @@ -365,62 +351,18 @@ namespace ts { // If the files are added to project root or node_modules directory, always run through the invalidation process // Otherwise run through invalidation only if adding to the immediate directory if (dirPath === rootPath || isNodeModulesDirectory(dirPath) || getDirectoryPath(fileOrFolderPath) === dirPath) { - // If the location results in update to failed lookup, schedule program update - if (dirPath === fileOrFolderPath) { - if (onAddOrRemoveDirectoryOfFailedLookup(dirPath)) { - resolutionHost.onInvalidatedResolution(); - } - } - else if (onFileAddOrRemoveInDirectoryOfFailedLookup(dirPath, fileOrFolderPath)) { + const isChangedFailedLookupLocation: (location: string) => boolean = dirPath === fileOrFolderPath ? + // If the file watched directory is created/deleted invalidate any resolution has failed lookup in this directory + location => isInDirectoryPath(dirPath, resolutionHost.toPath(location)) : + // Otherwise only the resolutions referencing the file or folder added + location => resolutionHost.toPath(location) === fileOrFolderPath; + if (invalidateResolutionOfFailedLookupLocation(isChangedFailedLookupLocation)) { resolutionHost.onInvalidatedResolution(); } } }, WatchDirectoryFlags.Recursive); } - function closeFailedLookupLocationWatcher(_failedLookupLocation: string, failedLookupLocationPath: Path) { - const failedLookupDirectory = getDirectoryPath(failedLookupLocationPath); - const dirPath = failedLookupLocationToDirPath.get(failedLookupDirectory); - const watches = directoryWatchesOfFailedLookups.get(dirPath); - const refCount = watches.mapLocations.get(failedLookupLocationPath); - if (refCount === 1) { - // If this was last failed lookup location being tracked by the dir watcher, - // remove the failed lookup location path to dir Path entry - watches.mapLocations.delete(failedLookupLocationPath); - - // If there are no more files that need this watcher alive, close the watcher - if (watches.mapLocations.size === 0) { - watches.watcher.close(); - directoryWatchesOfFailedLookups.delete(dirPath); - failedLookupLocationToDirPath.delete(failedLookupDirectory); - } - } - else { - watches.mapLocations.set(failedLookupLocationPath, refCount - 1); - } - } - - type FailedLookupLocationAction = (failedLookupLocation: string, failedLookupLocationPath: Path) => void; - function withFailedLookupLocations(failedLookupLocations: ReadonlyArray | undefined, fn: FailedLookupLocationAction, startIndex?: number) { - if (failedLookupLocations) { - for (let i = startIndex || 0; i < failedLookupLocations.length; i++) { - fn(failedLookupLocations[i], resolutionHost.toPath(failedLookupLocations[i])); - } - } - } - - function updateFailedLookupLocationWatches(failedLookupLocations: ReadonlyArray | undefined, existingFailedLookupLocations: ReadonlyArray | undefined) { - const index = existingFailedLookupLocations && failedLookupLocations ? - findDiffIndex(failedLookupLocations, existingFailedLookupLocations) : - 0; - - // Watch all the failed lookup locations - withFailedLookupLocations(failedLookupLocations, watchFailedLookupLocation, index); - - // Close existing watches for the failed locations - withFailedLookupLocations(existingFailedLookupLocations, closeFailedLookupLocationWatcher, index); - } - function invalidateResolutionCacheOfDeletedFile( deletedFilePath: Path, cache: Map>, @@ -429,9 +371,8 @@ namespace ts { cache.forEach((value, path) => { if (path === deletedFilePath) { cache.delete(path); - value.forEach(resolution => { - withFailedLookupLocations(resolution.failedLookupLocations, closeFailedLookupLocationWatcher); - }); + hasChangesInFailedLookupLocations = hasChangesInFailedLookupLocations || + value && forEachEntry(value, resolution => !!resolution.failedLookupLocations); } else if (value) { value.forEach(resolution => { @@ -449,7 +390,12 @@ namespace ts { }); } - function invalidateResolutionCacheOfChangedFailedLookupLocation( + function invalidateResolutionOfFile(filePath: Path) { + invalidateResolutionCacheOfDeletedFile(filePath, resolvedModuleNames, m => m.resolvedModule, r => r.resolvedFileName); + invalidateResolutionCacheOfDeletedFile(filePath, resolvedTypeReferenceDirectives, m => m.resolvedTypeReferenceDirective, r => r.resolvedFileName); + } + + function invalidateResolutionCacheOfFailedLookupLocation( cache: Map>, isChangedFailedLookupLocation: (location: string) => boolean ) { @@ -466,29 +412,11 @@ namespace ts { }); } - function invalidateResolutionOfFile(filePath: Path) { - invalidateResolutionCacheOfDeletedFile(filePath, resolvedModuleNames, m => m.resolvedModule, r => r.resolvedFileName); - invalidateResolutionCacheOfDeletedFile(filePath, resolvedTypeReferenceDirectives, m => m.resolvedTypeReferenceDirective, r => r.resolvedFileName); - } - - function onFileAddOrRemoveInDirectoryOfFailedLookup(dirPath: Path, fileOrFolder: Path) { - const watches = directoryWatchesOfFailedLookups.get(dirPath); - if (watches.mapLocations.has(fileOrFolder)) { - const invalidatedFiles = filesWithInvalidatedResolutions && filesWithInvalidatedResolutions.size; - const isFileOrFolder: (location: string) => boolean = location => resolutionHost.toPath(location) === fileOrFolder; - invalidateResolutionCacheOfChangedFailedLookupLocation(resolvedModuleNames, isFileOrFolder); - invalidateResolutionCacheOfChangedFailedLookupLocation(resolvedTypeReferenceDirectives, isFileOrFolder); - return filesWithInvalidatedResolutions && filesWithInvalidatedResolutions.size !== invalidatedFiles; - } - return false; - } - - function onAddOrRemoveDirectoryOfFailedLookup(dirPath: Path) { - const invalidatedFiles = filesWithInvalidatedResolutions && filesWithInvalidatedResolutions.size; - const isInDirPath: (location: string) => boolean = location => isInDirectoryPath(dirPath, resolutionHost.toPath(location)); - invalidateResolutionCacheOfChangedFailedLookupLocation(resolvedModuleNames, isInDirPath); - invalidateResolutionCacheOfChangedFailedLookupLocation(resolvedTypeReferenceDirectives, isInDirPath); - filesWithInvalidatedResolutions && filesWithInvalidatedResolutions.size !== invalidatedFiles; + function invalidateResolutionOfFailedLookupLocation(isChangedFailedLookupLocation: (location: string) => boolean) { + const invalidatedFilesCount = filesWithInvalidatedResolutions && filesWithInvalidatedResolutions.size; + invalidateResolutionCacheOfFailedLookupLocation(resolvedModuleNames, isChangedFailedLookupLocation); + invalidateResolutionCacheOfFailedLookupLocation(resolvedTypeReferenceDirectives, isChangedFailedLookupLocation); + return filesWithInvalidatedResolutions && filesWithInvalidatedResolutions.size !== invalidatedFilesCount; } function closeTypeRootsWatch() { diff --git a/src/server/project.ts b/src/server/project.ts index 3fb2d74795fe9..24b3cd03de423 100644 --- a/src/server/project.ts +++ b/src/server/project.ts @@ -789,6 +789,7 @@ namespace ts.server { private updateGraphWorker() { const oldProgram = this.program; + this.writeLog(`Starting Update graph worker: Project: ${this.getProjectName()}`); this.resolutionCache.startCachingPerDirectoryResolution(); this.program = this.languageService.getProgram(); this.resolutionCache.finishCachingPerDirectoryResolution(); @@ -843,6 +844,7 @@ namespace ts.server { } }); + this.writeLog(`Finishing Update graph worker: Project: ${this.getProjectName()}`); return hasChanges; } From 9e5e20c80fdb4e8bbccfec437f4ab498db59d381 Mon Sep 17 00:00:00 2001 From: Sheetal Nandi Date: Thu, 31 Aug 2017 17:58:09 -0700 Subject: [PATCH 087/109] Remove the configured project if on next open file if it has no open files instead of immediately when closing last open file --- src/server/editorServices.ts | 17 +++++++++++++---- src/server/project.ts | 4 ++++ 2 files changed, 17 insertions(+), 4 deletions(-) diff --git a/src/server/editorServices.ts b/src/server/editorServices.ts index e6784942c6664..0679ccd839318 100644 --- a/src/server/editorServices.ts +++ b/src/server/editorServices.ts @@ -878,10 +878,10 @@ namespace ts.server { if (info.hasMixedContent) { info.registerFileUpdate(); } - // last open file in configured project - close it - if ((p).deleteOpenRef() === 0) { - (projectsToRemove || (projectsToRemove = [])).push(p); - } + // Delete the reference to the open configured projects but + // do not remove the project so that we can reuse this project + // if it would need to be re-created with next file open + (p).deleteOpenRef(); } else if (p.projectKind === ProjectKind.Inferred && p.isRoot(info)) { // If this was the open root file of inferred project @@ -1881,6 +1881,15 @@ namespace ts.server { } this.addToListOfOpenFiles(info); + // Remove the configured projects that have zero references from open files. + // This was postponed from closeOpenFile to after opening next file, + // so that we can reuse the project if we need to right away + this.configuredProjects.forEach(project => { + if (!project.hasOpenRef()) { + this.removeProject(project); + } + }); + // Delete the orphan files here because there might be orphan script infos (which are not part of project) // when some file/s were closed which resulted in project removal. // It was then postponed to cleanup these script infos so that they can be reused if diff --git a/src/server/project.ts b/src/server/project.ts index 24b3cd03de423..37b0bb3cd7f59 100644 --- a/src/server/project.ts +++ b/src/server/project.ts @@ -1311,6 +1311,10 @@ namespace ts.server { return this.openRefCount; } + hasOpenRef() { + return !!this.openRefCount; + } + getEffectiveTypeRoots() { return getEffectiveTypeRoots(this.getCompilationSettings(), this.partialSystem) || []; } From 13aafa26ba84b7c8af5e11886bc175db63321be4 Mon Sep 17 00:00:00 2001 From: Sheetal Nandi Date: Fri, 1 Sep 2017 11:08:00 -0700 Subject: [PATCH 088/109] Update tests --- src/harness/unittests/telemetry.ts | 22 +-- .../unittests/tsserverProjectSystem.ts | 130 +++++++++++++++--- 2 files changed, 127 insertions(+), 25 deletions(-) diff --git a/src/harness/unittests/telemetry.ts b/src/harness/unittests/telemetry.ts index e7a64c59903a5..7c09806632180 100644 --- a/src/harness/unittests/telemetry.ts +++ b/src/harness/unittests/telemetry.ts @@ -11,18 +11,24 @@ namespace ts.projectSystem { }); it("only sends an event once", () => { - const file = makeFile("/a.ts"); - const tsconfig = makeFile("/tsconfig.json", {}); + const file = makeFile("/a/a.ts"); + const file2 = makeFile("/b.ts"); + const tsconfig = makeFile("/a/tsconfig.json", {}); - const et = new EventTracker([file, tsconfig]); + const et = new EventTracker([file, file2, tsconfig]); et.service.openClientFile(file.path); - et.assertProjectInfoTelemetryEvent({}); + et.assertProjectInfoTelemetryEvent({}, tsconfig.path); et.service.closeClientFile(file.path); - checkNumberOfProjects(et.service, { configuredProjects: 0 }); + checkNumberOfProjects(et.service, { configuredProjects: 1 }); + + et.service.openClientFile(file2.path); + checkNumberOfProjects(et.service, { inferredProjects: 1 }); + + assert.equal(et.getEvents().length, 0); et.service.openClientFile(file.path); - checkNumberOfProjects(et.service, { configuredProjects: 1 }); + checkNumberOfProjects(et.service, { configuredProjects: 1, inferredProjects: 1 }); assert.equal(et.getEvents().length, 0); }); @@ -249,9 +255,9 @@ namespace ts.projectSystem { return events; } - assertProjectInfoTelemetryEvent(partial: Partial): void { + assertProjectInfoTelemetryEvent(partial: Partial, configFile?: string): void { assert.deepEqual(this.getEvent(ts.server.ProjectInfoTelemetryEvent), { - projectId: Harness.mockHash("/tsconfig.json"), + projectId: Harness.mockHash(configFile || "/tsconfig.json"), fileStats: fileStats({ ts: 1 }), compilerOptions: {}, extends: false, diff --git a/src/harness/unittests/tsserverProjectSystem.ts b/src/harness/unittests/tsserverProjectSystem.ts index ae227bb683c86..d7de270645b85 100644 --- a/src/harness/unittests/tsserverProjectSystem.ts +++ b/src/harness/unittests/tsserverProjectSystem.ts @@ -958,7 +958,42 @@ namespace ts.projectSystem { checkProjectActualFiles(projectService.inferredProjects[0], [file2.path, file3.path, libFile.path]); }); - it("should close configured project after closing last open file", () => { + it("should resuse same project if file is opened from the configured project that has no open files", () => { + const file1 = { + path: "/a/b/main.ts", + content: "let x =1;" + }; + const file2 = { + path: "/a/b/main2.ts", + content: "let y =1;" + }; + const configFile: FileOrFolder = { + path: "/a/b/tsconfig.json", + content: `{ + "compilerOptions": { + "target": "es6" + }, + "files": [ "main.ts", "main2.ts" ] + }` + }; + const host = createServerHost([file1, file2, configFile, libFile]); + const projectService = createProjectService(host, { useSingleInferredProject: true }); + projectService.openClientFile(file1.path); + checkNumberOfConfiguredProjects(projectService, 1); + const project = projectService.configuredProjects.get(configFile.path); + + projectService.closeClientFile(file1.path); + checkNumberOfConfiguredProjects(projectService, 1); + assert.strictEqual(projectService.configuredProjects.get(configFile.path), project); + assert.equal(project.openRefCount, 0); + + projectService.openClientFile(file2.path); + checkNumberOfConfiguredProjects(projectService, 1); + assert.strictEqual(projectService.configuredProjects.get(configFile.path), project); + assert.equal(project.openRefCount, 1); + }); + + it("should not close configured project after closing last open file, but should be closed on next file open if its not the file from same project", () => { const file1 = { path: "/a/b/main.ts", content: "let x =1;" @@ -976,8 +1011,14 @@ namespace ts.projectSystem { const projectService = createProjectService(host, { useSingleInferredProject: true }); projectService.openClientFile(file1.path); checkNumberOfConfiguredProjects(projectService, 1); + const project = projectService.configuredProjects.get(configFile.path); projectService.closeClientFile(file1.path); + checkNumberOfConfiguredProjects(projectService, 1); + assert.strictEqual(projectService.configuredProjects.get(configFile.path), project); + assert.equal(project.openRefCount, 0); + + projectService.openClientFile(libFile.path); checkNumberOfConfiguredProjects(projectService, 0); }); @@ -1058,23 +1099,43 @@ namespace ts.projectSystem { }); checkNumberOfProjects(projectService, { configuredProjects: 2 }); + const proj1 = projectService.configuredProjects.get(config1.path); + const proj2 = projectService.configuredProjects.get(config2.path); + assert.isDefined(proj1); + assert.isDefined(proj2); // open client file - should not lead to creation of inferred project projectService.openClientFile(file1.path, file1.content); checkNumberOfProjects(projectService, { configuredProjects: 2 }); + assert.strictEqual(projectService.configuredProjects.get(config1.path), proj1); + assert.strictEqual(projectService.configuredProjects.get(config2.path), proj2); projectService.openClientFile(file3.path, file3.content); checkNumberOfProjects(projectService, { configuredProjects: 2, inferredProjects: 1 }); + assert.strictEqual(projectService.configuredProjects.get(config1.path), proj1); + assert.strictEqual(projectService.configuredProjects.get(config2.path), proj2); projectService.closeExternalProject(externalProjectName); // open file 'file1' from configured project keeps project alive checkNumberOfProjects(projectService, { configuredProjects: 1, inferredProjects: 1 }); + assert.strictEqual(projectService.configuredProjects.get(config1.path), proj1); + assert.isUndefined(projectService.configuredProjects.get(config2.path)); projectService.closeClientFile(file3.path); checkNumberOfProjects(projectService, { configuredProjects: 1 }); + assert.strictEqual(projectService.configuredProjects.get(config1.path), proj1); + assert.isUndefined(projectService.configuredProjects.get(config2.path)); projectService.closeClientFile(file1.path); - checkNumberOfProjects(projectService, {}); + checkNumberOfProjects(projectService, { configuredProjects: 1 }); + assert.strictEqual(projectService.configuredProjects.get(config1.path), proj1); + assert.isUndefined(projectService.configuredProjects.get(config2.path)); + + projectService.openClientFile(file2.path, file2.content); + checkNumberOfProjects(projectService, { configuredProjects: 1 }); + assert.isUndefined(projectService.configuredProjects.get(config1.path)); + assert.isDefined(projectService.configuredProjects.get(config2.path)); + }); it("reload regular file after closing", () => { @@ -1177,16 +1238,21 @@ namespace ts.projectSystem { path: "/a/b/f1.ts", content: "let x = 1" }; + const file2 = { + path: "/a/f2.ts", + content: "let x = 1" + }; const configFile = { path: "/a/b/tsconfig.json", content: JSON.stringify({ compilerOptions: {} }) }; const externalProjectName = "externalproject"; - const host = createServerHost([file1, configFile]); + const host = createServerHost([file1, file2, libFile, configFile]); const projectService = createProjectService(host); projectService.openClientFile(file1.path); checkNumberOfProjects(projectService, { configuredProjects: 1 }); + const project = projectService.configuredProjects.get(configFile.path); projectService.openExternalProject({ rootFiles: toExternalFiles([configFile.path]), @@ -1195,13 +1261,20 @@ namespace ts.projectSystem { }); checkNumberOfProjects(projectService, { configuredProjects: 1 }); + assert.strictEqual(projectService.configuredProjects.get(configFile.path), project); projectService.closeExternalProject(externalProjectName); // configured project is alive since file is still open checkNumberOfProjects(projectService, { configuredProjects: 1 }); + assert.strictEqual(projectService.configuredProjects.get(configFile.path), project); projectService.closeClientFile(file1.path); - checkNumberOfProjects(projectService, {}); + checkNumberOfProjects(projectService, { configuredProjects: 1 }); + assert.strictEqual(projectService.configuredProjects.get(configFile.path), project); + + projectService.openClientFile(file2.path); + checkNumberOfProjects(projectService, { inferredProjects: 1 }); + assert.isUndefined(projectService.configuredProjects.get(configFile.path)); }); it("changes in closed files are reflected in project structure", () => { @@ -1607,11 +1680,12 @@ namespace ts.projectSystem { projectService.checkNumberOfProjects({ configuredProjects: 1 }); checkProjectActualFiles(configuredProjectAt(projectService, 0), [f1.path, config.path]); + // Should close configured project with next file open projectService.closeClientFile(f1.path); projectService.openClientFile(f2.path); - projectService.checkNumberOfProjects({ configuredProjects: 1, inferredProjects: 1 }); - checkProjectActualFiles(configuredProjectAt(projectService, 0), [f1.path, config.path]); + projectService.checkNumberOfProjects({ inferredProjects: 1 }); + assert.isUndefined(projectService.configuredProjects.get(config.path)); checkProjectActualFiles(projectService.inferredProjects[0], [f2.path]); }); @@ -1910,15 +1984,16 @@ namespace ts.projectSystem { projectService.openClientFile(file2.path); checkNumberOfProjects(projectService, { configuredProjects: 1 }); - const project1 = configuredProjectAt(projectService, 0); + const project1 = projectService.configuredProjects.get(tsconfig1.path); assert.equal(project1.openRefCount, 1, "Open ref count in project1 - 1"); assert.equal(project1.getScriptInfo(file2.path).containingProjects.length, 1, "containing projects count"); projectService.openClientFile(file1.path); checkNumberOfProjects(projectService, { configuredProjects: 2 }); assert.equal(project1.openRefCount, 2, "Open ref count in project1 - 2"); + assert.strictEqual(projectService.configuredProjects.get(tsconfig1.path), project1); - const project2 = configuredProjectAt(projectService, 1); + const project2 = projectService.configuredProjects.get(tsconfig2.path); assert.equal(project2.openRefCount, 1, "Open ref count in project2 - 2"); assert.equal(project1.getScriptInfo(file1.path).containingProjects.length, 2, `${file1.path} containing projects count`); @@ -1928,9 +2003,21 @@ namespace ts.projectSystem { checkNumberOfProjects(projectService, { configuredProjects: 2 }); assert.equal(project1.openRefCount, 1, "Open ref count in project1 - 3"); assert.equal(project2.openRefCount, 1, "Open ref count in project2 - 3"); + assert.strictEqual(projectService.configuredProjects.get(tsconfig1.path), project1); + assert.strictEqual(projectService.configuredProjects.get(tsconfig2.path), project2); projectService.closeClientFile(file1.path); - checkNumberOfProjects(projectService, { configuredProjects: 0 }); + checkNumberOfProjects(projectService, { configuredProjects: 2 }); + assert.equal(project1.openRefCount, 0, "Open ref count in project1 - 4"); + assert.equal(project2.openRefCount, 0, "Open ref count in project2 - 4"); + assert.strictEqual(projectService.configuredProjects.get(tsconfig1.path), project1); + assert.strictEqual(projectService.configuredProjects.get(tsconfig2.path), project2); + + projectService.openClientFile(file2.path); + checkNumberOfProjects(projectService, { configuredProjects: 1 }); + assert.strictEqual(projectService.configuredProjects.get(tsconfig1.path), project1); + assert.isUndefined(projectService.configuredProjects.get(tsconfig2.path)); + assert.equal(project1.openRefCount, 1, "Open ref count in project1 - 5"); }); it("language service disabled state is updated in external projects", () => { @@ -2000,14 +2087,18 @@ namespace ts.projectSystem { const projectService = createProjectService(host); projectService.openClientFile(f1.path); projectService.checkNumberOfProjects({ configuredProjects: 1 }); + const project = projectService.configuredProjects.get(config.path); projectService.closeClientFile(f1.path); - projectService.checkNumberOfProjects({}); + projectService.checkNumberOfProjects({ configuredProjects: 1 }); + assert.strictEqual(projectService.configuredProjects.get(config.path), project); + assert.equal(project.openRefCount, 0); for (const f of [f1, f2, f3]) { // There shouldnt be any script info as we closed the file that resulted in creation of it const scriptInfo = projectService.getScriptInfoForNormalizedPath(server.toNormalizedPath(f.path)); - assert.equal(scriptInfo.containingProjects.length, 0, `expect 0 containing projects for '${f.path}'`); + assert.equal(scriptInfo.containingProjects.length, 1, `expect 1 containing projects for '${f.path}'`); + assert.equal(scriptInfo.containingProjects[0], project, `expect configured project to be the only containing project for '${f.path}'`); } }); @@ -2551,12 +2642,18 @@ namespace ts.projectSystem { const projectService = createProjectService(host); projectService.openClientFile(f.path); projectService.checkNumberOfProjects({ configuredProjects: 1 }); + const project = projectService.configuredProjects.get(config.path); + assert.equal(project.openRefCount, 1); projectService.closeClientFile(f.path); - projectService.checkNumberOfProjects({ configuredProjects: 0 }); + projectService.checkNumberOfProjects({ configuredProjects: 1 }); + assert.strictEqual(projectService.configuredProjects.get(config.path), project); + assert.equal(project.openRefCount, 0); projectService.openClientFile(f.path); projectService.checkNumberOfProjects({ configuredProjects: 1 }); + assert.strictEqual(projectService.configuredProjects.get(config.path), project); + assert.equal(project.openRefCount, 1); }); }); @@ -3237,12 +3334,10 @@ namespace ts.projectSystem { const projectService = createProjectService(host); projectService.openClientFile(file1.path); host.runQueuedTimeoutCallbacks(); - checkNumberOfConfiguredProjects(projectService, 1); + // Since there is no file open from configFile it would be closed + checkNumberOfConfiguredProjects(projectService, 0); checkNumberOfInferredProjects(projectService, 1); - const configuredProject = configuredProjectAt(projectService, 0); - checkProjectActualFiles(configuredProject, [configFile.path]); - const inferredProject = projectService.inferredProjects[0]; assert.isTrue(inferredProject.containsFile(file1.path)); }); @@ -3271,7 +3366,8 @@ namespace ts.projectSystem { const projectService = createProjectService(host); projectService.openClientFile(f.path); - projectService.checkNumberOfProjects({ configuredProjects: 1, inferredProjects: 1 }); + // Since no file from the configured project is open, it would be closed immediately + projectService.checkNumberOfProjects({ configuredProjects: 0, inferredProjects: 1 }); }); }); From 6c6129361f964cae2d0a80c9f7da25d8375af2f8 Mon Sep 17 00:00:00 2001 From: Sheetal Nandi Date: Fri, 1 Sep 2017 16:22:37 -0700 Subject: [PATCH 089/109] Test to verify calls to isProgramUptoDate return true when there is no change in compiler options --- .../unittests/reuseProgramStructure.ts | 168 ++++++++++++++++++ 1 file changed, 168 insertions(+) diff --git a/src/harness/unittests/reuseProgramStructure.ts b/src/harness/unittests/reuseProgramStructure.ts index f2e269763ab59..dee4d7494873b 100644 --- a/src/harness/unittests/reuseProgramStructure.ts +++ b/src/harness/unittests/reuseProgramStructure.ts @@ -869,4 +869,172 @@ namespace ts { createProgram([], {}); }); }); + + import TestSystem = ts.TestFSWithWatch.TestServerHost; + type FileOrFolder = ts.TestFSWithWatch.FileOrFolder; + import createTestSystem = ts.TestFSWithWatch.createWatchedSystem; + import libFile = ts.TestFSWithWatch.libFile; + + describe("isProgramUptoDate should return true when there is no change in compiler options and", () => { + function verifyProgramIsUptoDate( + program: Program, + newRootFileNames: string[], + newOptions: CompilerOptions + ) { + const actual = isProgramUptoDate( + program, newRootFileNames, newOptions, + path => program.getSourceFileByPath(path).version, /*fileExists*/ returnFalse, + /*hasInvalidatedResolution*/ returnFalse, + /*hasChangedAutomaticTypeDirectiveNames*/ returnFalse + ); + assert.isTrue(actual); + } + + function duplicate(options: CompilerOptions): CompilerOptions; + function duplicate(fileNames: string[]): string[]; + function duplicate(filesOrOptions: CompilerOptions | string[]) { + return JSON.parse(JSON.stringify(filesOrOptions)); + } + + function createWatchingSystemHost(host: TestSystem) { + return ts.createWatchingSystemHost(/*pretty*/ undefined, host); + } + + function verifyProgramWithoutConfigFile(watchingSystemHost: WatchingSystemHost, rootFiles: string[], options: CompilerOptions) { + const program = createWatchModeWithoutConfigFile(rootFiles, options, watchingSystemHost)(); + verifyProgramIsUptoDate(program, duplicate(rootFiles), duplicate(options)); + } + + function getConfigParseResult(watchingSystemHost: WatchingSystemHost, configFileName: string) { + return parseConfigFile(configFileName, {}, watchingSystemHost.system, watchingSystemHost.reportDiagnostic, watchingSystemHost.reportWatchDiagnostic); + } + + function verifyProgramWithConfigFile(watchingSystemHost: WatchingSystemHost, configFile: string) { + const result = getConfigParseResult(watchingSystemHost, configFile); + const program = createWatchModeWithConfigFile(result, {}, watchingSystemHost)(); + const { fileNames, options } = getConfigParseResult(watchingSystemHost, configFile); + verifyProgramIsUptoDate(program, fileNames, options); + } + + function verifyProgram(files: FileOrFolder[], rootFiles: string[], options: CompilerOptions, configFile: string) { + const watchingSystemHost = createWatchingSystemHost(createTestSystem(files)); + verifyProgramWithoutConfigFile(watchingSystemHost, rootFiles, options); + verifyProgramWithConfigFile(watchingSystemHost, configFile); + } + + it("has empty options", () => { + const file1: FileOrFolder = { + path: "/a/b/file1.ts", + content: "let x = 1" + }; + const file2: FileOrFolder = { + path: "/a/b/file2.ts", + content: "let y = 1" + }; + const configFile: FileOrFolder = { + path: "/a/b/tsconfig.json", + content: "{}" + }; + verifyProgram([file1, file2, libFile, configFile], [file1.path, file2.path], {}, configFile.path); + }); + + it("has lib specified in the options", () => { + const compilerOptions: CompilerOptions = { lib: ["es5", "es2015.promise"] }; + const app: FileOrFolder = { + path: "/src/app.ts", + content: "var x: Promise;" + }; + const configFile: FileOrFolder = { + path: "/src/tsconfig.json", + content: JSON.stringify({ compilerOptions }) + }; + const es5Lib: FileOrFolder = { + path: "/compiler/lib.es5.d.ts", + content: "declare const eval: any" + }; + const es2015Promise: FileOrFolder = { + path: "/compiler/lib.es2015.promise.d.ts", + content: "declare class Promise {}" + }; + + verifyProgram([app, configFile, es5Lib, es2015Promise], [app.path], compilerOptions, configFile.path); + }); + + it("has paths specified in the options", () => { + const compilerOptions: CompilerOptions = { + baseUrl: ".", + paths: { + "*": [ + "packages/mail/data/*", + "packages/styles/*", + "*" + ] + } + }; + const app: FileOrFolder = { + path: "/src/packages/framework/app.ts", + content: 'import classc from "module1/lib/file1";\ + import classD from "module3/file3";\ + let x = new classc();\ + let y = new classD();' + }; + const module1: FileOrFolder = { + path: "/src/packages/mail/data/module1/lib/file1.ts", + content: 'import classc from "module2/file2";export default classc;', + }; + const module2: FileOrFolder = { + path: "/src/packages/mail/data/module1/lib/module2/file2.ts", + content: 'class classc { method2() { return "hello"; } }\nexport default classc', + }; + const module3: FileOrFolder = { + path: "/src/packages/styles/module3/file3.ts", + content: "class classD { method() { return 10; } }\nexport default classD;" + }; + const configFile: FileOrFolder = { + path: "/src/tsconfig.json", + content: JSON.stringify({ compilerOptions }) + }; + + verifyProgram([app, module1, module2, module3, libFile, configFile], [app.path], compilerOptions, configFile.path); + }); + + it("has include paths specified in tsconfig file", () => { + const compilerOptions: CompilerOptions = { + baseUrl: ".", + paths: { + "*": [ + "packages/mail/data/*", + "packages/styles/*", + "*" + ] + } + }; + const app: FileOrFolder = { + path: "/src/packages/framework/app.ts", + content: 'import classc from "module1/lib/file1";\ + import classD from "module3/file3";\ + let x = new classc();\ + let y = new classD();' + }; + const module1: FileOrFolder = { + path: "/src/packages/mail/data/module1/lib/file1.ts", + content: 'import classc from "module2/file2";export default classc;', + }; + const module2: FileOrFolder = { + path: "/src/packages/mail/data/module1/lib/module2/file2.ts", + content: 'class classc { method2() { return "hello"; } }\nexport default classc', + }; + const module3: FileOrFolder = { + path: "/src/packages/styles/module3/file3.ts", + content: "class classD { method() { return 10; } }\nexport default classD;" + }; + const configFile: FileOrFolder = { + path: "/src/tsconfig.json", + content: JSON.stringify({ compilerOptions, include: ["packages/**/ *.ts"] }) + }; + + const watchingSystemHost = createWatchingSystemHost(createTestSystem([app, module1, module2, module3, libFile, configFile])); + verifyProgramWithConfigFile(watchingSystemHost, configFile.path); + }); + }); } From 7b2bab5b86cf24ad11417b19de42551f941de548 Mon Sep 17 00:00:00 2001 From: Sheetal Nandi Date: Tue, 5 Sep 2017 14:19:40 -0700 Subject: [PATCH 090/109] Revert to use refcount to keep track of directory watchers for failed lookup --- src/compiler/resolutionCache.ts | 172 ++++++++++++++++++-------------- src/server/editorServices.ts | 5 +- src/server/project.ts | 10 +- 3 files changed, 104 insertions(+), 83 deletions(-) diff --git a/src/compiler/resolutionCache.ts b/src/compiler/resolutionCache.ts index 1782127dc9f26..3d5d3e0a84506 100644 --- a/src/compiler/resolutionCache.ts +++ b/src/compiler/resolutionCache.ts @@ -43,6 +43,13 @@ namespace ts { writeLog(s: string): void; } + interface DirectoryWatchesOfFailedLookup { + /** watcher for the directory of failed lookup */ + watcher: FileWatcher; + /** ref count keeping this directory watch alive */ + refCount: number; + } + export function createResolutionCache(resolutionHost: ResolutionCacheHost): ResolutionCache { let filesWithChangedSetOfUnresolvedImports: Path[] | undefined; let filesWithInvalidatedResolutions: Map | undefined; @@ -56,8 +63,7 @@ namespace ts { const perDirectoryResolvedTypeReferenceDirectives = createMap>(); const getCurrentDirectory = memoize(() => resolutionHost.getCurrentDirectory()); - const directoryWatchesOfFailedLookups = createMap(); - let hasChangesInFailedLookupLocations = false; + const directoryWatchesOfFailedLookups = createMap(); let rootDir: string; let rootPath: Path; @@ -93,8 +99,7 @@ namespace ts { } function clear() { - clearMap(directoryWatchesOfFailedLookups, closeFileWatcher); - hasChangesInFailedLookupLocations = false; + clearMap(directoryWatchesOfFailedLookups, closeFileWatcherOf); closeTypeRootsWatch(); resolvedModuleNames.clear(); resolvedTypeReferenceDirectives.clear(); @@ -122,17 +127,12 @@ namespace ts { } function finishCachingPerDirectoryResolution() { - if (hasChangesInFailedLookupLocations) { - const seenDirectories = createMap(); - watchFailedLookupLocationForCache(perDirectoryResolvedModuleNames, seenDirectories); - watchFailedLookupLocationForCache(perDirectoryResolvedTypeReferenceDirectives, seenDirectories); - directoryWatchesOfFailedLookups.forEach((watcher, path) => { - if (!seenDirectories.has(path)) { - watcher.close(); - } - }); - hasChangesInFailedLookupLocations = false; - } + directoryWatchesOfFailedLookups.forEach((watcher, path) => { + if (watcher.refCount === 0) { + directoryWatchesOfFailedLookups.delete(path); + watcher.watcher.close(); + } + }); perDirectoryResolvedModuleNames.clear(); perDirectoryResolvedTypeReferenceDirectives.clear(); @@ -171,7 +171,7 @@ namespace ts { logChanges: boolean): R[] { const path = resolutionHost.toPath(containingFile); - const currentResolutionsInFile = cache.get(path); + const resolutionsInFile = cache.get(path) || cache.set(path, createMap()).get(path); const dirPath = getDirectoryPath(path); let perDirectoryResolution = perDirectoryCache.get(dirPath); if (!perDirectoryResolution) { @@ -179,53 +179,67 @@ namespace ts { perDirectoryCache.set(dirPath, perDirectoryResolution); } - const newResolutions: Map = createMap(); const resolvedModules: R[] = []; const compilerOptions = resolutionHost.getCompilationSettings(); + const seenNamesInFile = createMap(); for (const name of names) { // check if this is a duplicate entry in the list - let resolution = newResolutions.get(name); - if (!resolution) { - const existingResolution = currentResolutionsInFile && currentResolutionsInFile.get(name); - if (existingResolution) { - // Remove from the cache since we would update the resolution in new file ourselves - currentResolutionsInFile.delete(name); - } - - if (moduleResolutionIsValid(existingResolution)) { - // ok, it is safe to use existing name resolution results - resolution = existingResolution; + let resolution = resolutionsInFile.get(name); + if (!moduleResolutionIsValid(resolution, name)) { + const existingResolution = resolution; + const resolutionInDirectory = perDirectoryResolution.get(name); + if (resolutionInDirectory) { + resolution = resolutionInDirectory; } else { - const resolutionInDirectory = perDirectoryResolution && perDirectoryResolution.get(name); - if (resolutionInDirectory) { - resolution = resolutionInDirectory; - } - else { - resolution = loader(name, containingFile, compilerOptions, resolutionHost); - perDirectoryResolution.set(name, resolution); - hasChangesInFailedLookupLocations = hasChangesInFailedLookupLocations || - resolution.failedLookupLocations !== (existingResolution && existingResolution.failedLookupLocations); - } + resolution = loader(name, containingFile, compilerOptions, resolutionHost); + perDirectoryResolution.set(name, resolution); } - newResolutions.set(name, resolution); + resolutionsInFile.set(name, resolution); + const diffIndex = existingResolution && existingResolution.failedLookupLocations && resolution.failedLookupLocations && findDiffIndex(resolution.failedLookupLocations, existingResolution.failedLookupLocations); + watchFailedLookupLocationOfResolution(resolution, diffIndex || 0); + stopWatchFailedLookupLocationOfResolutionFrom(existingResolution, diffIndex || 0); if (logChanges && filesWithChangedSetOfUnresolvedImports && !resolutionIsEqualTo(existingResolution, resolution)) { filesWithChangedSetOfUnresolvedImports.push(path); // reset log changes to avoid recording the same file multiple times logChanges = false; } } - - Debug.assert(resolution !== undefined); - + Debug.assert(resolution !== undefined && !resolution.isInvalidated); + seenNamesInFile.set(name, true); resolvedModules.push(getResult(resolution)); } - // replace old results with a new one - cache.set(path, newResolutions); + // Stop watching and remove the unused name + resolutionsInFile.forEach((resolution, name) => { + if (!seenNamesInFile.has(name)) { + stopWatchFailedLookupLocationOfResolution(resolution); + resolutionsInFile.delete(name); + } + }); + return resolvedModules; + function moduleResolutionIsValid(resolution: T, name: string): boolean { + // This is already calculated resolution in this round of synchronization + if (seenNamesInFile.has(name)) { + return true; + } + + if (!resolution || resolution.isInvalidated) { + return false; + } + + const result = getResult(resolution); + if (result) { + return true; + } + // consider situation if we have no candidate locations as valid resolution. + // after all there is no point to invalidate it if we have no idea where to look for the module. + return resolution.failedLookupLocations.length === 0; + } + function resolutionIsEqualTo(oldResolution: T, newResolution: T): boolean { if (oldResolution === newResolution) { return true; @@ -243,25 +257,9 @@ namespace ts { } return getResultFileName(oldResult) === getResultFileName(newResult); } - - function moduleResolutionIsValid(resolution: T): boolean { - if (!resolution || resolution.isInvalidated) { - return false; - } - - const result = getResult(resolution); - if (result) { - return true; - } - - // consider situation if we have no candidate locations as valid resolution. - // after all there is no point to invalidate it if we have no idea where to look for the module. - return resolution.failedLookupLocations.length === 0; - } } function resolveTypeReferenceDirectives(typeDirectiveNames: string[], containingFile: string): ResolvedTypeReferenceDirective[] { - resolutionHost.writeLog(`resolveTypeReferenceDirectives[${resolvedTypeReferenceDirectives.size}"]: " + ${containingFile}`); return resolveNamesWithLocalCache( typeDirectiveNames, containingFile, resolvedTypeReferenceDirectives, perDirectoryResolvedTypeReferenceDirectives, @@ -271,7 +269,6 @@ namespace ts { } function resolveModuleNames(moduleNames: string[], containingFile: string, logChanges: boolean): ResolvedModuleFull[] { - resolutionHost.writeLog(`resolveModuleNames[${resolvedModuleNames.size}"]: " + ${containingFile}`); return resolveNamesWithLocalCache( moduleNames, containingFile, resolvedModuleNames, perDirectoryResolvedModuleNames, @@ -322,22 +319,42 @@ namespace ts { return { dir, dirPath }; } - function watchFailedLookupLocationForCache( - cache: Map>, seenDirectories: Map + function watchFailedLookupLocationOfResolution( + resolution: T, startIndex?: number ) { - cache.forEach(value => value && value.forEach( - resolution => forEach(resolution && resolution.failedLookupLocations, - failedLookupLocation => { - const { dir, dirPath } = getDirectoryToWatchFailedLookupLocation(failedLookupLocation, resolutionHost.toPath(failedLookupLocation)); - if (!seenDirectories.has(dirPath)) { - if (!directoryWatchesOfFailedLookups.has(dirPath)) { - directoryWatchesOfFailedLookups.set(dirPath, createDirectoryWatcher(dir, dirPath)); - } - seenDirectories.set(dirPath, true); - } + if (resolution && resolution.failedLookupLocations) { + for (let i = startIndex || 0; i < resolution.failedLookupLocations.length; i++) { + const failedLookupLocation = resolution.failedLookupLocations[i]; + const { dir, dirPath } = getDirectoryToWatchFailedLookupLocation(failedLookupLocation, resolutionHost.toPath(failedLookupLocation)); + const dirWatcher = directoryWatchesOfFailedLookups.get(dirPath); + if (dirWatcher) { + dirWatcher.refCount++; + } + else { + directoryWatchesOfFailedLookups.set(dirPath, { watcher: createDirectoryWatcher(dir, dirPath), refCount: 1 }); } - ) - )); + } + } + } + + function stopWatchFailedLookupLocationOfResolution( + resolution: T + ) { + stopWatchFailedLookupLocationOfResolutionFrom(resolution, 0); + } + + function stopWatchFailedLookupLocationOfResolutionFrom( + resolution: T, startIndex: number + ) { + if (resolution && resolution.failedLookupLocations) { + for (let i = startIndex; i < resolution.failedLookupLocations.length; i++) { + const failedLookupLocation = resolution.failedLookupLocations[i]; + const { dirPath } = getDirectoryToWatchFailedLookupLocation(failedLookupLocation, resolutionHost.toPath(failedLookupLocation)); + const dirWatcher = directoryWatchesOfFailedLookups.get(dirPath); + // Do not close the watcher yet since it might be needed by other failed lookup locations. + dirWatcher.refCount--; + } + } } function createDirectoryWatcher(directory: string, dirPath: Path) { @@ -371,8 +388,9 @@ namespace ts { cache.forEach((value, path) => { if (path === deletedFilePath) { cache.delete(path); - hasChangesInFailedLookupLocations = hasChangesInFailedLookupLocations || - value && forEachEntry(value, resolution => !!resolution.failedLookupLocations); + if (value) { + value.forEach(stopWatchFailedLookupLocationOfResolution); + } } else if (value) { value.forEach(resolution => { diff --git a/src/server/editorServices.ts b/src/server/editorServices.ts index 0679ccd839318..101f9fbb7207b 100644 --- a/src/server/editorServices.ts +++ b/src/server/editorServices.ts @@ -1214,16 +1214,17 @@ namespace ts.server { } private printProjects() { - if (!this.logger.hasLevel(LogLevel.verbose)) { + if (!this.logger.hasLevel(LogLevel.normal)) { return; } + const writeProjectFileNames = this.logger.hasLevel(LogLevel.verbose); this.logger.startGroup(); let counter = 0; const printProjects = (projects: Project[], counter: number): number => { for (const project of projects) { this.logger.info(`Project '${project.getProjectName()}' (${ProjectKind[project.projectKind]}) ${counter}`); - this.logger.info(project.filesToString()); + this.logger.info(project.filesToString(writeProjectFileNames)); this.logger.info("-----------------------------------------------"); counter++; } diff --git a/src/server/project.ts b/src/server/project.ts index 37b0bb3cd7f59..afeb533550bd1 100644 --- a/src/server/project.ts +++ b/src/server/project.ts @@ -576,7 +576,7 @@ namespace ts.server { if (!this.languageServiceEnabled) { return undefined; } - return this.getLanguageService().getEmitOutput(sourceFile.fileName, emitOnlyDtsFiles, isDetailed); + return this.getLanguageService(/*ensureSynchronized*/ false).getEmitOutput(sourceFile.fileName, emitOnlyDtsFiles, isDetailed); } getFileNames(excludeFilesFromExternalLibraries?: boolean, excludeConfigFiles?: boolean) { @@ -889,14 +889,16 @@ namespace ts.server { return this.getScriptInfoForNormalizedPath(toNormalizedPath(uncheckedFileName)); } - filesToString() { + filesToString(writeProjectFileNames: boolean) { if (!this.program) { return "\tFiles (0)\n"; } const sourceFiles = this.program.getSourceFiles(); let strBuilder = `\tFiles (${sourceFiles.length})\n`; - for (const file of sourceFiles) { - strBuilder += `\t${file.fileName}\n`; + if (writeProjectFileNames) { + for (const file of sourceFiles) { + strBuilder += `\t${file.fileName}\n`; + } } return strBuilder; } From 54f64a1695180a5d17c87169cc2fc385ee078d1f Mon Sep 17 00:00:00 2001 From: Sheetal Nandi Date: Tue, 5 Sep 2017 17:36:19 -0700 Subject: [PATCH 091/109] Resolution is valid unless it is invalidated --- Jakefile.js | 1 - src/compiler/moduleNameResolver.ts | 17 -- src/compiler/resolutionCache.ts | 23 +- src/compiler/utilities.ts | 17 ++ src/harness/tsconfig.json | 1 - .../unittests/cachingInServerLSHost.ts | 203 ------------------ .../unittests/tsserverProjectSystem.ts | 197 ++++++++++++++--- 7 files changed, 188 insertions(+), 271 deletions(-) delete mode 100644 src/harness/unittests/cachingInServerLSHost.ts diff --git a/Jakefile.js b/Jakefile.js index 46a744445d361..9e334a9bdedef 100644 --- a/Jakefile.js +++ b/Jakefile.js @@ -121,7 +121,6 @@ var harnessSources = harnessCoreSources.concat([ "transpile.ts", "reuseProgramStructure.ts", "textStorage.ts", - "cachingInServerLSHost.ts", "moduleResolution.ts", "tsconfigParsing.ts", "commandLineParsing.ts", diff --git a/src/compiler/moduleNameResolver.ts b/src/compiler/moduleNameResolver.ts index 4d4fc6c1d8d69..52c16f7a64607 100644 --- a/src/compiler/moduleNameResolver.ts +++ b/src/compiler/moduleNameResolver.ts @@ -1127,21 +1127,4 @@ namespace ts { function toSearchResult(value: T | undefined): SearchResult { return value !== undefined ? { value } : undefined; } - - /** Calls `callback` on `directory` and every ancestor directory it has, returning the first defined result. */ - function forEachAncestorDirectory(directory: string, callback: (directory: string) => SearchResult): SearchResult { - while (true) { - const result = callback(directory); - if (result !== undefined) { - return result; - } - - const parentPath = getDirectoryPath(directory); - if (parentPath === directory) { - return undefined; - } - - directory = parentPath; - } - } } diff --git a/src/compiler/resolutionCache.ts b/src/compiler/resolutionCache.ts index 3d5d3e0a84506..56dd48aaebfab 100644 --- a/src/compiler/resolutionCache.ts +++ b/src/compiler/resolutionCache.ts @@ -184,9 +184,9 @@ namespace ts { const seenNamesInFile = createMap(); for (const name of names) { - // check if this is a duplicate entry in the list let resolution = resolutionsInFile.get(name); - if (!moduleResolutionIsValid(resolution, name)) { + // Resolution is valid if it is present and not invalidated + if (!resolution || resolution.isInvalidated) { const existingResolution = resolution; const resolutionInDirectory = perDirectoryResolution.get(name); if (resolutionInDirectory) { @@ -221,25 +221,6 @@ namespace ts { return resolvedModules; - function moduleResolutionIsValid(resolution: T, name: string): boolean { - // This is already calculated resolution in this round of synchronization - if (seenNamesInFile.has(name)) { - return true; - } - - if (!resolution || resolution.isInvalidated) { - return false; - } - - const result = getResult(resolution); - if (result) { - return true; - } - // consider situation if we have no candidate locations as valid resolution. - // after all there is no point to invalidate it if we have no idea where to look for the module. - return resolution.failedLookupLocations.length === 0; - } - function resolutionIsEqualTo(oldResolution: T, newResolution: T): boolean { if (oldResolution === newResolution) { return true; diff --git a/src/compiler/utilities.ts b/src/compiler/utilities.ts index 19748bfe0dee3..e17e0e969977b 100644 --- a/src/compiler/utilities.ts +++ b/src/compiler/utilities.ts @@ -3609,6 +3609,23 @@ namespace ts { export function closeFileWatcherOf(objWithWatcher: T) { objWithWatcher.watcher.close(); } + + /** Calls `callback` on `directory` and every ancestor directory it has, returning the first defined result. */ + export function forEachAncestorDirectory(directory: string, callback: (directory: string) => T): T { + while (true) { + const result = callback(directory); + if (result !== undefined) { + return result; + } + + const parentPath = getDirectoryPath(directory); + if (parentPath === directory) { + return undefined; + } + + directory = parentPath; + } + } } namespace ts { diff --git a/src/harness/tsconfig.json b/src/harness/tsconfig.json index 7c3ad0bc5a35c..006359c802226 100644 --- a/src/harness/tsconfig.json +++ b/src/harness/tsconfig.json @@ -109,7 +109,6 @@ "./unittests/convertToBase64.ts", "./unittests/transpile.ts", "./unittests/reuseProgramStructure.ts", - "./unittests/cachingInServerLSHost.ts", "./unittests/moduleResolution.ts", "./unittests/tsconfigParsing.ts", "./unittests/commandLineParsing.ts", diff --git a/src/harness/unittests/cachingInServerLSHost.ts b/src/harness/unittests/cachingInServerLSHost.ts deleted file mode 100644 index a1095e7dc4899..0000000000000 --- a/src/harness/unittests/cachingInServerLSHost.ts +++ /dev/null @@ -1,203 +0,0 @@ -/// - -namespace ts { - interface File { - name: string; - content: string; - } - - function createDefaultServerHost(fileMap: Map): server.ServerHost { - const existingDirectories = createMap(); - forEachKey(fileMap, name => { - let dir = getDirectoryPath(name); - let previous: string; - do { - existingDirectories.set(dir, true); - previous = dir; - dir = getDirectoryPath(dir); - } while (dir !== previous); - }); - return { - args: [], - newLine: "\r\n", - useCaseSensitiveFileNames: false, - write: noop, - readFile: path => { - const file = fileMap.get(path); - return file && file.content; - }, - writeFile: notImplemented, - resolvePath: notImplemented, - fileExists: path => fileMap.has(path), - directoryExists: path => existingDirectories.get(path) || false, - createDirectory: noop, - getExecutingFilePath: () => "", - getCurrentDirectory: () => "", - getDirectories: () => [], - getEnvironmentVariable: () => "", - readDirectory: notImplemented, - exit: noop, - watchFile: () => ({ - close: noop - }), - watchDirectory: () => ({ - close: noop - }), - setTimeout, - clearTimeout, - setImmediate: typeof setImmediate !== "undefined" ? setImmediate : action => setTimeout(action, 0), - clearImmediate: typeof clearImmediate !== "undefined" ? clearImmediate : clearTimeout, - createHash: Harness.mockHash, - }; - } - - function createProject(rootFile: string, serverHost: server.ServerHost): { project: server.Project, rootScriptInfo: server.ScriptInfo } { - const svcOpts: server.ProjectServiceOptions = { - host: serverHost, - logger: projectSystem.nullLogger, - cancellationToken: { isCancellationRequested: () => false }, - useSingleInferredProject: false, - useInferredProjectPerProjectRoot: false, - typingsInstaller: undefined - }; - const projectService = new server.ProjectService(svcOpts); - const rootScriptInfo = projectService.getOrCreateScriptInfoForNormalizedPath(server.toNormalizedPath(rootFile), /*openedByClient*/ true); - - const project = projectService.assignOrphanScriptInfoToInferredProject(rootScriptInfo); - project.setCompilerOptions({ module: ts.ModuleKind.AMD, noLib: true } ); - return { - project, - rootScriptInfo - }; - } - - describe("Caching in LSHost", () => { - it("works using legacy resolution logic", () => { - const root: File = { - name: "c:/d/f0.ts", - content: `import {x} from "f1"` - }; - - const imported: File = { - name: "c:/f1.ts", - content: `foo()` - }; - - const serverHost = createDefaultServerHost(createMapFromTemplate({ [root.name]: root, [imported.name]: imported })); - const { project, rootScriptInfo } = createProject(root.name, serverHost); - - // ensure that imported file was found - let diags = project.getLanguageService().getSemanticDiagnostics(imported.name); - assert.equal(diags.length, 1); - - - const originalFileExists = serverHost.fileExists; - { - // patch fileExists to make sure that disk is not touched - serverHost.fileExists = notImplemented; - - const newContent = `import {x} from "f1" - var x: string = 1;`; - rootScriptInfo.editContent(0, root.content.length, newContent); - // trigger synchronization to make sure that import will be fetched from the cache - diags = project.getLanguageService().getSemanticDiagnostics(imported.name); - // ensure file has correct number of errors after edit - assert.equal(diags.length, 1); - } - { - let fileExistsIsCalled = false; - serverHost.fileExists = (fileName): boolean => { - if (fileName === "lib.d.ts") { - return false; - } - fileExistsIsCalled = true; - assert.isTrue(fileName.indexOf("/f2.") !== -1); - return originalFileExists.call(serverHost, fileName); - }; - const newContent = `import {x} from "f2"`; - rootScriptInfo.editContent(0, root.content.length, newContent); - - try { - // trigger synchronization to make sure that LSHost will try to find 'f2' module on disk - project.getLanguageService().getSemanticDiagnostics(imported.name); - assert.isTrue(false, `should not find file '${imported.name}'`); - } - catch (e) { - assert.isTrue(e.message.indexOf(`Could not find file: '${imported.name}'.`) === 0); - } - - assert.isTrue(fileExistsIsCalled); - } - { - let fileExistsCalled = false; - serverHost.fileExists = (fileName): boolean => { - if (fileName === "lib.d.ts") { - return false; - } - fileExistsCalled = true; - assert.isTrue(fileName.indexOf("/f1.") !== -1); - return originalFileExists.call(serverHost, fileName); - }; - - const newContent = `import {x} from "f1"`; - rootScriptInfo.editContent(0, root.content.length, newContent); - project.getLanguageService().getSemanticDiagnostics(imported.name); - assert.isTrue(fileExistsCalled); - - // setting compiler options discards module resolution cache - fileExistsCalled = false; - - const compilerOptions = ts.cloneCompilerOptions(project.getCompilationSettings()); - compilerOptions.target = ts.ScriptTarget.ES5; - project.setCompilerOptions(compilerOptions); - - project.getLanguageService().getSemanticDiagnostics(imported.name); - assert.isTrue(fileExistsCalled); - } - }); - - it("loads missing files from disk", () => { - const root: File = { - name: `c:/foo.ts`, - content: `import {x} from "bar"` - }; - - const imported: File = { - name: `c:/bar.d.ts`, - content: `export var y = 1` - }; - - const fileMap = createMapFromTemplate({ [root.name]: root }); - const serverHost = createDefaultServerHost(fileMap); - const originalFileExists = serverHost.fileExists; - - let fileExistsCalledForBar = false; - serverHost.fileExists = fileName => { - if (fileName === "lib.d.ts") { - return false; - } - if (!fileExistsCalledForBar) { - fileExistsCalledForBar = fileName.indexOf("/bar.") !== -1; - } - - return originalFileExists.call(serverHost, fileName); - }; - - const { project, rootScriptInfo } = createProject(root.name, serverHost); - - let diags = project.getLanguageService().getSemanticDiagnostics(root.name); - assert.isTrue(fileExistsCalledForBar, "'fileExists' should be called"); - assert.isTrue(diags.length === 1, "one diagnostic expected"); - const messageText = diags[0].messageText; - assert.isTrue(isString(messageText) && messageText.indexOf("Cannot find module") === 0, "should be 'cannot find module' message"); - - fileMap.set(imported.name, imported); - fileExistsCalledForBar = false; - rootScriptInfo.editContent(0, root.content.length, `import {y} from "bar"`); - - diags = project.getLanguageService().getSemanticDiagnostics(root.name); - assert.isTrue(fileExistsCalledForBar, "'fileExists' should be called."); - assert.isTrue(diags.length === 0, "The import should succeed once the imported file appears on disk."); - }); - }); -} diff --git a/src/harness/unittests/tsserverProjectSystem.ts b/src/harness/unittests/tsserverProjectSystem.ts index d7de270645b85..7c04f08938d45 100644 --- a/src/harness/unittests/tsserverProjectSystem.ts +++ b/src/harness/unittests/tsserverProjectSystem.ts @@ -249,14 +249,9 @@ namespace ts.projectSystem { function getNodeModuleDirectories(dir: string) { const result: string[] = []; - while (true) { - result.push(combinePaths(dir, "node_modules")); - const parentDir = getDirectoryPath(dir); - if (parentDir === dir) { - break; - } - dir = parentDir; - } + forEachAncestorDirectory(dir, ancestor => { + result.push(combinePaths(ancestor, "node_modules")); + }); return result; } @@ -343,14 +338,9 @@ namespace ts.projectSystem { export function getTypeRootsFromLocation(currentDirectory: string) { currentDirectory = normalizePath(currentDirectory); const result: string[] = []; - while (true) { - result.push(combinePaths(currentDirectory, "node_modules/@types")); - const parentDirectory = getDirectoryPath(currentDirectory); - if (parentDirectory === currentDirectory) { - break; - } - currentDirectory = parentDirectory; - } + forEachAncestorDirectory(currentDirectory, ancestor => { + result.push(combinePaths(ancestor, "node_modules/@types")); + }); return result; } @@ -4147,6 +4137,7 @@ namespace ts.projectSystem { verifyCalledOnEachEntry, verifyNoHostCalls, verifyNoHostCallsExceptFileExistsOnce, + verifyCalledOn, clear }; @@ -4179,6 +4170,12 @@ namespace ts.projectSystem { } } + function verifyCalledOn(callback: keyof CalledMaps, name: string) { + const calledMap = calledMaps[callback]; + const result = calledMap.get(name); + assert.isTrue(result && !!result.length, `${callback} should be called with name: ${name}: ${arrayFrom(calledMap.keys())}`); + } + function verifyNoCall(callback: keyof CalledMaps) { const calledMap = calledMaps[callback]; assert.equal(calledMap.size, 0, `${callback} shouldnt be called: ${arrayFrom(calledMap.keys())}`); @@ -4218,6 +4215,161 @@ namespace ts.projectSystem { } } + it("works using legacy resolution logic", () => { + let rootContent = `import {x} from "f1"`; + const root: FileOrFolder = { + path: "/c/d/f0.ts", + content: rootContent + }; + + const imported: FileOrFolder = { + path: "/c/f1.ts", + content: `foo()` + }; + + const host = createServerHost([root, imported]); + const projectService = createProjectService(host); + projectService.setCompilerOptionsForInferredProjects({ module: ts.ModuleKind.AMD, noLib: true }); + projectService.openClientFile(root.path); + checkNumberOfProjects(projectService, { inferredProjects: 1 }); + const project = projectService.inferredProjects[0]; + const rootScriptInfo = project.getRootScriptInfos()[0]; + assert.equal(rootScriptInfo.fileName, root.path); + + // ensure that imported file was found + verifyImportedDiagnostics(); + + const callsTrackingHost = createCallsTrackingHost(host); + + // trigger synchronization to make sure that import will be fetched from the cache + // ensure file has correct number of errors after edit + editContent(`import {x} from "f1"; + var x: string = 1;`); + verifyImportedDiagnostics(); + callsTrackingHost.verifyNoHostCalls(); + + // trigger synchronization to make sure that LSHost will try to find 'f2' module on disk + editContent(`import {x} from "f2"`); + try { + // trigger synchronization to make sure that LSHost will try to find 'f2' module on disk + verifyImportedDiagnostics(); + assert.isTrue(false, `should not find file '${imported.path}'`); + } + catch (e) { + assert.isTrue(e.message.indexOf(`Could not find file: '${imported.path}'.`) === 0); + } + const f2Lookups = getLocationsForModuleLookup("f2"); + callsTrackingHost.verifyCalledOnEachEntryNTimes("fileExists", f2Lookups, 1); + const f2DirLookups = getLocationsForDirectoryLookup(); + callsTrackingHost.verifyCalledOnEachEntry("directoryExists", f2DirLookups); + callsTrackingHost.verifyNoCall("getDirectories"); + callsTrackingHost.verifyNoCall("readFile"); + callsTrackingHost.verifyNoCall("readDirectory"); + + editContent(`import {x} from "f1"`); + verifyImportedDiagnostics(); + const f1Lookups = f2Lookups.map(s => s.replace("f2", "f1")); + f1Lookups.length = f1Lookups.indexOf(imported.path) + 1; + const f1DirLookups = ["/c/d", "/c", typeRootFromTsserverLocation]; + vertifyF1Lookups(); + + // setting compiler options discards module resolution cache + callsTrackingHost.clear(); + projectService.setCompilerOptionsForInferredProjects({ module: ts.ModuleKind.AMD, noLib: true, target: ts.ScriptTarget.ES5 }); + verifyImportedDiagnostics(); + vertifyF1Lookups(); + + function vertifyF1Lookups() { + callsTrackingHost.verifyCalledOnEachEntryNTimes("fileExists", f1Lookups, 1); + callsTrackingHost.verifyCalledOnEachEntryNTimes("directoryExists", f1DirLookups, 1); + callsTrackingHost.verifyNoCall("getDirectories"); + callsTrackingHost.verifyNoCall("readFile"); + callsTrackingHost.verifyNoCall("readDirectory"); + } + + function editContent(newContent: string) { + callsTrackingHost.clear(); + rootScriptInfo.editContent(0, rootContent.length, newContent); + rootContent = newContent; + } + + function verifyImportedDiagnostics() { + const diags = project.getLanguageService().getSemanticDiagnostics(imported.path); + assert.equal(diags.length, 1); + const diag = diags[0]; + assert.equal(diag.code, Diagnostics.Cannot_find_name_0.code); + assert.equal(flattenDiagnosticMessageText(diag.messageText, "\n"), "Cannot find name 'foo'."); + } + + function getLocationsForModuleLookup(module: string) { + const locations: string[] = []; + forEachAncestorDirectory(getDirectoryPath(root.path), ancestor => { + locations.push( + combinePaths(ancestor, `${module}.ts`), + combinePaths(ancestor, `${module}.tsx`), + combinePaths(ancestor, `${module}.d.ts`) + ); + }); + forEachAncestorDirectory(getDirectoryPath(root.path), ancestor => { + locations.push( + combinePaths(ancestor, `${module}.js`), + combinePaths(ancestor, `${module}.jsx`) + ); + }); + return locations; + } + + function getLocationsForDirectoryLookup() { + const result = createMap(); + // Type root + result.set(typeRootFromTsserverLocation, 1); + forEachAncestorDirectory(getDirectoryPath(root.path), ancestor => { + // To resolve modules + result.set(ancestor, 2); + // for type roots + result.set(combinePaths(ancestor, `node_modules`), 1); + }); + return result; + } + }); + + it("loads missing files from disk", () => { + const root: FileOrFolder = { + path: "/c/foo.ts", + content: `import {y} from "bar"` + }; + + const imported: FileOrFolder = { + path: "/c/bar.d.ts", + content: `export var y = 1` + }; + + const host = createServerHost([root]); + const projectService = createProjectService(host); + projectService.setCompilerOptionsForInferredProjects({ module: ts.ModuleKind.AMD, noLib: true }); + const callsTrackingHost = createCallsTrackingHost(host); + projectService.openClientFile(root.path); + checkNumberOfProjects(projectService, { inferredProjects: 1 }); + const project = projectService.inferredProjects[0]; + const rootScriptInfo = project.getRootScriptInfos()[0]; + assert.equal(rootScriptInfo.fileName, root.path); + + let diags = project.getLanguageService().getSemanticDiagnostics(root.path); + assert.equal(diags.length, 1); + const diag = diags[0]; + assert.equal(diag.code, Diagnostics.Cannot_find_module_0.code); + assert.equal(flattenDiagnosticMessageText(diag.messageText, "\n"), "Cannot find module 'bar'."); + callsTrackingHost.verifyCalledOn("fileExists", imported.path); + + + callsTrackingHost.clear(); + host.reloadFS([root, imported]); + host.runQueuedTimeoutCallbacks(); + diags = project.getLanguageService().getSemanticDiagnostics(root.path); + assert.equal(diags.length, 0); + callsTrackingHost.verifyCalledOn("fileExists", imported.path); + }); + it("when calling goto definition of module", () => { const clientFile: FileOrFolder = { path: "/a/b/controllers/vessels/client.ts", @@ -4371,18 +4523,7 @@ namespace ts.projectSystem { const canonicalFile3Path = useCaseSensitiveFileNames ? file3.path : file3.path.toLocaleLowerCase(); const numberOfTimesWatchInvoked = getNumberOfWatchesInvokedForRecursiveWatches(watchingRecursiveDirectories, canonicalFile3Path); callsTrackingHost.verifyCalledOnEachEntryNTimes("fileExists", [canonicalFile3Path], numberOfTimesWatchInvoked); - - // Called for type root resolution - const directoryExistsCalled = createMap(); - for (let dir = frontendDir; dir !== "/"; dir = getDirectoryPath(dir)) { - directoryExistsCalled.set(`${dir}/node_modules`, 2); - } - directoryExistsCalled.set(`/node_modules`, 2); - directoryExistsCalled.set(`${frontendDir}/types`, 2); - directoryExistsCalled.set(`${frontendDir}/node_modules/@types`, 2); - directoryExistsCalled.set(canonicalFile3Path, numberOfTimesWatchInvoked); - callsTrackingHost.verifyCalledOnEachEntry("directoryExists", directoryExistsCalled); - + callsTrackingHost.verifyCalledOnEachEntryNTimes("directoryExists", [canonicalFile3Path], numberOfTimesWatchInvoked); callsTrackingHost.verifyNoCall("getDirectories"); callsTrackingHost.verifyCalledOnEachEntryNTimes("readFile", [file3.path], 1); callsTrackingHost.verifyNoCall("readDirectory"); From 0ff160f93f14b4c04fd533d6d74bf14a86298b0b Mon Sep 17 00:00:00 2001 From: Sheetal Nandi Date: Tue, 5 Sep 2017 19:36:32 -0700 Subject: [PATCH 092/109] Add files to change set instead of delay reloading project on "change" command --- src/server/editorServices.ts | 17 +++++++++++------ src/server/session.ts | 2 +- 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/src/server/editorServices.ts b/src/server/editorServices.ts index 101f9fbb7207b..4b76128829011 100644 --- a/src/server/editorServices.ts +++ b/src/server/editorServices.ts @@ -1948,12 +1948,7 @@ namespace ts.server { const change = file.changes[i]; scriptInfo.editContent(change.span.start, change.span.start + change.span.length, change.newText); } - if (!this.changedFiles) { - this.changedFiles = [scriptInfo]; - } - else if (!contains(this.changedFiles, scriptInfo)) { - this.changedFiles.push(scriptInfo); - } + this.addChangedFile(scriptInfo); } } @@ -1969,6 +1964,16 @@ namespace ts.server { } } + /* @internal */ + addChangedFile(scriptInfo: ScriptInfo) { + if (!this.changedFiles) { + this.changedFiles = [scriptInfo]; + } + else if (!contains(this.changedFiles, scriptInfo)) { + this.changedFiles.push(scriptInfo); + } + } + private closeConfiguredProject(configFile: NormalizedPath): void { const configuredProject = this.findConfiguredProjectByProjectName(configFile); if (configuredProject && configuredProject.deleteOpenRef() === 0) { diff --git a/src/server/session.ts b/src/server/session.ts index b4f2b46f99c85..4b307d27970cb 100644 --- a/src/server/session.ts +++ b/src/server/session.ts @@ -1287,9 +1287,9 @@ namespace ts.server { const end = scriptInfo.lineOffsetToPosition(args.endLine, args.endOffset); if (start >= 0) { scriptInfo.editContent(start, end, args.insertString); + this.projectService.addChangedFile(scriptInfo); this.changeSeq++; } - this.projectService.delayUpdateProjectGraphAndInferredProjectsRefresh(project); } } From e6eede13ef12882e53e10c146985f1ffb2148c28 Mon Sep 17 00:00:00 2001 From: Sheetal Nandi Date: Wed, 6 Sep 2017 13:18:58 -0700 Subject: [PATCH 093/109] Update how we get project/script info so that it doesnt start unnecessary update graph --- src/server/editorServices.ts | 25 +++-- src/server/session.ts | 193 +++++++++++++++++------------------ 2 files changed, 111 insertions(+), 107 deletions(-) diff --git a/src/server/editorServices.ts b/src/server/editorServices.ts index 4b76128829011..3114bb97e8267 100644 --- a/src/server/editorServices.ts +++ b/src/server/editorServices.ts @@ -588,11 +588,16 @@ namespace ts.server { return this.findExternalProjectByProjectName(projectName) || this.findConfiguredProjectByProjectName(toNormalizedPath(projectName)); } - getDefaultProjectForFile(fileName: NormalizedPath, refreshInferredProjects: boolean) { - if (refreshInferredProjects) { + getDefaultProjectForFile(fileName: NormalizedPath, ensureProject: boolean) { + let scriptInfo = this.getScriptInfoForNormalizedPath(fileName); + if (ensureProject && !scriptInfo || scriptInfo.isOrphan()) { this.ensureProjectStructuresUptoDate(); + scriptInfo = this.getScriptInfoForNormalizedPath(fileName); + if (!scriptInfo) { + return Errors.ThrowNoProject(); + } + return scriptInfo.getDefaultProject(); } - const scriptInfo = this.getScriptInfoForNormalizedPath(fileName); return scriptInfo && !scriptInfo.isOrphan() && scriptInfo.getDefaultProject(); } @@ -1943,12 +1948,7 @@ namespace ts.server { for (const file of changedFiles) { const scriptInfo = this.getScriptInfo(file.fileName); Debug.assert(!!scriptInfo); - // apply changes in reverse order - for (let i = file.changes.length - 1; i >= 0; i--) { - const change = file.changes[i]; - scriptInfo.editContent(change.span.start, change.span.start + change.span.length, change.newText); - } - this.addChangedFile(scriptInfo); + this.applyChangesToFile(scriptInfo, file.changes); } } @@ -1965,7 +1965,12 @@ namespace ts.server { } /* @internal */ - addChangedFile(scriptInfo: ScriptInfo) { + applyChangesToFile(scriptInfo: ScriptInfo, changes: TextChange[]) { + // apply changes in reverse order + for (let i = changes.length - 1; i >= 0; i--) { + const change = changes[i]; + scriptInfo.editContent(change.span.start, change.span.start + change.span.length, change.newText); + } if (!this.changedFiles) { this.changedFiles = [scriptInfo]; } diff --git a/src/server/session.ts b/src/server/session.ts index 4b307d27970cb..92f4b34a46e1d 100644 --- a/src/server/session.ts +++ b/src/server/session.ts @@ -593,8 +593,7 @@ namespace ts.server { private getDefinition(args: protocol.FileLocationRequestArgs, simplifiedResult: boolean): ReadonlyArray | ReadonlyArray { const { file, project } = this.getFileAndProject(args); - const scriptInfo = project.getScriptInfoForNormalizedPath(file); - const position = this.getPosition(args, scriptInfo); + const position = this.getPositionInFile(args, file); const definitions = project.getLanguageService().getDefinitionAtPosition(file, position); if (!definitions) { @@ -618,8 +617,7 @@ namespace ts.server { private getTypeDefinition(args: protocol.FileLocationRequestArgs): ReadonlyArray { const { file, project } = this.getFileAndProject(args); - const scriptInfo = project.getScriptInfoForNormalizedPath(file); - const position = this.getPosition(args, scriptInfo); + const position = this.getPositionInFile(args, file); const definitions = project.getLanguageService().getTypeDefinitionAtPosition(file, position); if (!definitions) { @@ -638,7 +636,7 @@ namespace ts.server { private getImplementation(args: protocol.FileLocationRequestArgs, simplifiedResult: boolean): ReadonlyArray | ReadonlyArray { const { file, project } = this.getFileAndProject(args); - const position = this.getPosition(args, project.getScriptInfoForNormalizedPath(file)); + const position = this.getPositionInFile(args, file); const implementations = project.getLanguageService().getImplementationAtPosition(file, position); if (!implementations) { return emptyArray; @@ -660,8 +658,7 @@ namespace ts.server { private getOccurrences(args: protocol.FileLocationRequestArgs): ReadonlyArray { const { file, project } = this.getFileAndProject(args); - const scriptInfo = project.getScriptInfoForNormalizedPath(file); - const position = this.getPosition(args, scriptInfo); + const position = this.getPositionInFile(args, file); const occurrences = project.getLanguageService().getOccurrencesAtPosition(file, position); @@ -708,8 +705,7 @@ namespace ts.server { private getDocumentHighlights(args: protocol.DocumentHighlightsRequestArgs, simplifiedResult: boolean): ReadonlyArray | ReadonlyArray { const { file, project } = this.getFileAndProject(args); - const scriptInfo = project.getScriptInfoForNormalizedPath(file); - const position = this.getPosition(args, scriptInfo); + const position = this.getPositionInFile(args, file); const documentHighlights = project.getLanguageService().getDocumentHighlights(file, position, args.filesToSearch); if (!documentHighlights) { @@ -750,7 +746,8 @@ namespace ts.server { } private getProjectInfoWorker(uncheckedFileName: string, projectFileName: string, needFileNameList: boolean, excludeConfigFiles: boolean) { - const { project } = this.getFileAndProjectWorker(uncheckedFileName, projectFileName, /*refreshInferredProjects*/ true, /*errorOnMissingProject*/ true); + const { project } = this.getFileAndProjectWorker(uncheckedFileName, projectFileName); + project.updateGraph(); const projectInfo = { configFileName: project.getProjectName(), languageServiceDisabled: !project.languageServiceEnabled, @@ -761,8 +758,7 @@ namespace ts.server { private getRenameInfo(args: protocol.FileLocationRequestArgs) { const { file, project } = this.getFileAndProject(args); - const scriptInfo = project.getScriptInfoForNormalizedPath(file); - const position = this.getPosition(args, scriptInfo); + const position = this.getPositionInFile(args, file); return project.getLanguageService().getRenameInfo(file, position); } @@ -799,8 +795,7 @@ namespace ts.server { private getRenameLocations(args: protocol.RenameRequestArgs, simplifiedResult: boolean): protocol.RenameResponseBody | ReadonlyArray { const file = toNormalizedPath(args.file); - const info = this.projectService.getScriptInfoForNormalizedPath(file); - const position = this.getPosition(args, info); + const position = this.getPositionInFile(args, file); const projects = this.getProjects(args); if (simplifiedResult) { @@ -905,14 +900,13 @@ namespace ts.server { const projects = this.getProjects(args); const defaultProject = this.getDefaultProject(args); - const scriptInfo = defaultProject.getScriptInfoForNormalizedPath(file); + const scriptInfo = this.projectService.getScriptInfoForNormalizedPath(file); const position = this.getPosition(args, scriptInfo); if (simplifiedResult) { const nameInfo = defaultProject.getLanguageService().getQuickInfoAtPosition(file, position); if (!nameInfo) { return undefined; } - const displayString = displayPartsToString(nameInfo.displayParts); const nameSpan = nameInfo.textSpan; const nameColStart = scriptInfo.positionToLineOffset(nameSpan.start).offset; @@ -988,26 +982,38 @@ namespace ts.server { return args.position !== undefined ? args.position : scriptInfo.lineOffsetToPosition(args.line, args.offset); } - private getFileAndProject(args: protocol.FileRequestArgs, errorOnMissingProject = true) { - return this.getFileAndProjectWorker(args.file, args.projectFileName, /*refreshInferredProjects*/ true, errorOnMissingProject); + private getPositionInFile(args: protocol.FileLocationRequestArgs, file: NormalizedPath): number { + const scriptInfo = this.projectService.getScriptInfoForNormalizedPath(file); + return this.getPosition(args, scriptInfo); } - private getFileAndProjectWithoutRefreshingInferredProjects(args: protocol.FileRequestArgs, errorOnMissingProject = true) { - return this.getFileAndProjectWorker(args.file, args.projectFileName, /*refreshInferredProjects*/ false, errorOnMissingProject); + private getFileAndProject(args: protocol.FileRequestArgs) { + return this.getFileAndProjectWorker(args.file, args.projectFileName); } - private getFileAndProjectWorker(uncheckedFileName: string, projectFileName: string, refreshInferredProjects: boolean, errorOnMissingProject: boolean) { - const file = toNormalizedPath(uncheckedFileName); - const project: Project = this.getProject(projectFileName) || this.projectService.getDefaultProjectForFile(file, refreshInferredProjects); - if (!project && errorOnMissingProject) { + private getFileAndLanguageServiceForSyntacticOperation(args: protocol.FileRequestArgs) { + // Since this is syntactic operation, there should always be project for the file + // we wouldnt have to ensure project but rather throw if we dont get project + const file = toNormalizedPath(args.file); + const project = this.getProject(args.projectFileName) || this.projectService.getDefaultProjectForFile(file, /*ensureProject*/ false); + if (!project) { return Errors.ThrowNoProject(); } + return { + file, + languageService: project.getLanguageService(/*ensureSynchronized*/ false) + }; + } + + private getFileAndProjectWorker(uncheckedFileName: string, projectFileName: string) { + const file = toNormalizedPath(uncheckedFileName); + const project: Project = this.getProject(projectFileName) || this.projectService.getDefaultProjectForFile(file, /*ensureProject*/ true); return { file, project }; } private getOutliningSpans(args: protocol.FileRequestArgs) { - const { file, project } = this.getFileAndProjectWithoutRefreshingInferredProjects(args); - return project.getLanguageService(/*ensureSynchronized*/ false).getOutliningSpans(file); + const { file, languageService } = this.getFileAndLanguageServiceForSyntacticOperation(args); + return languageService.getOutliningSpans(file); } private getTodoComments(args: protocol.TodoCommentRequestArgs) { @@ -1016,49 +1022,47 @@ namespace ts.server { } private getDocCommentTemplate(args: protocol.FileLocationRequestArgs) { - const { file, project } = this.getFileAndProjectWithoutRefreshingInferredProjects(args); - const scriptInfo = project.getScriptInfoForNormalizedPath(file); - const position = this.getPosition(args, scriptInfo); - return project.getLanguageService(/*ensureSynchronized*/ false).getDocCommentTemplateAtPosition(file, position); + const { file, languageService } = this.getFileAndLanguageServiceForSyntacticOperation(args); + const position = this.getPositionInFile(args, file); + return languageService.getDocCommentTemplateAtPosition(file, position); } private getSpanOfEnclosingComment(args: protocol.SpanOfEnclosingCommentRequestArgs) { - const { file, project } = this.getFileAndProjectWithoutRefreshingInferredProjects(args); - const scriptInfo = project.getScriptInfoForNormalizedPath(file); + const { file, languageService } = this.getFileAndLanguageServiceForSyntacticOperation(args); const onlyMultiLine = args.onlyMultiLine; - const position = this.getPosition(args, scriptInfo); - return project.getLanguageService(/*ensureSynchronized*/ false).getSpanOfEnclosingComment(file, position, onlyMultiLine); + const position = this.getPositionInFile(args, file); + return languageService.getSpanOfEnclosingComment(file, position, onlyMultiLine); } private getIndentation(args: protocol.IndentationRequestArgs) { - const { file, project } = this.getFileAndProjectWithoutRefreshingInferredProjects(args); - const position = this.getPosition(args, project.getScriptInfoForNormalizedPath(file)); + const { file, languageService } = this.getFileAndLanguageServiceForSyntacticOperation(args); + const position = this.getPositionInFile(args, file); const options = args.options ? convertFormatOptions(args.options) : this.projectService.getFormatCodeOptions(file); - const indentation = project.getLanguageService(/*ensureSynchronized*/ false).getIndentationAtPosition(file, position, options); + const indentation = languageService.getIndentationAtPosition(file, position, options); return { position, indentation }; } private getBreakpointStatement(args: protocol.FileLocationRequestArgs) { - const { file, project } = this.getFileAndProjectWithoutRefreshingInferredProjects(args); - const position = this.getPosition(args, project.getScriptInfoForNormalizedPath(file)); - return project.getLanguageService(/*ensureSynchronized*/ false).getBreakpointStatementAtPosition(file, position); + const { file, languageService } = this.getFileAndLanguageServiceForSyntacticOperation(args); + const position = this.getPositionInFile(args, file); + return languageService.getBreakpointStatementAtPosition(file, position); } private getNameOrDottedNameSpan(args: protocol.FileLocationRequestArgs) { - const { file, project } = this.getFileAndProjectWithoutRefreshingInferredProjects(args); - const position = this.getPosition(args, project.getScriptInfoForNormalizedPath(file)); - return project.getLanguageService(/*ensureSynchronized*/ false).getNameOrDottedNameSpan(file, position, position); + const { file, languageService } = this.getFileAndLanguageServiceForSyntacticOperation(args); + const position = this.getPositionInFile(args, file); + return languageService.getNameOrDottedNameSpan(file, position, position); } private isValidBraceCompletion(args: protocol.BraceCompletionRequestArgs) { - const { file, project } = this.getFileAndProjectWithoutRefreshingInferredProjects(args); - const position = this.getPosition(args, project.getScriptInfoForNormalizedPath(file)); - return project.getLanguageService(/*ensureSynchronized*/ false).isValidBraceCompletionAtPosition(file, position, args.openingBrace.charCodeAt(0)); + const { file, languageService } = this.getFileAndLanguageServiceForSyntacticOperation(args); + const position = this.getPositionInFile(args, file); + return languageService.isValidBraceCompletionAtPosition(file, position, args.openingBrace.charCodeAt(0)); } private getQuickInfoWorker(args: protocol.FileLocationRequestArgs, simplifiedResult: boolean): protocol.QuickInfoResponseBody | QuickInfo { const { file, project } = this.getFileAndProject(args); - const scriptInfo = project.getScriptInfoForNormalizedPath(file); + const scriptInfo = this.projectService.getScriptInfoForNormalizedPath(file); const quickInfo = project.getLanguageService().getQuickInfoAtPosition(file, this.getPosition(args, scriptInfo)); if (!quickInfo) { return undefined; @@ -1084,14 +1088,14 @@ namespace ts.server { } private getFormattingEditsForRange(args: protocol.FormatRequestArgs): protocol.CodeEdit[] { - const { file, project } = this.getFileAndProjectWithoutRefreshingInferredProjects(args); - const scriptInfo = project.getScriptInfoForNormalizedPath(file); + const { file, languageService } = this.getFileAndLanguageServiceForSyntacticOperation(args); + const scriptInfo = this.projectService.getScriptInfoForNormalizedPath(file); const startPosition = scriptInfo.lineOffsetToPosition(args.line, args.offset); const endPosition = scriptInfo.lineOffsetToPosition(args.endLine, args.endOffset); // TODO: avoid duplicate code (with formatonkey) - const edits = project.getLanguageService(/*ensureSynchronized*/ false).getFormattingEditsForRange(file, startPosition, endPosition, + const edits = languageService.getFormattingEditsForRange(file, startPosition, endPosition, this.projectService.getFormatCodeOptions(file)); if (!edits) { return undefined; @@ -1101,29 +1105,29 @@ namespace ts.server { } private getFormattingEditsForRangeFull(args: protocol.FormatRequestArgs) { - const { file, project } = this.getFileAndProjectWithoutRefreshingInferredProjects(args); + const { file, languageService } = this.getFileAndLanguageServiceForSyntacticOperation(args); const options = args.options ? convertFormatOptions(args.options) : this.projectService.getFormatCodeOptions(file); - return project.getLanguageService(/*ensureSynchronized*/ false).getFormattingEditsForRange(file, args.position, args.endPosition, options); + return languageService.getFormattingEditsForRange(file, args.position, args.endPosition, options); } private getFormattingEditsForDocumentFull(args: protocol.FormatRequestArgs) { - const { file, project } = this.getFileAndProjectWithoutRefreshingInferredProjects(args); + const { file, languageService } = this.getFileAndLanguageServiceForSyntacticOperation(args); const options = args.options ? convertFormatOptions(args.options) : this.projectService.getFormatCodeOptions(file); - return project.getLanguageService(/*ensureSynchronized*/ false).getFormattingEditsForDocument(file, options); + return languageService.getFormattingEditsForDocument(file, options); } private getFormattingEditsAfterKeystrokeFull(args: protocol.FormatOnKeyRequestArgs) { - const { file, project } = this.getFileAndProjectWithoutRefreshingInferredProjects(args); + const { file, languageService } = this.getFileAndLanguageServiceForSyntacticOperation(args); const options = args.options ? convertFormatOptions(args.options) : this.projectService.getFormatCodeOptions(file); - return project.getLanguageService(/*ensureSynchronized*/ false).getFormattingEditsAfterKeystroke(file, args.position, args.key, options); + return languageService.getFormattingEditsAfterKeystroke(file, args.position, args.key, options); } private getFormattingEditsAfterKeystroke(args: protocol.FormatOnKeyRequestArgs): protocol.CodeEdit[] { - const { file, project } = this.getFileAndProjectWithoutRefreshingInferredProjects(args); - const scriptInfo = project.getScriptInfoForNormalizedPath(file); + const { file, languageService } = this.getFileAndLanguageServiceForSyntacticOperation(args); + const scriptInfo = this.projectService.getScriptInfoForNormalizedPath(file); const position = scriptInfo.lineOffsetToPosition(args.line, args.offset); const formatOptions = this.projectService.getFormatCodeOptions(file); - const edits = project.getLanguageService(/*ensureSynchronized*/ false).getFormattingEditsAfterKeystroke(file, position, args.key, + const edits = languageService.getFormattingEditsAfterKeystroke(file, position, args.key, formatOptions); // Check whether we should auto-indent. This will be when // the position is on a line containing only whitespace. @@ -1134,7 +1138,7 @@ namespace ts.server { if ((args.key === "\n") && ((!edits) || (edits.length === 0) || allEditsBeforePos(edits, position))) { const { lineText, absolutePosition } = scriptInfo.getLineInfo(args.line); if (lineText && lineText.search("\\S") < 0) { - const preferredIndent = project.getLanguageService(/*ensureSynchronized*/ false).getIndentationAtPosition(file, position, formatOptions); + const preferredIndent = languageService.getIndentationAtPosition(file, position, formatOptions); let hasIndent = 0; let i: number, len: number; for (i = 0, len = lineText.length; i < len; i++) { @@ -1175,8 +1179,7 @@ namespace ts.server { private getCompletions(args: protocol.CompletionsRequestArgs, simplifiedResult: boolean): ReadonlyArray | CompletionInfo | undefined { const prefix = args.prefix || ""; const { file, project } = this.getFileAndProject(args); - - const scriptInfo = project.getScriptInfoForNormalizedPath(file); + const scriptInfo = this.projectService.getScriptInfoForNormalizedPath(file); const position = this.getPosition(args, scriptInfo); const completions = project.getLanguageService().getCompletionsAtPosition(file, position); @@ -1196,8 +1199,7 @@ namespace ts.server { private getCompletionEntryDetails(args: protocol.CompletionDetailsRequestArgs): ReadonlyArray { const { file, project } = this.getFileAndProject(args); - const scriptInfo = project.getScriptInfoForNormalizedPath(file); - const position = this.getPosition(args, scriptInfo); + const position = this.getPositionInFile(args, file); return mapDefined(args.entryNames, entryName => project.getLanguageService().getCompletionEntryDetails(file, position, entryName)); @@ -1239,7 +1241,7 @@ namespace ts.server { private getSignatureHelpItems(args: protocol.SignatureHelpRequestArgs, simplifiedResult: boolean): protocol.SignatureHelpItems | SignatureHelpItems { const { file, project } = this.getFileAndProject(args); - const scriptInfo = project.getScriptInfoForNormalizedPath(file); + const scriptInfo = this.projectService.getScriptInfoForNormalizedPath(file); const position = this.getPosition(args, scriptInfo); const helpItems = project.getLanguageService().getSignatureHelpItems(file, position); if (!helpItems) { @@ -1267,7 +1269,7 @@ namespace ts.server { private createCheckList(fileNames: string[], defaultProject?: Project): PendingErrorCheck[] { return mapDefined(fileNames, uncheckedFileName => { const fileName = toNormalizedPath(uncheckedFileName); - const project = defaultProject || this.projectService.getDefaultProjectForFile(fileName, /*refreshInferredProjects*/ true); + const project = defaultProject || this.projectService.getDefaultProjectForFile(fileName, /*ensureProject*/ false); return project && { fileName, project }; }); } @@ -1280,29 +1282,27 @@ namespace ts.server { } private change(args: protocol.ChangeRequestArgs) { - const { file, project } = this.getFileAndProject(args, /*errorOnMissingProject*/ false); - if (project) { - const scriptInfo = project.getScriptInfoForNormalizedPath(file); - const start = scriptInfo.lineOffsetToPosition(args.line, args.offset); - const end = scriptInfo.lineOffsetToPosition(args.endLine, args.endOffset); - if (start >= 0) { - scriptInfo.editContent(start, end, args.insertString); - this.projectService.addChangedFile(scriptInfo); - this.changeSeq++; - } + const scriptInfo = this.projectService.getScriptInfo(args.file); + Debug.assert(!!scriptInfo); + const start = scriptInfo.lineOffsetToPosition(args.line, args.offset); + const end = scriptInfo.lineOffsetToPosition(args.endLine, args.endOffset); + if (start >= 0) { + this.projectService.applyChangesToFile(scriptInfo, [{ + span: { start, length: end - start }, + newText: args.insertString + }]); + this.changeSeq++; } } private reload(args: protocol.ReloadRequestArgs, reqSeq: number) { const file = toNormalizedPath(args.file); const tempFileName = args.tmpfile && toNormalizedPath(args.tmpfile); - const project = this.projectService.getDefaultProjectForFile(file, /*refreshInferredProjects*/ true); - if (project) { - this.changeSeq++; - // make sure no changes happen before this one is finished - if (project.reloadScript(file, tempFileName)) { - this.output(undefined, CommandNames.Reload, reqSeq); - } + const project = this.projectService.getDefaultProjectForFile(file, /*ensureProject*/ true); + this.changeSeq++; + // make sure no changes happen before this one is finished + if (project.reloadScript(file, tempFileName)) { + this.output(undefined, CommandNames.Reload, reqSeq); } } @@ -1333,12 +1333,12 @@ namespace ts.server { } private getNavigationBarItems(args: protocol.FileRequestArgs, simplifiedResult: boolean): protocol.NavigationBarItem[] | NavigationBarItem[] { - const { file, project } = this.getFileAndProject(args); - const items = project.getLanguageService(/*ensureSynchronized*/ false).getNavigationBarItems(file); + const { file, languageService } = this.getFileAndLanguageServiceForSyntacticOperation(args); + const items = languageService.getNavigationBarItems(file); return !items ? undefined : simplifiedResult - ? this.decorateNavigationBarItems(items, project.getScriptInfoForNormalizedPath(file)) + ? this.decorateNavigationBarItems(items, this.projectService.getScriptInfoForNormalizedPath(file)) : items; } @@ -1360,12 +1360,12 @@ namespace ts.server { } private getNavigationTree(args: protocol.FileRequestArgs, simplifiedResult: boolean): protocol.NavigationTree | NavigationTree { - const { file, project } = this.getFileAndProject(args); - const tree = project.getLanguageService(/*ensureSynchronized*/ false).getNavigationTree(file); + const { file, languageService } = this.getFileAndLanguageServiceForSyntacticOperation(args); + const tree = languageService.getNavigationTree(file); return !tree ? undefined : simplifiedResult - ? this.decorateNavigationTree(tree, project.getScriptInfoForNormalizedPath(file)) + ? this.decorateNavigationTree(tree, this.projectService.getScriptInfoForNormalizedPath(file)) : tree; } @@ -1475,14 +1475,14 @@ namespace ts.server { } private getApplicableRefactors(args: protocol.GetApplicableRefactorsRequestArgs): protocol.ApplicableRefactorInfo[] { - const { file, project } = this.getFileAndProjectWithoutRefreshingInferredProjects(args); + const { file, project } = this.getFileAndProject(args); const scriptInfo = project.getScriptInfoForNormalizedPath(file); const { position, textRange } = this.extractPositionAndRange(args, scriptInfo); return project.getLanguageService().getApplicableRefactors(file, position || textRange); } private getEditsForRefactor(args: protocol.GetEditsForRefactorRequestArgs, simplifiedResult: boolean): RefactorEditInfo | protocol.RefactorEditInfo { - const { file, project } = this.getFileAndProjectWithoutRefreshingInferredProjects(args); + const { file, project } = this.getFileAndProject(args); const scriptInfo = project.getScriptInfoForNormalizedPath(file); const { position, textRange } = this.extractPositionAndRange(args, scriptInfo); @@ -1522,7 +1522,7 @@ namespace ts.server { if (args.errorCodes.length === 0) { return undefined; } - const { file, project } = this.getFileAndProjectWithoutRefreshingInferredProjects(args); + const { file, project } = this.getFileAndProject(args); const scriptInfo = project.getScriptInfoForNormalizedPath(file); const { startPosition, endPosition } = this.getStartAndEndPosition(args, scriptInfo); @@ -1589,12 +1589,11 @@ namespace ts.server { } private getBraceMatching(args: protocol.FileLocationRequestArgs, simplifiedResult: boolean): protocol.TextSpan[] | TextSpan[] { - const { file, project } = this.getFileAndProjectWithoutRefreshingInferredProjects(args); - - const scriptInfo = project.getScriptInfoForNormalizedPath(file); + const { file, languageService } = this.getFileAndLanguageServiceForSyntacticOperation(args); + const scriptInfo = this.projectService.getScriptInfoForNormalizedPath(file); const position = this.getPosition(args, scriptInfo); - const spans = project.getLanguageService(/*ensureSynchronized*/ false).getBraceMatchingAtPosition(file, position); + const spans = languageService.getBraceMatchingAtPosition(file, position); return !spans ? undefined : simplifiedResult @@ -1617,7 +1616,7 @@ namespace ts.server { const lowPriorityFiles: NormalizedPath[] = []; const veryLowPriorityFiles: NormalizedPath[] = []; const normalizedFileName = toNormalizedPath(fileName); - const project = this.projectService.getDefaultProjectForFile(normalizedFileName, /*refreshInferredProjects*/ true); + const project = this.projectService.getDefaultProjectForFile(normalizedFileName, /*ensureProject*/ true); for (const fileNameInProject of fileNamesInProject) { if (this.getCanonicalFileName(fileNameInProject) === this.getCanonicalFileName(fileName)) { highPriorityFiles.push(fileNameInProject); From 2a5d95448650953e4f978f064ef6e5607f16e925 Mon Sep 17 00:00:00 2001 From: Sheetal Nandi Date: Wed, 6 Sep 2017 13:29:24 -0700 Subject: [PATCH 094/109] Reduce the file size for npm install test --- src/harness/unittests/tsserverProjectSystem.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/harness/unittests/tsserverProjectSystem.ts b/src/harness/unittests/tsserverProjectSystem.ts index 7c04f08938d45..3537fde9a49e1 100644 --- a/src/harness/unittests/tsserverProjectSystem.ts +++ b/src/harness/unittests/tsserverProjectSystem.ts @@ -4651,7 +4651,7 @@ namespace ts.projectSystem { { "path": "/a/b/node_modules/.staging/rxjs-22375c61/bundles" }, { "path": "/a/b/node_modules/.staging/rxjs-22375c61/operator" }, { "path": "/a/b/node_modules/.staging/rxjs-22375c61/src/add/observable/dom" }, - { "path": "/a/b/node_modules/.staging/@types/lodash-e56c4fe7/index.d.ts", "content": "// Type definitions for Lo-Dash 4.14\n// Project: http://lodash.com/\n// Definitions by: Brian Zengel ,\n// Ilya Mochalov ,\n// Stepan Mikhaylyuk ,\n// Eric L Anderson ,\n// AJ Richardson ,\n// Junyoung Clare Jang \n// Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped\n// TypeScript Version: 2.2\n\n/**\n### 4.0.0 Changelog (https://github.com/lodash/lodash/wiki/Changelog)\n\n#### TODO:\nremoved:\n- [x] Removed _.support\n- [x] Removed _.findWhere in favor of _.find with iteratee shorthand\n- [x] Removed _.where in favor of _.filter with iteratee shorthand\n- [x] Removed _.pluck in favor of _.map with iteratee shorthand\n\nrenamed:\n- [x] Renamed _.first to _.head\n- [x] Renamed _.indexBy to _.keyBy\n- [x] Renamed _.invoke to _.invokeMap\n- [x] Renamed _.overArgs to _.overArgs\n- [x] Renamed _.padLeft & _.padRight to _.padStart & _.padEnd\n- [x] Renamed _.pairs to _.toPairs\n- [x] Renamed _.rest to _.tail\n- [x] Renamed _.restParam to _.rest\n- [x] Renamed _.sortByOrder to _.orderBy\n- [x] Renamed _.trimLeft & _.trimRight to _.trimStart & _.trimEnd\n- [x] Renamed _.trunc to _.truncate\n\nsplit:\n- [x] Split _.indexOf & _.lastIndexOf into _.sortedIndexOf & _.sortedLastIndexOf\n- [x] Split _.max & _.min into _.maxBy & _.minBy\n- [x] Split _.omit & _.pick into _.omitBy & _.pickBy\n- [x] Split _.sample into _.sampleSize\n- [x] Split _.sortedIndex into _.sortedIndexBy\n- [x] Split _.sortedLastIndex into _.sortedLastIndexBy\n- [x] Split _.uniq into _.sortedUniq, _.sortedUniqBy, & _.uniqBy\n\nchanges:\n- [x] Absorbed _.sortByAll into _.sortBy\n- [x] Changed the category of _.at to “Object”\n- [x] Changed the category of _.bindAll to “Utility”\n- [x] Made _.capitalize uppercase the first character & lowercase the rest\n- [x] Made _.functions return only own method names\n\nadded 23 array methods:\n- [x] _.concat\n- [x] _.differenceBy\n- [x] _.differenceWith\n- [x] _.flatMap\n- [x] _.fromPairs\n- [x] _.intersectionBy\n- [x] _.intersectionWith\n- [x] _.join\n- [x] _.pullAll\n- [x] _.pullAllBy\n- [x] _.reverse\n- [x] _.sortedIndexBy\n- [x] _.sortedIndexOf\n- [x] _.sortedLastIndexBy\n- [x] _.sortedLastIndexOf\n- [x] _.sortedUniq\n- [x] _.sortedUniqBy\n- [x] _.unionBy\n- [x] _.unionWith\n- [x] _.uniqBy\n- [x] _.uniqWith\n- [x] _.xorBy\n- [x] _.xorWith\n\nadded 20 lang methods:\n- [x] _.cloneDeepWith\n- [x] _.cloneWith\n- [x] _.eq\n- [x] _.isArrayLike\n- [x] _.isArrayLikeObject\n- [x] _.isEqualWith\n- [x] _.isInteger\n- [x] _.isLength\n- [x] _.isMatchWith\n- [x] _.isNil\n- [x] _.isObjectLike\n- [x] _.isSafeInteger\n- [x] _.isSymbol\n- [x] _.toInteger\n- [x] _.toLength\n- [x] _.toNumber\n- [x] _.toSafeInteger\n- [x] _.toString\n- [X] _.conforms\n- [X] _.conformsTo\n\nadded 13 object methods:\n- [x] _.assignIn\n- [x] _.assignInWith\n- [x] _.assignWith\n- [x] _.functionsIn\n- [x] _.hasIn\n- [x] _.mergeWith\n- [x] _.omitBy\n- [x] _.pickBy\n\nadded 8 string methods:\n- [x] _.lowerCase\n- [x] _.lowerFirst\n- [x] _.upperCase\n- [x] _.upperFirst\n- [x] _.toLower\n- [x] _.toUpper\n\nadded 8 utility methods:\n- [x] _.toPath\n\nadded 4 math methods:\n- [x] _.maxBy\n- [x] _.mean\n- [x] _.minBy\n- [x] _.sumBy\n\nadded 2 function methods:\n- [x] _.flip\n- [x] _.unary\n\nadded 2 number methods:\n- [x] _.clamp\n- [x] _.subtract\n\nadded collection method:\n- [x] _.sampleSize\n\nAdded 3 aliases\n\n- [x] _.first as an alias of _.head\n\nRemoved 17 aliases\n- [x] Removed aliase _.all\n- [x] Removed aliase _.any\n- [x] Removed aliase _.backflow\n- [x] Removed aliase _.callback\n- [x] Removed aliase _.collect\n- [x] Removed aliase _.compose\n- [x] Removed aliase _.contains\n- [x] Removed aliase _.detect\n- [x] Removed aliase _.foldl\n- [x] Removed aliase _.foldr\n- [x] Removed aliase _.include\n- [x] Removed aliase _.inject\n- [x] Removed aliase _.methods\n- [x] Removed aliase _.object\n- [x] Removed aliase _.run\n- [x] Removed aliase _.select\n- [x] Removed aliase _.unique\n\nOther changes\n- [x] Added support for array buffers to _.isEqual\n- [x] Added support for converting iterators to _.toArray\n- [x] Added support for deep paths to _.zipObject\n- [x] Changed UMD to export to window or self when available regardless of other exports\n- [x] Ensured debounce cancel clears args & thisArg references\n- [x] Ensured _.add, _.subtract, & _.sum don’t skip NaN values\n- [x] Ensured _.clone treats generators like functions\n- [x] Ensured _.clone produces clones with the source’s [[Prototype]]\n- [x] Ensured _.defaults assigns properties that shadow Object.prototype\n- [x] Ensured _.defaultsDeep doesn’t merge a string into an array\n- [x] Ensured _.defaultsDeep & _.merge don’t modify sources\n- [x] Ensured _.defaultsDeep works with circular references\n- [x] Ensured _.keys skips “length” on strict mode arguments objects in Safari 9\n- [x] Ensured _.merge doesn’t convert strings to arrays\n- [x] Ensured _.merge merges plain-objects onto non plain-objects\n- [x] Ensured _#plant resets iterator data of cloned sequences\n- [x] Ensured _.random swaps min & max if min is greater than max\n- [x] Ensured _.range preserves the sign of start of -0\n- [x] Ensured _.reduce & _.reduceRight use getIteratee in their array branch\n- [x] Fixed rounding issue with the precision param of _.floor\n- [x] Added flush method to debounced & throttled functions\n\n** LATER **\nMisc:\n- [ ] Made _.forEach, _.forIn, _.forOwn, & _.times implicitly end a chain sequence\n- [ ] Removed thisArg params from most methods\n- [ ] Made “By” methods provide a single param to iteratees\n- [ ] Made _.words chainable by default\n- [ ] Removed isDeep params from _.clone & _.flatten\n- [ ] Removed _.bindAll support for binding all methods when no names are provided\n- [ ] Removed func-first param signature from _.before & _.after\n- [ ] _.extend as an alias of _.assignIn\n- [ ] _.extendWith as an alias of _.assignInWith\n- [ ] Added clear method to _.memoize.Cache\n- [ ] Added support for ES6 maps, sets, & symbols to _.clone, _.isEqual, & _.toArray\n- [x] Enabled _.flow & _.flowRight to accept an array of functions\n- [ ] Ensured “Collection” methods treat functions as objects\n- [ ] Ensured _.assign, _.defaults, & _.merge coerce object values to objects\n- [ ] Ensured _.bindKey bound functions call object[key] when called with the new operator\n- [ ] Ensured _.isFunction returns true for generator functions\n- [ ] Ensured _.merge assigns typed arrays directly\n- [ ] Made _(...) an iterator & iterable\n- [ ] Made _.drop, _.take, & right forms coerce n of undefined to 0\n\nMethods:\n- [ ] _.concat\n- [ ] _.differenceBy\n- [ ] _.differenceWith\n- [ ] _.flatMap\n- [ ] _.fromPairs\n- [ ] _.intersectionBy\n- [ ] _.intersectionWith\n- [ ] _.join\n- [ ] _.pullAll\n- [ ] _.pullAllBy\n- [ ] _.reverse\n- [ ] _.sortedLastIndexOf\n- [ ] _.unionBy\n- [ ] _.unionWith\n- [ ] _.uniqWith\n- [ ] _.xorBy\n- [ ] _.xorWith\n- [ ] _.toString\n\n- [ ] _.invoke\n- [ ] _.setWith\n- [ ] _.toPairs\n- [ ] _.toPairsIn\n- [ ] _.unset\n\n- [ ] _.replace\n- [ ] _.split\n\n- [ ] _.cond\n- [ ] _.nthArg\n- [ ] _.over\n- [ ] _.overEvery\n- [ ] _.overSome\n- [ ] _.rangeRight\n\n- [ ] _.next\n*/\n\nexport = _;\nexport as namespace _;\n\ndeclare var _: _.LoDashStatic;\n\ntype PartialObject = Partial;\n\ndeclare namespace _ {\n type Many = T | T[];\n\n interface LoDashStatic {\n /**\n * Creates a lodash object which wraps the given value to enable intuitive method chaining.\n *\n * In addition to Lo-Dash methods, wrappers also have the following Array methods:\n * concat, join, pop, push, reverse, shift, slice, sort, splice, and unshift\n *\n * Chaining is supported in custom builds as long as the value method is implicitly or\n * explicitly included in the build.\n *\n * The chainable wrapper functions are:\n * after, assign, bind, bindAll, bindKey, chain, chunk, compact, compose, concat, countBy,\n * createCallback, curry, debounce, defaults, defer, delay, difference, filter, flatten,\n * forEach, forEachRight, forIn, forInRight, forOwn, forOwnRight, functions, groupBy,\n * keyBy, initial, intersection, invert, invoke, keys, map, max, memoize, merge, min,\n * object, omit, once, pairs, partial, partialRight, pick, pluck, pull, push, range, reject,\n * remove, rest, reverse, sample, shuffle, slice, sort, sortBy, splice, tap, throttle, times,\n * toArray, transform, union, uniq, unset, unshift, unzip, values, where, without, wrap, and zip\n *\n * The non-chainable wrapper functions are:\n * clone, cloneDeep, contains, escape, every, find, findIndex, findKey, findLast,\n * findLastIndex, findLastKey, has, identity, indexOf, isArguments, isArray, isBoolean,\n * isDate, isElement, isEmpty, isEqual, isFinite, isFunction, isNaN, isNull, isNumber,\n * isObject, isPlainObject, isRegExp, isString, isUndefined, join, lastIndexOf, mixin,\n * noConflict, parseInt, pop, random, reduce, reduceRight, result, shift, size, some,\n * sortedIndex, runInContext, template, unescape, uniqueId, and value\n *\n * The wrapper functions first and last return wrapped values when n is provided, otherwise\n * they return unwrapped values.\n *\n * Explicit chaining can be enabled by using the _.chain method.\n **/\n (value: number): LoDashImplicitWrapper;\n (value: string): LoDashImplicitStringWrapper;\n (value: boolean): LoDashImplicitWrapper;\n (value: null | undefined): LoDashImplicitWrapper;\n (value: number[]): LoDashImplicitNumberArrayWrapper;\n (value: T[]): LoDashImplicitArrayWrapper;\n (value: T[] | null | undefined): LoDashImplicitNillableArrayWrapper;\n (value: T): LoDashImplicitObjectWrapper;\n (value: T | null | undefined): LoDashImplicitNillableObjectWrapper;\n (value: any): LoDashImplicitWrapper;\n\n /**\n * The semantic version number.\n **/\n VERSION: string;\n\n /**\n * By default, the template delimiters used by Lo-Dash are similar to those in embedded Ruby\n * (ERB). Change the following template settings to use alternative delimiters.\n **/\n templateSettings: TemplateSettings;\n }\n\n /**\n * By default, the template delimiters used by Lo-Dash are similar to those in embedded Ruby\n * (ERB). Change the following template settings to use alternative delimiters.\n **/\n interface TemplateSettings {\n /**\n * The \"escape\" delimiter.\n **/\n escape?: RegExp;\n\n /**\n * The \"evaluate\" delimiter.\n **/\n evaluate?: RegExp;\n\n /**\n * An object to import into the template as local variables.\n **/\n imports?: Dictionary;\n\n /**\n * The \"interpolate\" delimiter.\n **/\n interpolate?: RegExp;\n\n /**\n * Used to reference the data object in the template text.\n **/\n variable?: string;\n }\n\n /**\n * Creates a cache object to store key/value pairs.\n */\n interface MapCache {\n /**\n * Removes `key` and its value from the cache.\n * @param key The key of the value to remove.\n * @return Returns `true` if the entry was removed successfully, else `false`.\n */\n delete(key: string): boolean;\n\n /**\n * Gets the cached value for `key`.\n * @param key The key of the value to get.\n * @return Returns the cached value.\n */\n get(key: string): any;\n\n /**\n * Checks if a cached value for `key` exists.\n * @param key The key of the entry to check.\n * @return Returns `true` if an entry for `key` exists, else `false`.\n */\n has(key: string): boolean;\n\n /**\n * Sets `value` to `key` of the cache.\n * @param key The key of the value to cache.\n * @param value The value to cache.\n * @return Returns the cache object.\n */\n set(key: string, value: any): _.Dictionary;\n\n /**\n * Removes all key-value entries from the map.\n */\n clear(): void;\n }\n interface MapCacheConstructor {\n new (): MapCache;\n }\n\n interface LoDashWrapperBase { }\n\n interface LoDashImplicitWrapperBase extends LoDashWrapperBase { }\n\n interface LoDashExplicitWrapperBase extends LoDashWrapperBase { }\n\n interface LoDashImplicitWrapper extends LoDashImplicitWrapperBase> { }\n\n interface LoDashExplicitWrapper extends LoDashExplicitWrapperBase> { }\n\n interface LoDashImplicitStringWrapper extends LoDashImplicitWrapper { }\n\n interface LoDashExplicitStringWrapper extends LoDashExplicitWrapper { }\n\n interface LoDashImplicitObjectWrapperBase extends LoDashImplicitWrapperBase { }\n\n interface LoDashImplicitObjectWrapper extends LoDashImplicitObjectWrapperBase> { }\n\n interface LoDashImplicitNillableObjectWrapper extends LoDashImplicitObjectWrapperBase> { }\n\n interface LoDashExplicitObjectWrapperBase extends LoDashExplicitWrapperBase { }\n\n interface LoDashExplicitObjectWrapper extends LoDashExplicitObjectWrapperBase> { }\n\n interface LoDashExplicitNillableObjectWrapper extends LoDashExplicitObjectWrapperBase> { }\n\n interface LoDashImplicitArrayWrapperBase extends LoDashImplicitWrapperBase {\n pop(): T | undefined;\n push(...items: T[]): TWrapper;\n shift(): T | undefined;\n sort(compareFn?: (a: T, b: T) => number): TWrapper;\n splice(start: number): TWrapper;\n splice(start: number, deleteCount: number, ...items: T[]): TWrapper;\n unshift(...items: T[]): TWrapper;\n }\n\n interface LoDashImplicitArrayWrapper extends LoDashImplicitArrayWrapperBase> { }\n\n interface LoDashImplicitNillableArrayWrapper extends LoDashImplicitArrayWrapperBase> { }\n\n interface LoDashImplicitNumberArrayWrapperBase extends LoDashImplicitArrayWrapperBase { }\n\n interface LoDashImplicitNumberArrayWrapper extends LoDashImplicitArrayWrapper { }\n\n interface LoDashExplicitArrayWrapperBase extends LoDashExplicitWrapperBase {\n pop(): LoDashExplicitObjectWrapper;\n push(...items: T[]): TWrapper;\n shift(): LoDashExplicitObjectWrapper;\n sort(compareFn?: (a: T, b: T) => number): TWrapper;\n splice(start: number): TWrapper;\n splice(start: number, deleteCount: number, ...items: T[]): TWrapper;\n unshift(...items: T[]): TWrapper;\n }\n\n interface LoDashExplicitArrayWrapper extends LoDashExplicitArrayWrapperBase> { }\n\n interface LoDashExplicitNillableArrayWrapper extends LoDashExplicitArrayWrapperBase> { }\n\n interface LoDashExplicitNumberArrayWrapper extends LoDashExplicitArrayWrapper { }\n\n /*********\n * Array *\n *********/\n\n //_.chunk\n interface LoDashStatic {\n /**\n * Creates an array of elements split into groups the length of size. If collection can’t be split evenly, the\n * final chunk will be the remaining elements.\n *\n * @param array The array to process.\n * @param size The length of each chunk.\n * @return Returns the new array containing chunks.\n */\n chunk(\n array: List | null | undefined,\n size?: number\n ): T[][];\n }\n\n interface LoDashImplicitArrayWrapperBase {\n /**\n * @see _.chunk\n */\n chunk(size?: number): LoDashImplicitArrayWrapper;\n }\n\n interface LoDashImplicitObjectWrapperBase {\n /**\n * @see _.chunk\n */\n chunk(size?: number): LoDashImplicitArrayWrapper;\n }\n\n interface LoDashExplicitArrayWrapperBase {\n /**\n * @see _.chunk\n */\n chunk(size?: number): LoDashExplicitArrayWrapper;\n }\n\n interface LoDashExplicitObjectWrapperBase {\n /**\n * @see _.chunk\n */\n chunk(size?: number): LoDashExplicitArrayWrapper;\n }\n\n //_.compact\n interface LoDashStatic {\n /**\n * Creates an array with all falsey values removed. The values false, null, 0, \"\", undefined, and NaN are\n * falsey.\n *\n * @param array The array to compact.\n * @return (Array) Returns the new array of filtered values.\n */\n compact(array?: List | null | undefined): T[];\n }\n\n interface LoDashImplicitArrayWrapperBase {\n /**\n * @see _.compact\n */\n compact(): LoDashImplicitArrayWrapper;\n }\n\n interface LoDashImplicitObjectWrapperBase {\n /**\n * @see _.compact\n */\n compact(): LoDashImplicitArrayWrapper;\n }\n\n interface LoDashExplicitArrayWrapperBase {\n /**\n * @see _.compact\n */\n compact(): LoDashExplicitArrayWrapper;\n }\n\n interface LoDashExplicitObjectWrapperBase {\n /**\n * @see _.compact\n */\n compact(): LoDashExplicitArrayWrapper;\n }\n\n //_.concat DUMMY\n interface LoDashStatic {\n /**\n * Creates a new array concatenating `array` with any additional arrays\n * and/or values.\n *\n * @static\n * @memberOf _\n * @category Array\n * @param {Array} array The array to concatenate.\n * @param {...*} [values] The values to concatenate.\n * @returns {Array} Returns the new concatenated array.\n * @example\n *\n * var array = [1];\n * var other = _.concat(array, 2, [3], [[4]]);\n *\n * console.log(other);\n * // => [1, 2, 3, [4]]\n *\n * console.log(array);\n * // => [1]\n */\n concat(array: List, ...values: Array>): T[];\n }\n\n //_.difference\n interface LoDashStatic {\n /**\n * Creates an array of unique array values not included in the other provided arrays using SameValueZero for\n * equality comparisons.\n *\n * @param array The array to inspect.\n * @param values The arrays of values to exclude.\n * @return Returns the new array of filtered values.\n */\n difference(\n array: List | null | undefined,\n ...values: Array>\n ): T[];\n }\n\n interface LoDashImplicitArrayWrapperBase {\n /**\n * @see _.difference\n */\n difference(...values: Array>): LoDashImplicitArrayWrapper;\n }\n\n interface LoDashImplicitObjectWrapperBase {\n /**\n * @see _.difference\n */\n difference(...values: Array>): LoDashImplicitArrayWrapper;\n }\n\n interface LoDashExplicitArrayWrapperBase {\n /**\n * @see _.difference\n */\n difference(...values: Array>): LoDashExplicitArrayWrapper;\n }\n\n interface LoDashExplicitObjectWrapperBase {\n /**\n * @see _.difference\n */\n difference(...values: Array>): LoDashExplicitArrayWrapper;\n }\n\n //_.differenceBy\n interface LoDashStatic {\n /**\n * This method is like _.difference except that it accepts iteratee which is invoked for each element of array\n * and values to generate the criterion by which uniqueness is computed. The iteratee is invoked with one\n * argument: (value).\n *\n * @param array The array to inspect.\n * @param values The values to exclude.\n * @param iteratee The iteratee invoked per element.\n * @returns Returns the new array of filtered values.\n */\n differenceBy(\n array: List | null | undefined,\n values?: List,\n iteratee?: ((value: T) => any)|string\n ): T[];\n\n /**\n * @see _.differenceBy\n */\n differenceBy(\n array: List | null | undefined,\n values?: List,\n iteratee?: W\n ): T[];\n\n /**\n * @see _.differenceBy\n */\n differenceBy(\n array: List | null | undefined,\n values1?: List,\n values2?: List,\n iteratee?: ((value: T) => any)|string\n ): T[];\n\n /**\n * @see _.differenceBy\n */\n differenceBy(\n array: List | null | undefined,\n values1?: List,\n values2?: List,\n iteratee?: W\n ): T[];\n\n /**\n * @see _.differenceBy\n */\n differenceBy(\n array: List | null | undefined,\n values1?: List,\n values2?: List,\n values3?: List,\n iteratee?: ((value: T) => any)|string\n ): T[];\n\n /**\n * @see _.differenceBy\n */\n differenceBy(\n array: List | null | undefined,\n values1?: List,\n values2?: List,\n values3?: List,\n iteratee?: W\n ): T[];\n\n /**\n * @see _.differenceBy\n */\n differenceBy(\n array: List | null | undefined,\n values1?: List,\n values2?: List,\n values3?: List,\n values4?: List,\n iteratee?: W\n ): T[];\n\n /**\n * @see _.differenceBy\n */\n differenceBy(\n array: List | null | undefined,\n values1?: List,\n values2?: List,\n values3?: List,\n values4?: List,\n iteratee?: ((value: T) => any)|string\n ): T[];\n\n /**\n * @see _.differenceBy\n */\n differenceBy(\n array: List | null | undefined,\n values1?: List,\n values2?: List,\n values3?: List,\n values4?: List,\n values5?: List,\n iteratee?: ((value: T) => any)|string\n ): T[];\n\n /**\n * @see _.differenceBy\n */\n differenceBy(\n array: List | null | undefined,\n values1?: List,\n values2?: List,\n values3?: List,\n values4?: List,\n values5?: List,\n iteratee?: W\n ): T[];\n\n /**\n * @see _.differenceBy\n */\n differenceBy(\n array: List | null | undefined,\n ...values: any[]\n ): T[];\n }\n\n interface LoDashImplicitArrayWrapperBase {\n /**\n * @see _.differenceBy\n */\n differenceBy(\n values?: List,\n iteratee?: ((value: T) => any)|string\n ): LoDashImplicitArrayWrapper;\n\n /**\n * @see _.differenceBy\n */\n differenceBy(\n values?: List,\n iteratee?: W\n ): LoDashImplicitArrayWrapper;\n\n /**\n * @see _.differenceBy\n */\n differenceBy(\n values1?: List,\n values2?: List,\n iteratee?: ((value: T) => any)|string\n ): LoDashImplicitArrayWrapper;\n\n /**\n * @see _.differenceBy\n */\n differenceBy(\n values1?: List,\n values2?: List,\n iteratee?: W\n ): LoDashImplicitArrayWrapper;\n\n /**\n * @see _.differenceBy\n */\n differenceBy(\n values1?: List,\n values2?: List,\n values3?: List,\n iteratee?: ((value: T) => any)|string\n ): LoDashImplicitArrayWrapper;\n\n /**\n * @see _.differenceBy\n */\n differenceBy(\n values1?: List,\n values2?: List,\n values3?: List,\n iteratee?: W\n ): LoDashImplicitArrayWrapper;\n\n /**\n * @see _.differenceBy\n */\n differenceBy(\n values1?: List,\n values2?: List,\n values3?: List,\n values4?: List,\n iteratee?: ((value: T) => any)|string\n ): LoDashImplicitArrayWrapper;\n\n /**\n * @see _.differenceBy\n */\n differenceBy(\n values1?: List,\n values2?: List,\n values3?: List,\n values4?: List,\n iteratee?: W\n ): LoDashImplicitArrayWrapper;\n\n /**\n * @see _.differenceBy\n */\n differenceBy(\n values1?: List,\n values2?: List,\n values3?: List,\n values4?: List,\n values5?: List,\n iteratee?: ((value: T) => any)|string\n ): LoDashImplicitArrayWrapper;\n\n /**\n * @see _.differenceBy\n */\n differenceBy(\n values1?: List,\n values2?: List,\n values3?: List,\n values4?: List,\n values5?: List,\n iteratee?: W\n ): LoDashImplicitArrayWrapper;\n\n /**\n * @see _.differenceBy\n */\n differenceBy(\n ...values: any[]\n ): LoDashImplicitArrayWrapper;\n }\n\n interface LoDashImplicitObjectWrapperBase {\n /**\n * @see _.differenceBy\n */\n differenceBy(\n values?: List,\n iteratee?: ((value: T) => any)|string\n ): LoDashImplicitArrayWrapper;\n\n /**\n * @see _.differenceBy\n */\n differenceBy(\n values?: List,\n iteratee?: W\n ): LoDashImplicitArrayWrapper;\n\n /**\n * @see _.differenceBy\n */\n differenceBy(\n values1?: List,\n values2?: List,\n iteratee?: ((value: T) => any)|string\n ): LoDashImplicitArrayWrapper;\n\n /**\n * @see _.differenceBy\n */\n differenceBy(\n values1?: List,\n values2?: List,\n iteratee?: W\n ): LoDashImplicitArrayWrapper;\n\n /**\n * @see _.differenceBy\n */\n differenceBy(\n values1?: List,\n values2?: List,\n values3?: List,\n iteratee?: ((value: T) => any)|string\n ): LoDashImplicitArrayWrapper;\n\n /**\n * @see _.differenceBy\n */\n differenceBy(\n values1?: List,\n values2?: List,\n values3?: List,\n iteratee?: W\n ): LoDashImplicitArrayWrapper;\n\n /**\n * @see _.differenceBy\n */\n differenceBy(\n values1?: List,\n values2?: List,\n values3?: List,\n values4?: List,\n iteratee?: ((value: T) => any)|string\n ): LoDashImplicitArrayWrapper;\n\n /**\n * @see _.differenceBy\n */\n differenceBy(\n values1?: List,\n values2?: List,\n values3?: List,\n values4?: List,\n iteratee?: W\n ): LoDashImplicitArrayWrapper;\n\n /**\n * @see _.differenceBy\n */\n differenceBy(\n values1?: List,\n values2?: List,\n values3?: List,\n values4?: List,\n values5?: List,\n iteratee?: ((value: T) => any)|string\n ): LoDashImplicitArrayWrapper;\n\n /**\n * @see _.differenceBy\n */\n differenceBy(\n values1?: List,\n values2?: List,\n values3?: List,\n values4?: List,\n values5?: List,\n iteratee?: W\n ): LoDashImplicitArrayWrapper;\n\n /**\n * @see _.differenceBy\n */\n differenceBy(\n ...values: any[]\n ): LoDashImplicitArrayWrapper;\n }\n\n interface LoDashExplicitArrayWrapperBase {\n /**\n * @see _.differenceBy\n */\n differenceBy(\n values?: List,\n iteratee?: ((value: T) => any)|string\n ): LoDashExplicitArrayWrapper;\n\n /**\n * @see _.differenceBy\n */\n differenceBy(\n values?: List,\n iteratee?: W\n ): LoDashExplicitArrayWrapper;\n\n /**\n * @see _.differenceBy\n */\n differenceBy(\n values1?: List,\n values2?: List,\n iteratee?: ((value: T) => any)|string\n ): LoDashExplicitArrayWrapper;\n\n /**\n * @see _.differenceBy\n */\n differenceBy(\n values1?: List,\n values2?: List,\n iteratee?: W\n ): LoDashExplicitArrayWrapper;\n\n /**\n * @see _.differenceBy\n */\n differenceBy(\n values1?: List,\n values2?: List,\n values3?: List,\n iteratee?: ((value: T) => any)|string\n ): LoDashExplicitArrayWrapper;\n\n /**\n * @see _.differenceBy\n */\n differenceBy(\n values1?: List,\n values2?: List,\n values3?: List,\n iteratee?: W\n ): LoDashExplicitArrayWrapper;\n\n /**\n * @see _.differenceBy\n */\n differenceBy(\n values1?: List,\n values2?: List,\n values3?: List,\n values4?: List,\n iteratee?: ((value: T) => any)|string\n ): LoDashExplicitArrayWrapper;\n\n /**\n * @see _.differenceBy\n */\n differenceBy(\n values1?: List,\n values2?: List,\n values3?: List,\n values4?: List,\n iteratee?: W\n ): LoDashExplicitArrayWrapper;\n\n /**\n * @see _.differenceBy\n */\n differenceBy(\n values1?: List,\n values2?: List,\n values3?: List,\n values4?: List,\n values5?: List,\n iteratee?: ((value: T) => any)|string\n ): LoDashExplicitArrayWrapper;\n\n /**\n * @see _.differenceBy\n */\n differenceBy(\n values1?: List,\n values2?: List,\n values3?: List,\n values4?: List,\n values5?: List,\n iteratee?: W\n ): LoDashExplicitArrayWrapper;\n\n /**\n * @see _.differenceBy\n */\n differenceBy(\n ...values: any[]\n ): LoDashExplicitArrayWrapper;\n }\n\n interface LoDashExplicitObjectWrapperBase {\n /**\n * @see _.differenceBy\n */\n differenceBy(\n values?: List,\n iteratee?: ((value: T) => any)|string\n ): LoDashExplicitArrayWrapper;\n\n /**\n * @see _.differenceBy\n */\n differenceBy(\n values?: List,\n iteratee?: W\n ): LoDashExplicitArrayWrapper;\n\n /**\n * @see _.differenceBy\n */\n differenceBy(\n values1?: List,\n values2?: List,\n iteratee?: ((value: T) => any)|string\n ): LoDashExplicitArrayWrapper;\n\n /**\n * @see _.differenceBy\n */\n differenceBy(\n values1?: List,\n values2?: List,\n iteratee?: W\n ): LoDashExplicitArrayWrapper;\n\n /**\n * @see _.differenceBy\n */\n differenceBy(\n values1?: List,\n values2?: List,\n values3?: List,\n iteratee?: ((value: T) => any)|string\n ): LoDashExplicitArrayWrapper;\n\n /**\n * @see _.differenceBy\n */\n differenceBy(\n values1?: List,\n values2?: List,\n values3?: List,\n iteratee?: W\n ): LoDashExplicitArrayWrapper;\n\n /**\n * @see _.differenceBy\n */\n differenceBy(\n values1?: List,\n values2?: List,\n values3?: List,\n values4?: List,\n iteratee?: ((value: T) => any)|string\n ): LoDashExplicitArrayWrapper;\n\n /**\n * @see _.differenceBy\n */\n differenceBy(\n values1?: List,\n values2?: List,\n values3?: List,\n values4?: List,\n iteratee?: W\n ): LoDashExplicitArrayWrapper;\n\n /**\n * @see _.differenceBy\n */\n differenceBy(\n values1?: List,\n values2?: List,\n values3?: List,\n values4?: List,\n values5?: List,\n iteratee?: ((value: T) => any)|string\n ): LoDashExplicitArrayWrapper;\n\n /**\n * @see _.differenceBy\n */\n differenceBy(\n values1?: List,\n values2?: List,\n values3?: List,\n values4?: List,\n values5?: List,\n iteratee?: W\n ): LoDashExplicitArrayWrapper;\n\n /**\n * @see _.differenceBy\n */\n differenceBy(\n ...values: any[]\n ): LoDashExplicitArrayWrapper;\n }\n\n //_.differenceWith DUMMY\n interface LoDashStatic {\n /**\n * Creates an array of unique `array` values not included in the other\n * provided arrays using [`SameValueZero`](http://ecma-international.org/ecma-262/6.0/#sec-samevaluezero)\n * for equality comparisons.\n *\n * @static\n * @memberOf _\n * @category Array\n * @param {Array} array The array to inspect.\n * @param {...Array} [values] The values to exclude.\n * @returns {Array} Returns the new array of filtered values.\n * @example\n *\n * _.difference([3, 2, 1], [4, 2]);\n * // => [3, 1]\n */\n differenceWith(\n array: List,\n ...values: any[]\n ): any[];\n }\n\n //_.drop\n interface LoDashStatic {\n /**\n * Creates a slice of array with n elements dropped from the beginning.\n *\n * @param array The array to query.\n * @param n The number of elements to drop.\n * @return Returns the slice of array.\n */\n drop(array: List | null | undefined, n?: number): T[];\n }\n\n interface LoDashImplicitArrayWrapperBase {\n /**\n * @see _.drop\n */\n drop(n?: number): LoDashImplicitArrayWrapper;\n }\n\n interface LoDashImplicitObjectWrapperBase {\n /**\n * @see _.drop\n */\n drop(n?: number): LoDashImplicitArrayWrapper;\n }\n\n interface LoDashExplicitArrayWrapperBase {\n /**\n * @see _.drop\n */\n drop(n?: number): LoDashExplicitArrayWrapper;\n }\n\n interface LoDashExplicitObjectWrapperBase {\n /**\n * @see _.drop\n */\n drop(n?: number): LoDashExplicitArrayWrapper;\n }\n\n //_.dropRight\n interface LoDashStatic {\n /**\n * Creates a slice of array with n elements dropped from the end.\n *\n * @param array The array to query.\n * @param n The number of elements to drop.\n * @return Returns the slice of array.\n */\n dropRight(\n array: List | null | undefined,\n n?: number\n ): T[];\n }\n\n interface LoDashImplicitArrayWrapperBase {\n /**\n * @see _.dropRight\n */\n dropRight(n?: number): LoDashImplicitArrayWrapper;\n }\n\n interface LoDashImplicitObjectWrapperBase {\n /**\n * @see _.dropRight\n */\n dropRight(n?: number): LoDashImplicitArrayWrapper;\n }\n\n interface LoDashExplicitArrayWrapperBase {\n /**\n * @see _.dropRight\n */\n dropRight(n?: number): LoDashExplicitArrayWrapper;\n }\n\n interface LoDashExplicitObjectWrapperBase {\n /**\n * @see _.dropRight\n */\n dropRight(n?: number): LoDashExplicitArrayWrapper;\n }\n\n //_.dropRightWhile\n interface LoDashStatic {\n /**\n * Creates a slice of array excluding elements dropped from the end. Elements are dropped until predicate\n * returns falsey. The predicate is bound to thisArg and invoked with three arguments: (value, index, array).\n *\n * If a property name is provided for predicate the created _.property style callback returns the property\n * value of the given element.\n *\n * If a value is also provided for thisArg the created _.matchesProperty style callback returns true for\n * elements that have a matching property value, else false.\n *\n * If an object is provided for predicate the created _.matches style callback returns true for elements that\n * match the properties of the given object, else false.\n *\n * @param array The array to query.\n * @param predicate The function invoked per iteration.\n * @param thisArg The this binding of predicate.\n * @return Returns the slice of array.\n */\n dropRightWhile(\n array: List | null | undefined,\n predicate?: ListIterator\n ): TValue[];\n\n /**\n * @see _.dropRightWhile\n */\n dropRightWhile(\n array: List | null | undefined,\n predicate?: string\n ): TValue[];\n\n /**\n * @see _.dropRightWhile\n */\n dropRightWhile(\n array: List | null | undefined,\n predicate?: TWhere\n ): TValue[];\n }\n\n interface LoDashImplicitArrayWrapperBase {\n /**\n * @see _.dropRightWhile\n */\n dropRightWhile(\n predicate?: ListIterator\n ): LoDashImplicitArrayWrapper;\n\n /**\n * @see _.dropRightWhile\n */\n dropRightWhile(\n predicate?: string\n ): LoDashImplicitArrayWrapper;\n\n /**\n * @see _.dropRightWhile\n */\n dropRightWhile(\n predicate?: TWhere\n ): LoDashImplicitArrayWrapper;\n }\n\n interface LoDashImplicitObjectWrapperBase {\n /**\n * @see _.dropRightWhile\n */\n dropRightWhile(\n predicate?: ListIterator\n ): LoDashImplicitArrayWrapper;\n\n /**\n * @see _.dropRightWhile\n */\n dropRightWhile(\n predicate?: string\n ): LoDashImplicitArrayWrapper;\n\n /**\n * @see _.dropRightWhile\n */\n dropRightWhile(\n predicate?: TWhere\n ): LoDashImplicitArrayWrapper;\n }\n\n interface LoDashExplicitArrayWrapperBase {\n /**\n * @see _.dropRightWhile\n */\n dropRightWhile(\n predicate?: ListIterator\n ): LoDashExplicitArrayWrapper;\n\n /**\n * @see _.dropRightWhile\n */\n dropRightWhile(\n predicate?: string\n ): LoDashExplicitArrayWrapper;\n\n /**\n * @see _.dropRightWhile\n */\n dropRightWhile(\n predicate?: TWhere\n ): LoDashExplicitArrayWrapper;\n }\n\n interface LoDashExplicitObjectWrapperBase {\n /**\n * @see _.dropRightWhile\n */\n dropRightWhile(\n predicate?: ListIterator\n ): LoDashExplicitArrayWrapper;\n\n /**\n * @see _.dropRightWhile\n */\n dropRightWhile(\n predicate?: string\n ): LoDashExplicitArrayWrapper;\n\n /**\n * @see _.dropRightWhile\n */\n dropRightWhile(\n predicate?: TWhere\n ): LoDashExplicitArrayWrapper;\n }\n\n //_.dropWhile\n interface LoDashStatic {\n /**\n * Creates a slice of array excluding elements dropped from the beginning. Elements are dropped until predicate\n * returns falsey. The predicate is bound to thisArg and invoked with three arguments: (value, index, array).\n *\n * If a property name is provided for predicate the created _.property style callback returns the property\n * value of the given element.\n *\n * If a value is also provided for thisArg the created _.matchesProperty style callback returns true for\n * elements that have a matching property value, else false.\n *\n * If an object is provided for predicate the created _.matches style callback returns true for elements that\n * have the properties of the given object, else false.\n *\n * @param array The array to query.\n * @param predicate The function invoked per iteration.\n * @param thisArg The this binding of predicate.\n * @return Returns the slice of array.\n */\n dropWhile(\n array: List | null | undefined,\n predicate?: ListIterator\n ): TValue[];\n\n /**\n * @see _.dropWhile\n */\n dropWhile(\n array: List | null | undefined,\n predicate?: string\n ): TValue[];\n\n /**\n * @see _.dropWhile\n */\n dropWhile(\n array: List | null | undefined,\n predicate?: TWhere\n ): TValue[];\n }\n\n interface LoDashImplicitArrayWrapperBase {\n /**\n * @see _.dropWhile\n */\n dropWhile(\n predicate?: ListIterator\n ): LoDashImplicitArrayWrapper;\n\n /**\n * @see _.dropWhile\n */\n dropWhile(\n predicate?: string\n ): LoDashImplicitArrayWrapper;\n\n /**\n * @see _.dropWhile\n */\n dropWhile(\n predicate?: TWhere\n ): LoDashImplicitArrayWrapper;\n }\n\n interface LoDashImplicitObjectWrapperBase {\n /**\n * @see _.dropWhile\n */\n dropWhile(\n predicate?: ListIterator\n ): LoDashImplicitArrayWrapper;\n\n /**\n * @see _.dropWhile\n */\n dropWhile(\n predicate?: string\n ): LoDashImplicitArrayWrapper;\n\n /**\n * @see _.dropWhile\n */\n dropWhile(\n predicate?: TWhere\n ): LoDashImplicitArrayWrapper;\n }\n\n interface LoDashExplicitArrayWrapperBase {\n /**\n * @see _.dropWhile\n */\n dropWhile(\n predicate?: ListIterator\n ): LoDashExplicitArrayWrapper;\n\n /**\n * @see _.dropWhile\n */\n dropWhile(\n predicate?: string\n ): LoDashExplicitArrayWrapper;\n\n /**\n * @see _.dropWhile\n */\n dropWhile(\n predicate?: TWhere\n ): LoDashExplicitArrayWrapper;\n }\n\n interface LoDashExplicitObjectWrapperBase {\n /**\n * @see _.dropWhile\n */\n dropWhile(\n predicate?: ListIterator\n ): LoDashExplicitArrayWrapper;\n\n /**\n * @see _.dropWhile\n */\n dropWhile(\n predicate?: string\n ): LoDashExplicitArrayWrapper;\n\n /**\n * @see _.dropWhile\n */\n dropWhile(\n predicate?: TWhere\n ): LoDashExplicitArrayWrapper;\n }\n\n //_.fill\n interface LoDashStatic {\n /**\n * Fills elements of array with value from start up to, but not including, end.\n *\n * Note: This method mutates array.\n *\n * @param array The array to fill.\n * @param value The value to fill array with.\n * @param start The start position.\n * @param end The end position.\n * @return Returns array.\n */\n fill(\n array: any[] | null | undefined,\n value: T,\n start?: number,\n end?: number\n ): T[];\n\n /**\n * @see _.fill\n */\n fill(\n array: List | null | undefined,\n value: T,\n start?: number,\n end?: number\n ): List;\n }\n\n interface LoDashImplicitArrayWrapperBase {\n /**\n * @see _.fill\n */\n fill(\n value: T,\n start?: number,\n end?: number\n ): LoDashImplicitArrayWrapper;\n }\n\n interface LoDashImplicitObjectWrapperBase {\n /**\n * @see _.fill\n */\n fill(\n value: T,\n start?: number,\n end?: number\n ): LoDashImplicitObjectWrapper>;\n }\n\n interface LoDashExplicitArrayWrapperBase {\n /**\n * @see _.fill\n */\n fill(\n value: T,\n start?: number,\n end?: number\n ): LoDashExplicitArrayWrapper;\n }\n\n interface LoDashExplicitObjectWrapperBase {\n /**\n * @see _.fill\n */\n fill(\n value: T,\n start?: number,\n end?: number\n ): LoDashExplicitObjectWrapper>;\n }\n\n //_.findIndex\n interface LoDashStatic {\n /**\n * This method is like _.find except that it returns the index of the first element predicate returns truthy\n * for instead of the element itself.\n *\n * If a property name is provided for predicate the created _.property style callback returns the property\n * value of the given element.\n *\n * If a value is also provided for thisArg the created _.matchesProperty style callback returns true for\n * elements that have a matching property value, else false.\n *\n * If an object is provided for predicate the created _.matches style callback returns true for elements that\n * have the properties of the given object, else false.\n *\n * @param array The array to search.\n * @param predicate The function invoked per iteration.\n * @param fromIndex The index to search from.\n * @return Returns the index of the found element, else -1.\n */\n findIndex(\n array: List | null | undefined,\n predicate?: ListIterator,\n fromIndex?: number\n ): number;\n\n /**\n * @see _.findIndex\n */\n findIndex(\n array: List | null | undefined,\n predicate?: string,\n fromIndex?: number\n ): number;\n\n /**\n * @see _.findIndex\n */\n findIndex(\n array: List | null | undefined,\n predicate?: W,\n fromIndex?: number\n ): number;\n }\n\n interface LoDashImplicitArrayWrapperBase {\n /**\n * @see _.findIndex\n */\n findIndex(\n predicate?: ListIterator,\n fromIndex?: number\n ): number;\n\n /**\n * @see _.findIndex\n */\n findIndex(\n predicate?: string,\n fromIndex?: number\n ): number;\n\n /**\n * @see _.findIndex\n */\n findIndex(\n predicate?: W,\n fromIndex?: number\n ): number;\n }\n\n interface LoDashImplicitObjectWrapperBase {\n /**\n * @see _.findIndex\n */\n findIndex(\n predicate?: ListIterator,\n fromIndex?: number\n ): number;\n\n /**\n * @see _.findIndex\n */\n findIndex(\n predicate?: string,\n fromIndex?: number\n ): number;\n\n /**\n * @see _.findIndex\n */\n findIndex(\n predicate?: W,\n fromIndex?: number\n ): number;\n }\n\n interface LoDashExplicitArrayWrapperBase {\n /**\n * @see _.findIndex\n */\n findIndex(\n predicate?: ListIterator,\n fromIndex?: number\n ): LoDashExplicitWrapper;\n\n /**\n * @see _.findIndex\n */\n findIndex(\n predicate?: string,\n fromIndex?: number\n ): LoDashExplicitWrapper;\n\n /**\n * @see _.findIndex\n */\n findIndex(\n predicate?: W,\n fromIndex?: number\n ): LoDashExplicitWrapper;\n }\n\n interface LoDashExplicitObjectWrapperBase {\n /**\n * @see _.findIndex\n */\n findIndex(\n predicate?: ListIterator,\n fromIndex?: number\n ): LoDashExplicitWrapper;\n\n /**\n * @see _.findIndex\n */\n findIndex(\n predicate?: string,\n fromIndex?: number\n ): LoDashExplicitWrapper;\n\n /**\n * @see _.findIndex\n */\n findIndex(\n predicate?: W,\n fromIndex?: number\n ): LoDashExplicitWrapper;\n }\n\n //_.findLastIndex\n interface LoDashStatic {\n /**\n * This method is like _.findIndex except that it iterates over elements of collection from right to left.\n *\n * If a property name is provided for predicate the created _.property style callback returns the property\n * value of the given element.\n *\n * If a value is also provided for thisArg the created _.matchesProperty style callback returns true for\n * elements that have a matching property value, else false.\n *\n * If an object is provided for predicate the created _.matches style callback returns true for elements that\n * have the properties of the given object, else false.\n *\n * @param array The array to search.\n * @param predicate The function invoked per iteration.\n * @param fromIndex The index to search from.\n * @return Returns the index of the found element, else -1.\n */\n findLastIndex(\n array: List | null | undefined,\n predicate?: ListIterator,\n fromIndex?: number\n ): number;\n\n /**\n * @see _.findLastIndex\n */\n findLastIndex(\n array: List | null | undefined,\n predicate?: string,\n fromIndex?: number\n ): number;\n\n /**\n * @see _.findLastIndex\n */\n findLastIndex(\n array: List | null | undefined,\n predicate?: W,\n fromIndex?: number\n ): number;\n }\n\n interface LoDashImplicitArrayWrapperBase {\n /**\n * @see _.findLastIndex\n */\n findLastIndex(\n predicate?: ListIterator,\n fromIndex?: number\n ): number;\n\n /**\n * @see _.findLastIndex\n */\n findLastIndex(\n predicate?: string,\n fromIndex?: number\n ): number;\n\n /**\n * @see _.findLastIndex\n */\n findLastIndex(\n predicate?: W,\n fromIndex?: number\n ): number;\n }\n\n interface LoDashImplicitObjectWrapperBase {\n /**\n * @see _.findLastIndex\n */\n findLastIndex(\n predicate?: ListIterator,\n fromIndex?: number\n ): number;\n\n /**\n * @see _.findLastIndex\n */\n findLastIndex(\n predicate?: string,\n fromIndex?: number\n ): number;\n\n /**\n * @see _.findLastIndex\n */\n findLastIndex(\n predicate?: W,\n fromIndex?: number\n ): number;\n }\n\n interface LoDashExplicitArrayWrapperBase {\n /**\n * @see _.findLastIndex\n */\n findLastIndex(\n predicate?: ListIterator,\n fromIndex?: number\n ): LoDashExplicitWrapper;\n\n /**\n * @see _.findLastIndex\n */\n findLastIndex(\n predicate?: string,\n fromIndex?: number\n ): LoDashExplicitWrapper;\n\n /**\n * @see _.findLastIndex\n */\n findLastIndex(\n predicate?: W,\n fromIndex?: number\n ): LoDashExplicitWrapper;\n }\n\n interface LoDashExplicitObjectWrapperBase {\n /**\n * @see _.findLastIndex\n */\n findLastIndex(\n predicate?: ListIterator,\n fromIndex?: number\n ): LoDashExplicitWrapper;\n\n /**\n * @see _.findLastIndex\n */\n findLastIndex(\n predicate?: string,\n fromIndex?: number\n ): LoDashExplicitWrapper;\n\n /**\n * @see _.findLastIndex\n */\n findLastIndex(\n predicate?: W,\n fromIndex?: number\n ): LoDashExplicitWrapper;\n }\n\n //_.first\n interface LoDashStatic {\n /**\n * @see _.head\n */\n first(array: List | null | undefined): T | undefined;\n }\n\n interface LoDashImplicitWrapper {\n /**\n * @see _.head\n */\n first(): string | undefined;\n }\n\n interface LoDashImplicitArrayWrapperBase {\n /**\n * @see _.head\n */\n first(): T | undefined;\n }\n\n interface LoDashImplicitObjectWrapperBase {\n /**\n * @see _.head\n */\n first(): T | undefined;\n }\n\n interface LoDashExplicitWrapper {\n /**\n * @see _.head\n */\n first(): LoDashExplicitWrapper;\n }\n\n interface LoDashExplicitArrayWrapperBase {\n /**\n * @see _.head\n */\n first(): T;\n }\n\n interface LoDashExplicitObjectWrapperBase {\n /**\n * @see _.head\n */\n first(): T;\n }\n\n interface RecursiveArray extends Array> {}\n interface ListOfRecursiveArraysOrValues extends List> {}\n\n //_.flatten\n interface LoDashStatic {\n /**\n * Flattens a nested array. If isDeep is true the array is recursively flattened, otherwise it’s only\n * flattened a single level.\n *\n * @param array The array to flatten.\n * @param isDeep Specify a deep flatten.\n * @return Returns the new flattened array.\n */\n flatten(array: ListOfRecursiveArraysOrValues | null | undefined, isDeep: boolean): T[];\n\n /**\n * @see _.flatten\n */\n flatten(array: List> | null | undefined): T[];\n\n /**\n * @see _.flatten\n */\n flatten(array: ListOfRecursiveArraysOrValues | null | undefined): RecursiveArray;\n }\n\n interface LoDashImplicitWrapper {\n /**\n * @see _.flatten\n */\n flatten(): LoDashImplicitArrayWrapper;\n }\n\n interface LoDashImplicitArrayWrapperBase {\n /**\n * @see _.flatten\n */\n flatten(isDeep?: boolean): LoDashImplicitArrayWrapper;\n }\n\n interface LoDashImplicitObjectWrapperBase {\n /**\n * @see _.flatten\n */\n flatten(isDeep?: boolean): LoDashImplicitArrayWrapper;\n }\n\n interface LoDashExplicitWrapper {\n /**\n * @see _.flatten\n */\n flatten(): LoDashExplicitArrayWrapper;\n }\n\n interface LoDashExplicitArrayWrapperBase {\n /**\n * @see _.flatten\n */\n flatten(isDeep?: boolean): LoDashExplicitArrayWrapper;\n }\n\n interface LoDashExplicitObjectWrapperBase {\n /**\n * @see _.flatten\n */\n flatten(isDeep?: boolean): LoDashExplicitArrayWrapper;\n }\n\n //_.flattenDeep\n interface LoDashStatic {\n /**\n * Recursively flattens a nested array.\n *\n * @param array The array to recursively flatten.\n * @return Returns the new flattened array.\n */\n flattenDeep(array: ListOfRecursiveArraysOrValues | null | undefined): T[];\n }\n\n interface LoDashImplicitWrapper {\n /**\n * @see _.flattenDeep\n */\n flattenDeep(): LoDashImplicitArrayWrapper;\n }\n\n interface LoDashImplicitArrayWrapperBase {\n /**\n * @see _.flattenDeep\n */\n flattenDeep(): LoDashImplicitArrayWrapper;\n }\n\n interface LoDashImplicitObjectWrapperBase {\n /**\n * @see _.flattenDeep\n */\n flattenDeep(): LoDashImplicitArrayWrapper;\n }\n\n interface LoDashExplicitWrapper {\n /**\n * @see _.flattenDeep\n */\n flattenDeep(): LoDashExplicitArrayWrapper;\n }\n\n interface LoDashExplicitArrayWrapperBase {\n /**\n * @see _.flattenDeep\n */\n flattenDeep(): LoDashExplicitArrayWrapper;\n }\n\n interface LoDashExplicitObjectWrapperBase {\n /**\n * @see _.flattenDeep\n */\n flattenDeep(): LoDashExplicitArrayWrapper;\n }\n\n // _.flattenDepth\n interface LoDashStatic {\n /**\n * Recursively flatten array up to depth times.\n *\n * @param array The array to recursively flatten.\n * @param number The maximum recursion depth.\n * @return Returns the new flattened array.\n */\n flattenDepth(array: ListOfRecursiveArraysOrValues | null | undefined, depth?: number): T[];\n }\n\n //_.fromPairs\n interface LoDashStatic {\n /**\n * The inverse of `_.toPairs`; this method returns an object composed\n * from key-value `pairs`.\n *\n * @static\n * @memberOf _\n * @category Array\n * @param {Array} pairs The key-value pairs.\n * @returns {Object} Returns the new object.\n * @example\n *\n * _.fromPairs([['fred', 30], ['barney', 40]]);\n * // => { 'fred': 30, 'barney': 40 }\n */\n fromPairs(\n array: List<[_.StringRepresentable, T]> | null | undefined\n ): Dictionary;\n\n /**\n @see _.fromPairs\n */\n fromPairs(\n array: List | null | undefined\n ): Dictionary;\n }\n\n //_.fromPairs DUMMY\n interface LoDashImplicitArrayWrapperBase {\n /**\n * @see _.fromPairs\n */\n fromPairs(): LoDashImplicitObjectWrapper;\n }\n\n //_.fromPairs DUMMY\n interface LoDashExplicitArrayWrapperBase {\n /**\n * @see _.fromPairs\n */\n fromPairs(): LoDashExplicitObjectWrapper;\n }\n\n //_.head\n interface LoDashStatic {\n /**\n * Gets the first element of array.\n *\n * @alias _.first\n *\n * @param array The array to query.\n * @return Returns the first element of array.\n */\n head(array: List | null | undefined): T | undefined;\n }\n\n interface LoDashImplicitWrapper {\n /**\n * @see _.head\n */\n head(): string | undefined;\n }\n\n interface LoDashImplicitArrayWrapperBase {\n /**\n * @see _.head\n */\n head(): T | undefined;\n }\n\n interface LoDashImplicitObjectWrapperBase {\n /**\n * @see _.head\n */\n head(): T | undefined;\n }\n\n interface LoDashExplicitWrapper {\n /**\n * @see _.head\n */\n head(): LoDashExplicitWrapper;\n }\n\n interface LoDashExplicitArrayWrapperBase {\n /**\n * @see _.head\n */\n head(): T;\n }\n\n interface LoDashExplicitObjectWrapperBase {\n /**\n * @see _.head\n */\n head(): T;\n }\n\n //_.indexOf\n interface LoDashStatic {\n /**\n * Gets the index at which the first occurrence of `value` is found in `array`\n * using [`SameValueZero`](http://ecma-international.org/ecma-262/6.0/#sec-samevaluezero)\n * for equality comparisons. If `fromIndex` is negative, it's used as the offset\n * from the end of `array`. If `array` is sorted providing `true` for `fromIndex`\n * performs a faster binary search.\n *\n * @static\n * @memberOf _\n * @category Array\n * @param {Array} array The array to search.\n * @param {*} value The value to search for.\n * @param {number} [fromIndex=0] The index to search from.\n * @returns {number} Returns the index of the matched value, else `-1`.\n * @example\n *\n * _.indexOf([1, 2, 1, 2], 2);\n * // => 1\n *\n * // using `fromIndex`\n * _.indexOf([1, 2, 1, 2], 2, 2);\n * // => 3\n */\n indexOf(\n array: List | null | undefined,\n value: T,\n fromIndex?: boolean|number\n ): number;\n }\n\n interface LoDashImplicitArrayWrapperBase {\n /**\n * @see _.indexOf\n */\n indexOf(\n value: T,\n fromIndex?: boolean|number\n ): number;\n }\n\n interface LoDashImplicitObjectWrapperBase {\n /**\n * @see _.indexOf\n */\n indexOf(\n value: TValue,\n fromIndex?: boolean|number\n ): number;\n }\n\n interface LoDashExplicitArrayWrapperBase {\n /**\n * @see _.indexOf\n */\n indexOf(\n value: T,\n fromIndex?: boolean|number\n ): LoDashExplicitWrapper;\n }\n\n interface LoDashExplicitObjectWrapperBase {\n /**\n * @see _.indexOf\n */\n indexOf(\n value: TValue,\n fromIndex?: boolean|number\n ): LoDashExplicitWrapper;\n }\n\n //_.intersectionBy DUMMY\n interface LoDashStatic {\n /**\n * This method is like `_.intersection` except that it accepts `iteratee`\n * which is invoked for each element of each `arrays` to generate the criterion\n * by which uniqueness is computed. The iteratee is invoked with one argument: (value).\n *\n * @static\n * @memberOf _\n * @category Array\n * @param {...Array} [arrays] The arrays to inspect.\n * @param {Function|Object|string} [iteratee=_.identity] The iteratee invoked per element.\n * @returns {Array} Returns the new array of shared values.\n * @example\n *\n * _.intersectionBy([2.1, 1.2], [4.3, 2.4], Math.floor);\n * // => [2.1]\n *\n * // using the `_.property` iteratee shorthand\n * _.intersectionBy([{ 'x': 1 }], [{ 'x': 2 }, { 'x': 1 }], 'x');\n * // => [{ 'x': 1 }]\n */\n intersectionBy(\n array: List,\n ...values: any[]\n ): any[];\n }\n\n //_.intersectionWith DUMMY\n interface LoDashStatic {\n /**\n * This method is like `_.intersection` except that it accepts `comparator`\n * which is invoked to compare elements of `arrays`. The comparator is invoked\n * with two arguments: (arrVal, othVal).\n *\n * @static\n * @memberOf _\n * @category Array\n * @param {...Array} [arrays] The arrays to inspect.\n * @param {Function} [comparator] The comparator invoked per element.\n * @returns {Array} Returns the new array of shared values.\n * @example\n *\n * var objects = [{ 'x': 1, 'y': 2 }, { 'x': 2, 'y': 1 }];\n * var others = [{ 'x': 1, 'y': 1 }, { 'x': 1, 'y': 2 }];\n *\n * _.intersectionWith(objects, others, _.isEqual);\n * // => [{ 'x': 1, 'y': 2 }]\n */\n intersectionWith(\n array: List,\n ...values: any[]\n ): any[];\n }\n\n //_.join\n interface LoDashStatic {\n /**\n * Converts all elements in `array` into a string separated by `separator`.\n *\n * @param array The array to convert.\n * @param separator The element separator.\n * @returns Returns the joined string.\n */\n join(\n array: List | null | undefined,\n separator?: string\n ): string;\n }\n\n interface LoDashImplicitWrapper {\n /**\n * @see _.join\n */\n join(separator?: string): string;\n }\n\n interface LoDashImplicitArrayWrapperBase {\n /**\n * @see _.join\n */\n join(separator?: string): string;\n }\n\n interface LoDashImplicitObjectWrapperBase {\n /**\n * @see _.join\n */\n join(separator?: string): string;\n }\n\n interface LoDashExplicitWrapper {\n /**\n * @see _.join\n */\n join(separator?: string): LoDashExplicitWrapper;\n }\n\n interface LoDashExplicitArrayWrapperBase {\n /**\n * @see _.join\n */\n join(separator?: string): LoDashExplicitWrapper;\n }\n\n interface LoDashExplicitObjectWrapperBase {\n /**\n * @see _.join\n */\n join(separator?: string): LoDashExplicitWrapper;\n }\n\n //_.pullAll DUMMY\n interface LoDashStatic {\n /**\n * This method is like `_.pull` except that it accepts an array of values to remove.\n *\n * **Note:** Unlike `_.difference`, this method mutates `array`.\n *\n * @static\n * @memberOf _\n * @category Array\n * @param {Array} array The array to modify.\n * @param {Array} values The values to remove.\n * @returns {Array} Returns `array`.\n * @example\n *\n * var array = [1, 2, 3, 1, 2, 3];\n *\n * _.pull(array, [2, 3]);\n * console.log(array);\n * // => [1, 1]\n */\n pullAll(\n array: List,\n ...values: any[]\n ): any[];\n }\n\n //_.pullAllBy DUMMY\n interface LoDashStatic {\n /**\n * This method is like `_.pullAll` except that it accepts `iteratee` which is\n * invoked for each element of `array` and `values` to to generate the criterion\n * by which uniqueness is computed. The iteratee is invoked with one argument: (value).\n *\n * **Note:** Unlike `_.differenceBy`, this method mutates `array`.\n *\n * @static\n * @memberOf _\n * @category Array\n * @param {Array} array The array to modify.\n * @param {Array} values The values to remove.\n * @param {Function|Object|string} [iteratee=_.identity] The iteratee invoked per element.\n * @returns {Array} Returns `array`.\n * @example\n *\n * var array = [{ 'x': 1 }, { 'x': 2 }, { 'x': 3 }, { 'x': 1 }];\n *\n * _.pullAllBy(array, [{ 'x': 1 }, { 'x': 3 }], 'x');\n * console.log(array);\n * // => [{ 'x': 2 }]\n */\n pullAllBy(\n array: List,\n ...values: any[]\n ): any[];\n }\n\n //_.reverse DUMMY\n interface LoDashStatic {\n /**\n * Reverses `array` so that the first element becomes the last, the second\n * element becomes the second to last, and so on.\n *\n * **Note:** This method mutates `array` and is based on\n * [`Array#reverse`](https://mdn.io/Array/reverse).\n *\n * @memberOf _\n * @category Array\n * @returns {Array} Returns `array`.\n * @example\n *\n * var array = [1, 2, 3];\n *\n * _.reverse(array);\n * // => [3, 2, 1]\n *\n * console.log(array);\n * // => [3, 2, 1]\n */\n reverse(\n array: List,\n ...values: any[]\n ): any[];\n }\n\n //_.sortedIndexOf\n interface LoDashStatic {\n /**\n * This method is like `_.indexOf` except that it performs a binary\n * search on a sorted `array`.\n *\n * @static\n * @memberOf _\n * @category Array\n * @param {Array} array The array to search.\n * @param {*} value The value to search for.\n * @returns {number} Returns the index of the matched value, else `-1`.\n * @example\n *\n * _.sortedIndexOf([1, 1, 2, 2], 2);\n * // => 2\n */\n sortedIndexOf(\n array: List | null | undefined,\n value: T\n ): number;\n }\n\n interface LoDashImplicitArrayWrapperBase {\n /**\n * @see _.sortedIndexOf\n */\n sortedIndexOf(\n value: T\n ): number;\n }\n\n interface LoDashImplicitObjectWrapperBase {\n /**\n * @see _.sortedIndexOf\n */\n sortedIndexOf(\n value: TValue\n ): number;\n }\n\n interface LoDashExplicitArrayWrapperBase {\n /**\n * @see _.sortedIndexOf\n */\n sortedIndexOf(\n value: T\n ): LoDashExplicitWrapper;\n }\n\n interface LoDashExplicitObjectWrapperBase {\n /**\n * @see _.sortedIndexOf\n */\n sortedIndexOf(\n value: TValue\n ): LoDashExplicitWrapper;\n }\n\n //_.initial\n interface LoDashStatic {\n /**\n * Gets all but the last element of array.\n *\n * @param array The array to query.\n * @return Returns the slice of array.\n */\n initial(array: List | null | undefined): T[];\n }\n\n interface LoDashImplicitArrayWrapperBase {\n /**\n * @see _.initial\n */\n initial(): LoDashImplicitArrayWrapper;\n }\n\n interface LoDashImplicitObjectWrapperBase {\n /**\n * @see _.initial\n */\n initial(): LoDashImplicitArrayWrapper;\n }\n\n interface LoDashExplicitArrayWrapperBase {\n /**\n * @see _.initial\n */\n initial(): LoDashExplicitArrayWrapper;\n }\n\n interface LoDashExplicitObjectWrapperBase {\n /**\n * @see _.initial\n */\n initial(): LoDashExplicitArrayWrapper;\n }\n\n //_.intersection\n interface LoDashStatic {\n /**\n * Creates an array of unique values that are included in all of the provided arrays using SameValueZero for\n * equality comparisons.\n *\n * @param arrays The arrays to inspect.\n * @return Returns the new array of shared values.\n */\n intersection(...arrays: Array | null | undefined>): T[];\n }\n\n interface LoDashImplicitArrayWrapperBase {\n /**\n * @see _.intersection\n */\n intersection(...arrays: Array>): LoDashImplicitArrayWrapper;\n }\n\n interface LoDashImplicitObjectWrapperBase {\n /**\n * @see _.intersection\n */\n intersection(...arrays: Array>): LoDashImplicitArrayWrapper;\n }\n\n interface LoDashExplicitArrayWrapperBase {\n /**\n * @see _.intersection\n */\n intersection(...arrays: Array>): LoDashExplicitArrayWrapper;\n }\n\n interface LoDashExplicitObjectWrapperBase {\n /**\n * @see _.intersection\n */\n intersection(...arrays: Array>): LoDashExplicitArrayWrapper;\n }\n\n //_.last\n interface LoDashStatic {\n /**\n * Gets the last element of array.\n *\n * @param array The array to query.\n * @return Returns the last element of array.\n */\n last(array: List | null | undefined): T | undefined;\n }\n\n interface LoDashImplicitWrapper {\n /**\n * @see _.last\n */\n last(): string | undefined;\n }\n\n interface LoDashImplicitArrayWrapperBase {\n /**\n * @see _.last\n */\n last(): T | undefined;\n }\n\n interface LoDashImplicitObjectWrapperBase {\n /**\n * @see _.last\n */\n last(): T | undefined;\n }\n\n interface LoDashExplicitWrapper {\n /**\n * @see _.last\n */\n last(): LoDashExplicitWrapper;\n }\n\n interface LoDashExplicitArrayWrapperBase {\n /**\n * @see _.last\n */\n last(): T;\n }\n\n interface LoDashExplicitObjectWrapperBase {\n /**\n * @see _.last\n */\n last(): T;\n }\n\n //_.lastIndexOf\n interface LoDashStatic {\n /**\n * This method is like _.indexOf except that it iterates over elements of array from right to left.\n *\n * @param array The array to search.\n * @param value The value to search for.\n * @param fromIndex The index to search from or true to perform a binary search on a sorted array.\n * @return Returns the index of the matched value, else -1.\n */\n lastIndexOf(\n array: List | null | undefined,\n value: T,\n fromIndex?: boolean|number\n ): number;\n }\n\n interface LoDashImplicitArrayWrapperBase {\n /**\n * @see _.lastIndexOf\n */\n lastIndexOf(\n value: T,\n fromIndex?: boolean|number\n ): number;\n }\n\n interface LoDashImplicitObjectWrapperBase {\n /**\n * @see _.lastIndexOf\n */\n lastIndexOf(\n value: TResult,\n fromIndex?: boolean|number\n ): number;\n }\n\n interface LoDashExplicitArrayWrapperBase {\n /**\n * @see _.lastIndexOf\n */\n lastIndexOf(\n value: T,\n fromIndex?: boolean|number\n ): LoDashExplicitWrapper;\n }\n\n interface LoDashExplicitObjectWrapperBase {\n /**\n * @see _.lastIndexOf\n */\n lastIndexOf(\n value: TResult,\n fromIndex?: boolean|number\n ): LoDashExplicitWrapper;\n }\n\n //_.nth\n interface LoDashStatic {\n /**\n * Gets the element at index `n` of `array`. If `n` is negative, the nth element from the end is returned.\n *\n * @param array array The array to query.\n * @param value The index of the element to return.\n * @return Returns the nth element of `array`.\n */\n nth(\n array: List | null | undefined,\n n?: number\n ): T | undefined;\n }\n\n interface LoDashImplicitArrayWrapperBase {\n /**\n * @see _.nth\n */\n nth(\n n?: number\n ): T | undefined;\n }\n\n interface LoDashImplicitObjectWrapperBase {\n /**\n * @see _.nth\n */\n nth(\n n?:number\n ): TResult | undefined;\n }\n\n interface LoDashExplicitArrayWrapperBase {\n /**\n * @see _.nth\n */\n nth(\n n?:number\n ): LoDashExplicitWrapper;\n }\n\n interface LoDashExplicitObjectWrapperBase {\n /**\n * @see _.nth\n */\n nth(\n n?:number\n ): LoDashExplicitWrapper;\n }\n\n //_.pull\n interface LoDashStatic {\n /**\n * Removes all provided values from array using SameValueZero for equality comparisons.\n *\n * Note: Unlike _.without, this method mutates array.\n *\n * @param array The array to modify.\n * @param values The values to remove.\n * @return Returns array.\n */\n pull(\n array: T[],\n ...values: T[]\n ): T[];\n\n /**\n * @see _.pull\n */\n pull(\n array: List,\n ...values: T[]\n ): List;\n }\n\n interface LoDashImplicitArrayWrapper {\n /**\n * @see _.pull\n */\n pull(...values: T[]): LoDashImplicitArrayWrapper;\n }\n\n interface LoDashImplicitObjectWrapper {\n /**\n * @see _.pull\n */\n pull(...values: TValue[]): LoDashImplicitObjectWrapper>;\n }\n\n interface LoDashExplicitArrayWrapper {\n /**\n * @see _.pull\n */\n pull(...values: T[]): LoDashExplicitArrayWrapper;\n }\n\n interface LoDashExplicitObjectWrapper {\n /**\n * @see _.pull\n */\n pull(...values: TValue[]): LoDashExplicitObjectWrapper>;\n }\n\n //_.pullAt\n interface LoDashStatic {\n /**\n * Removes elements from array corresponding to the given indexes and returns an array of the removed elements.\n * Indexes may be specified as an array of indexes or as individual arguments.\n *\n * Note: Unlike _.at, this method mutates array.\n *\n * @param array The array to modify.\n * @param indexes The indexes of elements to remove, specified as individual indexes or arrays of indexes.\n * @return Returns the new array of removed elements.\n */\n pullAt(\n array: List,\n ...indexes: Array>\n ): T[];\n }\n\n interface LoDashImplicitArrayWrapper {\n /**\n * @see _.pullAt\n */\n pullAt(...indexes: Array>): LoDashImplicitArrayWrapper;\n }\n\n interface LoDashImplicitObjectWrapper {\n /**\n * @see _.pullAt\n */\n pullAt(...indexes: Array>): LoDashImplicitArrayWrapper;\n }\n\n interface LoDashExplicitArrayWrapper {\n /**\n * @see _.pullAt\n */\n pullAt(...indexes: Array>): LoDashExplicitArrayWrapper;\n }\n\n interface LoDashExplicitObjectWrapper {\n /**\n * @see _.pullAt\n */\n pullAt(...indexes: Array>): LoDashExplicitArrayWrapper;\n }\n\n //_.remove\n interface LoDashStatic {\n /**\n * Removes all elements from array that predicate returns truthy for and returns an array of the removed\n * elements. The predicate is bound to thisArg and invoked with three arguments: (value, index, array).\n *\n * If a property name is provided for predicate the created _.property style callback returns the property\n * value of the given element.\n *\n * If a value is also provided for thisArg the created _.matchesProperty style callback returns true for\n * elements that have a matching property value, else false.\n *\n * If an object is provided for predicate the created _.matches style callback returns true for elements that\n * have the properties of the given object, else false.\n *\n * Note: Unlike _.filter, this method mutates array.\n *\n * @param array The array to modify.\n * @param predicate The function invoked per iteration.\n * @param thisArg The this binding of predicate.\n * @return Returns the new array of removed elements.\n */\n remove(\n array: List,\n predicate?: ListIterator\n ): T[];\n\n /**\n * @see _.remove\n */\n remove(\n array: List,\n predicate?: string\n ): T[];\n\n /**\n * @see _.remove\n */\n remove(\n array: List,\n predicate?: W\n ): T[];\n }\n\n interface LoDashImplicitArrayWrapper {\n /**\n * @see _.remove\n */\n remove(\n predicate?: ListIterator\n ): LoDashImplicitArrayWrapper;\n\n /**\n * @see _.remove\n */\n remove(\n predicate?: string\n ): LoDashImplicitArrayWrapper;\n\n /**\n * @see _.remove\n */\n remove(\n predicate?: W\n ): LoDashImplicitArrayWrapper;\n }\n\n interface LoDashImplicitObjectWrapper {\n /**\n * @see _.remove\n */\n remove(\n predicate?: ListIterator\n ): LoDashImplicitArrayWrapper;\n\n /**\n * @see _.remove\n */\n remove(\n predicate?: string\n ): LoDashImplicitArrayWrapper;\n\n /**\n * @see _.remove\n */\n remove(\n predicate?: W\n ): LoDashImplicitArrayWrapper;\n }\n\n interface LoDashExplicitArrayWrapper {\n /**\n * @see _.remove\n */\n remove(\n predicate?: ListIterator\n ): LoDashExplicitArrayWrapper;\n\n /**\n * @see _.remove\n */\n remove(\n predicate?: string\n ): LoDashExplicitArrayWrapper;\n\n /**\n * @see _.remove\n */\n remove(\n predicate?: W\n ): LoDashExplicitArrayWrapper;\n }\n\n interface LoDashExplicitObjectWrapper {\n /**\n * @see _.remove\n */\n remove(\n predicate?: ListIterator\n ): LoDashExplicitArrayWrapper;\n\n /**\n * @see _.remove\n */\n remove(\n predicate?: string\n ): LoDashExplicitArrayWrapper;\n\n /**\n * @see _.remove\n */\n remove(\n predicate?: W\n ): LoDashExplicitArrayWrapper;\n }\n\n //_.tail\n interface LoDashStatic {\n /**\n * Gets all but the first element of array.\n *\n * @alias _.tail\n *\n * @param array The array to query.\n * @return Returns the slice of array.\n */\n tail(array: List | null | undefined): T[];\n }\n\n interface LoDashImplicitArrayWrapperBase {\n /**\n * @see _.tail\n */\n tail(): LoDashImplicitArrayWrapper;\n }\n\n interface LoDashImplicitObjectWrapperBase {\n /**\n * @see _.tail\n */\n tail(): LoDashImplicitArrayWrapper;\n }\n\n interface LoDashExplicitArrayWrapperBase {\n /**\n * @see _.tail\n */\n tail(): LoDashExplicitArrayWrapper;\n }\n\n interface LoDashExplicitObjectWrapperBase {\n /**\n * @see _.tail\n */\n tail(): LoDashExplicitArrayWrapper;\n }\n\n //_.slice\n interface LoDashStatic {\n /**\n * Creates a slice of array from start up to, but not including, end.\n *\n * @param array The array to slice.\n * @param start The start position.\n * @param end The end position.\n * @return Returns the slice of array.\n */\n slice(\n array: T[] | null | undefined,\n start?: number,\n end?: number\n ): T[];\n }\n\n interface LoDashImplicitArrayWrapperBase {\n /**\n * @see _.slice\n */\n slice(\n start?: number,\n end?: number\n ): LoDashImplicitArrayWrapper;\n }\n\n interface LoDashExplicitArrayWrapperBase {\n /**\n * @see _.slice\n */\n slice(\n start?: number,\n end?: number\n ): LoDashExplicitArrayWrapper;\n }\n\n //_.sortedIndex\n interface LoDashStatic {\n /**\n * Uses a binary search to determine the lowest index at which `value` should\n * be inserted into `array` in order to maintain its sort order.\n *\n * @static\n * @memberOf _\n * @category Array\n * @param {Array} array The sorted array to inspect.\n * @param {*} value The value to evaluate.\n * @returns {number} Returns the index at which `value` should be inserted into `array`.\n * @example\n *\n * _.sortedIndex([30, 50], 40);\n * // => 1\n *\n * _.sortedIndex([4, 5], 4);\n * // => 0\n */\n sortedIndex(\n array: List | null | undefined,\n value: T\n ): number;\n }\n\n interface LoDashImplicitWrapper {\n /**\n * @see _.sortedIndex\n */\n sortedIndex(\n value: string\n ): number;\n }\n\n interface LoDashImplicitArrayWrapperBase {\n /**\n * @see _.sortedIndex\n */\n sortedIndex(\n value: T\n ): number;\n }\n\n interface LoDashImplicitObjectWrapperBase {\n /**\n * @see _.sortedIndex\n */\n sortedIndex(\n value: T\n ): number;\n }\n\n interface LoDashExplicitWrapper {\n /**\n * @see _.sortedIndex\n */\n sortedIndex(\n value: string\n ): LoDashExplicitWrapper;\n }\n\n interface LoDashExplicitArrayWrapperBase {\n /**\n * @see _.sortedIndex\n */\n sortedIndex(\n value: T\n ): LoDashExplicitWrapper;\n }\n\n interface LoDashExplicitObjectWrapperBase {\n /**\n * @see _.sortedIndex\n */\n sortedIndex(\n value: T\n ): LoDashExplicitWrapper;\n }\n\n // _.sortedIndexBy\n interface LoDashStatic {\n /**\n * This method is like `_.sortedIndex` except that it accepts `iteratee`\n * which is invoked for `value` and each element of `array` to compute their\n * sort ranking. The iteratee is invoked with one argument: (value).\n *\n * @static\n * @memberOf _\n * @category Array\n * @param {Array} array The sorted array to inspect.\n * @param {*} value The value to evaluate.\n * @param {Function|Object|string} [iteratee=_.identity] The iteratee invoked per element.\n * @returns {number} Returns the index at which `value` should be inserted into `array`.\n * @example\n *\n * var dict = { 'thirty': 30, 'forty': 40, 'fifty': 50 };\n *\n * _.sortedIndexBy(['thirty', 'fifty'], 'forty', _.propertyOf(dict));\n * // => 1\n *\n * // using the `_.property` iteratee shorthand\n * _.sortedIndexBy([{ 'x': 4 }, { 'x': 5 }], { 'x': 4 }, 'x');\n * // => 0\n */\n sortedIndexBy(\n array: List | null | undefined,\n value: T,\n iteratee: (x: T) => TSort\n ): number;\n\n /**\n * @see _.sortedIndexBy\n */\n sortedIndexBy(\n array: List | null | undefined,\n value: T,\n iteratee: (x: T) => any\n ): number;\n\n /**\n * @see _.sortedIndexBy\n */\n sortedIndexBy(\n array: List | null | undefined,\n value: T,\n iteratee: string\n ): number;\n\n /**\n * @see _.sortedIndexBy\n */\n sortedIndexBy(\n array: List | null | undefined,\n value: T,\n iteratee: W\n ): number;\n\n /**\n * @see _.sortedIndexBy\n */\n sortedIndexBy(\n array: List | null | undefined,\n value: T,\n iteratee: Object\n ): number;\n }\n\n interface LoDashImplicitWrapper {\n /**\n * @see _.sortedIndexBy\n */\n sortedIndexBy(\n value: string,\n iteratee: (x: string) => TSort\n ): number;\n }\n\n interface LoDashImplicitArrayWrapperBase {\n /**\n * @see _.sortedIndexBy\n */\n sortedIndexBy(\n value: T,\n iteratee: (x: T) => TSort\n ): number;\n\n /**\n * @see _.sortedIndexBy\n */\n sortedIndexBy(\n value: T,\n iteratee: string\n ): number;\n\n /**\n * @see _.sortedIndexBy\n */\n sortedIndexBy(\n value: T,\n iteratee: W\n ): number;\n }\n\n interface LoDashImplicitObjectWrapperBase {\n /**\n * @see _.sortedIndexBy\n */\n sortedIndexBy(\n value: T,\n iteratee: (x: T) => TSort\n ): number;\n\n /**\n * @see _.sortedIndexBy\n */\n sortedIndexBy(\n value: T,\n iteratee: (x: T) => any\n ): number;\n\n /**\n * @see _.sortedIndexBy\n */\n sortedIndexBy(\n value: T,\n iteratee: string\n ): number;\n\n /**\n * @see _.sortedIndexBy\n */\n sortedIndexBy(\n value: T,\n iteratee: W\n ): number;\n\n /**\n * @see _.sortedIndexBy\n */\n sortedIndexBy(\n value: T,\n iteratee: Object\n ): number;\n }\n\n interface LoDashExplicitWrapper {\n /**\n * @see _.sortedIndexBy\n */\n sortedIndexBy(\n value: string,\n iteratee: (x: string) => TSort\n ): LoDashExplicitWrapper;\n }\n\n interface LoDashExplicitArrayWrapperBase {\n /**\n * @see _.sortedIndexBy\n */\n sortedIndexBy(\n value: T,\n iteratee: (x: T) => TSort\n ): LoDashExplicitWrapper;\n\n /**\n * @see _.sortedIndexBy\n */\n sortedIndexBy(\n value: T,\n iteratee: string\n ): LoDashExplicitWrapper;\n\n /**\n * @see _.sortedIndexBy\n */\n sortedIndexBy(\n value: T,\n iteratee: W\n ): LoDashExplicitWrapper;\n }\n\n interface LoDashExplicitObjectWrapperBase {\n /**\n * @see _.sortedIndexBy\n */\n sortedIndexBy(\n value: T,\n iteratee: (x: T) => TSort\n ): LoDashExplicitWrapper;\n\n /**\n * @see _.sortedIndexBy\n */\n sortedIndexBy(\n value: T,\n iteratee: (x: T) => any\n ): LoDashExplicitWrapper;\n\n /**\n * @see _.sortedIndexBy\n */\n sortedIndexBy(\n value: T,\n iteratee: string\n ): LoDashExplicitWrapper;\n\n /**\n * @see _.sortedIndexBy\n */\n sortedIndexBy(\n value: T,\n iteratee: W\n ): LoDashExplicitWrapper;\n\n /**\n * @see _.sortedIndexBy\n */\n sortedIndexBy(\n value: T,\n iteratee: Object\n ): LoDashExplicitWrapper;\n }\n\n //_.sortedLastIndex\n interface LoDashStatic {\n /**\n * This method is like `_.sortedIndex` except that it returns the highest\n * index at which `value` should be inserted into `array` in order to\n * maintain its sort order.\n *\n * @static\n * @memberOf _\n * @category Array\n * @param {Array} array The sorted array to inspect.\n * @param {*} value The value to evaluate.\n * @returns {number} Returns the index at which `value` should be inserted into `array`.\n * @example\n *\n * _.sortedLastIndex([4, 5], 4);\n * // => 1\n */\n sortedLastIndex(\n array: List | null | undefined,\n value: T\n ): number;\n }\n\n interface LoDashImplicitWrapper {\n /**\n * @see _.sortedLastIndex\n */\n sortedLastIndex(\n value: string\n ): number;\n }\n\n interface LoDashImplicitArrayWrapperBase {\n /**\n * @see _.sortedLastIndex\n */\n sortedLastIndex(\n value: T\n ): number;\n }\n\n interface LoDashImplicitObjectWrapperBase {\n /**\n * @see _.sortedLastIndex\n */\n sortedLastIndex(\n value: T\n ): number;\n }\n\n interface LoDashExplicitWrapper {\n /**\n * @see _.sortedLastIndex\n */\n sortedLastIndex(\n value: string\n ): LoDashExplicitWrapper;\n }\n\n interface LoDashExplicitArrayWrapperBase {\n /**\n * @see _.sortedLastIndex\n */\n sortedLastIndex(\n value: T\n ): LoDashExplicitWrapper;\n }\n\n interface LoDashExplicitObjectWrapperBase {\n /**\n * @see _.sortedLastIndex\n */\n sortedLastIndex(\n value: T\n ): LoDashExplicitWrapper;\n }\n\n //_.sortedLastIndexBy\n interface LoDashStatic {\n /**\n * This method is like `_.sortedLastIndex` except that it accepts `iteratee`\n * which is invoked for `value` and each element of `array` to compute their\n * sort ranking. The iteratee is invoked with one argument: (value).\n *\n * @static\n * @memberOf _\n * @category Array\n * @param {Array} array The sorted array to inspect.\n * @param {*} value The value to evaluate.\n * @param {Function|Object|string} [iteratee=_.identity] The iteratee invoked per element.\n * @returns {number} Returns the index at which `value` should be inserted into `array`.\n * @example\n *\n * // using the `_.property` iteratee shorthand\n * _.sortedLastIndexBy([{ 'x': 4 }, { 'x': 5 }], { 'x': 4 }, 'x');\n * // => 1\n */\n sortedLastIndexBy(\n array: List | null | undefined,\n value: T,\n iteratee: (x: T) => TSort\n ): number;\n\n /**\n * @see _.sortedLastIndexBy\n */\n sortedLastIndexBy(\n array: List | null | undefined,\n value: T,\n iteratee: (x: T) => any\n ): number;\n\n /**\n * @see _.sortedLastIndexBy\n */\n sortedLastIndexBy(\n array: List | null | undefined,\n value: T,\n iteratee: string\n ): number;\n\n /**\n * @see _.sortedLastIndexBy\n */\n sortedLastIndexBy(\n array: List | null | undefined,\n value: T,\n iteratee: W\n ): number;\n\n /**\n * @see _.sortedLastIndexBy\n */\n sortedLastIndexBy(\n array: List | null | undefined,\n value: T,\n iteratee: Object\n ): number;\n }\n\n interface LoDashImplicitWrapper {\n /**\n * @see _.sortedLastIndexBy\n */\n sortedLastIndexBy(\n value: string,\n iteratee: (x: string) => TSort\n ): number;\n }\n\n interface LoDashImplicitArrayWrapperBase {\n /**\n * @see _.sortedLastIndexBy\n */\n sortedLastIndexBy(\n value: T,\n iteratee: (x: T) => TSort\n ): number;\n\n /**\n * @see _.sortedLastIndexBy\n */\n sortedLastIndexBy(\n value: T,\n iteratee: string\n ): number;\n\n /**\n * @see _.sortedLastIndexBy\n */\n sortedLastIndexBy(\n value: T,\n iteratee: W\n ): number;\n }\n\n interface LoDashImplicitObjectWrapperBase {\n /**\n * @see _.sortedLastIndexBy\n */\n sortedLastIndexBy(\n value: T,\n iteratee: (x: T) => TSort\n ): number;\n\n /**\n * @see _.sortedLastIndexBy\n */\n sortedLastIndexBy(\n value: T,\n iteratee: (x: T) => any\n ): number;\n\n /**\n * @see _.sortedLastIndexBy\n */\n sortedLastIndexBy(\n value: T,\n iteratee: string\n ): number;\n\n /**\n * @see _.sortedLastIndexBy\n */\n sortedLastIndexBy(\n value: T,\n iteratee: W\n ): number;\n\n /**\n * @see _.sortedLastIndexBy\n */\n sortedLastIndexBy(\n value: T,\n iteratee: Object\n ): number;\n }\n\n interface LoDashExplicitWrapper {\n /**\n * @see _.sortedLastIndexBy\n */\n sortedLastIndexBy(\n value: string,\n iteratee: (x: string) => TSort\n ): LoDashExplicitWrapper;\n }\n\n interface LoDashExplicitArrayWrapperBase {\n /**\n * @see _.sortedLastIndexBy\n */\n sortedLastIndexBy(\n value: T,\n iteratee: (x: T) => TSort\n ): LoDashExplicitWrapper;\n\n /**\n * @see _.sortedLastIndexBy\n */\n sortedLastIndexBy(\n value: T,\n iteratee: string\n ): LoDashExplicitWrapper;\n\n /**\n * @see _.sortedLastIndexBy\n */\n sortedLastIndexBy(\n value: T,\n iteratee: W\n ): LoDashExplicitWrapper;\n }\n\n interface LoDashExplicitObjectWrapperBase {\n /**\n * @see _.sortedLastIndexBy\n */\n sortedLastIndexBy(\n value: T,\n iteratee: (x: T) => TSort\n ): LoDashExplicitWrapper;\n\n /**\n * @see _.sortedLastIndexBy\n */\n sortedLastIndexBy(\n value: T,\n iteratee: (x: T) => any\n ): LoDashExplicitWrapper;\n\n /**\n * @see _.sortedLastIndexBy\n */\n sortedLastIndexBy(\n value: T,\n iteratee: string\n ): LoDashExplicitWrapper;\n\n /**\n * @see _.sortedLastIndexBy\n */\n sortedLastIndexBy(\n value: T,\n iteratee: W\n ): LoDashExplicitWrapper;\n\n /**\n * @see _.sortedLastIndexBy\n */\n sortedLastIndexBy(\n value: T,\n iteratee: Object\n ): LoDashExplicitWrapper;\n }\n\n //_.sortedLastIndexOf DUMMY\n interface LoDashStatic {\n /**\n * This method is like `_.lastIndexOf` except that it performs a binary\n * search on a sorted `array`.\n *\n * @static\n * @memberOf _\n * @category Array\n * @param {Array} array The array to search.\n * @param {*} value The value to search for.\n * @returns {number} Returns the index of the matched value, else `-1`.\n * @example\n *\n * _.sortedLastIndexOf([1, 1, 2, 2], 2);\n * // => 3\n */\n sortedLastIndexOf(\n array: List | null | undefined,\n value: T\n ): number;\n }\n\n //_.tail\n interface LoDashStatic {\n /**\n * @see _.rest\n */\n tail(array: List | null | undefined): T[];\n }\n\n interface LoDashImplicitArrayWrapperBase {\n /**\n * @see _.rest\n */\n tail(): LoDashImplicitArrayWrapper;\n }\n\n interface LoDashImplicitObjectWrapperBase {\n /**\n * @see _.rest\n */\n tail(): LoDashImplicitArrayWrapper;\n }\n\n interface LoDashExplicitArrayWrapperBase {\n /**\n * @see _.rest\n */\n tail(): LoDashExplicitArrayWrapper;\n }\n\n interface LoDashExplicitObjectWrapperBase {\n /**\n * @see _.rest\n */\n tail(): LoDashExplicitArrayWrapper;\n }\n\n //_.take\n interface LoDashStatic {\n /**\n * Creates a slice of array with n elements taken from the beginning.\n *\n * @param array The array to query.\n * @param n The number of elements to take.\n * @return Returns the slice of array.\n */\n take(\n array: List | null | undefined,\n n?: number\n ): T[];\n }\n\n interface LoDashImplicitArrayWrapperBase {\n /**\n * @see _.take\n */\n take(n?: number): LoDashImplicitArrayWrapper;\n }\n\n interface LoDashImplicitObjectWrapperBase {\n /**\n * @see _.take\n */\n take(n?: number): LoDashImplicitArrayWrapper;\n }\n\n interface LoDashExplicitArrayWrapperBase {\n /**\n * @see _.take\n */\n take(n?: number): LoDashExplicitArrayWrapper;\n }\n\n interface LoDashExplicitObjectWrapperBase {\n /**\n * @see _.take\n */\n take(n?: number): LoDashExplicitArrayWrapper;\n }\n\n //_.takeRight\n interface LoDashStatic {\n /**\n * Creates a slice of array with n elements taken from the end.\n *\n * @param array The array to query.\n * @param n The number of elements to take.\n * @return Returns the slice of array.\n */\n takeRight(\n array: List | null | undefined,\n n?: number\n ): T[];\n }\n\n interface LoDashImplicitArrayWrapperBase {\n /**\n * @see _.takeRight\n */\n takeRight(n?: number): LoDashImplicitArrayWrapper;\n }\n\n interface LoDashImplicitObjectWrapperBase {\n /**\n * @see _.takeRight\n */\n takeRight(n?: number): LoDashImplicitArrayWrapper;\n }\n\n interface LoDashExplicitArrayWrapperBase {\n /**\n * @see _.takeRight\n */\n takeRight(n?: number): LoDashExplicitArrayWrapper;\n }\n\n interface LoDashExplicitObjectWrapperBase {\n /**\n * @see _.takeRight\n */\n takeRight(n?: number): LoDashExplicitArrayWrapper;\n }\n\n //_.takeRightWhile\n interface LoDashStatic {\n /**\n * Creates a slice of array with elements taken from the end. Elements are taken until predicate returns\n * falsey. The predicate is bound to thisArg and invoked with three arguments: (value, index, array).\n *\n * If a property name is provided for predicate the created _.property style callback returns the property\n * value of the given element.\n *\n * If a value is also provided for thisArg the created _.matchesProperty style callback returns true for\n * elements that have a matching property value, else false.\n *\n * If an object is provided for predicate the created _.matches style callback returns true for elements that\n * have the properties of the given object, else false.\n *\n * @param array The array to query.\n * @param predicate The function invoked per iteration.\n * @param thisArg The this binding of predicate.\n * @return Returns the slice of array.\n */\n takeRightWhile(\n array: List | null | undefined,\n predicate?: ListIterator\n ): TValue[];\n\n /**\n * @see _.takeRightWhile\n */\n takeRightWhile(\n array: List | null | undefined,\n predicate?: string\n ): TValue[];\n\n /**\n * @see _.takeRightWhile\n */\n takeRightWhile(\n array: List | null | undefined,\n predicate?: TWhere\n ): TValue[];\n }\n\n interface LoDashImplicitArrayWrapperBase {\n /**\n * @see _.takeRightWhile\n */\n takeRightWhile(\n predicate?: ListIterator\n ): LoDashImplicitArrayWrapper;\n\n /**\n * @see _.takeRightWhile\n */\n takeRightWhile(\n predicate?: string\n ): LoDashImplicitArrayWrapper;\n\n /**\n * @see _.takeRightWhile\n */\n takeRightWhile(\n predicate?: TWhere\n ): LoDashImplicitArrayWrapper;\n }\n\n interface LoDashImplicitObjectWrapperBase {\n /**\n * @see _.takeRightWhile\n */\n takeRightWhile(\n predicate?: ListIterator\n ): LoDashImplicitArrayWrapper;\n\n /**\n * @see _.takeRightWhile\n */\n takeRightWhile(\n predicate?: string\n ): LoDashImplicitArrayWrapper;\n\n /**\n * @see _.takeRightWhile\n */\n takeRightWhile(\n predicate?: TWhere\n ): LoDashImplicitArrayWrapper;\n }\n\n interface LoDashExplicitArrayWrapperBase {\n /**\n * @see _.takeRightWhile\n */\n takeRightWhile(\n predicate?: ListIterator\n ): LoDashExplicitArrayWrapper;\n\n /**\n * @see _.takeRightWhile\n */\n takeRightWhile(\n predicate?: string\n ): LoDashExplicitArrayWrapper;\n\n /**\n * @see _.takeRightWhile\n */\n takeRightWhile(\n predicate?: TWhere\n ): LoDashExplicitArrayWrapper;\n }\n\n interface LoDashExplicitObjectWrapperBase {\n /**\n * @see _.takeRightWhile\n */\n takeRightWhile(\n predicate?: ListIterator\n ): LoDashExplicitArrayWrapper;\n\n /**\n * @see _.takeRightWhile\n */\n takeRightWhile(\n predicate?: string\n ): LoDashExplicitArrayWrapper;\n\n /**\n * @see _.takeRightWhile\n */\n takeRightWhile(\n predicate?: TWhere\n ): LoDashExplicitArrayWrapper;\n }\n\n //_.takeWhile\n interface LoDashStatic {\n /**\n * Creates a slice of array with elements taken from the beginning. Elements are taken until predicate returns\n * falsey. The predicate is bound to thisArg and invoked with three arguments: (value, index, array).\n *\n * If a property name is provided for predicate the created _.property style callback returns the property\n * value of the given element.\n *\n * If a value is also provided for thisArg the created _.matchesProperty style callback returns true for\n * elements that have a matching property value, else false.\n *\n * If an object is provided for predicate the created _.matches style callback returns true for elements that\n * have the properties of the given object, else false.\n *\n * @param array The array to query.\n * @param predicate The function invoked per iteration.\n * @param thisArg The this binding of predicate.\n * @return Returns the slice of array.\n */\n takeWhile(\n array: List | null | undefined,\n predicate?: ListIterator\n ): TValue[];\n\n /**\n * @see _.takeWhile\n */\n takeWhile(\n array: List | null | undefined,\n predicate?: string\n ): TValue[];\n\n /**\n * @see _.takeWhile\n */\n takeWhile(\n array: List | null | undefined,\n predicate?: TWhere\n ): TValue[];\n }\n\n interface LoDashImplicitArrayWrapperBase {\n /**\n * @see _.takeWhile\n */\n takeWhile(\n predicate?: ListIterator\n ): LoDashImplicitArrayWrapper;\n\n /**\n * @see _.takeWhile\n */\n takeWhile(\n predicate?: string\n ): LoDashImplicitArrayWrapper;\n\n /**\n * @see _.takeWhile\n */\n takeWhile(\n predicate?: TWhere\n ): LoDashImplicitArrayWrapper;\n }\n\n interface LoDashImplicitObjectWrapperBase {\n /**\n * @see _.takeWhile\n */\n takeWhile(\n predicate?: ListIterator\n ): LoDashImplicitArrayWrapper;\n\n /**\n * @see _.takeWhile\n */\n takeWhile(\n predicate?: string\n ): LoDashImplicitArrayWrapper;\n\n /**\n * @see _.takeWhile\n */\n takeWhile(\n predicate?: TWhere\n ): LoDashImplicitArrayWrapper;\n }\n\n interface LoDashExplicitArrayWrapperBase {\n /**\n * @see _.takeWhile\n */\n takeWhile(\n predicate?: ListIterator\n ): LoDashExplicitArrayWrapper;\n\n /**\n * @see _.takeWhile\n */\n takeWhile(\n predicate?: string\n ): LoDashExplicitArrayWrapper;\n\n /**\n * @see _.takeWhile\n */\n takeWhile(\n predicate?: TWhere\n ): LoDashExplicitArrayWrapper;\n }\n\n interface LoDashExplicitObjectWrapperBase {\n /**\n * @see _.takeWhile\n */\n takeWhile(\n predicate?: ListIterator\n ): LoDashExplicitArrayWrapper;\n\n /**\n * @see _.takeWhile\n */\n takeWhile(\n predicate?: string\n ): LoDashExplicitArrayWrapper;\n\n /**\n * @see _.takeWhile\n */\n takeWhile(\n predicate?: TWhere\n ): LoDashExplicitArrayWrapper;\n }\n\n //_.union\n interface LoDashStatic {\n /**\n * Creates an array of unique values, in order, from all of the provided arrays using SameValueZero for\n * equality comparisons.\n *\n * @param arrays The arrays to inspect.\n * @return Returns the new array of combined values.\n */\n union(...arrays: Array | null | undefined>): T[];\n }\n\n interface LoDashImplicitArrayWrapperBase {\n /**\n * @see _.union\n */\n union(...arrays: Array | null | undefined>): LoDashImplicitArrayWrapper;\n\n /**\n * @see _.union\n */\n union(...arrays: Array | null | undefined>): LoDashImplicitArrayWrapper;\n }\n\n interface LoDashImplicitObjectWrapperBase {\n /**\n * @see _.union\n */\n union(...arrays: Array | null | undefined>): LoDashImplicitArrayWrapper;\n }\n\n interface LoDashExplicitArrayWrapperBase {\n /**\n * @see _.union\n */\n union(...arrays: Array | null | undefined>): LoDashExplicitArrayWrapper;\n\n /**\n * @see _.union\n */\n union(...arrays: Array | null | undefined>): LoDashExplicitArrayWrapper;\n }\n\n interface LoDashExplicitObjectWrapperBase {\n /**\n * @see _.union\n */\n union(...arrays: Array | null | undefined>): LoDashExplicitArrayWrapper;\n }\n\n //_.unionBy\n interface LoDashStatic {\n /**\n * This method is like `_.union` except that it accepts `iteratee` which is\n * invoked for each element of each `arrays` to generate the criterion by which\n * uniqueness is computed. The iteratee is invoked with one argument: (value).\n *\n * @param arrays The arrays to inspect.\n * @param iteratee The iteratee invoked per element.\n * @return Returns the new array of combined values.\n */\n unionBy(\n arrays: List | null | undefined,\n iteratee?: (value: T) => any\n ): T[];\n\n /**\n * @see _.unionBy\n */\n unionBy(\n arrays: List | null | undefined,\n iteratee?: W\n ): T[];\n\n /**\n * @see _.unionBy\n */\n unionBy(\n arrays1: List | null | undefined,\n arrays2: List | null | undefined,\n iteratee?: (value: T) => any\n ): T[];\n\n /**\n * @see _.unionBy\n */\n unionBy(\n arrays1: List | null | undefined,\n arrays2: List | null | undefined,\n iteratee?: W\n ): T[];\n\n /**\n * @see _.unionBy\n */\n unionBy(\n arrays1: List | null | undefined,\n arrays2: List | null | undefined,\n arrays3: List | null | undefined,\n iteratee?: (value: T) => any\n ): T[];\n\n /**\n * @see _.unionBy\n */\n unionBy(\n arrays1: List | null | undefined,\n arrays2: List | null | undefined,\n arrays3: List | null | undefined,\n iteratee?: W\n ): T[];\n\n /**\n * @see _.unionBy\n */\n unionBy(\n arrays1: List | null | undefined,\n arrays2: List | null | undefined,\n arrays3: List | null | undefined,\n arrays4: List | null | undefined,\n iteratee?: (value: T) => any\n ): T[];\n\n /**\n * @see _.unionBy\n */\n unionBy(\n arrays1: List | null | undefined,\n arrays2: List | null | undefined,\n arrays3: List | null | undefined,\n arrays4: List | null | undefined,\n iteratee?: W\n ): T[];\n\n /**\n * @see _.unionBy\n */\n unionBy(\n arrays1: List | null | undefined,\n arrays2: List | null | undefined,\n arrays3: List | null | undefined,\n arrays4: List | null | undefined,\n arrays5: List | null | undefined,\n iteratee?: (value: T) => any\n ): T[];\n\n /**\n * @see _.unionBy\n */\n unionBy(\n arrays1: List | null | undefined,\n arrays2: List | null | undefined,\n arrays3: List | null | undefined,\n arrays4: List | null | undefined,\n arrays5: List | null | undefined,\n iteratee?: W\n ): T[];\n\n /**\n * @see _.unionBy\n */\n unionBy(\n arrays: List | null | undefined,\n ...iteratee: any[]\n ): T[];\n }\n\n interface LoDashImplicitArrayWrapperBase {\n /**\n * @see _.unionBy\n */\n unionBy(\n iteratee?: (value: T) => any\n ): LoDashImplicitArrayWrapper;\n\n /**\n * @see _.unionBy\n */\n unionBy(\n iteratee?: W\n ): LoDashImplicitArrayWrapper;\n\n /**\n * @see _.unionBy\n */\n unionBy(\n arrays2: List | null | undefined,\n iteratee?: (value: T) => any\n ): LoDashImplicitArrayWrapper;\n\n /**\n * @see _.unionBy\n */\n unionBy(\n arrays2: List | null | undefined,\n iteratee?: W\n ): LoDashImplicitArrayWrapper;\n\n /**\n * @see _.unionBy\n */\n unionBy(\n arrays2: List | null | undefined,\n arrays3: List | null | undefined,\n iteratee?: (value: T) => any\n ): LoDashImplicitArrayWrapper;\n\n /**\n * @see _.unionBy\n */\n unionBy(\n arrays2: List | null | undefined,\n arrays3: List | null | undefined,\n iteratee?: W\n ): LoDashImplicitArrayWrapper;\n\n /**\n * @see _.unionBy\n */\n unionBy(\n arrays2: List | null | undefined,\n arrays3: List | null | undefined,\n arrays4: List | null | undefined,\n iteratee?: (value: T) => any\n ): LoDashImplicitArrayWrapper;\n\n /**\n * @see _.unionBy\n */\n unionBy(\n arrays2: List | null | undefined,\n arrays3: List | null | undefined,\n arrays4: List | null | undefined,\n iteratee?: W\n ): LoDashImplicitArrayWrapper;\n\n /**\n * @see _.unionBy\n */\n unionBy(\n arrays2: List | null | undefined,\n arrays3: List | null | undefined,\n arrays4: List | null | undefined,\n arrays5: List | null | undefined,\n iteratee?: (value: T) => any\n ): LoDashImplicitArrayWrapper;\n\n /**\n * @see _.unionBy\n */\n unionBy(\n arrays2: List | null | undefined,\n arrays3: List | null | undefined,\n arrays4: List | null | undefined,\n arrays5: List | null | undefined,\n iteratee?: W\n ): LoDashImplicitArrayWrapper;\n\n /**\n * @see _.unionBy\n */\n unionBy(\n ...iteratee: any[]\n ): LoDashImplicitArrayWrapper;\n }\n\n interface LoDashImplicitObjectWrapperBase {\n /**\n * @see _.unionBy\n */\n unionBy(\n iteratee?: (value: T) => any\n ): LoDashImplicitArrayWrapper;\n\n /**\n * @see _.unionBy\n */\n unionBy(\n iteratee?: W\n ): LoDashImplicitArrayWrapper;\n\n /**\n * @see _.unionBy\n */\n unionBy(\n arrays2: List | null | undefined,\n iteratee?: (value: T) => any\n ): LoDashImplicitArrayWrapper;\n\n /**\n * @see _.unionBy\n */\n unionBy(\n arrays2: List | null | undefined,\n iteratee?: W\n ): LoDashImplicitArrayWrapper;\n\n /**\n * @see _.unionBy\n */\n unionBy(\n arrays2: List | null | undefined,\n arrays3: List | null | undefined,\n iteratee?: (value: T) => any\n ): LoDashImplicitArrayWrapper;\n\n /**\n * @see _.unionBy\n */\n unionBy(\n arrays2: List | null | undefined,\n arrays3: List | null | undefined,\n iteratee?: W\n ): LoDashImplicitArrayWrapper;\n\n /**\n * @see _.unionBy\n */\n unionBy(\n arrays2: List | null | undefined,\n arrays3: List | null | undefined,\n arrays4: List | null | undefined,\n iteratee?: (value: T) => any\n ): LoDashImplicitArrayWrapper;\n\n /**\n * @see _.unionBy\n */\n unionBy(\n arrays2: List | null | undefined,\n arrays3: List | null | undefined,\n arrays4: List | null | undefined,\n iteratee?: W\n ): LoDashImplicitArrayWrapper;\n\n /**\n * @see _.unionBy\n */\n unionBy(\n arrays2: List | null | undefined,\n arrays3: List | null | undefined,\n arrays4: List | null | undefined,\n arrays5: List | null | undefined,\n iteratee?: (value: T) => any\n ): LoDashImplicitArrayWrapper;\n\n /**\n * @see _.unionBy\n */\n unionBy(\n arrays2: List | null | undefined,\n arrays3: List | null | undefined,\n arrays4: List | null | undefined,\n arrays5: List | null | undefined,\n iteratee?: W\n ): LoDashImplicitArrayWrapper;\n\n /**\n * @see _.unionBy\n */\n unionBy(\n ...iteratee: any[]\n ): LoDashImplicitArrayWrapper;\n }\n\n interface LoDashExplicitArrayWrapperBase {\n /**\n * @see _.unionBy\n */\n unionBy(\n iteratee?: (value: T) => any\n ): LoDashExplicitArrayWrapper;\n\n /**\n * @see _.unionBy\n */\n unionBy(\n iteratee?: W\n ): LoDashExplicitArrayWrapper;\n\n /**\n * @see _.unionBy\n */\n unionBy(\n arrays2: List | null | undefined,\n iteratee?: (value: T) => any\n ): LoDashExplicitArrayWrapper;\n\n /**\n * @see _.unionBy\n */\n unionBy(\n arrays2: List | null | undefined,\n iteratee?: W\n ): LoDashExplicitArrayWrapper;\n\n /**\n * @see _.unionBy\n */\n unionBy(\n arrays2: List | null | undefined,\n arrays3: List | null | undefined,\n iteratee?: (value: T) => any\n ): LoDashExplicitArrayWrapper;\n\n /**\n * @see _.unionBy\n */\n unionBy(\n arrays2: List | null | undefined,\n arrays3: List | null | undefined,\n iteratee?: W\n ): LoDashExplicitArrayWrapper;\n\n /**\n * @see _.unionBy\n */\n unionBy(\n arrays2: List | null | undefined,\n arrays3: List | null | undefined,\n arrays4: List | null | undefined,\n iteratee?: (value: T) => any\n ): LoDashExplicitArrayWrapper;\n\n /**\n * @see _.unionBy\n */\n unionBy(\n arrays2: List | null | undefined,\n arrays3: List | null | undefined,\n arrays4: List | null | undefined,\n iteratee?: W\n ): LoDashExplicitArrayWrapper;\n\n /**\n * @see _.unionBy\n */\n unionBy(\n arrays2: List | null | undefined,\n arrays3: List | null | undefined,\n arrays4: List | null | undefined,\n arrays5: List | null | undefined,\n iteratee?: (value: T) => any\n ): LoDashExplicitArrayWrapper;\n\n /**\n * @see _.unionBy\n */\n unionBy(\n arrays2: List | null | undefined,\n arrays3: List | null | undefined,\n arrays4: List | null | undefined,\n arrays5: List | null | undefined,\n iteratee?: W\n ): LoDashExplicitArrayWrapper;\n\n /**\n * @see _.unionBy\n */\n unionBy(\n ...iteratee: any[]\n ): LoDashExplicitArrayWrapper;\n }\n\n interface LoDashExplicitObjectWrapperBase {\n /**\n * @see _.unionBy\n */\n unionBy(\n iteratee?: (value: T) => any\n ): LoDashExplicitArrayWrapper;\n\n /**\n * @see _.unionBy\n */\n unionBy(\n iteratee?: W\n ): LoDashExplicitArrayWrapper;\n\n /**\n * @see _.unionBy\n */\n unionBy(\n arrays2: List | null | undefined,\n iteratee?: (value: T) => any\n ): LoDashExplicitArrayWrapper;\n\n /**\n * @see _.unionBy\n */\n unionBy(\n arrays2: List | null | undefined,\n iteratee?: W\n ): LoDashExplicitArrayWrapper;\n\n /**\n * @see _.unionBy\n */\n unionBy(\n arrays2: List | null | undefined,\n arrays3: List | null | undefined,\n iteratee?: (value: T) => any\n ): LoDashExplicitArrayWrapper;\n\n /**\n * @see _.unionBy\n */\n unionBy(\n arrays2: List | null | undefined,\n arrays3: List | null | undefined,\n iteratee?: W\n ): LoDashExplicitArrayWrapper;\n\n /**\n * @see _.unionBy\n */\n unionBy(\n arrays2: List | null | undefined,\n arrays3: List | null | undefined,\n arrays4: List | null | undefined,\n iteratee?: (value: T) => any\n ): LoDashExplicitArrayWrapper;\n\n /**\n * @see _.unionBy\n */\n unionBy(\n arrays2: List | null | undefined,\n arrays3: List | null | undefined,\n arrays4: List | null | undefined,\n iteratee?: W\n ): LoDashExplicitArrayWrapper;\n\n /**\n * @see _.unionBy\n */\n unionBy(\n arrays2: List | null | undefined,\n arrays3: List | null | undefined,\n arrays4: List | null | undefined,\n arrays5: List | null | undefined,\n iteratee?: (value: T) => any\n ): LoDashExplicitArrayWrapper;\n\n /**\n * @see _.unionBy\n */\n unionBy(\n arrays2: List | null | undefined,\n arrays3: List | null | undefined,\n arrays4: List | null | undefined,\n arrays5: List | null | undefined,\n iteratee?: W\n ): LoDashExplicitArrayWrapper;\n\n /**\n * @see _.unionBy\n */\n unionBy(\n ...iteratee: any[]\n ): LoDashExplicitArrayWrapper;\n }\n\n //_.uniq\n interface LoDashStatic {\n /**\n * Creates a duplicate-free version of an array, using\n * [`SameValueZero`](http://ecma-international.org/ecma-262/6.0/#sec-samevaluezero)\n * for equality comparisons, in which only the first occurrence of each element\n * is kept.\n *\n * @static\n * @memberOf _\n * @category Array\n * @param {Array} array The array to inspect.\n * @returns {Array} Returns the new duplicate free array.\n * @example\n *\n * _.uniq([2, 1, 2]);\n * // => [2, 1]\n */\n uniq(\n array: List | null | undefined\n ): T[];\n\n /**\n * @see _.uniq\n */\n uniq(\n array: List | null | undefined\n ): T[];\n }\n\n interface LoDashImplicitWrapper {\n /**\n * @see _.uniq\n */\n uniq(): LoDashImplicitArrayWrapper;\n }\n\n interface LoDashImplicitArrayWrapperBase {\n /**\n * @see _.uniq\n */\n uniq(): LoDashImplicitArrayWrapper;\n\n /**\n * @see _.uniq\n */\n uniq(): LoDashImplicitArrayWrapper;\n }\n\n interface LoDashImplicitObjectWrapperBase {\n uniq(): LoDashImplicitArrayWrapper;\n\n /**\n * @see _.uniq\n */\n uniq(): LoDashImplicitArrayWrapper;\n }\n\n interface LoDashExplicitWrapper {\n /**\n * @see _.uniq\n */\n uniq(): LoDashExplicitArrayWrapper;\n }\n\n interface LoDashExplicitArrayWrapperBase {\n /**\n * @see _.uniq\n */\n uniq(): LoDashExplicitArrayWrapper;\n\n /**\n * @see _.uniq\n */\n uniq(): LoDashExplicitArrayWrapper;\n }\n\n interface LoDashExplicitObjectWrapperBase {\n /**\n * @see _.uniq\n */\n uniq(): LoDashExplicitArrayWrapper;\n\n /**\n * @see _.uniq\n */\n uniq(): LoDashExplicitArrayWrapper;\n }\n\n //_.uniqBy\n interface LoDashStatic {\n /**\n * This method is like `_.uniq` except that it accepts `iteratee` which is\n * invoked for each element in `array` to generate the criterion by which\n * uniqueness is computed. The iteratee is invoked with one argument: (value).\n *\n * @static\n * @memberOf _\n * @category Array\n * @param {Array} array The array to inspect.\n * @param {Function|Object|string} [iteratee=_.identity] The iteratee invoked per element.\n * @returns {Array} Returns the new duplicate free array.\n * @example\n *\n * _.uniqBy([2.1, 1.2, 2.3], Math.floor);\n * // => [2.1, 1.2]\n *\n * // using the `_.property` iteratee shorthand\n * _.uniqBy([{ 'x': 1 }, { 'x': 2 }, { 'x': 1 }], 'x');\n * // => [{ 'x': 1 }, { 'x': 2 }]\n */\n uniqBy(\n array: List | null | undefined,\n iteratee: ListIterator\n ): T[];\n\n /**\n * @see _.uniqBy\n */\n uniqBy(\n array: List | null | undefined,\n iteratee: ListIterator\n ): T[];\n\n /**\n * @see _.uniqBy\n */\n uniqBy(\n array: List | null | undefined,\n iteratee: string\n ): T[];\n\n /**\n * @see _.uniqBy\n */\n uniqBy(\n array: List | null | undefined,\n iteratee: Object\n ): T[];\n\n /**\n * @see _.uniqBy\n */\n uniqBy(\n array: List | null | undefined,\n iteratee: TWhere\n ): T[];\n }\n\n interface LoDashImplicitWrapper {\n /**\n * @see _.uniqBy\n */\n uniqBy(\n iteratee: ListIterator\n ): LoDashImplicitArrayWrapper;\n }\n\n interface LoDashImplicitArrayWrapperBase {\n /**\n * @see _.uniqBy\n */\n uniqBy(\n iteratee: ListIterator\n ): LoDashImplicitArrayWrapper;\n\n /**\n * @see _.uniqBy\n */\n uniqBy(\n iteratee: string\n ): LoDashImplicitArrayWrapper;\n\n /**\n * @see _.uniqBy\n */\n uniqBy(\n iteratee: TWhere\n ): LoDashImplicitArrayWrapper;\n }\n\n interface LoDashImplicitObjectWrapperBase {\n /**\n * @see _.uniqBy\n */\n uniqBy(\n iteratee: ListIterator\n ): LoDashImplicitArrayWrapper;\n\n /**\n * @see _.uniqBy\n */\n uniqBy(\n iteratee: ListIterator\n ): LoDashImplicitArrayWrapper;\n\n /**\n * @see _.uniqBy\n */\n uniqBy(\n iteratee: string\n ): LoDashImplicitArrayWrapper;\n\n /**\n * @see _.uniqBy\n */\n uniqBy(\n iteratee: Object\n ): LoDashImplicitArrayWrapper;\n\n /**\n * @see _.uniqBy\n */\n uniqBy(\n iteratee: TWhere\n ): LoDashImplicitArrayWrapper;\n }\n\n interface LoDashExplicitWrapper {\n /**\n * @see _.uniqBy\n */\n uniqBy(\n iteratee: ListIterator\n ): LoDashExplicitArrayWrapper;\n }\n\n interface LoDashExplicitArrayWrapperBase {\n /**\n * @see _.uniqBy\n */\n uniqBy(\n iteratee: ListIterator\n ): LoDashExplicitArrayWrapper;\n\n /**\n * @see _.uniqBy\n */\n uniqBy(\n iteratee: string\n ): LoDashExplicitArrayWrapper;\n\n /**\n * @see _.uniqBy\n */\n uniqBy(\n iteratee: TWhere\n ): LoDashExplicitArrayWrapper;\n }\n\n interface LoDashExplicitObjectWrapperBase {\n /**\n * @see _.uniqBy\n */\n uniqBy(\n iteratee: ListIterator\n ): LoDashExplicitArrayWrapper;\n\n /**\n * @see _.uniqBy\n */\n uniqBy(\n iteratee: ListIterator\n ): LoDashExplicitArrayWrapper;\n\n /**\n * @see _.uniqBy\n */\n uniqBy(\n iteratee: string\n ): LoDashExplicitArrayWrapper;\n\n /**\n * @see _.uniqBy\n */\n uniqBy(\n iteratee: Object\n ): LoDashExplicitArrayWrapper;\n\n /**\n * @see _.uniqBy\n */\n uniqBy(\n iteratee: TWhere\n ): LoDashExplicitArrayWrapper;\n }\n\n //_.sortedUniq\n interface LoDashStatic {\n /**\n * This method is like `_.uniq` except that it's designed and optimized\n * for sorted arrays.\n *\n * @static\n * @memberOf _\n * @category Array\n * @param {Array} array The array to inspect.\n * @returns {Array} Returns the new duplicate free array.\n * @example\n *\n * _.sortedUniq([1, 1, 2]);\n * // => [1, 2]\n */\n sortedUniq(\n array: List | null | undefined\n ): T[];\n\n /**\n * @see _.sortedUniq\n */\n sortedUniq(\n array: List | null | undefined\n ): T[];\n }\n\n interface LoDashImplicitWrapper {\n /**\n * @see _.sortedUniq\n */\n sortedUniq(): LoDashImplicitArrayWrapper;\n }\n\n interface LoDashImplicitArrayWrapperBase {\n /**\n * @see _.sortedUniq\n */\n sortedUniq(): LoDashImplicitArrayWrapper;\n\n /**\n * @see _.sortedUniq\n */\n sortedUniq(): LoDashImplicitArrayWrapper;\n }\n\n interface LoDashImplicitObjectWrapperBase {\n sortedUniq(): LoDashImplicitArrayWrapper;\n\n /**\n * @see _.sortedUniq\n */\n sortedUniq(): LoDashImplicitArrayWrapper;\n }\n\n interface LoDashExplicitWrapper {\n /**\n * @see _.sortedUniq\n */\n sortedUniq(): LoDashExplicitArrayWrapper;\n }\n\n interface LoDashExplicitArrayWrapperBase {\n /**\n * @see _.sortedUniq\n */\n sortedUniq(): LoDashExplicitArrayWrapper;\n\n /**\n * @see _.sortedUniq\n */\n sortedUniq(): LoDashExplicitArrayWrapper;\n }\n\n interface LoDashExplicitObjectWrapperBase {\n /**\n * @see _.sortedUniq\n */\n sortedUniq(): LoDashExplicitArrayWrapper;\n\n /**\n * @see _.sortedUniq\n */\n sortedUniq(): LoDashExplicitArrayWrapper;\n }\n\n //_.sortedUniqBy\n interface LoDashStatic {\n /**\n * This method is like `_.uniqBy` except that it's designed and optimized\n * for sorted arrays.\n *\n * @static\n * @memberOf _\n * @category Array\n * @param {Array} array The array to inspect.\n * @param {Function} [iteratee] The iteratee invoked per element.\n * @returns {Array} Returns the new duplicate free array.\n * @example\n *\n * _.sortedUniqBy([1.1, 1.2, 2.3, 2.4], Math.floor);\n * // => [1.1, 2.2]\n */\n sortedUniqBy(\n array: List | null | undefined,\n iteratee: ListIterator\n ): T[];\n\n /**\n * @see _.sortedUniqBy\n */\n sortedUniqBy(\n array: List | null | undefined,\n iteratee: ListIterator\n ): T[];\n\n /**\n * @see _.sortedUniqBy\n */\n sortedUniqBy(\n array: List | null | undefined,\n iteratee: string\n ): T[];\n\n /**\n * @see _.sortedUniqBy\n */\n sortedUniqBy(\n array: List | null | undefined,\n iteratee: Object\n ): T[];\n\n /**\n * @see _.sortedUniqBy\n */\n sortedUniqBy(\n array: List | null | undefined,\n iteratee: TWhere\n ): T[];\n }\n\n interface LoDashImplicitWrapper {\n /**\n * @see _.sortedUniqBy\n */\n sortedUniqBy(\n iteratee: ListIterator\n ): LoDashImplicitArrayWrapper;\n }\n\n interface LoDashImplicitArrayWrapperBase {\n /**\n * @see _.sortedUniqBy\n */\n sortedUniqBy(\n iteratee: ListIterator\n ): LoDashImplicitArrayWrapper;\n\n /**\n * @see _.sortedUniqBy\n */\n sortedUniqBy(\n iteratee: string\n ): LoDashImplicitArrayWrapper;\n\n /**\n * @see _.sortedUniqBy\n */\n sortedUniqBy(\n iteratee: TWhere\n ): LoDashImplicitArrayWrapper;\n }\n\n interface LoDashImplicitObjectWrapperBase {\n /**\n * @see _.sortedUniqBy\n */\n sortedUniqBy(\n iteratee: ListIterator\n ): LoDashImplicitArrayWrapper;\n\n /**\n * @see _.sortedUniqBy\n */\n sortedUniqBy(\n iteratee: ListIterator\n ): LoDashImplicitArrayWrapper;\n\n /**\n * @see _.sortedUniqBy\n */\n sortedUniqBy(\n iteratee: string\n ): LoDashImplicitArrayWrapper;\n\n /**\n * @see _.sortedUniqBy\n */\n sortedUniqBy(\n iteratee: Object\n ): LoDashImplicitArrayWrapper;\n\n /**\n * @see _.sortedUniqBy\n */\n sortedUniqBy(\n iteratee: TWhere\n ): LoDashImplicitArrayWrapper;\n }\n\n interface LoDashExplicitWrapper {\n /**\n * @see _.sortedUniqBy\n */\n sortedUniqBy(\n iteratee: ListIterator\n ): LoDashExplicitArrayWrapper;\n }\n\n interface LoDashExplicitArrayWrapperBase {\n /**\n * @see _.sortedUniqBy\n */\n sortedUniqBy(\n iteratee: ListIterator\n ): LoDashExplicitArrayWrapper;\n\n /**\n * @see _.sortedUniqBy\n */\n sortedUniqBy(\n iteratee: string\n ): LoDashExplicitArrayWrapper;\n\n /**\n * @see _.sortedUniqBy\n */\n sortedUniqBy(\n iteratee: TWhere\n ): LoDashExplicitArrayWrapper;\n }\n\n interface LoDashExplicitObjectWrapperBase {\n /**\n * @see _.sortedUniqBy\n */\n sortedUniqBy(\n iteratee: ListIterator\n ): LoDashExplicitArrayWrapper;\n\n /**\n * @see _.sortedUniqBy\n */\n sortedUniqBy(\n iteratee: ListIterator\n ): LoDashExplicitArrayWrapper;\n\n /**\n * @see _.sortedUniqBy\n */\n sortedUniqBy(\n iteratee: string\n ): LoDashExplicitArrayWrapper;\n\n /**\n * @see _.sortedUniqBy\n */\n sortedUniqBy(\n iteratee: Object\n ): LoDashExplicitArrayWrapper;\n\n /**\n * @see _.sortedUniqBy\n */\n sortedUniqBy(\n iteratee: TWhere\n ): LoDashExplicitArrayWrapper;\n }\n\n //_.unionWith DUMMY\n interface LoDashStatic {\n /**\n * This method is like `_.union` except that it accepts `comparator` which\n * is invoked to compare elements of `arrays`. The comparator is invoked\n * with two arguments: (arrVal, othVal).\n *\n * @static\n * @memberOf _\n * @category Array\n * @param {...Array} [arrays] The arrays to inspect.\n * @param {Function} [comparator] The comparator invoked per element.\n * @returns {Array} Returns the new array of combined values.\n * @example\n *\n * var objects = [{ 'x': 1, 'y': 2 }, { 'x': 2, 'y': 1 }];\n * var others = [{ 'x': 1, 'y': 1 }, { 'x': 1, 'y': 2 }];\n *\n * _.unionWith(objects, others, _.isEqual);\n * // => [{ 'x': 1, 'y': 2 }, { 'x': 2, 'y': 1 }, { 'x': 1, 'y': 1 }]\n */\n unionWith(\n array: List,\n ...values: any[]\n ): any[];\n }\n\n //_.uniqWith DUMMY\n interface LoDashStatic {\n /**\n * This method is like `_.uniq` except that it accepts `comparator` which\n * is invoked to compare elements of `array`. The comparator is invoked with\n * two arguments: (arrVal, othVal).\n *\n * @static\n * @memberOf _\n * @category Array\n * @param {Array} array The array to inspect.\n * @param {Function} [comparator] The comparator invoked per element.\n * @returns {Array} Returns the new duplicate free array.\n * @example\n *\n * var objects = [{ 'x': 1, 'y': 2 }, { 'x': 2, 'y': 1 }, { 'x': 1, 'y': 2 }];\n *\n * _.uniqWith(objects, _.isEqual);\n * // => [{ 'x': 1, 'y': 2 }, { 'x': 2, 'y': 1 }]\n */\n uniqWith(\n array: List,\n ...values: any[]\n ): any[];\n }\n\n //_.unzip\n interface LoDashStatic {\n /**\n * This method is like _.zip except that it accepts an array of grouped elements and creates an array\n * regrouping the elements to their pre-zip configuration.\n *\n * @param array The array of grouped elements to process.\n * @return Returns the new array of regrouped elements.\n */\n unzip(array: List> | null | undefined): T[][];\n }\n\n interface LoDashImplicitArrayWrapperBase {\n /**\n * @see _.unzip\n */\n unzip(): LoDashImplicitArrayWrapper;\n }\n\n interface LoDashImplicitObjectWrapperBase {\n /**\n * @see _.unzip\n */\n unzip(): LoDashImplicitArrayWrapper;\n }\n\n interface LoDashExplicitArrayWrapperBase {\n /**\n * @see _.unzip\n */\n unzip(): LoDashExplicitArrayWrapper;\n }\n\n interface LoDashExplicitObjectWrapperBase {\n /**\n * @see _.unzip\n */\n unzip(): LoDashExplicitArrayWrapper;\n }\n\n //_.unzipWith\n interface LoDashStatic {\n /**\n * This method is like _.unzip except that it accepts an iteratee to specify how regrouped values should be\n * combined. The iteratee is bound to thisArg and invoked with four arguments: (accumulator, value, index,\n * group).\n *\n * @param array The array of grouped elements to process.\n * @param iteratee The function to combine regrouped values.\n * @param thisArg The this binding of iteratee.\n * @return Returns the new array of regrouped elements.\n */\n unzipWith(\n array: List> | null | undefined,\n iteratee?: MemoIterator\n ): TResult[];\n }\n\n interface LoDashImplicitArrayWrapperBase {\n /**\n * @see _.unzipWith\n */\n unzipWith(\n iteratee?: MemoIterator\n ): LoDashImplicitArrayWrapper;\n }\n\n interface LoDashImplicitObjectWrapperBase {\n /**\n * @see _.unzipWith\n */\n unzipWith(\n iteratee?: MemoIterator\n ): LoDashImplicitArrayWrapper;\n }\n\n //_.without\n interface LoDashStatic {\n /**\n * Creates an array excluding all provided values using SameValueZero for equality comparisons.\n *\n * @param array The array to filter.\n * @param values The values to exclude.\n * @return Returns the new array of filtered values.\n */\n without(\n array: List | null | undefined,\n ...values: T[]\n ): T[];\n }\n\n interface LoDashImplicitArrayWrapperBase {\n /**\n * @see _.without\n */\n without(...values: T[]): LoDashImplicitArrayWrapper;\n }\n\n interface LoDashImplicitObjectWrapperBase {\n /**\n * @see _.without\n */\n without(...values: T[]): LoDashImplicitArrayWrapper;\n }\n\n interface LoDashExplicitArrayWrapperBase {\n /**\n * @see _.without\n */\n without(...values: T[]): LoDashExplicitArrayWrapper;\n }\n\n interface LoDashExplicitObjectWrapperBase {\n /**\n * @see _.without\n */\n without(...values: T[]): LoDashExplicitArrayWrapper;\n }\n\n //_.xor\n interface LoDashStatic {\n /**\n * Creates an array of unique values that is the symmetric difference of the provided arrays.\n *\n * @param arrays The arrays to inspect.\n * @return Returns the new array of values.\n */\n xor(...arrays: Array | null | undefined>): T[];\n }\n\n interface LoDashImplicitArrayWrapperBase {\n /**\n * @see _.xor\n */\n xor(...arrays: Array | null | undefined>): LoDashImplicitArrayWrapper;\n }\n\n interface LoDashImplicitObjectWrapperBase {\n /**\n * @see _.xor\n */\n xor(...arrays: Array | null | undefined>): LoDashImplicitArrayWrapper;\n }\n\n interface LoDashExplicitArrayWrapperBase {\n /**\n * @see _.xor\n */\n xor(...arrays: Array | null | undefined>): LoDashExplicitArrayWrapper;\n }\n\n interface LoDashExplicitObjectWrapperBase {\n /**\n * @see _.xor\n */\n xor(...arrays: Array | null | undefined>): LoDashExplicitArrayWrapper;\n }\n\n //_.xorBy DUMMY\n interface LoDashStatic {\n /**\n * This method is like `_.xor` except that it accepts `iteratee` which is\n * invoked for each element of each `arrays` to generate the criterion by which\n * uniqueness is computed. The iteratee is invoked with one argument: (value).\n *\n * @static\n * @memberOf _\n * @category Array\n * @param {...Array} [arrays] The arrays to inspect.\n * @param {Function|Object|string} [iteratee=_.identity] The iteratee invoked per element.\n * @returns {Array} Returns the new array of values.\n * @example\n *\n * _.xorBy([2.1, 1.2], [4.3, 2.4], Math.floor);\n * // => [1.2, 4.3]\n *\n * // using the `_.property` iteratee shorthand\n * _.xorBy([{ 'x': 1 }], [{ 'x': 2 }, { 'x': 1 }], 'x');\n * // => [{ 'x': 2 }]\n */\n xorBy(\n array: List,\n ...values: any[]\n ): any[];\n }\n\n //_.xorWith DUMMY\n interface LoDashStatic {\n /**\n * This method is like `_.xor` except that it accepts `comparator` which is\n * invoked to compare elements of `arrays`. The comparator is invoked with\n * two arguments: (arrVal, othVal).\n *\n * @static\n * @memberOf _\n * @category Array\n * @param {...Array} [arrays] The arrays to inspect.\n * @param {Function} [comparator] The comparator invoked per element.\n * @returns {Array} Returns the new array of values.\n * @example\n *\n * var objects = [{ 'x': 1, 'y': 2 }, { 'x': 2, 'y': 1 }];\n * var others = [{ 'x': 1, 'y': 1 }, { 'x': 1, 'y': 2 }];\n *\n * _.xorWith(objects, others, _.isEqual);\n * // => [{ 'x': 2, 'y': 1 }, { 'x': 1, 'y': 1 }]\n */\n xorWith(\n array: List,\n ...values: any[]\n ): any[];\n }\n\n //_.zip\n interface LoDashStatic {\n /**\n * Creates an array of grouped elements, the first of which contains the first elements of the given arrays,\n * the second of which contains the second elements of the given arrays, and so on.\n *\n * @param arrays The arrays to process.\n * @return Returns the new array of grouped elements.\n */\n zip(...arrays: Array | null | undefined>): T[][];\n }\n\n interface LoDashImplicitArrayWrapperBase {\n /**\n * @see _.zip\n */\n zip(...arrays: Array | null | undefined>): _.LoDashImplicitArrayWrapper;\n }\n\n interface LoDashImplicitObjectWrapperBase {\n /**\n * @see _.zip\n */\n zip(...arrays: Array | null | undefined>): _.LoDashImplicitArrayWrapper;\n }\n\n interface LoDashExplicitArrayWrapperBase {\n /**\n * @see _.zip\n */\n zip(...arrays: Array | null | undefined>): _.LoDashExplicitArrayWrapper;\n }\n\n interface LoDashExplicitObjectWrapperBase {\n /**\n * @see _.zip\n */\n zip(...arrays: Array | null | undefined>): _.LoDashExplicitArrayWrapper;\n }\n\n //_.zipObject\n interface LoDashStatic {\n /**\n * This method is like _.fromPairs except that it accepts two arrays, one of property\n * identifiers and one of corresponding values.\n *\n * @param props The property names.\n * @param values The property values.\n * @return Returns the new object.\n */\n zipObject(\n props: List|List>,\n values?: List\n ): TResult;\n /**\n * This method is like _.zipObject except that it supports property paths.\n *\n * @param props The property names.\n * @param values The property values.\n * @return Returns the new object.\n */\n zipObjectDeep(\n props: List|List>,\n values?: List\n ): TResult;\n\n /**\n * @see _.zipObject\n */\n zipObject(\n props: List|List>,\n values?: List\n ): TResult;\n /**\n * @see _.zipObjectDeep\n */\n zipObjectDeep(\n props: List|List>,\n values?: List\n ): TResult;\n\n /**\n * @see _.zipObject\n */\n zipObject(\n props: List|List>,\n values?: List\n ): _.Dictionary;\n /**\n * @see _.zipObjectDeep\n */\n zipObjectDeep(\n props: List|List>,\n values?: List\n ): _.Dictionary;\n }\n\n interface LoDashImplicitArrayWrapper {\n /**\n * @see _.zipObject\n */\n zipObject(\n values?: List\n ): _.LoDashImplicitObjectWrapper;\n /**\n * @see _.zipObjectDeep\n */\n zipObjectDeep(\n values?: List\n ): _.LoDashImplicitObjectWrapper;\n\n /**\n * @see _.zipObject\n */\n zipObject(\n values?: List\n ): _.LoDashImplicitObjectWrapper;\n /**\n * @see _.zipObjectDeep\n */\n zipObjectDeep(\n values?: List\n ): _.LoDashImplicitObjectWrapper;\n\n /**\n * @see _.zipObject\n */\n zipObject(\n values?: List\n ): _.LoDashImplicitObjectWrapper<_.Dictionary>;\n /**\n * @see _.zipObjectDeep\n */\n zipObjectDeep(\n values?: List\n ): _.LoDashImplicitObjectWrapper<_.Dictionary>;\n }\n\n interface LoDashImplicitObjectWrapper {\n /**\n * @see _.zipObject\n */\n zipObject(\n values?: List\n ): _.LoDashImplicitObjectWrapper;\n /**\n * @see _.zipObjectDeep\n */\n zipObjectDeep(\n values?: List\n ): _.LoDashImplicitObjectWrapper;\n\n /**\n * @see _.zipObject\n */\n zipObject(\n values?: List\n ): _.LoDashImplicitObjectWrapper;\n /**\n * @see _.zipObjectDeep\n */\n zipObjectDeep(\n values?: List\n ): _.LoDashImplicitObjectWrapper;\n\n /**\n * @see _.zipObject\n */\n zipObject(\n values?: List\n ): _.LoDashImplicitObjectWrapper<_.Dictionary>;\n /**\n * @see _.zipObjectDeep\n */\n zipObjectDeep(\n values?: List\n ): _.LoDashImplicitObjectWrapper<_.Dictionary>;\n }\n\n interface LoDashExplicitArrayWrapper {\n /**\n * @see _.zipObject\n */\n zipObject(\n values?: List\n ): _.LoDashExplicitObjectWrapper;\n /**\n * @see _.zipObjectDeep\n */\n zipObjectDeep(\n values?: List\n ): _.LoDashExplicitObjectWrapper;\n\n /**\n * @see _.zipObject\n */\n zipObject(\n values?: List\n ): _.LoDashExplicitObjectWrapper;\n /**\n * @see _.zipObjectDeep\n */\n zipObjectDeep(\n values?: List\n ): _.LoDashExplicitObjectWrapper;\n\n /**\n * @see _.zipObject\n */\n zipObject(\n values?: List\n ): _.LoDashExplicitObjectWrapper<_.Dictionary>;\n /**\n * @see _.zipObjectDeep\n */\n zipObjectDeep(\n values?: List\n ): _.LoDashExplicitObjectWrapper<_.Dictionary>;\n }\n\n interface LoDashExplicitObjectWrapper {\n /**\n * @see _.zipObject\n */\n zipObject(\n values?: List\n ): _.LoDashExplicitObjectWrapper;\n /**\n * @see _.zipObjectDeep\n */\n zipObjectDeep(\n values?: List\n ): _.LoDashExplicitObjectWrapper;\n\n /**\n * @see _.zipObject\n */\n zipObject(\n values?: List\n ): _.LoDashExplicitObjectWrapper;\n /**\n * @see _.zipObjectDeep\n */\n zipObjectDeep(\n values?: List\n ): _.LoDashExplicitObjectWrapper;\n\n /**\n * @see _.zipObject\n */\n zipObject(\n values?: List\n ): _.LoDashExplicitObjectWrapper<_.Dictionary>;\n /**\n * @see _.zipObjectDeep\n */\n zipObjectDeep(\n values?: List\n ): _.LoDashExplicitObjectWrapper<_.Dictionary>;\n }\n\n //_.zipWith\n interface LoDashStatic {\n /**\n * This method is like _.zip except that it accepts an iteratee to specify how grouped values should be\n * combined. The iteratee is bound to thisArg and invoked with four arguments: (accumulator, value, index,\n * group).\n * @param {...Array} [arrays] The arrays to process.\n * @param {Function} [iteratee] The function to combine grouped values.\n * @param {*} [thisArg] The `this` binding of `iteratee`.\n * @return Returns the new array of grouped elements.\n */\n zipWith(...args: any[]): TResult[];\n }\n\n interface LoDashImplicitArrayWrapper {\n /**\n * @see _.zipWith\n */\n zipWith(...args: any[]): LoDashImplicitArrayWrapper;\n }\n\n /*********\n * Chain *\n *********/\n\n //_.chain\n interface LoDashStatic {\n /**\n * Creates a lodash object that wraps value with explicit method chaining enabled.\n *\n * @param value The value to wrap.\n * @return Returns the new lodash wrapper instance.\n */\n chain(value: number): LoDashExplicitWrapper;\n chain(value: string): LoDashExplicitWrapper;\n chain(value: boolean): LoDashExplicitWrapper;\n chain(value: null | undefined): LoDashExplicitWrapper;\n chain(value: T[]): LoDashExplicitArrayWrapper;\n chain(value: ReadonlyArray): LoDashExplicitArrayWrapper;\n chain(value: T[] | null | undefined): LoDashExplicitNillableArrayWrapper;\n chain(value: ReadonlyArray | null | undefined): LoDashExplicitNillableArrayWrapper;\n chain(value: T): LoDashExplicitObjectWrapper;\n chain(value: T | null | undefined): LoDashExplicitObjectWrapper;\n chain(value: any): LoDashExplicitWrapper;\n }\n\n interface LoDashImplicitWrapper {\n /**\n * @see _.chain\n */\n chain(): LoDashExplicitWrapper;\n }\n\n interface LoDashImplicitArrayWrapper {\n /**\n * @see _.chain\n */\n chain(): LoDashExplicitArrayWrapper;\n }\n\n interface LoDashImplicitNillableArrayWrapper {\n /**\n * @see _.chain\n */\n chain(): LoDashExplicitNillableArrayWrapper;\n }\n\n interface LoDashImplicitObjectWrapper {\n /**\n * @see _.chain\n */\n chain(): LoDashExplicitObjectWrapper;\n }\n\n interface LoDashImplicitNillableObjectWrapper {\n /**\n * @see _.chain\n */\n chain(): LoDashExplicitNillableObjectWrapper;\n }\n\n interface LoDashExplicitWrapperBase {\n /**\n * @see _.chain\n */\n chain(): TWrapper;\n }\n\n //_.tap\n interface LoDashStatic {\n /**\n * This method invokes interceptor and returns value. The interceptor is bound to thisArg and invoked with one\n * argument; (value). The purpose of this method is to \"tap into\" a method chain in order to perform operations\n * on intermediate results within the chain.\n *\n * @param value The value to provide to interceptor.\n * @param interceptor The function to invoke.\n * @parem thisArg The this binding of interceptor.\n * @return Returns value.\n **/\n tap(\n value: T,\n interceptor: (value: T) => void\n ): T;\n }\n\n interface LoDashImplicitWrapperBase {\n /**\n * @see _.tap\n */\n tap(\n interceptor: (value: T) => void\n ): TWrapper;\n }\n\n interface LoDashExplicitWrapperBase {\n /**\n * @see _.tap\n */\n tap(\n interceptor: (value: T) => void\n ): TWrapper;\n }\n\n //_.thru\n interface LoDashStatic {\n /**\n * This method is like _.tap except that it returns the result of interceptor.\n *\n * @param value The value to provide to interceptor.\n * @param interceptor The function to invoke.\n * @param thisArg The this binding of interceptor.\n * @return Returns the result of interceptor.\n */\n thru(\n value: T,\n interceptor: (value: T) => TResult\n ): TResult;\n }\n\n interface LoDashImplicitWrapperBase {\n /**\n * @see _.thru\n */\n thru(\n interceptor: (value: T) => TResult): LoDashImplicitWrapper;\n\n /**\n * @see _.thru\n */\n thru(\n interceptor: (value: T) => TResult): LoDashImplicitWrapper;\n\n /**\n * @see _.thru\n */\n thru(\n interceptor: (value: T) => TResult): LoDashImplicitWrapper;\n\n /**\n * @see _.thru\n */\n thru(\n interceptor: (value: T) => TResult): LoDashImplicitObjectWrapper;\n\n /**\n * @see _.thru\n */\n thru(\n interceptor: (value: T) => TResult[]): LoDashImplicitArrayWrapper;\n }\n\n interface LoDashExplicitWrapperBase {\n /**\n * @see _.thru\n */\n thru(\n interceptor: (value: T) => TResult\n ): LoDashExplicitWrapper;\n\n /**\n * @see _.thru\n */\n thru(\n interceptor: (value: T) => TResult\n ): LoDashExplicitWrapper;\n\n /**\n * @see _.thru\n */\n thru(\n interceptor: (value: T) => TResult\n ): LoDashExplicitWrapper;\n\n /**\n * @see _.thru\n */\n thru(\n interceptor: (value: T) => TResult\n ): LoDashExplicitObjectWrapper;\n\n /**\n * @see _.thru\n */\n thru(\n interceptor: (value: T) => TResult[]\n ): LoDashExplicitArrayWrapper;\n }\n\n //_.prototype.commit\n interface LoDashImplicitWrapperBase {\n /**\n * Executes the chained sequence and returns the wrapped result.\n *\n * @return Returns the new lodash wrapper instance.\n */\n commit(): TWrapper;\n }\n\n interface LoDashExplicitWrapperBase {\n /**\n * @see _.commit\n */\n commit(): TWrapper;\n }\n\n //_.prototype.concat\n interface LoDashImplicitWrapperBase {\n /**\n * Creates a new array joining a wrapped array with any additional arrays and/or values.\n *\n * @param items\n * @return Returns the new concatenated array.\n */\n concat(...items: Array>): LoDashImplicitArrayWrapper;\n\n /**\n * @see _.concat\n */\n concat(...items: Array>): LoDashImplicitArrayWrapper;\n }\n\n interface LoDashExplicitWrapperBase {\n /**\n * @see _.concat\n */\n concat(...items: Array>): LoDashExplicitArrayWrapper;\n\n /**\n * @see _.concat\n */\n concat(...items: Array>): LoDashExplicitArrayWrapper;\n }\n\n //_.prototype.plant\n interface LoDashImplicitWrapperBase {\n /**\n * Creates a clone of the chained sequence planting value as the wrapped value.\n * @param value The value to plant as the wrapped value.\n * @return Returns the new lodash wrapper instance.\n */\n plant(value: number): LoDashImplicitWrapper;\n\n /**\n * @see _.plant\n */\n plant(value: string): LoDashImplicitStringWrapper;\n\n /**\n * @see _.plant\n */\n plant(value: boolean): LoDashImplicitWrapper;\n\n /**\n * @see _.plant\n */\n plant(value: number[]): LoDashImplicitNumberArrayWrapper;\n\n /**\n * @see _.plant\n */\n plant(value: T[]): LoDashImplicitArrayWrapper;\n\n /**\n * @see _.plant\n */\n plant(value: ReadonlyArray): LoDashImplicitArrayWrapper;\n\n /**\n * @see _.plant\n */\n plant(value: T): LoDashImplicitObjectWrapper;\n\n /**\n * @see _.plant\n */\n plant(value: any): LoDashImplicitWrapper;\n }\n\n interface LoDashExplicitWrapperBase {\n /**\n * @see _.plant\n */\n plant(value: number): LoDashExplicitWrapper;\n\n /**\n * @see _.plant\n */\n plant(value: string): LoDashExplicitStringWrapper;\n\n /**\n * @see _.plant\n */\n plant(value: boolean): LoDashExplicitWrapper;\n\n /**\n * @see _.plant\n */\n plant(value: number[]): LoDashExplicitNumberArrayWrapper;\n\n /**\n * @see _.plant\n */\n plant(value: T[]): LoDashExplicitArrayWrapper;\n\n /**\n * @see _.plant\n */\n plant(value: ReadonlyArray): LoDashExplicitArrayWrapper;\n\n /**\n * @see _.plant\n */\n plant(value: T): LoDashExplicitObjectWrapper;\n\n /**\n * @see _.plant\n */\n plant(value: any): LoDashExplicitWrapper;\n }\n\n //_.prototype.reverse\n interface LoDashImplicitArrayWrapper {\n /**\n * Reverses the wrapped array so the first element becomes the last, the second element becomes the second to\n * last, and so on.\n *\n * Note: This method mutates the wrapped array.\n *\n * @return Returns the new reversed lodash wrapper instance.\n */\n reverse(): LoDashImplicitArrayWrapper;\n }\n\n interface LoDashExplicitArrayWrapper {\n /**\n * @see _.reverse\n */\n reverse(): LoDashExplicitArrayWrapper;\n }\n\n //_.prototype.toJSON\n interface LoDashWrapperBase {\n /**\n * @see _.value\n */\n toJSON(): T;\n }\n\n //_.prototype.toString\n interface LoDashWrapperBase {\n /**\n * Produces the result of coercing the unwrapped value to a string.\n *\n * @return Returns the coerced string value.\n */\n toString(): string;\n }\n\n //_.prototype.value\n interface LoDashWrapperBase {\n /**\n * Executes the chained sequence to extract the unwrapped value.\n *\n * @alias _.toJSON, _.valueOf\n *\n * @return Returns the resolved unwrapped value.\n */\n value(): T;\n }\n\n //_.valueOf\n interface LoDashWrapperBase {\n /**\n * @see _.value\n */\n valueOf(): T;\n }\n\n /**************\n * Collection *\n **************/\n\n //_.at\n interface LoDashStatic {\n /**\n * Creates an array of elements corresponding to the given keys, or indexes, of collection. Keys may be\n * specified as individual arguments or as arrays of keys.\n *\n * @param collection The collection to iterate over.\n * @param props The property names or indexes of elements to pick, specified individually or in arrays.\n * @return Returns the new array of picked elements.\n */\n at(\n collection: List|Dictionary | null | undefined,\n ...props: Array>\n ): T[];\n }\n\n interface LoDashImplicitArrayWrapperBase {\n /**\n * @see _.at\n */\n at(...props: Array>): LoDashImplicitArrayWrapper;\n }\n\n interface LoDashImplicitObjectWrapperBase {\n /**\n * @see _.at\n */\n at(...props: Array>): LoDashImplicitArrayWrapper;\n }\n\n interface LoDashExplicitArrayWrapperBase {\n /**\n * @see _.at\n */\n at(...props: Array>): LoDashExplicitArrayWrapper;\n }\n\n interface LoDashExplicitObjectWrapperBase {\n /**\n * @see _.at\n */\n at(...props: Array>): LoDashExplicitArrayWrapper;\n }\n\n //_.countBy\n interface LoDashStatic {\n /**\n * Creates an object composed of keys generated from the results of running each element of collection through\n * iteratee. The corresponding value of each key is the number of times the key was returned by iteratee. The\n * iteratee is bound to thisArg and invoked with three arguments:\n * (value, index|key, collection).\n *\n * If a property name is provided for iteratee the created _.property style callback returns the property\n * value of the given element.\n *\n * If a value is also provided for thisArg the created _.matchesProperty style callback returns true for\n * elements that have a matching property value, else false.\n *\n * If an object is provided for iteratee the created _.matches style callback returns true for elements that\n * have the properties of the given object, else false.\n *\n * @param collection The collection to iterate over.\n * @param iteratee The function invoked per iteration.\n * @param thisArg The this binding of iteratee.\n * @return Returns the composed aggregate object.\n */\n countBy(\n collection: List | null | undefined,\n iteratee?: ListIterator\n ): Dictionary;\n\n /**\n * @see _.countBy\n */\n countBy(\n collection: Dictionary | null | undefined,\n iteratee?: DictionaryIterator\n ): Dictionary;\n\n /**\n * @see _.countBy\n */\n countBy(\n collection: NumericDictionary | null | undefined,\n iteratee?: NumericDictionaryIterator\n ): Dictionary;\n\n /**\n * @see _.countBy\n */\n countBy(\n collection: List|Dictionary|NumericDictionary | null | undefined,\n iteratee?: string\n ): Dictionary;\n\n /**\n * @see _.countBy\n */\n countBy(\n collection: List|Dictionary|NumericDictionary | null | undefined,\n iteratee?: W\n ): Dictionary;\n\n /**\n * @see _.countBy\n */\n countBy(\n collection: List|Dictionary|NumericDictionary | null | undefined,\n iteratee?: Object\n ): Dictionary;\n }\n\n interface LoDashImplicitWrapper {\n /**\n * @see _.countBy\n */\n countBy(\n iteratee?: ListIterator\n ): LoDashImplicitObjectWrapper>;\n }\n\n interface LoDashImplicitArrayWrapperBase {\n /**\n * @see _.countBy\n */\n countBy(\n iteratee?: ListIterator\n ): LoDashImplicitObjectWrapper>;\n\n /**\n * @see _.countBy\n */\n countBy(\n iteratee?: string\n ): LoDashImplicitObjectWrapper>;\n\n /**\n * @see _.countBy\n */\n countBy(\n iteratee?: W\n ): LoDashImplicitObjectWrapper>;\n }\n\n interface LoDashImplicitObjectWrapperBase {\n /**\n * @see _.countBy\n */\n countBy(\n iteratee?: ListIterator|DictionaryIterator|NumericDictionaryIterator\n ): LoDashImplicitObjectWrapper>;\n\n /**\n * @see _.countBy\n */\n countBy(\n iteratee?: string\n ): LoDashImplicitObjectWrapper>;\n\n /**\n * @see _.countBy\n */\n countBy(\n iteratee?: W\n ): LoDashImplicitObjectWrapper>;\n }\n\n interface LoDashExplicitWrapper {\n /**\n * @see _.countBy\n */\n countBy(\n iteratee?: ListIterator\n ): LoDashExplicitObjectWrapper>;\n }\n\n interface LoDashExplicitArrayWrapperBase {\n /**\n * @see _.countBy\n */\n countBy(\n iteratee?: ListIterator\n ): LoDashExplicitObjectWrapper>;\n\n /**\n * @see _.countBy\n */\n countBy(\n iteratee?: string\n ): LoDashExplicitObjectWrapper>;\n\n /**\n * @see _.countBy\n */\n countBy(\n iteratee?: W\n ): LoDashExplicitObjectWrapper>;\n }\n\n interface LoDashExplicitObjectWrapperBase {\n /**\n * @see _.countBy\n */\n countBy(\n iteratee?: ListIterator|DictionaryIterator|NumericDictionaryIterator\n ): LoDashExplicitObjectWrapper>;\n\n /**\n * @see _.countBy\n */\n countBy(\n iteratee?: string\n ): LoDashExplicitObjectWrapper>;\n\n /**\n * @see _.countBy\n */\n countBy(\n iteratee?: W\n ): LoDashExplicitObjectWrapper>;\n }\n\n //_.each\n interface LoDashStatic {\n each: typeof _.forEach;\n }\n\n interface LoDashImplicitWrapper {\n /**\n * @see _.forEach\n */\n each(\n iteratee: StringIterator\n ): LoDashImplicitWrapper;\n }\n\n interface LoDashImplicitArrayWrapperBase {\n /**\n * @see _.forEach\n */\n each(\n iteratee: ListIterator\n ): TWrapper;\n }\n\n interface LoDashImplicitObjectWrapperBase {\n /**\n * @see _.forEach\n */\n each(\n iteratee?: ListIterator|DictionaryIterator\n ): TWrapper;\n }\n\n interface LoDashExplicitWrapper {\n /**\n * @see _.forEach\n */\n each(\n iteratee: StringIterator\n ): LoDashExplicitWrapper;\n }\n\n interface LoDashExplicitArrayWrapperBase {\n /**\n * @see _.forEach\n */\n each(\n iteratee: ListIterator\n ): TWrapper;\n }\n\n interface LoDashExplicitObjectWrapperBase {\n /**\n * @see _.forEach\n */\n each(\n iteratee?: ListIterator|DictionaryIterator\n ): TWrapper;\n }\n\n //_.eachRight\n interface LoDashStatic {\n eachRight: typeof _.forEachRight;\n }\n\n interface LoDashImplicitWrapper {\n /**\n * @see _.forEachRight\n */\n eachRight(\n iteratee: StringIterator\n ): LoDashImplicitWrapper;\n }\n\n interface LoDashImplicitArrayWrapperBase {\n /**\n * @see _.forEachRight\n */\n eachRight(\n iteratee: ListIterator\n ): TWrapper;\n }\n\n interface LoDashImplicitObjectWrapperBase {\n /**\n * @see _.forEachRight\n */\n eachRight(\n iteratee?: ListIterator|DictionaryIterator\n ): TWrapper;\n }\n\n interface LoDashExplicitWrapper {\n /**\n * @see _.forEachRight\n */\n eachRight(\n iteratee: StringIterator\n ): LoDashExplicitWrapper;\n }\n\n interface LoDashExplicitArrayWrapperBase {\n /**\n * @see _.forEachRight\n */\n eachRight(\n iteratee: ListIterator\n ): TWrapper;\n }\n\n interface LoDashExplicitObjectWrapperBase {\n /**\n * @see _.forEachRight\n */\n eachRight(\n iteratee?: ListIterator|DictionaryIterator\n ): TWrapper;\n }\n\n //_.every\n interface LoDashStatic {\n /**\n * Checks if predicate returns truthy for all elements of collection. Iteration is stopped once predicate\n * returns falsey. The predicate is invoked with three arguments: (value, index|key, collection).\n *\n * @param collection The collection to iterate over.\n * @param predicate The function invoked per iteration.\n * @return Returns true if all elements pass the predicate check, else false.\n */\n every(\n collection: List | null | undefined,\n predicate?: ListIterator\n ): boolean;\n\n /**\n * @see _.every\n */\n every(\n collection: Dictionary | null | undefined,\n predicate?: DictionaryIterator\n ): boolean;\n\n /**\n * @see _.every\n */\n every(\n collection: NumericDictionary | null | undefined,\n predicate?: NumericDictionaryIterator\n ): boolean;\n\n /**\n * @see _.every\n */\n every(\n collection: List|Dictionary|NumericDictionary | null | undefined,\n predicate?: string|any[]|PartialObject\n ): boolean;\n }\n\n interface LoDashImplicitArrayWrapperBase {\n /**\n * @see _.every\n */\n every(\n predicate?: ListIterator|NumericDictionaryIterator\n ): boolean;\n\n /**\n * @see _.every\n */\n every(\n predicate?: string|any[]\n ): boolean;\n\n /**\n * @see _.every\n */\n every(\n predicate?: PartialObject\n ): boolean;\n }\n\n interface LoDashImplicitObjectWrapperBase {\n /**\n * @see _.every\n */\n every(\n predicate?: ListIterator|DictionaryIterator|NumericDictionaryIterator\n ): boolean;\n\n /**\n * @see _.every\n */\n every(\n predicate?: string|any[]\n ): boolean;\n\n /**\n * @see _.every\n */\n every(\n predicate?: PartialObject\n ): boolean;\n }\n\n interface LoDashExplicitArrayWrapperBase {\n /**\n * @see _.every\n */\n every(\n predicate?: ListIterator|NumericDictionaryIterator\n ): LoDashExplicitWrapper;\n\n /**\n * @see _.every\n */\n every(\n predicate?: string|any[]\n ): LoDashExplicitWrapper;\n\n /**\n * @see _.every\n */\n every(\n predicate?: PartialObject\n ): LoDashExplicitWrapper;\n }\n\n interface LoDashExplicitObjectWrapperBase {\n /**\n * @see _.every\n */\n every(\n predicate?: ListIterator|DictionaryIterator|NumericDictionaryIterator\n ): LoDashExplicitWrapper;\n\n /**\n * @see _.every\n */\n every(\n predicate?: string|any[]\n ): LoDashExplicitWrapper;\n\n /**\n * @see _.every\n */\n every(\n predicate?: PartialObject\n ): LoDashExplicitWrapper;\n }\n\n //_.filter\n interface LoDashStatic {\n /**\n * Iterates over elements of collection, returning an array of all elements predicate returns truthy for. The\n * predicate is bound to thisArg and invoked with three arguments: (value, index|key, collection).\n *\n * If a property name is provided for predicate the created _.property style callback returns the property\n * value of the given element.\n *\n * If a value is also provided for thisArg the created _.matchesProperty style callback returns true for\n * elements that have a matching property value, else false.\n *\n * If an object is provided for predicate the created _.matches style callback returns true for elements that\n * have the properties of the given object, else false.\n *\n * @param collection The collection to iterate over.\n * @param predicate The function invoked per iteration.\n * @param thisArg The this binding of predicate.\n * @return Returns the new filtered array.\n */\n filter(\n collection: List | null | undefined,\n predicate: ListIteratorTypeGuard\n ): S[];\n\n /**\n * @see _.filter\n */\n filter(\n collection: List | null | undefined,\n predicate?: ListIterator\n ): T[];\n\n /**\n * @see _.filter\n */\n filter(\n collection: Dictionary | null | undefined,\n predicate: DictionaryIteratorTypeGuard\n ): S[];\n\n /**\n * @see _.filter\n */\n filter(\n collection: Dictionary | null | undefined,\n predicate?: DictionaryIterator\n ): T[];\n\n /**\n * @see _.filter\n */\n filter(\n collection: string | null | undefined,\n predicate?: StringIterator\n ): string[];\n\n /**\n * @see _.filter\n */\n filter(\n collection: List|Dictionary | null | undefined,\n predicate: string | [string, any] | RegExp | PartialObject\n ): T[];\n }\n\n interface LoDashImplicitWrapper {\n /**\n * @see _.filter\n */\n filter(\n predicate?: StringIterator\n ): LoDashImplicitArrayWrapper;\n }\n\n interface LoDashImplicitArrayWrapperBase {\n /**\n * @see _.filter\n */\n filter(\n predicate: ListIteratorTypeGuard\n ): LoDashImplicitArrayWrapper;\n\n /**\n * @see _.filter\n */\n filter(\n predicate: ListIterator | string | [string, any] | RegExp | PartialObject\n ): LoDashImplicitArrayWrapper;\n }\n\n interface LoDashImplicitObjectWrapperBase {\n /**\n * @see _.filter\n */\n filter(\n predicate: ListIteratorTypeGuard\n ): LoDashImplicitArrayWrapper;\n\n /**\n * @see _.filter\n */\n filter(\n predicate: ListIterator | DictionaryIterator | string | [string, any] | RegExp | PartialObject\n ): LoDashImplicitArrayWrapper;\n }\n\n interface LoDashExplicitWrapper {\n /**\n * @see _.filter\n */\n filter(\n predicate?: StringIterator\n ): LoDashExplicitArrayWrapper;\n }\n\n interface LoDashExplicitArrayWrapperBase {\n /**\n * @see _.filter\n */\n filter(\n predicate: ListIteratorTypeGuard\n ): LoDashExplicitArrayWrapper;\n\n /**\n * @see _.filter\n */\n filter(\n predicate: ListIterator | string | [string, any] | RegExp | PartialObject\n ): LoDashExplicitArrayWrapper;\n }\n\n interface LoDashExplicitObjectWrapperBase {\n /**\n * @see _.filter\n */\n filter(\n predicate: ListIteratorTypeGuard\n ): LoDashExplicitArrayWrapper;\n\n /**\n * @see _.filter\n */\n filter(\n predicate: ListIterator | DictionaryIterator | string | [string, any] | RegExp | PartialObject\n ): LoDashExplicitArrayWrapper;\n }\n\n //_.find\n interface LoDashStatic {\n /**\n * Iterates over elements of collection, returning the first element predicate returns truthy for.\n * The predicate is bound to thisArg and invoked with three arguments: (value, index|key, collection).\n *\n * If a property name is provided for predicate the created _.property style callback returns the property\n * value of the given element.\n *\n * If a value is also provided for thisArg the created _.matchesProperty style callback returns true for\n * elements that have a matching property value, else false.\n *\n * If an object is provided for predicate the created _.matches style callback returns true for elements that\n * have the properties of the given object, else false.\n *\n * @param collection The collection to search.\n * @param predicate The function invoked per iteration.\n * @param fromIndex The index to search from.\n * @return Returns the matched element, else undefined.\n */\n find(\n collection: List | null | undefined,\n predicate: ListIteratorTypeGuard,\n fromIndex?: number\n ): S|undefined;\n \n /**\n * @see _.find\n */\n find(\n collection: List | null | undefined,\n predicate?: ListIterator,\n fromIndex?: number\n ): T|undefined;\n\n /**\n * @see _.find\n */\n find(\n collection: Dictionary | null | undefined,\n predicate: DictionaryIteratorTypeGuard,\n fromIndex?: number\n ): S|undefined;\n\n /**\n * @see _.find\n */\n find(\n collection: Dictionary | null | undefined,\n predicate?: DictionaryIterator,\n fromIndex?: number\n ): T|undefined;\n\n /**\n * @see _.find\n */\n find(\n collection: List|Dictionary | null | undefined,\n predicate?: string | PartialObject | [string, any],\n fromIndex?: number\n ): T|undefined;\n }\n\n interface LoDashImplicitArrayWrapperBase {\n /**\n * @see _.find\n */\n find(\n predicate: ListIteratorTypeGuard,\n fromIndex?: number\n ): S|undefined;\n\n /**\n * @see _.find\n */\n find(\n predicate?: ListIterator | string | PartialObject | [string, any],\n fromIndex?: number\n ): T|undefined;\n }\n\n interface LoDashImplicitObjectWrapperBase {\n /**\n * @see _.find\n */\n find(\n predicate?: ListIterator | DictionaryIterator | string | PartialObject | [string, any],\n fromIndex?: number\n ): TResult|undefined;\n }\n\n interface LoDashExplicitWrapperBase {\n /**\n * @see _.find\n */\n find(\n predicate?: ListIterator | string | PartialObject | [string, any],\n fromIndex?: number\n ): any;\n }\n\n //_.findLast\n interface LoDashStatic {\n /**\n * This method is like _.find except that it iterates over elements of a collection from\n * right to left.\n * @param collection Searches for a value in this list.\n * @param predicate The function called per iteration.\n * @param fromIndex The index to search from.\n * @return The found element, else undefined.\n **/\n findLast(\n collection: List | null | undefined,\n predicate: ListIteratorTypeGuard,\n fromIndex?: number\n ): S|undefined;\n \n /**\n * @see _.findLast\n */\n findLast(\n collection: List | null | undefined,\n predicate?: ListIterator,\n fromIndex?: number\n ): T|undefined;\n\n /**\n * @see _.findLast\n */\n findLast(\n collection: Dictionary | null | undefined,\n predicate: DictionaryIteratorTypeGuard,\n fromIndex?: number\n ): S|undefined;\n\n /**\n * @see _.findLast\n */\n findLast(\n collection: Dictionary | null | undefined,\n predicate?: DictionaryIterator,\n fromIndex?: number\n ): T|undefined;\n\n /**\n * @see _.findLast\n */\n findLast(\n collection: List|Dictionary | null | undefined,\n predicate?: string | PartialObject | [string, any],\n fromIndex?: number\n ): T|undefined;\n }\n\n interface LoDashImplicitArrayWrapperBase {\n /**\n * @see _.findLast\n */\n findLast(\n predicate: ListIteratorTypeGuard,\n fromIndex?: number\n ): S|undefined;\n\n /**\n * @see _.findLast\n */\n findLast(\n predicate?: ListIterator | string | PartialObject | [string, any],\n fromIndex?: number\n ): T|undefined;\n }\n\n interface LoDashImplicitObjectWrapperBase {\n /**\n * @see _.findLast\n */\n findLast(\n predicate?: ListIterator | DictionaryIterator | string | PartialObject | [string, any],\n fromIndex?: number\n ): TResult|undefined;\n }\n\n interface LoDashExplicitWrapperBase {\n /**\n * @see _.findLast\n */\n findLast(\n predicate?: ListIterator | string | PartialObject | [string, any],\n fromIndex?: number\n ): any;\n }\n\n //_.flatMap\n interface LoDashStatic {\n /**\n * Creates an array of flattened values by running each element in collection through iteratee\n * and concating its result to the other mapped values. The iteratee is invoked with three arguments:\n * (value, index|key, collection).\n *\n * @param collection The collection to iterate over.\n * @param iteratee The function invoked per iteration.\n * @return Returns the new flattened array.\n */\n flatMap(\n collection: List> | Dictionary> | NumericDictionary> | null | undefined\n ): T[];\n\n /**\n * @see _.flatMap\n */\n flatMap(\n collection: List | null | undefined,\n iteratee: ListIterator> | string\n ): TResult[];\n\n /**\n * @see _.flatMap\n */\n flatMap(\n collection: Dictionary | null | undefined,\n iteratee: DictionaryIterator> | string\n ): TResult[];\n\n /**\n * @see _.flatMap\n */\n flatMap(\n collection: NumericDictionary | null | undefined,\n iteratee: NumericDictionaryIterator> | string\n ): TResult[];\n\n /**\n * @see _.flatMap\n */\n flatMap(\n collection: object | null | undefined,\n iteratee?: ObjectIterator> | string\n ): TResult[];\n\n /**\n * @see _.flatMap\n */\n flatMap(\n collection: object | null | undefined,\n iteratee: object\n ): boolean[];\n }\n\n interface LoDashImplicitWrapper {\n /**\n * @see _.flatMap\n */\n flatMap(\n iteratee: StringIterator>\n ): LoDashImplicitArrayWrapper;\n\n /**\n * @see _.flatMap\n */\n flatMap(): LoDashImplicitArrayWrapper;\n }\n\n interface LoDashImplicitArrayWrapperBase {\n /**\n * @see _.flatMap\n */\n flatMap(\n iteratee: ListIterator>|string\n ): LoDashImplicitArrayWrapper;\n\n /**\n * @see _.flatMap\n */\n flatMap(\n iteratee: object\n ): LoDashImplicitArrayWrapper;\n\n /**\n * @see _.flatMap\n */\n flatMap(): LoDashImplicitArrayWrapper;\n }\n\n interface LoDashImplicitObjectWrapperBase {\n /**\n * @see _.flatMap\n */\n flatMap(\n iteratee: ListIterator>|DictionaryIterator>|NumericDictionaryIterator>\n ): LoDashImplicitArrayWrapper;\n\n /**\n * @see _.flatMap\n */\n flatMap(\n iteratee: ObjectIterator>|string\n ): LoDashImplicitArrayWrapper;\n\n /**\n * @see _.flatMap\n */\n flatMap(\n iteratee: object\n ): LoDashImplicitArrayWrapper;\n\n /**\n * @see _.flatMap\n */\n flatMap(): LoDashImplicitArrayWrapper;\n }\n\n interface LoDashExplicitWrapper {\n /**\n * @see _.flatMap\n */\n flatMap(\n iteratee: StringIterator>\n ): LoDashExplicitArrayWrapper;\n\n /**\n * @see _.flatMap\n */\n flatMap(): LoDashExplicitArrayWrapper;\n }\n\n interface LoDashExplicitArrayWrapperBase {\n /**\n * @see _.flatMap\n */\n flatMap(\n iteratee: ListIterator>|string\n ): LoDashExplicitArrayWrapper;\n\n /**\n * @see _.flatMap\n */\n flatMap(\n iteratee: object\n ): LoDashExplicitArrayWrapper;\n\n /**\n * @see _.flatMap\n */\n flatMap(): LoDashExplicitArrayWrapper;\n }\n\n interface LoDashExplicitObjectWrapperBase {\n /**\n * @see _.flatMap\n */\n flatMap(\n iteratee: ListIterator>|DictionaryIterator>|NumericDictionaryIterator>\n ): LoDashExplicitArrayWrapper;\n\n /**\n * @see _.flatMap\n */\n flatMap(\n iteratee: ObjectIterator>|string\n ): LoDashExplicitArrayWrapper;\n\n /**\n * @see _.flatMap\n */\n flatMap(\n iteratee: object\n ): LoDashExplicitArrayWrapper;\n\n /**\n * @see _.flatMap\n */\n flatMap(): LoDashExplicitArrayWrapper;\n }\n\n //_.forEach\n interface LoDashStatic {\n /**\n * Iterates over elements of collection invoking iteratee for each element. The iteratee is bound to thisArg\n * and invoked with three arguments:\n * (value, index|key, collection). Iteratee functions may exit iteration early by explicitly returning false.\n *\n * Note: As with other \"Collections\" methods, objects with a \"length\" property are iterated like arrays. To\n * avoid this behavior _.forIn or _.forOwn may be used for object iteration.\n *\n * @alias _.each\n *\n * @param collection The collection to iterate over.\n * @param iteratee The function invoked per iteration.\n * @param thisArg The this binding of iteratee.\n */\n forEach(\n collection: TString,\n iteratee?: StringIterator\n ): TString;\n\n /**\n * @see _.forEach\n */\n forEach | null | undefined>(\n collection: TList & (List | null | undefined),\n iteratee?: ListIterator\n ): TList;\n\n /**\n * @see _.forEach\n */\n forEach | null | undefined>(\n collection: TDictionary & (Dictionary | null | undefined),\n iteratee?: DictionaryIterator\n ): TDictionary;\n\n /**\n * @see _.forEach\n */\n forEach(\n collection: T,\n iteratee?: ObjectIterator\n ): T;\n }\n\n interface LoDashImplicitWrapper {\n /**\n * @see _.forEach\n */\n forEach(\n iteratee: StringIterator\n ): LoDashImplicitWrapper;\n }\n\n interface LoDashImplicitArrayWrapperBase {\n /**\n * @see _.forEach\n */\n forEach(\n iteratee: ListIterator\n ): TWrapper;\n }\n\n interface LoDashImplicitObjectWrapperBase {\n /**\n * @see _.forEach\n */\n forEach(\n iteratee?: ListIterator|DictionaryIterator\n ): TWrapper;\n }\n\n interface LoDashExplicitWrapper {\n /**\n * @see _.forEach\n */\n forEach(\n iteratee: StringIterator\n ): LoDashExplicitWrapper;\n }\n\n interface LoDashExplicitArrayWrapperBase {\n /**\n * @see _.forEach\n */\n forEach(\n iteratee: ListIterator\n ): TWrapper;\n }\n\n interface LoDashExplicitObjectWrapperBase {\n /**\n * @see _.forEach\n */\n forEach(\n iteratee?: ListIterator|DictionaryIterator\n ): TWrapper;\n }\n\n //_.forEachRight\n interface LoDashStatic {\n /**\n * This method is like _.forEach except that it iterates over elements of collection from right to left.\n *\n * @alias _.eachRight\n *\n * @param collection The collection to iterate over.\n * @param iteratee The function called per iteration.\n * @param thisArg The this binding of callback.\n */\n forEachRight(\n collection: TString,\n iteratee?: StringIterator\n ): TString;\n\n /**\n * @see _.forEachRight\n */\n forEachRight | null | undefined>(\n collection: TList & (List | null | undefined),\n iteratee?: ListIterator\n ): TList;\n\n /**\n * @see _.forEachRight\n */\n forEachRight | null | undefined>(\n collection: TDictionary & (Dictionary | null | undefined),\n iteratee?: DictionaryIterator\n ): TDictionary;\n\n /**\n * @see _.forEachRight\n */\n forEachRight(\n collection: T,\n iteratee?: ObjectIterator\n ): T;\n }\n\n interface LoDashImplicitWrapper {\n /**\n * @see _.forEachRight\n */\n forEachRight(\n iteratee: StringIterator\n ): LoDashImplicitWrapper;\n }\n\n interface LoDashImplicitArrayWrapperBase {\n /**\n * @see _.forEachRight\n */\n forEachRight(\n iteratee: ListIterator\n ): TWrapper;\n }\n\n interface LoDashImplicitObjectWrapperBase {\n /**\n * @see _.forEachRight\n */\n forEachRight(\n iteratee?: ListIterator|DictionaryIterator\n ): TWrapper;\n }\n\n interface LoDashExplicitWrapper {\n /**\n * @see _.forEachRight\n */\n forEachRight(\n iteratee: StringIterator\n ): LoDashExplicitWrapper;\n }\n\n interface LoDashExplicitArrayWrapperBase {\n /**\n * @see _.forEachRight\n */\n forEachRight(\n iteratee: ListIterator\n ): TWrapper;\n }\n\n interface LoDashExplicitObjectWrapperBase {\n /**\n * @see _.forEachRight\n */\n forEachRight(\n iteratee?: ListIterator|DictionaryIterator\n ): TWrapper;\n }\n\n //_.groupBy\n interface LoDashStatic {\n /**\n * Creates an object composed of keys generated from the results of running each element of collection through\n * iteratee. The corresponding value of each key is an array of the elements responsible for generating the\n * key. The iteratee is bound to thisArg and invoked with three arguments:\n * (value, index|key, collection).\n *\n * If a property name is provided for iteratee the created _.property style callback returns the property\n * value of the given element.\n *\n * If a value is also provided for thisArg the created _.matchesProperty style callback returns true for\n * elements that have a matching property value, else false.\n *\n * If an object is provided for iteratee the created _.matches style callback returns true for elements that\n * have the properties of the given object, else false.\n *\n * @param collection The collection to iterate over.\n * @param iteratee The function invoked per iteration.\n * @param thisArg The this binding of iteratee.\n * @return Returns the composed aggregate object.\n */\n groupBy(\n collection: List | null | undefined,\n iteratee?: ListIterator\n ): Dictionary;\n\n /**\n * @see _.groupBy\n */\n groupBy(\n collection: List | null | undefined,\n iteratee?: ListIterator\n ): Dictionary;\n\n /**\n * @see _.groupBy\n */\n groupBy(\n collection: Dictionary | null | undefined,\n iteratee?: DictionaryIterator\n ): Dictionary;\n\n /**\n * @see _.groupBy\n */\n groupBy(\n collection: Dictionary | null | undefined,\n iteratee?: DictionaryIterator\n ): Dictionary;\n\n /**\n * @see _.groupBy\n */\n groupBy(\n collection: List|Dictionary | null | undefined,\n iteratee?: string\n ): Dictionary;\n\n /**\n * @see _.groupBy\n */\n groupBy(\n collection: List|Dictionary | null | undefined,\n iteratee?: string\n ): Dictionary;\n\n /**\n * @see _.groupBy\n */\n groupBy(\n collection: List|Dictionary | null | undefined,\n iteratee?: TWhere\n ): Dictionary;\n\n /**\n * @see _.groupBy\n */\n groupBy(\n collection: List|Dictionary | null | undefined,\n iteratee?: Object\n ): Dictionary;\n }\n\n interface LoDashImplicitWrapper {\n /**\n * @see _.groupBy\n */\n groupBy(\n iteratee?: ListIterator\n ): LoDashImplicitObjectWrapper>;\n }\n\n interface LoDashImplicitArrayWrapperBase {\n /**\n * @see _.groupBy\n */\n groupBy(\n iteratee?: ListIterator\n ): LoDashImplicitObjectWrapper>;\n\n /**\n * @see _.groupBy\n */\n groupBy(\n iteratee?: string\n ): LoDashImplicitObjectWrapper>;\n\n /**\n * @see _.groupBy\n */\n groupBy(\n iteratee?: TWhere\n ): LoDashImplicitObjectWrapper>;\n }\n\n interface LoDashImplicitObjectWrapperBase {\n /**\n * @see _.groupBy\n */\n groupBy(\n iteratee?: ListIterator|DictionaryIterator\n ): LoDashImplicitObjectWrapper>;\n\n /**\n * @see _.groupBy\n */\n groupBy(\n iteratee?: ListIterator|DictionaryIterator\n ): LoDashImplicitObjectWrapper>;\n\n /**\n * @see _.groupBy\n */\n groupBy(\n iteratee?: string\n ): LoDashImplicitObjectWrapper>;\n\n /**\n * @see _.groupBy\n */\n groupBy(\n iteratee?: string\n ): LoDashImplicitObjectWrapper>;\n\n /**\n * @see _.groupBy\n */\n groupBy(\n iteratee?: TWhere\n ): LoDashImplicitObjectWrapper>;\n\n /**\n * @see _.groupBy\n */\n groupBy(\n iteratee?: Object\n ): LoDashImplicitObjectWrapper>;\n }\n\n interface LoDashExplicitWrapper {\n /**\n * @see _.groupBy\n */\n groupBy(\n iteratee?: ListIterator\n ): LoDashExplicitObjectWrapper>;\n }\n\n interface LoDashExplicitArrayWrapperBase {\n /**\n * @see _.groupBy\n */\n groupBy(\n iteratee?: ListIterator\n ): LoDashExplicitObjectWrapper>;\n\n /**\n * @see _.groupBy\n */\n groupBy(\n iteratee?: string\n ): LoDashExplicitObjectWrapper>;\n\n /**\n * @see _.groupBy\n */\n groupBy(\n iteratee?: TWhere\n ): LoDashExplicitObjectWrapper>;\n }\n\n interface LoDashExplicitObjectWrapperBase {\n /**\n * @see _.groupBy\n */\n groupBy(\n iteratee?: ListIterator|DictionaryIterator\n ): LoDashExplicitObjectWrapper>;\n\n /**\n * @see _.groupBy\n */\n groupBy(\n iteratee?: ListIterator|DictionaryIterator\n ): LoDashExplicitObjectWrapper>;\n\n /**\n * @see _.groupBy\n */\n groupBy(\n iteratee?: string\n ): LoDashExplicitObjectWrapper>;\n\n /**\n * @see _.groupBy\n */\n groupBy(\n iteratee?: string\n ): LoDashExplicitObjectWrapper>;\n\n /**\n * @see _.groupBy\n */\n groupBy(\n iteratee?: TWhere\n ): LoDashExplicitObjectWrapper>;\n\n /**\n * @see _.groupBy\n */\n groupBy(\n iteratee?: Object\n ): LoDashExplicitObjectWrapper>;\n }\n\n //_.includes\n interface LoDashStatic {\n /**\n * Checks if target is in collection using SameValueZero for equality comparisons. If fromIndex is negative,\n * it’s used as the offset from the end of collection.\n *\n * @param collection The collection to search.\n * @param target The value to search for.\n * @param fromIndex The index to search from.\n * @return True if the target element is found, else false.\n */\n includes(\n collection: List|Dictionary | null | undefined,\n target: T,\n fromIndex?: number\n ): boolean;\n\n /**\n * @see _.includes\n */\n includes(\n collection: string | null | undefined,\n target: string,\n fromIndex?: number\n ): boolean;\n }\n\n interface LoDashImplicitArrayWrapperBase {\n /**\n * @see _.includes\n */\n includes(\n target: T,\n fromIndex?: number\n ): boolean;\n }\n\n interface LoDashImplicitObjectWrapperBase {\n /**\n * @see _.includes\n */\n includes(\n target: TValue,\n fromIndex?: number\n ): boolean;\n }\n\n interface LoDashImplicitWrapper {\n /**\n * @see _.includes\n */\n includes(\n target: string,\n fromIndex?: number\n ): boolean;\n }\n\n interface LoDashExplicitArrayWrapperBase {\n /**\n * @see _.includes\n */\n includes(\n target: T,\n fromIndex?: number\n ): LoDashExplicitWrapper;\n }\n\n interface LoDashExplicitObjectWrapperBase {\n /**\n * @see _.includes\n */\n includes(\n target: TValue,\n fromIndex?: number\n ): LoDashExplicitWrapper;\n }\n\n interface LoDashExplicitWrapper {\n /**\n * @see _.includes\n */\n includes(\n target: string,\n fromIndex?: number\n ): LoDashExplicitWrapper;\n }\n\n //_.keyBy\n interface LoDashStatic {\n /**\n * Creates an object composed of keys generated from the results of running each element of collection through\n * iteratee. The corresponding value of each key is the last element responsible for generating the key. The\n * iteratee function is bound to thisArg and invoked with three arguments:\n * (value, index|key, collection).\n *\n * If a property name is provided for iteratee the created _.property style callback returns the property\n * value of the given element.\n *\n * If a value is also provided for thisArg the created _.matchesProperty style callback returns true for\n * elements that have a matching property value, else false.\n *\n * If an object is provided for iteratee the created _.matches style callback returns true for elements that\n * have the properties of the given object, else false.\n *\n * @param collection The collection to iterate over.\n * @param iteratee The function invoked per iteration.\n * @param thisArg The this binding of iteratee.\n * @return Returns the composed aggregate object.\n */\n keyBy(\n collection: List | null | undefined,\n iteratee?: ListIterator\n ): Dictionary;\n\n /**\n * @see _.keyBy\n */\n keyBy(\n collection: NumericDictionary | null | undefined,\n iteratee?: NumericDictionaryIterator\n ): Dictionary;\n\n /**\n * @see _.keyBy\n */\n keyBy(\n collection: Dictionary | null | undefined,\n iteratee?: DictionaryIterator\n ): Dictionary;\n\n /**\n * @see _.keyBy\n */\n keyBy(\n collection: List|NumericDictionary|Dictionary | null | undefined,\n iteratee?: string\n ): Dictionary;\n\n /**\n * @see _.keyBy\n */\n keyBy(\n collection: List|NumericDictionary|Dictionary | null | undefined,\n iteratee?: W\n ): Dictionary;\n\n /**\n * @see _.keyBy\n */\n keyBy(\n collection: List|NumericDictionary|Dictionary | null | undefined,\n iteratee?: Object\n ): Dictionary;\n }\n\n interface LoDashImplicitWrapper {\n /**\n * @see _.keyBy\n */\n keyBy(\n iteratee?: ListIterator\n ): LoDashImplicitObjectWrapper>;\n }\n\n interface LoDashImplicitArrayWrapperBase {\n /**\n * @see _.keyBy\n */\n keyBy(\n iteratee?: ListIterator\n ): LoDashImplicitObjectWrapper>;\n\n /**\n * @see _.keyBy\n */\n keyBy(\n iteratee?: string\n ): LoDashImplicitObjectWrapper>;\n\n /**\n * @see _.keyBy\n */\n keyBy(\n iteratee?: W\n ): LoDashImplicitObjectWrapper>;\n }\n\n interface LoDashImplicitObjectWrapperBase {\n /**\n * @see _.keyBy\n */\n keyBy(\n iteratee?: ListIterator|NumericDictionaryIterator|DictionaryIterator\n ): LoDashImplicitObjectWrapper>;\n\n /**\n * @see _.keyBy\n */\n keyBy(\n iteratee?: string\n ): LoDashImplicitObjectWrapper>;\n\n /**\n * @see _.keyBy\n */\n keyBy(\n iteratee?: W\n ): LoDashImplicitObjectWrapper>;\n\n /**\n * @see _.keyBy\n */\n keyBy(\n iteratee?: Object\n ): LoDashImplicitObjectWrapper>;\n }\n\n interface LoDashExplicitWrapper {\n /**\n * @see _.keyBy\n */\n keyBy(\n iteratee?: ListIterator\n ): LoDashExplicitObjectWrapper>;\n }\n\n interface LoDashExplicitArrayWrapperBase {\n /**\n * @see _.keyBy\n */\n keyBy(\n iteratee?: ListIterator\n ): LoDashExplicitObjectWrapper>;\n\n /**\n * @see _.keyBy\n */\n keyBy(\n iteratee?: string\n ): LoDashExplicitObjectWrapper>;\n\n /**\n * @see _.keyBy\n */\n keyBy(\n iteratee?: W\n ): LoDashExplicitObjectWrapper>;\n }\n\n interface LoDashExplicitObjectWrapperBase {\n /**\n * @see _.keyBy\n */\n keyBy(\n iteratee?: ListIterator|NumericDictionaryIterator|DictionaryIterator\n ): LoDashExplicitObjectWrapper>;\n\n /**\n * @see _.keyBy\n */\n keyBy(\n iteratee?: string\n ): LoDashExplicitObjectWrapper>;\n\n /**\n * @see _.keyBy\n */\n keyBy(\n iteratee?: W\n ): LoDashExplicitObjectWrapper>;\n\n /**\n * @see _.keyBy\n */\n keyBy(\n iteratee?: Object\n ): LoDashExplicitObjectWrapper>;\n }\n\n //_.invoke\n interface LoDashStatic {\n /**\n * Invokes the method at path of object.\n * @param object The object to query.\n * @param path The path of the method to invoke.\n * @param args The arguments to invoke the method with.\n **/\n invoke(\n object: TObject,\n path: Many,\n ...args: any[]): TResult;\n\n /**\n * @see _.invoke\n **/\n invoke(\n object: Dictionary|TValue[],\n path: Many,\n ...args: any[]): TResult;\n\n /**\n * @see _.invoke\n **/\n invoke(\n object: any,\n path: Many,\n ...args: any[]): TResult;\n }\n\n interface LoDashImplicitArrayWrapper {\n /**\n * @see _.invoke\n **/\n invoke(\n path: Many,\n ...args: any[]): TResult;\n }\n\n interface LoDashImplicitObjectWrapper {\n /**\n * @see _.invoke\n **/\n invoke(\n path: Many,\n ...args: any[]): TResult;\n }\n\n interface LoDashExplicitArrayWrapper {\n /**\n * @see _.invoke\n **/\n invoke(\n path: Many,\n ...args: any[]): TResult;\n }\n\n interface LoDashExplicitObjectWrapper {\n /**\n * @see _.invoke\n **/\n invoke(\n path: Many,\n ...args: any[]): TResult;\n }\n\n //_.invokeMap\n interface LoDashStatic {\n /**\n * Invokes the method named by methodName on each element in the collection returning\n * an array of the results of each invoked method. Additional arguments will be provided\n * to each invoked method. If methodName is a function it will be invoked for, and this\n * bound to, each element in the collection.\n * @param collection The collection to iterate over.\n * @param methodName The name of the method to invoke.\n * @param args Arguments to invoke the method with.\n **/\n invokeMap(\n collection: TValue[] | null | undefined,\n methodName: string,\n ...args: any[]): TResult[];\n\n /**\n * @see _.invokeMap\n **/\n invokeMap(\n collection: Dictionary | null | undefined,\n methodName: string,\n ...args: any[]): TResult[];\n\n /**\n * @see _.invokeMap\n **/\n invokeMap(\n collection: Array<{}> | null | undefined,\n methodName: string,\n ...args: any[]): TResult[];\n\n /**\n * @see _.invokeMap\n **/\n invokeMap(\n collection: Dictionary<{}> | null | undefined,\n methodName: string,\n ...args: any[]): TResult[];\n\n /**\n * @see _.invokeMap\n **/\n invokeMap(\n collection: TValue[] | null | undefined,\n method: (...args: any[]) => TResult,\n ...args: any[]): TResult[];\n\n /**\n * @see _.invokeMap\n **/\n invokeMap(\n collection: Dictionary | null | undefined,\n method: (...args: any[]) => TResult,\n ...args: any[]): TResult[];\n\n /**\n * @see _.invokeMap\n **/\n invokeMap(\n collection: Array<{}> | null | undefined,\n method: (...args: any[]) => TResult,\n ...args: any[]): TResult[];\n\n /**\n * @see _.invokeMap\n **/\n invokeMap(\n collection: Dictionary<{}> | null | undefined,\n method: (...args: any[]) => TResult,\n ...args: any[]): TResult[];\n }\n\n interface LoDashImplicitArrayWrapperBase {\n /**\n * @see _.invokeMap\n **/\n invokeMap(\n methodName: string,\n ...args: any[]): LoDashImplicitArrayWrapper;\n\n /**\n * @see _.invokeMap\n **/\n invokeMap(\n method: (...args: any[]) => TResult,\n ...args: any[]): LoDashImplicitArrayWrapper;\n }\n\n interface LoDashImplicitObjectWrapperBase {\n /**\n * @see _.invokeMap\n **/\n invokeMap(\n methodName: string,\n ...args: any[]): LoDashImplicitArrayWrapper;\n\n /**\n * @see _.invokeMap\n **/\n invokeMap(\n method: (...args: any[]) => TResult,\n ...args: any[]): LoDashImplicitArrayWrapper;\n }\n\n interface LoDashExplicitArrayWrapperBase {\n /**\n * @see _.invokeMap\n **/\n invokeMap(\n methodName: string,\n ...args: any[]): LoDashExplicitArrayWrapper;\n\n /**\n * @see _.invokeMap\n **/\n invokeMap(\n method: (...args: any[]) => TResult,\n ...args: any[]): LoDashExplicitArrayWrapper;\n }\n\n interface LoDashExplicitObjectWrapperBase {\n /**\n * @see _.invokeMap\n **/\n invokeMap(\n methodName: string,\n ...args: any[]): LoDashExplicitArrayWrapper;\n\n /**\n * @see _.invokeMap\n **/\n invokeMap(\n method: (...args: any[]) => TResult,\n ...args: any[]): LoDashExplicitArrayWrapper;\n }\n\n //_.map\n interface LoDashStatic {\n /**\n * Creates an array of values by running each element in collection through iteratee. The iteratee is bound to\n * thisArg and invoked with three arguments: (value, index|key, collection).\n *\n * If a property name is provided for iteratee the created _.property style callback returns the property value\n * of the given element.\n *\n * If a value is also provided for thisArg the created _.matchesProperty style callback returns true for\n * elements that have a matching property value, else false.\n *\n * If an object is provided for iteratee the created _.matches style callback returns true for elements that\n * have the properties of the given object, else false.\n *\n * Many lodash methods are guarded to work as iteratees for methods like _.every, _.filter, _.map, _.mapValues,\n * _.reject, and _.some.\n *\n * The guarded methods are:\n * ary, callback, chunk, clone, create, curry, curryRight, drop, dropRight, every, fill, flatten, invert, max,\n * min, parseInt, slice, sortBy, take, takeRight, template, trim, trimLeft, trimRight, trunc, random, range,\n * sample, some, sum, uniq, and words\n *\n * @param collection The collection to iterate over.\n * @param iteratee The function invoked per iteration.\n * @param thisArg The this binding of iteratee.\n * @return Returns the new mapped array.\n */\n map(\n collection: List | null | undefined,\n iteratee: ListIterator\n ): TResult[];\n\n /**\n * @see _.map\n */\n map(collection: List | null | undefined): T[];\n\n /**\n * @see _.map\n */\n map(\n collection: Dictionary | null | undefined,\n iteratee: DictionaryIterator\n ): TResult[];\n\n /** @see _.map */\n map(\n collection: Dictionary | null | undefined,\n iteratee: K\n ): T[K][];\n\n /** @see _.map */\n map(collection: Dictionary | null | undefined): T[];\n\n map(\n collection: NumericDictionary | null | undefined,\n iteratee?: NumericDictionaryIterator\n ): TResult[];\n\n /** @see _.map */\n map(collection: List | null | undefined, iteratee: K): T[K][];\n\n /**\n * @see _.map\n */\n map(\n collection: List|Dictionary|NumericDictionary | null | undefined,\n iteratee?: string\n ): TResult[];\n\n /**\n * @see _.map\n */\n map(\n collection: List|Dictionary|NumericDictionary | null | undefined,\n iteratee?: TObject\n ): boolean[];\n }\n\n interface LoDashImplicitArrayWrapperBase {\n /**\n * @see _.map\n */\n map(\n iteratee: ListIterator\n ): LoDashImplicitArrayWrapper;\n\n /**\n * @see _.map\n */\n map(): LoDashImplicitArrayWrapper;\n\n /** @see _.map */\n map(iteratee: K): LoDashImplicitArrayWrapper;\n\n /**\n * @see _.map\n */\n map(\n iteratee: string\n ): LoDashImplicitArrayWrapper;\n\n /**\n * @see _.map\n */\n map(\n iteratee: TObject\n ): LoDashImplicitArrayWrapper;\n }\n\n interface LoDashImplicitObjectWrapperBase {\n /**\n * @see _.map\n */\n map(\n iteratee: ListIterator|DictionaryIterator\n ): LoDashImplicitArrayWrapper;\n\n /** @see _.map */\n map(): LoDashImplicitArrayWrapper;\n\n /** @see _.map */\n map(iteratee: K): LoDashImplicitArrayWrapper;\n\n /**\n * @see _.map\n */\n map(\n iteratee: string\n ): LoDashImplicitArrayWrapper;\n\n /**\n * @see _.map\n */\n map(\n iteratee: TObject\n ): LoDashImplicitArrayWrapper;\n }\n\n interface LoDashExplicitArrayWrapperBase {\n /**\n * @see _.map\n */\n map(\n iteratee: ListIterator\n ): LoDashExplicitArrayWrapper;\n\n /** @see _.map */\n map(): LoDashExplicitArrayWrapper;\n\n /** @see _.map */\n map(iteratee: K): LoDashExplicitArrayWrapper;\n\n /**\n * @see _.map\n */\n map(\n iteratee: string\n ): LoDashExplicitArrayWrapper;\n\n /**\n * @see _.map\n */\n map(\n iteratee: TObject\n ): LoDashExplicitArrayWrapper;\n }\n\n interface LoDashExplicitObjectWrapperBase {\n /**\n * @see _.map\n */\n map(\n iteratee: ListIterator|DictionaryIterator\n ): LoDashExplicitArrayWrapper;\n\n /** @see _.map */\n map(): LoDashExplicitArrayWrapper;\n\n /**\n * @see _.map\n */\n map(\n iteratee: string\n ): LoDashExplicitArrayWrapper;\n\n /**\n * @see _.map\n */\n map(\n iteratee: TObject\n ): LoDashExplicitArrayWrapper;\n }\n\n //_.partition\n interface LoDashStatic {\n /**\n * Creates an array of elements split into two groups, the first of which contains elements predicate returns truthy for,\n * while the second of which contains elements predicate returns falsey for.\n * The predicate is bound to thisArg and invoked with three arguments: (value, index|key, collection).\n *\n * If a property name is provided for predicate the created _.property style callback\n * returns the property value of the given element.\n *\n * If a value is also provided for thisArg the created _.matchesProperty style callback\n * returns true for elements that have a matching property value, else false.\n *\n * If an object is provided for predicate the created _.matches style callback returns\n * true for elements that have the properties of the given object, else false.\n *\n * @param collection The collection to iterate over.\n * @param callback The function called per iteration.\n * @param thisArg The this binding of predicate.\n * @return Returns the array of grouped elements.\n **/\n partition(\n collection: List | null | undefined,\n callback: ListIterator): T[][];\n\n /**\n * @see _.partition\n **/\n partition(\n collection: Dictionary | null | undefined,\n callback: DictionaryIterator): T[][];\n\n /**\n * @see _.partition\n **/\n partition(\n collection: List | null | undefined,\n whereValue: W): T[][];\n\n /**\n * @see _.partition\n **/\n partition(\n collection: Dictionary | null | undefined,\n whereValue: W): T[][];\n\n /**\n * @see _.partition\n **/\n partition(\n collection: List | null | undefined,\n path: string,\n srcValue: any): T[][];\n\n /**\n * @see _.partition\n **/\n partition(\n collection: Dictionary | null | undefined,\n path: string,\n srcValue: any): T[][];\n\n /**\n * @see _.partition\n **/\n partition(\n collection: List | null | undefined,\n pluckValue: string): T[][];\n\n /**\n * @see _.partition\n **/\n partition(\n collection: Dictionary | null | undefined,\n pluckValue: string): T[][];\n }\n\n interface LoDashImplicitStringWrapper {\n /**\n * @see _.partition\n */\n partition(\n callback: ListIterator): LoDashImplicitArrayWrapper;\n }\n\n interface LoDashImplicitArrayWrapperBase {\n /**\n * @see _.partition\n */\n partition(\n callback: ListIterator): LoDashImplicitArrayWrapper;\n /**\n * @see _.partition\n */\n partition(\n whereValue: W): LoDashImplicitArrayWrapper;\n /**\n * @see _.partition\n */\n partition(\n path: string,\n srcValue: any): LoDashImplicitArrayWrapper;\n /**\n * @see _.partition\n */\n partition(\n pluckValue: string): LoDashImplicitArrayWrapper;\n }\n\n interface LoDashImplicitObjectWrapperBase {\n /**\n * @see _.partition\n */\n partition(\n callback: ListIterator): LoDashImplicitArrayWrapper;\n\n /**\n * @see _.partition\n */\n partition(\n callback: DictionaryIterator): LoDashImplicitArrayWrapper;\n\n /**\n * @see _.partition\n */\n partition(\n whereValue: W): LoDashImplicitArrayWrapper;\n\n /**\n * @see _.partition\n */\n partition(\n path: string,\n srcValue: any): LoDashImplicitArrayWrapper;\n\n /**\n * @see _.partition\n */\n partition(\n pluckValue: string): LoDashImplicitArrayWrapper;\n }\n\n //_.reduce\n interface LoDashStatic {\n /**\n * Reduces a collection to a value which is the accumulated result of running each\n * element in the collection through the callback, where each successive callback execution\n * consumes the return value of the previous execution. If accumulator is not provided the\n * first element of the collection will be used as the initial accumulator value. The callback\n * is bound to thisArg and invoked with four arguments; (accumulator, value, index|key, collection).\n * @param collection The collection to iterate over.\n * @param callback The function called per iteration.\n * @param accumulator Initial value of the accumulator.\n * @param thisArg The this binding of callback.\n * @return Returns the accumulated value.\n **/\n reduce(\n collection: T[] | null | undefined,\n callback: MemoIterator,\n accumulator: TResult): TResult;\n\n /**\n * @see _.reduce\n **/\n reduce(\n collection: List | null | undefined,\n callback: MemoIterator,\n accumulator: TResult): TResult;\n\n /**\n * @see _.reduce\n **/\n reduce(\n collection: Dictionary | null | undefined,\n callback: MemoIterator,\n accumulator: TResult): TResult;\n\n /**\n * @see _.reduce\n **/\n reduce(\n collection: NumericDictionary | null | undefined,\n callback: MemoIterator,\n accumulator: TResult): TResult;\n\n /**\n * @see _.reduce\n **/\n reduce(\n collection: T[] | null | undefined,\n callback: MemoIterator): TResult | undefined;\n\n /**\n * @see _.reduce\n **/\n reduce(\n collection: List | null | undefined,\n callback: MemoIterator): TResult | undefined;\n\n /**\n * @see _.reduce\n **/\n reduce(\n collection: Dictionary | null | undefined,\n callback: MemoIterator): TResult | undefined;\n\n /**\n * @see _.reduce\n **/\n reduce(\n collection: NumericDictionary | null | undefined,\n callback: MemoIterator): TResult | undefined;\n }\n\n interface LoDashImplicitArrayWrapperBase {\n /**\n * @see _.reduce\n **/\n reduce(\n callback: MemoIterator,\n accumulator: TResult): TResult;\n\n /**\n * @see _.reduce\n **/\n reduce(\n callback: MemoIterator): TResult | undefined;\n }\n\n interface LoDashImplicitObjectWrapperBase {\n /**\n * @see _.reduce\n **/\n reduce(\n callback: MemoIterator,\n accumulator: TResult): TResult;\n\n /**\n * @see _.reduce\n **/\n reduce(\n callback: MemoIterator): TResult | undefined;\n }\n\n interface LoDashExplicitObjectWrapperBase {\n /**\n * @see _.reduce\n **/\n reduce(\n callback: MemoIterator,\n accumulator: TResult): LoDashExplicitObjectWrapper;\n\n /**\n * @see _.reduce\n **/\n reduce(\n callback: MemoIterator): LoDashExplicitObjectWrapper;\n }\n\n interface LoDashExplicitArrayWrapperBase {\n /**LoDashExplicitWrapper\n * @see _.reduce\n */\n reduce(\n callback: MemoIterator,\n accumulator: TResult): LoDashExplicitWrapper;\n\n /**\n * @see _.reduce\n */\n reduce(\n callback: MemoIterator): LoDashExplicitWrapper;\n }\n\n //_.reduceRight\n interface LoDashStatic {\n /**\n * This method is like _.reduce except that it iterates over elements of a collection from\n * right to left.\n * @param collection The collection to iterate over.\n * @param callback The function called per iteration.\n * @param accumulator Initial value of the accumulator.\n * @param thisArg The this binding of callback.\n * @return The accumulated value.\n **/\n reduceRight(\n collection: T[] | null | undefined,\n callback: MemoIterator,\n accumulator: TResult): TResult;\n\n /**\n * @see _.reduceRight\n **/\n reduceRight(\n collection: List | null | undefined,\n callback: MemoIterator,\n accumulator: TResult): TResult;\n\n /**\n * @see _.reduceRight\n **/\n reduceRight(\n collection: Dictionary | null | undefined,\n callback: MemoIterator,\n accumulator: TResult): TResult;\n\n /**\n * @see _.reduceRight\n **/\n reduceRight(\n collection: T[] | null | undefined,\n callback: MemoIterator): TResult | undefined;\n\n /**\n * @see _.reduceRight\n **/\n reduceRight(\n collection: List | null | undefined,\n callback: MemoIterator): TResult | undefined;\n\n /**\n * @see _.reduceRight\n **/\n reduceRight(\n collection: Dictionary | null | undefined,\n callback: MemoIterator): TResult | undefined;\n }\n\n //_.reject\n interface LoDashStatic {\n /**\n * The opposite of _.filter; this method returns the elements of collection that predicate does not return\n * truthy for.\n *\n * @param collection The collection to iterate over.\n * @param predicate The function invoked per iteration.\n * @param thisArg The this binding of predicate.\n * @return Returns the new filtered array.\n */\n reject(\n collection: List | null | undefined,\n predicate?: ListIterator\n ): T[];\n\n /**\n * @see _.reject\n */\n reject(\n collection: Dictionary | null | undefined,\n predicate?: DictionaryIterator\n ): T[];\n\n /**\n * @see _.reject\n */\n reject(\n collection: string | null | undefined,\n predicate?: StringIterator\n ): string[];\n\n /**\n * @see _.reject\n */\n reject(\n collection: List|Dictionary | null | undefined,\n predicate: string\n ): T[];\n\n /**\n * @see _.reject\n */\n reject(\n collection: List|Dictionary | null | undefined,\n predicate: W\n ): T[];\n }\n\n interface LoDashImplicitWrapper {\n /**\n * @see _.reject\n */\n reject(\n predicate?: StringIterator\n ): LoDashImplicitArrayWrapper;\n }\n\n interface LoDashImplicitArrayWrapperBase {\n /**\n * @see _.reject\n */\n reject(\n predicate: ListIterator\n ): LoDashImplicitArrayWrapper;\n\n /**\n * @see _.reject\n */\n reject(\n predicate: string\n ): LoDashImplicitArrayWrapper;\n\n /**\n * @see _.reject\n */\n reject(predicate: W): LoDashImplicitArrayWrapper;\n }\n\n interface LoDashImplicitObjectWrapperBase {\n /**\n * @see _.reject\n */\n reject(\n predicate: ListIterator|DictionaryIterator\n ): LoDashImplicitArrayWrapper;\n\n /**\n * @see _.reject\n */\n reject(\n predicate: string\n ): LoDashImplicitArrayWrapper;\n\n /**\n * @see _.reject\n */\n reject(predicate: W): LoDashImplicitArrayWrapper;\n }\n\n interface LoDashExplicitWrapper {\n /**\n * @see _.reject\n */\n reject(\n predicate?: StringIterator\n ): LoDashExplicitArrayWrapper;\n }\n\n interface LoDashExplicitArrayWrapperBase {\n /**\n * @see _.reject\n */\n reject(\n predicate: ListIterator\n ): LoDashExplicitArrayWrapper;\n\n /**\n * @see _.reject\n */\n reject(\n predicate: string\n ): LoDashExplicitArrayWrapper;\n\n /**\n * @see _.reject\n */\n reject(predicate: W): LoDashExplicitArrayWrapper;\n }\n\n interface LoDashExplicitObjectWrapperBase {\n /**\n * @see _.reject\n */\n reject(\n predicate: ListIterator|DictionaryIterator\n ): LoDashExplicitArrayWrapper;\n\n /**\n * @see _.reject\n */\n reject(\n predicate: string\n ): LoDashExplicitArrayWrapper;\n\n /**\n * @see _.reject\n */\n reject(predicate: W): LoDashExplicitArrayWrapper;\n }\n\n //_.sample\n interface LoDashStatic {\n /**\n * Gets a random element from collection.\n *\n * @param collection The collection to sample.\n * @return Returns the random element.\n */\n sample(\n collection: List | Dictionary | NumericDictionary | object | null | undefined\n ): T | undefined;\n }\n\n interface LoDashImplicitWrapper {\n /**\n * @see _.sample\n */\n sample(): string | undefined;\n }\n\n interface LoDashImplicitArrayWrapperBase {\n /**\n * @see _.sample\n */\n sample(): T | undefined;\n }\n\n interface LoDashImplicitObjectWrapperBase {\n /**\n * @see _.sample\n */\n sample(): T | undefined;\n }\n\n interface LoDashExplicitWrapper {\n /**\n * @see _.sample\n */\n sample(): LoDashExplicitWrapper;\n }\n\n interface LoDashExplicitArrayWrapperBase {\n /**\n * @see _.sample\n */\n sample(): TWrapper;\n }\n\n interface LoDashExplicitObjectWrapperBase {\n /**\n * @see _.sample\n */\n sample(): TWrapper;\n }\n\n //_.sampleSize\n interface LoDashStatic {\n /**\n * Gets n random elements at unique keys from collection up to the size of collection.\n *\n * @param collection The collection to sample.\n * @param n The number of elements to sample.\n * @return Returns the random elements.\n */\n sampleSize(\n collection: List|Dictionary|NumericDictionary | null | undefined,\n n?: number\n ): T[];\n\n /**\n * @see _.sampleSize\n */\n sampleSize(\n collection: O | null | undefined,\n n?: number\n ): T[];\n\n /**\n * @see _.sampleSize\n */\n sampleSize(\n collection: Object | null | undefined,\n n?: number\n ): T[];\n }\n\n interface LoDashImplicitWrapper {\n /**\n * @see _.sampleSize\n */\n sampleSize(\n n?: number\n ): LoDashImplicitArrayWrapper;\n }\n\n interface LoDashImplicitArrayWrapperBase {\n /**\n * @see _.sampleSize\n */\n sampleSize(\n n?: number\n ): LoDashImplicitArrayWrapper;\n }\n\n interface LoDashImplicitObjectWrapperBase {\n /**\n * @see _.sampleSize\n */\n sampleSize(\n n?: number\n ): LoDashImplicitArrayWrapper;\n }\n\n interface LoDashExplicitWrapper {\n /**\n * @see _.sampleSize\n */\n sampleSize(\n n?: number\n ): LoDashExplicitArrayWrapper;\n }\n\n interface LoDashExplicitArrayWrapperBase {\n /**\n * @see _.sampleSize\n */\n sampleSize(\n n?: number\n ): LoDashExplicitArrayWrapper;\n }\n\n interface LoDashExplicitObjectWrapperBase {\n /**\n * @see _.sampleSize\n */\n sampleSize(\n n?: number\n ): LoDashExplicitArrayWrapper;\n }\n\n //_.shuffle\n interface LoDashStatic {\n /**\n * Creates an array of shuffled values, using a version of the Fisher-Yates shuffle.\n *\n * @param collection The collection to shuffle.\n * @return Returns the new shuffled array.\n */\n shuffle(collection: List|Dictionary | null | undefined): T[];\n\n /**\n * @see _.shuffle\n */\n shuffle(collection: string | null | undefined): string[];\n }\n\n interface LoDashImplicitWrapper {\n /**\n * @see _.shuffle\n */\n shuffle(): LoDashImplicitArrayWrapper;\n }\n\n interface LoDashImplicitArrayWrapperBase {\n /**\n * @see _.shuffle\n */\n shuffle(): LoDashImplicitArrayWrapper;\n }\n\n interface LoDashImplicitObjectWrapperBase {\n /**\n * @see _.shuffle\n */\n shuffle(): LoDashImplicitArrayWrapper;\n }\n\n interface LoDashExplicitWrapper {\n /**\n * @see _.shuffle\n */\n shuffle(): LoDashExplicitArrayWrapper;\n }\n\n interface LoDashExplicitArrayWrapperBase {\n /**\n * @see _.shuffle\n */\n shuffle(): LoDashExplicitArrayWrapper;\n }\n\n interface LoDashExplicitObjectWrapperBase {\n /**\n * @see _.shuffle\n */\n shuffle(): LoDashExplicitArrayWrapper;\n }\n\n //_.size\n interface LoDashStatic {\n /**\n * Gets the size of collection by returning its length for array-like values or the number of own enumerable\n * properties for objects.\n *\n * @param collection The collection to inspect.\n * @return Returns the size of collection.\n */\n size(collection: List|Dictionary | null | undefined): number;\n\n /**\n * @see _.size\n */\n size(collection: string | null | undefined): number;\n }\n\n interface LoDashImplicitWrapper {\n /**\n * @see _.size\n */\n size(): number;\n }\n\n interface LoDashImplicitArrayWrapperBase {\n /**\n * @see _.size\n */\n size(): number;\n }\n\n interface LoDashImplicitObjectWrapperBase {\n /**\n * @see _.size\n */\n size(): number;\n }\n\n interface LoDashExplicitWrapper {\n /**\n * @see _.size\n */\n size(): LoDashExplicitWrapper;\n }\n\n interface LoDashExplicitArrayWrapperBase {\n /**\n * @see _.size\n */\n size(): LoDashExplicitWrapper;\n }\n\n interface LoDashExplicitObjectWrapperBase {\n /**\n * @see _.size\n */\n size(): LoDashExplicitWrapper;\n }\n\n //_.some\n interface LoDashStatic {\n /**\n * Checks if predicate returns truthy for any element of collection. Iteration is stopped once predicate\n * returns truthy. The predicate is invoked with three arguments: (value, index|key, collection).\n *\n * @param collection The collection to iterate over.\n * @param predicate The function invoked per iteration.\n * @return Returns true if any element passes the predicate check, else false.\n */\n some(\n collection: List | null | undefined,\n predicate?: ListIterator\n ): boolean;\n\n /**\n * @see _.some\n */\n some(\n collection: Dictionary | null | undefined,\n predicate?: DictionaryIterator\n ): boolean;\n\n /**\n * @see _.some\n */\n some(\n collection: NumericDictionary | null | undefined,\n predicate?: NumericDictionaryIterator\n ): boolean;\n\n /**\n * @see _.some\n */\n some(\n collection: Object | null | undefined,\n predicate?: ObjectIterator\n ): boolean;\n\n /**\n * @see _.some\n */\n some(\n collection: List|Dictionary|NumericDictionary | null | undefined,\n predicate?: string|[string, any]\n ): boolean;\n\n /**\n * @see _.some\n */\n some(\n collection: Object | null | undefined,\n predicate?: string|[string, any]\n ): boolean;\n\n /**\n * @see _.some\n */\n some(\n collection: List|Dictionary|NumericDictionary | null | undefined,\n predicate?: PartialObject\n ): boolean;\n\n /**\n * @see _.some\n */\n some(\n collection: List|Dictionary|NumericDictionary | null | undefined,\n predicate?: PartialObject\n ): boolean;\n\n /**\n * @see _.some\n */\n some(\n collection: Object | null | undefined,\n predicate?: PartialObject\n ): boolean;\n }\n\n interface LoDashImplicitArrayWrapperBase {\n /**\n * @see _.some\n */\n some(\n predicate?: ListIterator|NumericDictionaryIterator\n ): boolean;\n\n /**\n * @see _.some\n */\n some(\n predicate?: string|[string, any]\n ): boolean;\n\n /**\n * @see _.some\n */\n some(\n predicate?: PartialObject\n ): boolean;\n }\n\n interface LoDashImplicitObjectWrapperBase {\n /**\n * @see _.some\n */\n some(\n predicate?: ListIterator|DictionaryIterator|NumericDictionaryIterator|ObjectIterator\n ): boolean;\n /**\n * @see _.some\n */\n some(\n predicate?: string|[string, any]\n ): boolean;\n\n /**\n * @see _.some\n */\n some(\n predicate?: PartialObject\n ): boolean;\n }\n\n interface LoDashExplicitArrayWrapperBase {\n /**\n * @see _.some\n */\n some(\n predicate?: ListIterator|NumericDictionaryIterator\n ): LoDashExplicitWrapper;\n\n /**\n * @see _.some\n */\n some(\n predicate?: string|[string, any]\n ): LoDashExplicitWrapper;\n\n /**\n * @see _.some\n */\n some(\n predicate?: PartialObject\n ): LoDashExplicitWrapper;\n }\n\n interface LoDashExplicitObjectWrapperBase {\n /**\n * @see _.some\n */\n some(\n predicate?: ListIterator|DictionaryIterator|NumericDictionaryIterator|ObjectIterator\n ): LoDashExplicitWrapper;\n\n /**\n * @see _.some\n */\n some(\n predicate?: string|[string, any]\n ): LoDashExplicitWrapper;\n\n /**\n * @see _.some\n */\n some(\n predicate?: PartialObject\n ): LoDashExplicitWrapper;\n }\n\n //_.sortBy\n interface LoDashStatic {\n /**\n * Creates an array of elements, sorted in ascending order by the results of\n * running each element in a collection through each iteratee. This method\n * performs a stable sort, that is, it preserves the original sort order of\n * equal elements. The iteratees are invoked with one argument: (value).\n *\n * @static\n * @memberOf _\n * @category Collection\n * @param {Array|Object} collection The collection to iterate over.\n * @param {...(Function|Function[]|Object|Object[]|string|string[])} [iteratees=[_.identity]]\n * The iteratees to sort by, specified individually or in arrays.\n * @returns {Array} Returns the new sorted array.\n * @example\n *\n * var users = [\n * { 'user': 'fred', 'age': 48 },\n * { 'user': 'barney', 'age': 36 },\n * { 'user': 'fred', 'age': 42 },\n * { 'user': 'barney', 'age': 34 }\n * ];\n *\n * _.sortBy(users, function(o) { return o.user; });\n * // => objects for [['barney', 36], ['barney', 34], ['fred', 48], ['fred', 42]]\n *\n * _.sortBy(users, ['user', 'age']);\n * // => objects for [['barney', 34], ['barney', 36], ['fred', 42], ['fred', 48]]\n *\n * _.sortBy(users, 'user', function(o) {\n * return Math.floor(o.age / 10);\n * });\n * // => objects for [['barney', 36], ['barney', 34], ['fred', 48], ['fred', 42]]\n */\n sortBy(\n collection: List | null | undefined,\n iteratee?: ListIterator\n ): T[];\n\n /**\n * @see _.sortBy\n */\n sortBy(\n collection: Dictionary | null | undefined,\n iteratee?: DictionaryIterator\n ): T[];\n\n /**\n * @see _.sortBy\n */\n sortBy(\n collection: List|Dictionary | null | undefined,\n iteratee: string\n ): T[];\n\n /**\n * @see _.sortBy\n */\n sortBy(\n collection: List|Dictionary | null | undefined,\n whereValue: W\n ): T[];\n\n /**\n * @see _.sortBy\n */\n sortBy(\n collection: List|Dictionary | null | undefined\n ): T[];\n\n /**\n * @see _.sortBy\n */\n sortBy(\n collection: List | null | undefined,\n iteratees: Array|string|Object>\n ): T[];\n\n /**\n * @see _.sortBy\n */\n sortBy(\n collection: List | null | undefined,\n ...iteratees: Array|Object|string>\n ): T[];\n }\n\n interface LoDashImplicitArrayWrapperBase {\n /**\n * @see _.sortBy\n */\n sortBy(\n iteratee?: ListIterator\n ): LoDashImplicitArrayWrapper;\n\n /**\n * @see _.sortBy\n */\n sortBy(iteratee: string): LoDashImplicitArrayWrapper;\n\n /**\n * @see _.sortBy\n */\n sortBy(whereValue: W): LoDashImplicitArrayWrapper;\n\n /**\n * @see _.sortBy\n */\n sortBy(): LoDashImplicitArrayWrapper;\n\n /**\n * @see _.sortBy\n */\n sortBy(...iteratees: Array|Object|string>): LoDashImplicitArrayWrapper;\n\n /**\n * @see _.sortBy\n **/\n sortBy(iteratees: Array|string|Object>): LoDashImplicitArrayWrapper;\n }\n\n interface LoDashImplicitObjectWrapperBase {\n /**\n * @see _.sortBy\n */\n sortBy(\n iteratee?: ListIterator|DictionaryIterator\n ): LoDashImplicitArrayWrapper;\n\n /**\n * @see _.sortBy\n */\n sortBy(iteratee: string): LoDashImplicitArrayWrapper;\n\n /**\n * @see _.sortBy\n */\n sortBy(whereValue: W): LoDashImplicitArrayWrapper;\n\n /**\n * @see _.sortBy\n */\n sortBy(): LoDashImplicitArrayWrapper;\n }\n\n interface LoDashExplicitArrayWrapperBase {\n /**\n * @see _.sortBy\n */\n sortBy(\n iteratee?: ListIterator\n ): LoDashExplicitArrayWrapper;\n\n /**\n * @see _.sortBy\n */\n sortBy(iteratee: string): LoDashExplicitArrayWrapper;\n\n /**\n * @see _.sortBy\n */\n sortBy(whereValue: W): LoDashExplicitArrayWrapper;\n\n /**\n * @see _.sortBy\n */\n sortBy(): LoDashExplicitArrayWrapper;\n }\n\n interface LoDashExplicitObjectWrapperBase {\n /**\n * @see _.sortBy\n */\n sortBy(\n iteratee?: ListIterator|DictionaryIterator\n ): LoDashExplicitArrayWrapper;\n\n /**\n * @see _.sortBy\n */\n sortBy(iteratee: string): LoDashExplicitArrayWrapper;\n\n /**\n * @see _.sortBy\n */\n sortBy(whereValue: W): LoDashExplicitArrayWrapper;\n\n /**\n * @see _.sortBy\n */\n sortBy(): LoDashExplicitArrayWrapper;\n }\n\n //_.orderBy\n interface LoDashStatic {\n /**\n * This method is like `_.sortBy` except that it allows specifying the sort\n * orders of the iteratees to sort by. If `orders` is unspecified, all values\n * are sorted in ascending order. Otherwise, specify an order of \"desc\" for\n * descending or \"asc\" for ascending sort order of corresponding values.\n *\n * @static\n * @memberOf _\n * @category Collection\n * @param {Array|Object} collection The collection to iterate over.\n * @param {Function[]|Object[]|string[]} [iteratees=[_.identity]] The iteratees to sort by.\n * @param {string[]} [orders] The sort orders of `iteratees`.\n * @param- {Object} [guard] Enables use as an iteratee for functions like `_.reduce`.\n * @returns {Array} Returns the new sorted array.\n * @example\n *\n * var users = [\n * { 'user': 'fred', 'age': 48 },\n * { 'user': 'barney', 'age': 34 },\n * { 'user': 'fred', 'age': 42 },\n * { 'user': 'barney', 'age': 36 }\n * ];\n *\n * // sort by `user` in ascending order and by `age` in descending order\n * _.orderBy(users, ['user', 'age'], ['asc', 'desc']);\n * // => objects for [['barney', 36], ['barney', 34], ['fred', 48], ['fred', 42]]\n */\n orderBy(\n collection: List | null | undefined,\n iteratees?: Many|string|W>,\n orders?: Many\n ): T[];\n\n /**\n * @see _.orderBy\n */\n orderBy(\n collection: List | null | undefined,\n iteratees?: Many|string|Object>,\n orders?: Many\n ): T[];\n\n /**\n * @see _.orderBy\n */\n orderBy(\n collection: NumericDictionary | null | undefined,\n iteratees?: Many|string|W>,\n orders?: Many\n ): T[];\n\n /**\n * @see _.orderBy\n */\n orderBy(\n collection: NumericDictionary | null | undefined,\n iteratees?: Many|string|Object>,\n orders?: Many\n ): T[];\n\n /**\n * @see _.orderBy\n */\n orderBy(\n collection: Dictionary | null | undefined,\n iteratees?: Many|string|W>,\n orders?: Many\n ): T[];\n\n /**\n * @see _.orderBy\n */\n orderBy(\n collection: Dictionary | null | undefined,\n iteratees?: Many|string|Object>,\n orders?: Many\n ): T[];\n }\n\n interface LoDashImplicitWrapper {\n /**\n * @see _.orderBy\n */\n orderBy(\n iteratees?: Many|string>,\n orders?: Many\n ): LoDashImplicitArrayWrapper;\n }\n\n interface LoDashImplicitArrayWrapperBase {\n /**\n * @see _.orderBy\n */\n orderBy(\n iteratees?: Many|string|W>,\n orders?: Many\n ): LoDashImplicitArrayWrapper;\n }\n\n interface LoDashImplicitObjectWrapperBase {\n /**\n * @see _.orderBy\n */\n orderBy(\n iteratees?: Many|string|W>,\n orders?: Many\n ): LoDashImplicitArrayWrapper;\n\n /**\n * @see _.orderBy\n */\n orderBy(\n iteratees?: Many|string|Object>,\n orders?: Many\n ): LoDashImplicitArrayWrapper;\n\n /**\n * @see _.orderBy\n */\n orderBy(\n iteratees?: Many|string|W>,\n orders?: Many\n ): LoDashImplicitArrayWrapper;\n\n /**\n * @see _.orderBy\n */\n orderBy(\n iteratees?: Many|string|Object>,\n orders?: Many\n ): LoDashImplicitArrayWrapper;\n\n /**\n * @see _.orderBy\n */\n orderBy(\n iteratees?: Many|string|W>,\n orders?: Many\n ): LoDashImplicitArrayWrapper;\n\n /**\n * @see _.orderBy\n */\n orderBy(\n iteratees?: Many|string|Object>,\n orders?: Many\n ): LoDashImplicitArrayWrapper;\n }\n\n interface LoDashExplicitWrapper {\n /**\n * @see _.orderBy\n */\n orderBy(\n iteratees?: Many|string>,\n orders?: Many\n ): LoDashExplicitArrayWrapper;\n }\n\n interface LoDashExplicitArrayWrapperBase {\n /**\n * @see _.orderBy\n */\n orderBy(\n iteratees?: Many|string|W|(ListIterator|string|W)>,\n orders?: Many\n ): LoDashExplicitArrayWrapper;\n }\n\n interface LoDashExplicitObjectWrapperBase {\n /**\n * @see _.orderBy\n */\n orderBy(\n iteratees?: Many|string|W>,\n orders?: Many\n ): LoDashExplicitArrayWrapper;\n\n /**\n * @see _.orderBy\n */\n orderBy(\n iteratees?: Many|string|Object>,\n orders?: Many\n ): LoDashExplicitArrayWrapper;\n\n /**\n * @see _.orderBy\n */\n orderBy(\n iteratees?: Many|string|W>,\n orders?: Many\n ): LoDashExplicitArrayWrapper;\n\n /**\n * @see _.orderBy\n */\n orderBy(\n iteratees?: Many|string|Object>,\n orders?: Many\n ): LoDashExplicitArrayWrapper;\n\n /**\n * @see _.orderBy\n */\n orderBy(\n iteratees?: Many|string|W>,\n orders?: Many\n ): LoDashExplicitArrayWrapper;\n\n /**\n * @see _.orderBy\n */\n orderBy(\n iteratees?: Many|string|Object>,\n orders?: Many\n ): LoDashExplicitArrayWrapper;\n }\n\n /********\n * Date *\n ********/\n\n //_.now\n interface LoDashStatic {\n /**\n * Gets the number of milliseconds that have elapsed since the Unix epoch (1 January 1970 00:00:00 UTC).\n *\n * @return The number of milliseconds.\n */\n now(): number;\n }\n\n interface LoDashImplicitWrapperBase {\n /**\n * @see _.now\n */\n now(): number;\n }\n\n interface LoDashExplicitWrapperBase {\n /**\n * @see _.now\n */\n now(): LoDashExplicitWrapper;\n }\n\n /*************\n * Functions *\n *************/\n\n //_.after\n interface LoDashStatic {\n /**\n * The opposite of _.before; this method creates a function that invokes func once it’s called n or more times.\n *\n * @param n The number of calls before func is invoked.\n * @param func The function to restrict.\n * @return Returns the new restricted function.\n */\n after(\n n: number,\n func: TFunc\n ): TFunc;\n }\n\n interface LoDashImplicitWrapper {\n /**\n * @see _.after\n **/\n after(func: TFunc): LoDashImplicitObjectWrapper;\n }\n\n interface LoDashExplicitWrapper {\n /**\n * @see _.after\n **/\n after(func: TFunc): LoDashExplicitObjectWrapper;\n }\n\n //_.ary\n interface LoDashStatic {\n /**\n * Creates a function that accepts up to n arguments ignoring any additional arguments.\n *\n * @param func The function to cap arguments for.\n * @param n The arity cap.\n * @returns Returns the new function.\n */\n ary(\n func: Function,\n n?: number\n ): TResult;\n\n ary(\n func: T,\n n?: number\n ): TResult;\n }\n\n interface LoDashImplicitObjectWrapper {\n /**\n * @see _.ary\n */\n ary(n?: number): LoDashImplicitObjectWrapper;\n }\n\n interface LoDashExplicitObjectWrapper {\n /**\n * @see _.ary\n */\n ary(n?: number): LoDashExplicitObjectWrapper;\n }\n\n //_.before\n interface LoDashStatic {\n /**\n * Creates a function that invokes func, with the this binding and arguments of the created function, while\n * it’s called less than n times. Subsequent calls to the created function return the result of the last func\n * invocation.\n *\n * @param n The number of calls at which func is no longer invoked.\n * @param func The function to restrict.\n * @return Returns the new restricted function.\n */\n before(\n n: number,\n func: TFunc\n ): TFunc;\n }\n\n interface LoDashImplicitWrapper {\n /**\n * @see _.before\n **/\n before(func: TFunc): LoDashImplicitObjectWrapper;\n }\n\n interface LoDashExplicitWrapper {\n /**\n * @see _.before\n **/\n before(func: TFunc): LoDashExplicitObjectWrapper;\n }\n\n //_.bind\n interface FunctionBind {\n placeholder: any;\n\n (\n func: T,\n thisArg: any,\n ...partials: any[]\n ): TResult;\n\n (\n func: Function,\n thisArg: any,\n ...partials: any[]\n ): TResult;\n }\n\n interface LoDashStatic {\n /**\n * Creates a function that invokes func with the this binding of thisArg and prepends any additional _.bind\n * arguments to those provided to the bound function.\n *\n * The _.bind.placeholder value, which defaults to _ in monolithic builds, may be used as a placeholder for\n * partially applied arguments.\n *\n * Note: Unlike native Function#bind this method does not set the \"length\" property of bound functions.\n *\n * @param func The function to bind.\n * @param thisArg The this binding of func.\n * @param partials The arguments to be partially applied.\n * @return Returns the new bound function.\n */\n bind: FunctionBind;\n }\n\n interface LoDashImplicitObjectWrapper {\n /**\n * @see _.bind\n */\n bind(\n thisArg: any,\n ...partials: any[]\n ): LoDashImplicitObjectWrapper;\n }\n\n interface LoDashExplicitObjectWrapper {\n /**\n * @see _.bind\n */\n bind(\n thisArg: any,\n ...partials: any[]\n ): LoDashExplicitObjectWrapper;\n }\n\n //_.bindAll\n interface LoDashStatic {\n /**\n * Binds methods of an object to the object itself, overwriting the existing method. Method names may be\n * specified as individual arguments or as arrays of method names. If no method names are provided all\n * enumerable function properties, own and inherited, of object are bound.\n *\n * Note: This method does not set the \"length\" property of bound functions.\n *\n * @param object The object to bind and assign the bound methods to.\n * @param methodNames The object method names to bind, specified as individual method names or arrays of\n * method names.\n * @return Returns object.\n */\n bindAll(\n object: T,\n ...methodNames: Array>\n ): T;\n }\n\n interface LoDashImplicitObjectWrapper {\n /**\n * @see _.bindAll\n */\n bindAll(...methodNames: Array>): LoDashImplicitObjectWrapper;\n }\n\n interface LoDashExplicitObjectWrapper {\n /**\n * @see _.bindAll\n */\n bindAll(...methodNames: Array>): LoDashExplicitObjectWrapper;\n }\n\n //_.bindKey\n interface FunctionBindKey {\n placeholder: any;\n\n (\n object: T,\n key: any,\n ...partials: any[]\n ): TResult;\n\n (\n object: Object,\n key: any,\n ...partials: any[]\n ): TResult;\n }\n\n interface LoDashStatic {\n /**\n * Creates a function that invokes the method at object[key] and prepends any additional _.bindKey arguments\n * to those provided to the bound function.\n *\n * This method differs from _.bind by allowing bound functions to reference methods that may be redefined\n * or don’t yet exist. See Peter Michaux’s article for more details.\n *\n * The _.bindKey.placeholder value, which defaults to _ in monolithic builds, may be used as a placeholder\n * for partially applied arguments.\n *\n * @param object The object the method belongs to.\n * @param key The key of the method.\n * @param partials The arguments to be partially applied.\n * @return Returns the new bound function.\n */\n bindKey: FunctionBindKey;\n }\n\n interface LoDashImplicitObjectWrapper {\n /**\n * @see _.bindKey\n */\n bindKey(\n key: any,\n ...partials: any[]\n ): LoDashImplicitObjectWrapper;\n }\n\n interface LoDashExplicitObjectWrapper {\n /**\n * @see _.bindKey\n */\n bindKey(\n key: any,\n ...partials: any[]\n ): LoDashExplicitObjectWrapper;\n }\n\n //_.createCallback\n interface LoDashStatic {\n /**\n * Produces a callback bound to an optional thisArg. If func is a property name the created\n * callback will return the property value for a given element. If func is an object the created\n * callback will return true for elements that contain the equivalent object properties,\n * otherwise it will return false.\n * @param func The value to convert to a callback.\n * @param thisArg The this binding of the created callback.\n * @param argCount The number of arguments the callback accepts.\n * @return A callback function.\n **/\n createCallback(\n func: string,\n argCount?: number): () => any;\n\n /**\n * @see _.createCallback\n **/\n createCallback(\n func: Dictionary,\n argCount?: number): () => boolean;\n }\n\n interface LoDashImplicitWrapper {\n /**\n * @see _.createCallback\n **/\n createCallback(\n argCount?: number): LoDashImplicitObjectWrapper<() => any>;\n }\n\n interface LoDashImplicitObjectWrapper {\n /**\n * @see _.createCallback\n **/\n createCallback(\n argCount?: number): LoDashImplicitObjectWrapper<() => any>;\n }\n\n //_.curry\n interface LoDashStatic {\n /**\n * Creates a function that accepts one or more arguments of func that when called either invokes func returning\n * its result, if all func arguments have been provided, or returns a function that accepts one or more of the\n * remaining func arguments, and so on. The arity of func may be specified if func.length is not sufficient.\n * @param func The function to curry.\n * @return Returns the new curried function.\n */\n curry(func: (t1: T1) => R):\n CurriedFunction1;\n /**\n * Creates a function that accepts one or more arguments of func that when called either invokes func returning\n * its result, if all func arguments have been provided, or returns a function that accepts one or more of the\n * remaining func arguments, and so on. The arity of func may be specified if func.length is not sufficient.\n * @param func The function to curry.\n * @return Returns the new curried function.\n */\n curry(func: (t1: T1, t2: T2) => R):\n CurriedFunction2;\n /**\n * Creates a function that accepts one or more arguments of func that when called either invokes func returning\n * its result, if all func arguments have been provided, or returns a function that accepts one or more of the\n * remaining func arguments, and so on. The arity of func may be specified if func.length is not sufficient.\n * @param func The function to curry.\n * @return Returns the new curried function.\n */\n curry(func: (t1: T1, t2: T2, t3: T3) => R):\n CurriedFunction3;\n /**\n * Creates a function that accepts one or more arguments of func that when called either invokes func returning\n * its result, if all func arguments have been provided, or returns a function that accepts one or more of the\n * remaining func arguments, and so on. The arity of func may be specified if func.length is not sufficient.\n * @param func The function to curry.\n * @return Returns the new curried function.\n */\n curry(func: (t1: T1, t2: T2, t3: T3, t4: T4) => R):\n CurriedFunction4;\n /**\n * Creates a function that accepts one or more arguments of func that when called either invokes func returning\n * its result, if all func arguments have been provided, or returns a function that accepts one or more of the\n * remaining func arguments, and so on. The arity of func may be specified if func.length is not sufficient.\n * @param func The function to curry.\n * @return Returns the new curried function.\n */\n curry(func: (t1: T1, t2: T2, t3: T3, t4: T4, t5: T5) => R):\n CurriedFunction5;\n /**\n * Creates a function that accepts one or more arguments of func that when called either invokes func returning\n * its result, if all func arguments have been provided, or returns a function that accepts one or more of the\n * remaining func arguments, and so on. The arity of func may be specified if func.length is not sufficient.\n * @param func The function to curry.\n * @param arity The arity of func.\n * @return Returns the new curried function.\n */\n curry(\n func: Function,\n arity?: number): TResult;\n }\n\n interface CurriedFunction1 {\n (): CurriedFunction1;\n (t1: T1): R;\n }\n\n interface CurriedFunction2 {\n (): CurriedFunction2;\n (t1: T1): CurriedFunction1;\n (t1: T1, t2: T2): R;\n }\n\n interface CurriedFunction3 {\n (): CurriedFunction3;\n (t1: T1): CurriedFunction2;\n (t1: T1, t2: T2): CurriedFunction1;\n (t1: T1, t2: T2, t3: T3): R;\n }\n\n interface CurriedFunction4 {\n (): CurriedFunction4;\n (t1: T1): CurriedFunction3;\n (t1: T1, t2: T2): CurriedFunction2;\n (t1: T1, t2: T2, t3: T3): CurriedFunction1;\n (t1: T1, t2: T2, t3: T3, t4: T4): R;\n }\n\n interface CurriedFunction5 {\n (): CurriedFunction5;\n (t1: T1): CurriedFunction4;\n (t1: T1, t2: T2): CurriedFunction3;\n (t1: T1, t2: T2, t3: T3): CurriedFunction2;\n (t1: T1, t2: T2, t3: T3, t4: T4): CurriedFunction1;\n (t1: T1, t2: T2, t3: T3, t4: T4, t5: T5): R;\n }\n interface RightCurriedFunction1{\n ():RightCurriedFunction1\n (t1:T1):R\n }\n interface RightCurriedFunction2{\n ():RightCurriedFunction2\n (t2:T2):RightCurriedFunction1\n (t1:T1,t2:T2):R\n }\n interface RightCurriedFunction3{\n ():RightCurriedFunction3\n (t3:T3):RightCurriedFunction2\n (t2:T2,t3:T3):RightCurriedFunction1\n (t1:T1,t2:T2,t3:T3):R\n }\n interface RightCurriedFunction4{\n ():RightCurriedFunction4\n (t4:T4):RightCurriedFunction3\n (t3:T3,t4:T4):RightCurriedFunction2\n (t2:T2,t3:T3,t4:T4):RightCurriedFunction1\n (t1:T1,t2:T2,t3:T3,t4:T4):R\n }\n interface RightCurriedFunction5{\n ():RightCurriedFunction5\n (t5:T5):RightCurriedFunction4\n (t4:T4,t5:T5):RightCurriedFunction3\n (t3:T3,t4:T4,t5:T5):RightCurriedFunction2\n (t2:T2,t3:T3,t4:T4,t5:T5):RightCurriedFunction1\n (t1:T1,t2:T2,t3:T3,t4:T4,t5:T5):R\n }\n\n interface LoDashImplicitObjectWrapper {\n /**\n * @see _.curry\n **/\n curry(arity?: number): LoDashImplicitObjectWrapper;\n }\n\n //_.curryRight\n interface LoDashStatic {\n /**\n * This method is like _.curry except that arguments are applied to func in the manner of _.partialRight\n * instead of _.partial.\n * @param func The function to curry.\n * @return Returns the new curried function.\n */\n curryRight(func: (t1: T1) => R):\n RightCurriedFunction1;\n /**\n * This method is like _.curry except that arguments are applied to func in the manner of _.partialRight\n * instead of _.partial.\n * @param func The function to curry.\n * @return Returns the new curried function.\n */\n curryRight(func: (t1: T1, t2: T2) => R):\n RightCurriedFunction2;\n /**\n * This method is like _.curry except that arguments are applied to func in the manner of _.partialRight\n * instead of _.partial.\n * @param func The function to curry.\n * @return Returns the new curried function.\n */\n curryRight(func: (t1: T1, t2: T2, t3: T3) => R):\n RightCurriedFunction3;\n /**\n * This method is like _.curry except that arguments are applied to func in the manner of _.partialRight\n * instead of _.partial.\n * @param func The function to curry.\n * @return Returns the new curried function.\n */\n curryRight(func: (t1: T1, t2: T2, t3: T3, t4: T4) => R):\n RightCurriedFunction4;\n /**\n * This method is like _.curry except that arguments are applied to func in the manner of _.partialRight\n * instead of _.partial.\n * @param func The function to curry.\n * @return Returns the new curried function.\n */\n curryRight(func: (t1: T1, t2: T2, t3: T3, t4: T4, t5: T5) => R):\n RightCurriedFunction5;\n /**\n * This method is like _.curry except that arguments are applied to func in the manner of _.partialRight\n * instead of _.partial.\n * @param func The function to curry.\n * @param arity The arity of func.\n * @return Returns the new curried function.\n */\n curryRight(\n func: Function,\n arity?: number): TResult;\n }\n\n interface LoDashImplicitObjectWrapper {\n /**\n * @see _.curryRight\n **/\n curryRight(arity?: number): LoDashImplicitObjectWrapper;\n }\n\n //_.debounce\n interface DebounceSettings {\n /**\n * Specify invoking on the leading edge of the timeout.\n */\n leading?: boolean;\n\n /**\n * The maximum time func is allowed to be delayed before it’s invoked.\n */\n maxWait?: number;\n\n /**\n * Specify invoking on the trailing edge of the timeout.\n */\n trailing?: boolean;\n }\n\n interface LoDashStatic {\n /**\n * Creates a debounced function that delays invoking func until after wait milliseconds have elapsed since\n * the last time the debounced function was invoked. The debounced function comes with a cancel method to\n * cancel delayed invocations and a flush method to immediately invoke them. Provide an options object to\n * indicate that func should be invoked on the leading and/or trailing edge of the wait timeout. Subsequent\n * calls to the debounced function return the result of the last func invocation.\n *\n * Note: If leading and trailing options are true, func is invoked on the trailing edge of the timeout only\n * if the the debounced function is invoked more than once during the wait timeout.\n *\n * See David Corbacho’s article for details over the differences between _.debounce and _.throttle.\n *\n * @param func The function to debounce.\n * @param wait The number of milliseconds to delay.\n * @param options The options object.\n * @param options.leading Specify invoking on the leading edge of the timeout.\n * @param options.maxWait The maximum time func is allowed to be delayed before it’s invoked.\n * @param options.trailing Specify invoking on the trailing edge of the timeout.\n * @return Returns the new debounced function.\n */\n debounce(\n func: T,\n wait?: number,\n options?: DebounceSettings\n ): T & Cancelable;\n }\n\n interface LoDashImplicitObjectWrapper {\n /**\n * @see _.debounce\n */\n debounce(\n wait?: number,\n options?: DebounceSettings\n ): LoDashImplicitObjectWrapper;\n }\n\n interface LoDashExplicitObjectWrapper {\n /**\n * @see _.debounce\n */\n debounce(\n wait?: number,\n options?: DebounceSettings\n ): LoDashExplicitObjectWrapper;\n }\n\n //_.defer\n interface LoDashStatic {\n /**\n * Defers invoking the func until the current call stack has cleared. Any additional arguments are provided to\n * func when it’s invoked.\n *\n * @param func The function to defer.\n * @param args The arguments to invoke the function with.\n * @return Returns the timer id.\n */\n defer(\n func: T,\n ...args: any[]\n ): number;\n }\n\n interface LoDashImplicitObjectWrapper {\n /**\n * @see _.defer\n */\n defer(...args: any[]): LoDashImplicitWrapper;\n }\n\n interface LoDashExplicitObjectWrapper {\n /**\n * @see _.defer\n */\n defer(...args: any[]): LoDashExplicitWrapper;\n }\n\n //_.delay\n interface LoDashStatic {\n /**\n * Invokes func after wait milliseconds. Any additional arguments are provided to func when it’s invoked.\n *\n * @param func The function to delay.\n * @param wait The number of milliseconds to delay invocation.\n * @param args The arguments to invoke the function with.\n * @return Returns the timer id.\n */\n delay(\n func: T,\n wait: number,\n ...args: any[]\n ): number;\n }\n\n interface LoDashImplicitObjectWrapper {\n /**\n * @see _.delay\n */\n delay(\n wait: number,\n ...args: any[]\n ): LoDashImplicitWrapper;\n }\n\n interface LoDashExplicitObjectWrapper {\n /**\n * @see _.delay\n */\n delay(\n wait: number,\n ...args: any[]\n ): LoDashExplicitWrapper;\n }\n\n interface LoDashStatic {\n /**\n * Creates a function that invokes `func` with arguments reversed.\n *\n * @static\n * @memberOf _\n * @category Function\n * @param {Function} func The function to flip arguments for.\n * @returns {Function} Returns the new function.\n * @example\n *\n * var flipped = _.flip(function() {\n * return _.toArray(arguments);\n * });\n *\n * flipped('a', 'b', 'c', 'd');\n * // => ['d', 'c', 'b', 'a']\n */\n flip(func: T): T;\n }\n\n interface LoDashImplicitObjectWrapper {\n /**\n * @see _.flip\n */\n flip(): LoDashImplicitObjectWrapper;\n }\n\n interface LoDashExplicitObjectWrapper {\n /**\n * @see _.flip\n */\n flip(): LoDashExplicitObjectWrapper;\n }\n\n //_.flow\n interface LoDashStatic {\n /**\n * Creates a function that returns the result of invoking the provided functions with the this binding of the\n * created function, where each successive invocation is supplied the return value of the previous.\n *\n * @param funcs Functions to invoke.\n * @return Returns the new function.\n */\n // 0-argument first function\n flow(f1: () => R1, f2: (a: R1) => R2): () => R2;\n flow(f1: () => R1, f2: (a: R1) => R2, f3: (a: R2) => R3): () => R3;\n flow(f1: () => R1, f2: (a: R1) => R2, f3: (a: R2) => R3, f4: (a: R3) => R4): () => R4;\n flow(f1: () => R1, f2: (a: R1) => R2, f3: (a: R2) => R3, f4: (a: R3) => R4, f5: (a: R4) => R5): () => R5;\n flow(f1: () => R1, f2: (a: R1) => R2, f3: (a: R2) => R3, f4: (a: R3) => R4, f5: (a: R4) => R5, f6: (a: R5) => R6): () => R6;\n flow(f1: () => R1, f2: (a: R1) => R2, f3: (a: R2) => R3, f4: (a: R3) => R4, f5: (a: R4) => R5, f6: (a: R5) => R6, f7: (a: R6) => R7): () => R7;\n // 1-argument first function\n flow(f1: (a1: A1) => R1, f2: (a: R1) => R2): (a1: A1) => R2;\n flow(f1: (a1: A1) => R1, f2: (a: R1) => R2, f3: (a: R2) => R3): (a1: A1) => R3;\n flow(f1: (a1: A1) => R1, f2: (a: R1) => R2, f3: (a: R2) => R3, f4: (a: R3) => R4): (a1: A1) => R4;\n flow(f1: (a1: A1) => R1, f2: (a: R1) => R2, f3: (a: R2) => R3, f4: (a: R3) => R4, f5: (a: R4) => R5): (a1: A1) => R5;\n flow(f1: (a1: A1) => R1, f2: (a: R1) => R2, f3: (a: R2) => R3, f4: (a: R3) => R4, f5: (a: R4) => R5, f6: (a: R5) => R6): (a1: A1) => R6;\n flow(f1: (a1: A1) => R1, f2: (a: R1) => R2, f3: (a: R2) => R3, f4: (a: R3) => R4, f5: (a: R4) => R5, f6: (a: R5) => R6, f7: (a: R6) => R7): (a1: A1) => R7;\n // 2-argument first function\n flow(f1: (a1: A1, a2: A2) => R1, f2: (a: R1) => R2): (a1: A1, a2: A2) => R2;\n flow(f1: (a1: A1, a2: A2) => R1, f2: (a: R1) => R2, f3: (a: R2) => R3): (a1: A1, a2: A2) => R3;\n flow(f1: (a1: A1, a2: A2) => R1, f2: (a: R1) => R2, f3: (a: R2) => R3, f4: (a: R3) => R4): (a1: A1, a2: A2) => R4;\n flow(f1: (a1: A1, a2: A2) => R1, f2: (a: R1) => R2, f3: (a: R2) => R3, f4: (a: R3) => R4, f5: (a: R4) => R5): (a1: A1, a2: A2) => R5;\n flow(f1: (a1: A1, a2: A2) => R1, f2: (a: R1) => R2, f3: (a: R2) => R3, f4: (a: R3) => R4, f5: (a: R4) => R5, f6: (a: R5) => R6): (a1: A1, a2: A2) => R6;\n flow(f1: (a1: A1, a2: A2) => R1, f2: (a: R1) => R2, f3: (a: R2) => R3, f4: (a: R3) => R4, f5: (a: R4) => R5, f6: (a: R5) => R6, f7: (a: R6) => R7): (a1: A1, a2: A2) => R7;\n // 3-argument first function\n flow(f1: (a1: A1, a2: A2, a3: A3) => R1, f2: (a: R1) => R2): (a1: A1, a2: A2, a3: A3) => R2;\n flow(f1: (a1: A1, a2: A2, a3: A3) => R1, f2: (a: R1) => R2, f3: (a: R2) => R3): (a1: A1, a2: A2, a3: A3) => R3;\n flow(f1: (a1: A1, a2: A2, a3: A3) => R1, f2: (a: R1) => R2, f3: (a: R2) => R3, f4: (a: R3) => R4): (a1: A1, a2: A2, a3: A3) => R4;\n flow(f1: (a1: A1, a2: A2, a3: A3) => R1, f2: (a: R1) => R2, f3: (a: R2) => R3, f4: (a: R3) => R4, f5: (a: R4) => R5): (a1: A1, a2: A2, a3: A3) => R5;\n flow(f1: (a1: A1, a2: A2, a3: A3) => R1, f2: (a: R1) => R2, f3: (a: R2) => R3, f4: (a: R3) => R4, f5: (a: R4) => R5, f6: (a: R5) => R6): (a1: A1, a2: A2, a3: A3) => R6;\n flow(f1: (a1: A1, a2: A2, a3: A3) => R1, f2: (a: R1) => R2, f3: (a: R2) => R3, f4: (a: R3) => R4, f5: (a: R4) => R5, f6: (a: R5) => R6, f7: (a: R6) => R7): (a1: A1, a2: A2, a3: A3) => R7;\n // 4-argument first function\n flow(f1: (a1: A1, a2: A2, a3: A3, a4: A4) => R1, f2: (a: R1) => R2): (a1: A1, a2: A2, a3: A3, a4: A4) => R2;\n flow(f1: (a1: A1, a2: A2, a3: A3, a4: A4) => R1, f2: (a: R1) => R2, f3: (a: R2) => R3): (a1: A1, a2: A2, a3: A3, a4: A4) => R3;\n flow(f1: (a1: A1, a2: A2, a3: A3, a4: A4) => R1, f2: (a: R1) => R2, f3: (a: R2) => R3, f4: (a: R3) => R4): (a1: A1, a2: A2, a3: A3, a4: A4) => R4;\n flow(f1: (a1: A1, a2: A2, a3: A3, a4: A4) => R1, f2: (a: R1) => R2, f3: (a: R2) => R3, f4: (a: R3) => R4, f5: (a: R4) => R5): (a1: A1, a2: A2, a3: A3, a4: A4) => R5;\n flow(f1: (a1: A1, a2: A2, a3: A3, a4: A4) => R1, f2: (a: R1) => R2, f3: (a: R2) => R3, f4: (a: R3) => R4, f5: (a: R4) => R5, f6: (a: R5) => R6): (a1: A1, a2: A2, a3: A3, a4: A4) => R6;\n flow(f1: (a1: A1, a2: A2, a3: A3, a4: A4) => R1, f2: (a: R1) => R2, f3: (a: R2) => R3, f4: (a: R3) => R4, f5: (a: R4) => R5, f6: (a: R5) => R6, f7: (a: R6) => R7): (a1: A1, a2: A2, a3: A3, a4: A4) => R7;\n // generic function\n flow(...funcs: Function[]): TResult;\n flow(funcs: Function[]): TResult;\n }\n\n interface LoDashImplicitObjectWrapper {\n /**\n * @see _.flow\n */\n flow(...funcs: Function[]): LoDashImplicitObjectWrapper;\n /**\n * @see _.flow\n */\n flow(funcs: Function[]): LoDashImplicitObjectWrapper;\n }\n\n interface LoDashExplicitObjectWrapper {\n /**\n * @see _.flow\n */\n flow(...funcs: Function[]): LoDashExplicitObjectWrapper;\n /**\n * @see _.flow\n */\n flow(funcs: Function[]): LoDashExplicitObjectWrapper;\n }\n\n //_.flowRight\n interface LoDashStatic {\n /**\n * This method is like _.flow except that it creates a function that invokes the provided functions from right\n * to left.\n *\n * @param funcs Functions to invoke.\n * @return Returns the new function.\n */\n flowRight(...funcs: Function[]): TResult;\n /**\n * @see _.flowRight\n */\n flowRight(funcs: Function[]): TResult;\n }\n\n interface LoDashImplicitObjectWrapper {\n /**\n * @see _.flowRight\n */\n flowRight(...funcs: Function[]): LoDashImplicitObjectWrapper;\n /**\n * @see _.flowRight\n */\n flowRight(funcs: Function[]): LoDashImplicitObjectWrapper;\n }\n\n interface LoDashExplicitObjectWrapper {\n /**\n * @see _.flowRight\n */\n flowRight(...funcs: Function[]): LoDashExplicitObjectWrapper;\n /**\n * @see _.flowRight\n */\n flowRight(funcs: Function[]): LoDashExplicitObjectWrapper;\n }\n\n //_.memoize\n interface MemoizedFunction extends Function {\n cache: MapCache;\n }\n\n interface LoDashStatic {\n /**\n * Creates a function that memoizes the result of func. If resolver is provided it determines the cache key for\n * storing the result based on the arguments provided to the memoized function. By default, the first argument\n * provided to the memoized function is coerced to a string and used as the cache key. The func is invoked with\n * the this binding of the memoized function.\n *\n * @param func The function to have its output memoized.\n * @param resolver The function to resolve the cache key.\n * @return Returns the new memoizing function.\n */\n memoize: {\n (func: T, resolver?: Function): T & MemoizedFunction;\n Cache: MapCacheConstructor;\n };\n }\n\n interface LoDashImplicitObjectWrapper {\n /**\n * @see _.memoize\n */\n memoize(resolver?: Function): LoDashImplicitObjectWrapper;\n }\n\n interface LoDashExplicitObjectWrapper {\n /**\n * @see _.memoize\n */\n memoize(resolver?: Function): LoDashExplicitObjectWrapper;\n }\n\n //_.overArgs (was _.modArgs)\n interface LoDashStatic {\n /**\n * Creates a function that runs each argument through a corresponding transform function.\n *\n * @param func The function to wrap.\n * @param transforms The functions to transform arguments, specified as individual functions or arrays\n * of functions.\n * @return Returns the new function.\n */\n overArgs(\n func: T,\n ...transforms: Function[]\n ): TResult;\n\n /**\n * @see _.overArgs\n */\n overArgs(\n func: T,\n transforms: Function[]\n ): TResult;\n }\n\n interface LoDashImplicitObjectWrapper {\n /**\n * @see _.overArgs\n */\n overArgs(...transforms: Function[]): LoDashImplicitObjectWrapper;\n\n /**\n * @see _.overArgs\n */\n overArgs(transforms: Function[]): LoDashImplicitObjectWrapper;\n }\n\n interface LoDashExplicitObjectWrapper {\n /**\n * @see _.overArgs\n */\n overArgs(...transforms: Function[]): LoDashExplicitObjectWrapper;\n\n /**\n * @see _.overArgs\n */\n overArgs(transforms: Function[]): LoDashExplicitObjectWrapper;\n }\n\n //_.negate\n interface LoDashStatic {\n /**\n * Creates a function that negates the result of the predicate func. The func predicate is invoked with\n * the this binding and arguments of the created function.\n *\n * @param predicate The predicate to negate.\n * @return Returns the new function.\n */\n negate(predicate: T): (...args: any[]) => boolean;\n\n /**\n * @see _.negate\n */\n negate(predicate: T): TResult;\n }\n\n interface LoDashImplicitObjectWrapper {\n /**\n * @see _.negate\n */\n negate(): LoDashImplicitObjectWrapper<(...args: any[]) => boolean>;\n\n /**\n * @see _.negate\n */\n negate(): LoDashImplicitObjectWrapper;\n }\n\n interface LoDashExplicitObjectWrapper {\n /**\n * @see _.negate\n */\n negate(): LoDashExplicitObjectWrapper<(...args: any[]) => boolean>;\n\n /**\n * @see _.negate\n */\n negate(): LoDashExplicitObjectWrapper;\n }\n\n //_.once\n interface LoDashStatic {\n /**\n * Creates a function that is restricted to invoking func once. Repeat calls to the function return the value\n * of the first call. The func is invoked with the this binding and arguments of the created function.\n *\n * @param func The function to restrict.\n * @return Returns the new restricted function.\n */\n once(func: T): T;\n }\n\n interface LoDashImplicitObjectWrapper {\n /**\n * @see _.once\n */\n once(): LoDashImplicitObjectWrapper;\n }\n\n interface LoDashExplicitObjectWrapper {\n /**\n * @see _.once\n */\n once(): LoDashExplicitObjectWrapper;\n }\n\n //_.partial\n interface LoDashStatic {\n /**\n * Creates a function that, when called, invokes func with any additional partial arguments\n * prepended to those provided to the new function. This method is similar to _.bind except\n * it does not alter the this binding.\n * @param func The function to partially apply arguments to.\n * @param args Arguments to be partially applied.\n * @return The new partially applied function.\n **/\n partial: Partial;\n }\n\n type PH = LoDashStatic;\n\n type Function0 = () => R;\n type Function1 = (t1: T1) => R;\n type Function2 = (t1: T1, t2: T2) => R;\n type Function3 = (t1: T1, t2: T2, t3: T3) => R;\n type Function4 = (t1: T1, t2: T2, t3: T3, t4: T4) => R;\n\n interface Partial {\n // arity 0\n (func: Function0): Function0;\n // arity 1\n (func: Function1): Function1;\n (func: Function1, arg1: T1): Function0;\n // arity 2\n (func: Function2): Function2;\n (func: Function2, arg1: T1): Function1< T2, R>;\n (func: Function2, plc1: PH, arg2: T2): Function1;\n (func: Function2, arg1: T1, arg2: T2): Function0< R>;\n // arity 3\n (func: Function3): Function3;\n (func: Function3, arg1: T1): Function2< T2, T3, R>;\n (func: Function3, plc1: PH, arg2: T2): Function2;\n (func: Function3, arg1: T1, arg2: T2): Function1< T3, R>;\n (func: Function3, plc1: PH, plc2: PH, arg3: T3): Function2;\n (func: Function3, arg1: T1, plc2: PH, arg3: T3): Function1< T2, R>;\n (func: Function3, plc1: PH, arg2: T2, arg3: T3): Function1;\n (func: Function3, arg1: T1, arg2: T2, arg3: T3): Function0< R>;\n // arity 4\n (func: Function4): Function4;\n (func: Function4, arg1: T1): Function3< T2, T3, T4, R>;\n (func: Function4, plc1: PH, arg2: T2): Function3;\n (func: Function4, arg1: T1, arg2: T2): Function2< T3, T4, R>;\n (func: Function4, plc1: PH, plc2: PH, arg3: T3): Function3;\n (func: Function4, arg1: T1, plc2: PH, arg3: T3): Function2< T2, T4, R>;\n (func: Function4, plc1: PH, arg2: T2, arg3: T3): Function2;\n (func: Function4, arg1: T1, arg2: T2, arg3: T3): Function1< T4, R>;\n (func: Function4, plc1: PH, plc2: PH, plc3: PH, arg4: T4): Function3;\n (func: Function4, arg1: T1, plc2: PH, plc3: PH, arg4: T4): Function2< T2, T3, R>;\n (func: Function4, plc1: PH, arg2: T2, plc3: PH, arg4: T4): Function2;\n (func: Function4, arg1: T1, arg2: T2, plc3: PH, arg4: T4): Function1< T3, R>;\n (func: Function4, plc1: PH, plc2: PH, arg3: T3, arg4: T4): Function2;\n (func: Function4, arg1: T1, plc2: PH, arg3: T3, arg4: T4): Function1< T2, R>;\n (func: Function4, plc1: PH, arg2: T2, arg3: T3, arg4: T4): Function1;\n (func: Function4, arg1: T1, arg2: T2, arg3: T3, arg4: T4): Function0< R>;\n // catch-all\n (func: Function, ...args: any[]): Function;\n }\n\n //_.partialRight\n interface LoDashStatic {\n /**\n * This method is like _.partial except that partial arguments are appended to those provided\n * to the new function.\n * @param func The function to partially apply arguments to.\n * @param args Arguments to be partially applied.\n * @return The new partially applied function.\n **/\n partialRight: PartialRight;\n }\n\n interface PartialRight {\n // arity 0\n (func: Function0): Function0;\n // arity 1\n (func: Function1): Function1;\n (func: Function1, arg1: T1): Function0;\n // arity 2\n (func: Function2): Function2;\n (func: Function2, arg1: T1, plc2: PH): Function1< T2, R>;\n (func: Function2, arg2: T2): Function1;\n (func: Function2, arg1: T1, arg2: T2): Function0< R>;\n // arity 3\n (func: Function3): Function3;\n (func: Function3, arg1: T1, plc2: PH, plc3: PH): Function2< T2, T3, R>;\n (func: Function3, arg2: T2, plc3: PH): Function2;\n (func: Function3, arg1: T1, arg2: T2, plc3: PH): Function1< T3, R>;\n (func: Function3, arg3: T3): Function2;\n (func: Function3, arg1: T1, plc2: PH, arg3: T3): Function1< T2, R>;\n (func: Function3, arg2: T2, arg3: T3): Function1;\n (func: Function3, arg1: T1, arg2: T2, arg3: T3): Function0< R>;\n // arity 4\n (func: Function4): Function4;\n (func: Function4, arg1: T1, plc2: PH, plc3: PH, plc4: PH): Function3< T2, T3, T4, R>;\n (func: Function4, arg2: T2, plc3: PH, plc4: PH): Function3;\n (func: Function4, arg1: T1, arg2: T2, plc3: PH, plc4: PH): Function2< T3, T4, R>;\n (func: Function4, arg3: T3, plc4: PH): Function3;\n (func: Function4, arg1: T1, plc2: PH, arg3: T3, plc4: PH): Function2< T2, T4, R>;\n (func: Function4, arg2: T2, arg3: T3, plc4: PH): Function2;\n (func: Function4, arg1: T1, arg2: T2, arg3: T3, plc4: PH): Function1< T4, R>;\n (func: Function4, arg4: T4): Function3;\n (func: Function4, arg1: T1, plc2: PH, plc3: PH, arg4: T4): Function2< T2, T3, R>;\n (func: Function4, arg2: T2, plc3: PH, arg4: T4): Function2;\n (func: Function4, arg1: T1, arg2: T2, plc3: PH, arg4: T4): Function1< T3, R>;\n (func: Function4, arg3: T3, arg4: T4): Function2;\n (func: Function4, arg1: T1, plc2: PH, arg3: T3, arg4: T4): Function1< T2, R>;\n (func: Function4, arg2: T2, arg3: T3, arg4: T4): Function1;\n (func: Function4, arg1: T1, arg2: T2, arg3: T3, arg4: T4): Function0< R>;\n // catch-all\n (func: Function, ...args: any[]): Function;\n }\n\n //_.rearg\n interface LoDashStatic {\n /**\n * Creates a function that invokes func with arguments arranged according to the specified indexes where the\n * argument value at the first index is provided as the first argument, the argument value at the second index\n * is provided as the second argument, and so on.\n * @param func The function to rearrange arguments for.\n * @param indexes The arranged argument indexes, specified as individual indexes or arrays of indexes.\n * @return Returns the new function.\n */\n rearg(func: Function, indexes: number[]): TResult;\n\n /**\n * @see _.rearg\n */\n rearg(func: Function, ...indexes: number[]): TResult;\n }\n\n interface LoDashImplicitObjectWrapper {\n /**\n * @see _.rearg\n */\n rearg(indexes: number[]): LoDashImplicitObjectWrapper;\n\n /**\n * @see _.rearg\n */\n rearg(...indexes: number[]): LoDashImplicitObjectWrapper;\n }\n\n //_.rest\n interface LoDashStatic {\n /**\n * Creates a function that invokes func with the this binding of the created function and arguments from start\n * and beyond provided as an array.\n *\n * Note: This method is based on the rest parameter.\n *\n * @param func The function to apply a rest parameter to.\n * @param start The start position of the rest parameter.\n * @return Returns the new function.\n */\n rest(\n func: Function,\n start?: number\n ): TResult;\n\n /**\n * @see _.rest\n */\n rest(\n func: TFunc,\n start?: number\n ): TResult;\n }\n\n interface LoDashImplicitObjectWrapper {\n /**\n * @see _.rest\n */\n rest(start?: number): LoDashImplicitObjectWrapper;\n }\n\n interface LoDashExplicitObjectWrapper {\n /**\n * @see _.rest\n */\n rest(start?: number): LoDashExplicitObjectWrapper;\n }\n\n //_.spread\n interface LoDashStatic {\n /**\n * Creates a function that invokes func with the this binding of the created function and an array of arguments\n * much like Function#apply.\n *\n * Note: This method is based on the spread operator.\n *\n * @param func The function to spread arguments over.\n * @return Returns the new function.\n */\n spread(func: F): T;\n\n /**\n * @see _.spread\n */\n spread(func: Function): T;\n }\n\n interface LoDashImplicitObjectWrapper {\n /**\n * @see _.spread\n */\n spread(): LoDashImplicitObjectWrapper;\n }\n\n interface LoDashExplicitObjectWrapper {\n /**\n * @see _.spread\n */\n spread(): LoDashExplicitObjectWrapper;\n }\n\n //_.throttle\n interface ThrottleSettings {\n /**\n * If you'd like to disable the leading-edge call, pass this as false.\n */\n leading?: boolean;\n\n /**\n * If you'd like to disable the execution on the trailing-edge, pass false.\n */\n trailing?: boolean;\n }\n\n interface LoDashStatic {\n /**\n * Creates a throttled function that only invokes func at most once per every wait milliseconds. The throttled\n * function comes with a cancel method to cancel delayed invocations and a flush method to immediately invoke\n * them. Provide an options object to indicate that func should be invoked on the leading and/or trailing edge\n * of the wait timeout. Subsequent calls to the throttled function return the result of the last func call.\n *\n * Note: If leading and trailing options are true, func is invoked on the trailing edge of the timeout only if\n * the the throttled function is invoked more than once during the wait timeout.\n *\n * @param func The function to throttle.\n * @param wait The number of milliseconds to throttle invocations to.\n * @param options The options object.\n * @param options.leading Specify invoking on the leading edge of the timeout.\n * @param options.trailing Specify invoking on the trailing edge of the timeout.\n * @return Returns the new throttled function.\n */\n throttle(\n func: T,\n wait?: number,\n options?: ThrottleSettings\n ): T & Cancelable;\n }\n\n interface LoDashImplicitObjectWrapper {\n /**\n * @see _.throttle\n */\n throttle(\n wait?: number,\n options?: ThrottleSettings\n ): LoDashImplicitObjectWrapper;\n }\n\n interface LoDashExplicitObjectWrapper {\n /**\n * @see _.throttle\n */\n throttle(\n wait?: number,\n options?: ThrottleSettings\n ): LoDashExplicitObjectWrapper;\n }\n\n //_.unary\n interface LoDashStatic {\n /**\n * Creates a function that accepts up to one argument, ignoring any\n * additional arguments.\n *\n * @static\n * @memberOf _\n * @category Function\n * @param {Function} func The function to cap arguments for.\n * @returns {Function} Returns the new function.\n * @example\n *\n * _.map(['6', '8', '10'], _.unary(parseInt));\n * // => [6, 8, 10]\n */\n unary(func: T): T;\n }\n\n interface LoDashImplicitObjectWrapper {\n /**\n * @see _.unary\n */\n unary(): LoDashImplicitObjectWrapper;\n }\n\n interface LoDashExplicitObjectWrapper {\n /**\n * @see _.unary\n */\n unary(): LoDashExplicitObjectWrapper;\n }\n\n //_.wrap\n interface LoDashStatic {\n /**\n * Creates a function that provides value to the wrapper function as its first argument. Any additional\n * arguments provided to the function are appended to those provided to the wrapper function. The wrapper is\n * invoked with the this binding of the created function.\n *\n * @param value The value to wrap.\n * @param wrapper The wrapper function.\n * @return Returns the new function.\n */\n wrap(\n value: V,\n wrapper: W\n ): R;\n\n /**\n * @see _.wrap\n */\n wrap(\n value: V,\n wrapper: Function\n ): R;\n\n /**\n * @see _.wrap\n */\n wrap(\n value: any,\n wrapper: Function\n ): R;\n }\n\n interface LoDashImplicitWrapper {\n /**\n * @see _.wrap\n */\n wrap(wrapper: W): LoDashImplicitObjectWrapper;\n\n /**\n * @see _.wrap\n */\n wrap(wrapper: Function): LoDashImplicitObjectWrapper;\n }\n\n interface LoDashImplicitArrayWrapperBase {\n /**\n * @see _.wrap\n */\n wrap(wrapper: W): LoDashImplicitObjectWrapper;\n\n /**\n * @see _.wrap\n */\n wrap(wrapper: Function): LoDashImplicitObjectWrapper;\n }\n\n interface LoDashImplicitObjectWrapperBase {\n /**\n * @see _.wrap\n */\n wrap(wrapper: W): LoDashImplicitObjectWrapper;\n\n /**\n * @see _.wrap\n */\n wrap(wrapper: Function): LoDashImplicitObjectWrapper;\n }\n\n interface LoDashExplicitWrapper {\n /**\n * @see _.wrap\n */\n wrap(wrapper: W): LoDashExplicitObjectWrapper;\n\n /**\n * @see _.wrap\n */\n wrap(wrapper: Function): LoDashExplicitObjectWrapper;\n }\n\n interface LoDashExplicitArrayWrapperBase {\n /**\n * @see _.wrap\n */\n wrap(wrapper: W): LoDashExplicitObjectWrapper;\n\n /**\n * @see _.wrap\n */\n wrap(wrapper: Function): LoDashExplicitObjectWrapper;\n }\n\n interface LoDashExplicitObjectWrapperBase {\n /**\n * @see _.wrap\n */\n wrap(wrapper: W): LoDashExplicitObjectWrapper;\n\n /**\n * @see _.wrap\n */\n wrap(wrapper: Function): LoDashExplicitObjectWrapper;\n }\n\n /********\n * Lang *\n ********/\n\n //_.castArray\n interface LoDashStatic {\n /**\n * Casts value as an array if it’s not one.\n *\n * @param value The value to inspect.\n * @return Returns the cast array.\n */\n castArray(value?: Many): T[];\n }\n\n interface LoDashImplicitWrapper {\n /**\n * @see _.castArray\n */\n castArray(): LoDashImplicitArrayWrapper;\n }\n\n interface LoDashImplicitArrayWrapperBase {\n /**\n * @see _.castArray\n */\n castArray(): TWrapper;\n }\n\n interface LoDashImplicitObjectWrapperBase {\n /**\n * @see _.castArray\n */\n castArray(): LoDashImplicitArrayWrapper;\n }\n\n interface LoDashExplicitWrapper {\n /**\n * @see _.castArray\n */\n castArray(): LoDashExplicitArrayWrapper;\n }\n\n interface LoDashExplicitArrayWrapperBase {\n /**\n * @see _.castArray\n */\n castArray(): TWrapper;\n }\n\n interface LoDashExplicitObjectWrapperBase {\n /**\n * @see _.castArray\n */\n castArray(): LoDashExplicitArrayWrapper;\n }\n\n //_.clone\n interface LoDashStatic {\n /**\n * Creates a shallow clone of value.\n *\n * Note: This method is loosely based on the structured clone algorithm and supports cloning arrays,\n * array buffers, booleans, date objects, maps, numbers, Object objects, regexes, sets, strings, symbols,\n * and typed arrays. The own enumerable properties of arguments objects are cloned as plain objects. An empty\n * object is returned for uncloneable values such as error objects, functions, DOM nodes, and WeakMaps.\n *\n * @param value The value to clone.\n * @return Returns the cloned value.\n */\n clone(value: T): T;\n }\n\n interface LoDashImplicitWrapper {\n /**\n * @see _.clone\n */\n clone(): T;\n }\n\n interface LoDashImplicitArrayWrapperBase {\n /**\n * @see _.clone\n */\n clone(): TArray;\n }\n\n interface LoDashImplicitObjectWrapperBase {\n /**\n * @see _.clone\n */\n clone(): TObject;\n }\n\n interface LoDashExplicitWrapper {\n /**\n * @see _.clone\n */\n clone(): LoDashExplicitWrapper;\n }\n\n interface LoDashExplicitArrayWrapperBase {\n /**\n * @see _.clone\n */\n clone(): TWrapper;\n }\n\n interface LoDashExplicitObjectWrapperBase {\n /**\n * @see _.clone\n */\n clone(): TWrapper;\n }\n\n //_.cloneDeep\n interface LoDashStatic {\n /**\n * This method is like _.clone except that it recursively clones value.\n *\n * @param value The value to recursively clone.\n * @return Returns the deep cloned value.\n */\n cloneDeep(value: T): T;\n }\n\n interface LoDashImplicitWrapper {\n /**\n * @see _.cloneDeep\n */\n cloneDeep(): T;\n }\n\n interface LoDashImplicitArrayWrapperBase {\n /**\n * @see _.cloneDeep\n */\n cloneDeep(): TArray;\n }\n\n interface LoDashImplicitObjectWrapperBase {\n /**\n * @see _.cloneDeep\n */\n cloneDeep(): TObject;\n }\n\n interface LoDashExplicitWrapper {\n /**\n * @see _.cloneDeep\n */\n cloneDeep(): LoDashExplicitWrapper;\n }\n\n interface LoDashExplicitArrayWrapperBase {\n /**\n * @see _.cloneDeep\n */\n cloneDeep(): TWrapper;\n }\n\n interface LoDashExplicitObjectWrapperBase {\n /**\n * @see _.cloneDeep\n */\n cloneDeep(): TWrapper;\n }\n\n //_.cloneDeepWith\n type CloneDeepWithCustomizer = (value: TValue, key?: number|string, object?: any, stack?: any) => TResult;\n\n interface LoDashStatic {\n /**\n * This method is like _.cloneWith except that it recursively clones value.\n *\n * @param value The value to recursively clone.\n * @param customizer The function to customize cloning.\n * @return Returns the deep cloned value.\n */\n cloneDeepWith(\n value: any,\n customizer?: CloneDeepWithCustomizer\n ): TResult;\n\n /**\n * @see _.clonDeepeWith\n */\n cloneDeepWith(\n value: T,\n customizer?: CloneDeepWithCustomizer\n ): TResult;\n }\n\n interface LoDashImplicitWrapper {\n /**\n * @see _.cloneDeepWith\n */\n cloneDeepWith(\n customizer?: CloneDeepWithCustomizer\n ): TResult;\n }\n\n interface LoDashImplicitArrayWrapperBase {\n /**\n * @see _.cloneDeepWith\n */\n cloneDeepWith(\n customizer?: CloneDeepWithCustomizer\n ): TResult;\n }\n\n interface LoDashImplicitObjectWrapperBase {\n /**\n * @see _.cloneDeepWith\n */\n cloneDeepWith(\n customizer?: CloneDeepWithCustomizer\n ): TResult;\n }\n\n interface LoDashExplicitWrapper {\n /**\n * @see _.cloneDeepWith\n */\n cloneDeepWith(\n customizer?: CloneDeepWithCustomizer\n ): LoDashExplicitWrapper;\n\n /**\n * @see _.cloneDeepWith\n */\n cloneDeepWith(\n customizer?: CloneDeepWithCustomizer\n ): LoDashExplicitArrayWrapper;\n\n /**\n * @see _.cloneDeepWith\n */\n cloneDeepWith(\n customizer?: CloneDeepWithCustomizer\n ): LoDashExplicitObjectWrapper;\n }\n\n interface LoDashExplicitArrayWrapperBase {\n /**\n * @see _.cloneDeepWith\n */\n cloneDeepWith(\n customizer?: CloneDeepWithCustomizer\n ): LoDashExplicitWrapper;\n\n /**\n * @see _.cloneDeepWith\n */\n cloneDeepWith(\n customizer?: CloneDeepWithCustomizer\n ): LoDashExplicitArrayWrapper;\n\n /**\n * @see _.cloneDeepWith\n */\n cloneDeepWith(\n customizer?: CloneDeepWithCustomizer\n ): LoDashExplicitObjectWrapper;\n }\n\n interface LoDashExplicitObjectWrapperBase {\n /**\n * @see _.cloneDeepWith\n */\n cloneDeepWith(\n customizer?: CloneDeepWithCustomizer\n ): LoDashExplicitWrapper;\n\n /**\n * @see _.cloneDeepWith\n */\n cloneDeepWith(\n customizer?: CloneDeepWithCustomizer\n ): LoDashExplicitArrayWrapper;\n\n /**\n * @see _.cloneDeepWith\n */\n cloneDeepWith(\n customizer?: CloneDeepWithCustomizer\n ): LoDashExplicitObjectWrapper;\n }\n\n //_.cloneWith\n type CloneWithCustomizer = (value: TValue, key?: number|string, object?: any, stack?: any) => TResult;\n\n interface LoDashStatic {\n /**\n * This method is like _.clone except that it accepts customizer which is invoked to produce the cloned value.\n * If customizer returns undefined cloning is handled by the method instead.\n *\n * @param value The value to clone.\n * @param customizer The function to customize cloning.\n * @return Returns the cloned value.\n */\n cloneWith(\n value: any,\n customizer?: CloneWithCustomizer\n ): TResult;\n\n /**\n * @see _.cloneWith\n */\n cloneWith(\n value: T,\n customizer?: CloneWithCustomizer\n ): TResult;\n }\n\n interface LoDashImplicitWrapper {\n /**\n * @see _.cloneWith\n */\n cloneWith(\n customizer?: CloneWithCustomizer\n ): TResult;\n }\n\n interface LoDashImplicitArrayWrapperBase {\n /**\n * @see _.cloneWith\n */\n cloneWith(\n customizer?: CloneWithCustomizer\n ): TResult;\n }\n\n interface LoDashImplicitObjectWrapperBase {\n /**\n * @see _.cloneWith\n */\n cloneWith(\n customizer?: CloneWithCustomizer\n ): TResult;\n }\n\n interface LoDashExplicitWrapper {\n /**\n * @see _.cloneWith\n */\n cloneWith(\n customizer?: CloneWithCustomizer\n ): LoDashExplicitWrapper;\n\n /**\n * @see _.cloneWith\n */\n cloneWith(\n customizer?: CloneWithCustomizer\n ): LoDashExplicitArrayWrapper;\n\n /**\n * @see _.cloneWith\n */\n cloneWith(\n customizer?: CloneWithCustomizer\n ): LoDashExplicitObjectWrapper;\n }\n\n interface LoDashExplicitArrayWrapperBase {\n /**\n * @see _.cloneWith\n */\n cloneWith(\n customizer?: CloneWithCustomizer\n ): LoDashExplicitWrapper;\n\n /**\n * @see _.cloneWith\n */\n cloneWith(\n customizer?: CloneWithCustomizer\n ): LoDashExplicitArrayWrapper;\n\n /**\n * @see _.cloneWith\n */\n cloneWith(\n customizer?: CloneWithCustomizer\n ): LoDashExplicitObjectWrapper;\n }\n\n interface LoDashExplicitObjectWrapperBase {\n /**\n * @see _.cloneWith\n */\n cloneWith(\n customizer?: CloneWithCustomizer\n ): LoDashExplicitWrapper;\n\n /**\n * @see _.cloneWith\n */\n cloneWith(\n customizer?: CloneWithCustomizer\n ): LoDashExplicitArrayWrapper;\n\n /**\n * @see _.cloneWith\n */\n cloneWith(\n customizer?: CloneWithCustomizer\n ): LoDashExplicitObjectWrapper;\n }\n\n /**\n * An object containing predicate functions for each property of T\n */\n type ConformsPredicateObject = {\n [P in keyof T]: (val: T[P]) => boolean;\n };\n\n //_.conforms\n interface LoDashStatic {\n /**\n * Creates a function that invokes the predicate properties of `source` with the corresponding\n * property values of a given object, returning true if all predicates return truthy, else false.\n */\n conforms(source: ConformsPredicateObject): (Target: T) => boolean;\n }\n\n //_.conformsTo\n interface LoDashStatic {\n /**\n * Checks if object conforms to source by invoking the predicate properties of source with the\n * corresponding property values of object.\n *\n * Note: This method is equivalent to _.conforms when source is partially applied.\n */\n conformsTo(object: T, source: ConformsPredicateObject): boolean;\n }\n\n //_.eq\n interface LoDashStatic {\n /**\n * Performs a [`SameValueZero`](http://ecma-international.org/ecma-262/6.0/#sec-samevaluezero)\n * comparison between two values to determine if they are equivalent.\n *\n * @static\n * @memberOf _\n * @category Lang\n * @param {*} value The value to compare.\n * @param {*} other The other value to compare.\n * @returns {boolean} Returns `true` if the values are equivalent, else `false`.\n * @example\n *\n * var object = { 'user': 'fred' };\n * var other = { 'user': 'fred' };\n *\n * _.eq(object, object);\n * // => true\n *\n * _.eq(object, other);\n * // => false\n *\n * _.eq('a', 'a');\n * // => true\n *\n * _.eq('a', Object('a'));\n * // => false\n *\n * _.eq(NaN, NaN);\n * // => true\n */\n eq(\n value: any,\n other: any\n ): boolean;\n }\n\n interface LoDashImplicitWrapperBase {\n /**\n * @see _.isEqual\n */\n eq(\n other: any\n ): boolean;\n }\n\n interface LoDashExplicitWrapperBase {\n /**\n * @see _.isEqual\n */\n eq(\n other: any\n ): LoDashExplicitWrapper;\n }\n\n //_.gt\n interface LoDashStatic {\n /**\n * Checks if value is greater than other.\n *\n * @param value The value to compare.\n * @param other The other value to compare.\n * @return Returns true if value is greater than other, else false.\n */\n gt(\n value: any,\n other: any\n ): boolean;\n }\n\n interface LoDashImplicitWrapperBase {\n /**\n * @see _.gt\n */\n gt(other: any): boolean;\n }\n\n interface LoDashExplicitWrapperBase {\n /**\n * @see _.gt\n */\n gt(other: any): LoDashExplicitWrapper;\n }\n\n //_.gte\n interface LoDashStatic {\n /**\n * Checks if value is greater than or equal to other.\n *\n * @param value The value to compare.\n * @param other The other value to compare.\n * @return Returns true if value is greater than or equal to other, else false.\n */\n gte(\n value: any,\n other: any\n ): boolean;\n }\n\n interface LoDashImplicitWrapperBase {\n /**\n * @see _.gte\n */\n gte(other: any): boolean;\n }\n\n interface LoDashExplicitWrapperBase {\n /**\n * @see _.gte\n */\n gte(other: any): LoDashExplicitWrapper;\n }\n\n //_.isArguments\n interface LoDashStatic {\n /**\n * Checks if value is classified as an arguments object.\n *\n * @param value The value to check.\n * @return Returns true if value is correctly classified, else false.\n */\n isArguments(value?: any): value is IArguments;\n }\n\n interface LoDashImplicitWrapperBase {\n /**\n * @see _.isArguments\n */\n isArguments(): boolean;\n }\n\n interface LoDashExplicitWrapperBase {\n /**\n * @see _.isArguments\n */\n isArguments(): LoDashExplicitWrapper;\n }\n\n //_.isArray\n interface LoDashStatic {\n /**\n * Checks if value is classified as an Array object.\n * @param value The value to check.\n *\n * @return Returns true if value is correctly classified, else false.\n */\n isArray(value?: any): value is any[];\n\n /**\n * DEPRECATED\n */\n isArray(value?: any): value is any[];\n }\n\n interface LoDashImplicitWrapperBase {\n /**\n * @see _.isArray\n */\n isArray(): boolean;\n }\n\n interface LoDashExplicitWrapperBase {\n /**\n * @see _.isArray\n */\n isArray(): LoDashExplicitWrapper;\n }\n\n //_.isArrayBuffer\n interface LoDashStatic {\n /**\n * Checks if value is classified as an ArrayBuffer object.\n *\n * @param value The value to check.\n * @return Returns true if value is correctly classified, else false.\n */\n isArrayBuffer(value?: any): value is ArrayBuffer;\n }\n\n interface LoDashImplicitWrapperBase {\n /**\n * @see _.isArrayBuffer\n */\n isArrayBuffer(): boolean;\n }\n\n interface LoDashExplicitWrapperBase {\n /**\n * @see _.isArrayBuffer\n */\n isArrayBuffer(): LoDashExplicitWrapper;\n }\n\n //_.isArrayLike\n interface LoDashStatic {\n /**\n * Checks if `value` is array-like. A value is considered array-like if it's\n * not a function and has a `value.length` that's an integer greater than or\n * equal to `0` and less than or equal to `Number.MAX_SAFE_INTEGER`.\n *\n * @static\n * @memberOf _\n * @type Function\n * @category Lang\n * @param {*} value The value to check.\n * @returns {boolean} Returns `true` if `value` is array-like, else `false`.\n * @example\n *\n * _.isArrayLike([1, 2, 3]);\n * // => true\n *\n * _.isArrayLike(document.body.children);\n * // => true\n *\n * _.isArrayLike('abc');\n * // => true\n *\n * _.isArrayLike(_.noop);\n * // => false\n */\n isArrayLike(value: T & string & number): boolean; // should only match if T = any\n\n /**\n * @see _.isArrayLike\n */\n isArrayLike(value?: Function): value is never;\n\n /**\n * @see _.isArrayLike\n */\n isArrayLike(value: T | Function): value is T & { length: number };\n\n /**\n * DEPRECATED\n */\n isArrayLike(value?: any): value is any[];\n }\n\n interface LoDashImplicitWrapperBase {\n /**\n * @see _.isArrayLike\n */\n isArrayLike(): boolean;\n }\n\n interface LoDashExplicitWrapperBase {\n /**\n * @see _.isArrayLike\n */\n isArrayLike(): LoDashExplicitWrapper;\n }\n\n //_.isArrayLikeObject\n interface LoDashStatic {\n /**\n * This method is like `_.isArrayLike` except that it also checks if `value`\n * is an object.\n *\n * @static\n * @memberOf _\n * @type Function\n * @category Lang\n * @param {*} value The value to check.\n * @returns {boolean} Returns `true` if `value` is an array-like object, else `false`.\n * @example\n *\n * _.isArrayLikeObject([1, 2, 3]);\n * // => true\n *\n * _.isArrayLikeObject(document.body.children);\n * // => true\n *\n * _.isArrayLikeObject('abc');\n * // => false\n *\n * _.isArrayLikeObject(_.noop);\n * // => false\n */\n isArrayLikeObject(value: T & string & number): boolean; // should only match if T = any\n\n /**\n * @see _.isArrayLike\n */\n isArrayLikeObject(value?: Function | string | boolean | number): value is never;\n\n /**\n * @see _.isArrayLike\n */\n isArrayLikeObject(value: T | Function | string | boolean | number): value is T & { length: number };\n\n /**\n * DEPRECATED\n */\n isArrayLikeObject(value?: any): value is any[];\n }\n\n interface LoDashImplicitWrapperBase {\n /**\n * @see _.isArrayLikeObject\n */\n isArrayLikeObject(): boolean;\n }\n\n interface LoDashExplicitWrapperBase {\n /**\n * @see _.isArrayLikeObject\n */\n isArrayLikeObject(): LoDashExplicitWrapper;\n }\n\n //_.isBoolean\n interface LoDashStatic {\n /**\n * Checks if value is classified as a boolean primitive or object.\n *\n * @param value The value to check.\n * @return Returns true if value is correctly classified, else false.\n */\n isBoolean(value?: any): value is boolean;\n }\n\n interface LoDashImplicitWrapperBase {\n /**\n * @see _.isBoolean\n */\n isBoolean(): boolean;\n }\n\n interface LoDashExplicitWrapperBase {\n /**\n * @see _.isBoolean\n */\n isBoolean(): LoDashExplicitWrapper;\n }\n\n //_.isBuffer\n interface LoDashStatic {\n /**\n * Checks if value is a buffer.\n *\n * @param value The value to check.\n * @return Returns true if value is a buffer, else false.\n */\n isBuffer(value?: any): boolean;\n }\n\n interface LoDashImplicitWrapperBase {\n /**\n * @see _.isBuffer\n */\n isBuffer(): boolean;\n }\n\n interface LoDashExplicitWrapperBase {\n /**\n * @see _.isBuffer\n */\n isBuffer(): LoDashExplicitWrapper;\n }\n\n //_.isDate\n interface LoDashStatic {\n /**\n * Checks if value is classified as a Date object.\n * @param value The value to check.\n *\n * @return Returns true if value is correctly classified, else false.\n */\n isDate(value?: any): value is Date;\n }\n\n interface LoDashImplicitWrapperBase {\n /**\n * @see _.isDate\n */\n isDate(): boolean;\n }\n\n interface LoDashExplicitWrapperBase {\n /**\n * @see _.isDate\n */\n isDate(): LoDashExplicitWrapper;\n }\n\n //_.isElement\n interface LoDashStatic {\n /**\n * Checks if value is a DOM element.\n *\n * @param value The value to check.\n * @return Returns true if value is a DOM element, else false.\n */\n isElement(value?: any): boolean;\n }\n\n interface LoDashImplicitWrapperBase {\n /**\n * @see _.isElement\n */\n isElement(): boolean;\n }\n\n interface LoDashExplicitWrapperBase {\n /**\n * @see _.isElement\n */\n isElement(): LoDashExplicitWrapper;\n }\n\n //_.isEmpty\n interface LoDashStatic {\n /**\n * Checks if value is empty. A value is considered empty unless it’s an arguments object, array, string, or\n * jQuery-like collection with a length greater than 0 or an object with own enumerable properties.\n *\n * @param value The value to inspect.\n * @return Returns true if value is empty, else false.\n */\n isEmpty(value?: any): boolean;\n }\n\n interface LoDashImplicitWrapperBase {\n /**\n * @see _.isEmpty\n */\n isEmpty(): boolean;\n }\n\n interface LoDashExplicitWrapperBase {\n /**\n * @see _.isEmpty\n */\n isEmpty(): LoDashExplicitWrapper;\n }\n\n //_.isEqual\n interface LoDashStatic {\n /**\n * Performs a deep comparison between two values to determine if they are\n * equivalent.\n *\n * **Note:** This method supports comparing arrays, array buffers, booleans,\n * date objects, error objects, maps, numbers, `Object` objects, regexes,\n * sets, strings, symbols, and typed arrays. `Object` objects are compared\n * by their own, not inherited, enumerable properties. Functions and DOM\n * nodes are **not** supported.\n *\n * @static\n * @memberOf _\n * @category Lang\n * @param {*} value The value to compare.\n * @param {*} other The other value to compare.\n * @returns {boolean} Returns `true` if the values are equivalent, else `false`.\n * @example\n *\n * var object = { 'user': 'fred' };\n * var other = { 'user': 'fred' };\n *\n * _.isEqual(object, other);\n * // => true\n *\n * object === other;\n * // => false\n */\n isEqual(\n value: any,\n other: any\n ): boolean;\n }\n\n interface LoDashImplicitWrapperBase {\n /**\n * @see _.isEqual\n */\n isEqual(\n other: any\n ): boolean;\n }\n\n interface LoDashExplicitWrapperBase {\n /**\n * @see _.isEqual\n */\n isEqual(\n other: any\n ): LoDashExplicitWrapper;\n }\n\n // _.isEqualWith\n type IsEqualCustomizer = (value: any, other: any, indexOrKey: number|string|undefined, parent: any, otherParent: any, stack: any) => boolean|undefined;\n\n interface LoDashStatic {\n /**\n * This method is like `_.isEqual` except that it accepts `customizer` which is\n * invoked to compare values. If `customizer` returns `undefined` comparisons are\n * handled by the method instead. The `customizer` is invoked with up to seven arguments:\n * (objValue, othValue [, index|key, object, other, stack]).\n *\n * @static\n * @memberOf _\n * @category Lang\n * @param {*} value The value to compare.\n * @param {*} other The other value to compare.\n * @param {Function} [customizer] The function to customize comparisons.\n * @returns {boolean} Returns `true` if the values are equivalent, else `false`.\n * @example\n *\n * function isGreeting(value) {\n * return /^h(?:i|ello)$/.test(value);\n * }\n *\n * function customizer(objValue, othValue) {\n * if (isGreeting(objValue) && isGreeting(othValue)) {\n * return true;\n * }\n * }\n *\n * var array = ['hello', 'goodbye'];\n * var other = ['hi', 'goodbye'];\n *\n * _.isEqualWith(array, other, customizer);\n * // => true\n */\n isEqualWith(\n value: any,\n other: any,\n customizer?: IsEqualCustomizer\n ): boolean;\n }\n\n interface LoDashImplicitWrapperBase {\n /**\n * @see _.isEqualWith\n */\n isEqualWith(\n other: any,\n customizer?: IsEqualCustomizer\n ): boolean;\n }\n\n interface LoDashExplicitWrapperBase {\n /**\n * @see _.isEqualWith\n */\n isEqualWith(\n other: any,\n customizer?: IsEqualCustomizer\n ): LoDashExplicitWrapper;\n }\n\n //_.isError\n interface LoDashStatic {\n /**\n * Checks if value is an Error, EvalError, RangeError, ReferenceError, SyntaxError, TypeError, or URIError\n * object.\n *\n * @param value The value to check.\n * @return Returns true if value is an error object, else false.\n */\n isError(value: any): value is Error;\n }\n\n interface LoDashImplicitWrapperBase {\n /**\n * @see _.isError\n */\n isError(): boolean;\n }\n\n interface LoDashExplicitWrapperBase {\n /**\n * @see _.isError\n */\n isError(): LoDashExplicitWrapper;\n }\n\n //_.isFinite\n interface LoDashStatic {\n /**\n * Checks if value is a finite primitive number.\n *\n * Note: This method is based on Number.isFinite.\n *\n * @param value The value to check.\n * @return Returns true if value is a finite number, else false.\n */\n isFinite(value?: any): boolean;\n }\n\n interface LoDashImplicitWrapperBase {\n /**\n * @see _.isFinite\n */\n isFinite(): boolean;\n }\n\n interface LoDashExplicitWrapperBase {\n /**\n * @see _.isFinite\n */\n isFinite(): LoDashExplicitWrapper;\n }\n\n //_.isFunction\n interface LoDashStatic {\n /**\n * Checks if value is classified as a Function object.\n *\n * @param value The value to check.\n * @return Returns true if value is correctly classified, else false.\n */\n isFunction(value?: any): value is Function;\n }\n\n interface LoDashImplicitWrapperBase {\n /**\n * @see _.isFunction\n */\n isFunction(): boolean;\n }\n\n interface LoDashExplicitWrapperBase {\n /**\n * @see _.isFunction\n */\n isFunction(): LoDashExplicitWrapper;\n }\n\n //_.isInteger\n interface LoDashStatic {\n /**\n * Checks if `value` is an integer.\n *\n * **Note:** This method is based on [`Number.isInteger`](https://mdn.io/Number/isInteger).\n *\n * @static\n * @memberOf _\n * @category Lang\n * @param {*} value The value to check.\n * @returns {boolean} Returns `true` if `value` is an integer, else `false`.\n * @example\n *\n * _.isInteger(3);\n * // => true\n *\n * _.isInteger(Number.MIN_VALUE);\n * // => false\n *\n * _.isInteger(Infinity);\n * // => false\n *\n * _.isInteger('3');\n * // => false\n */\n isInteger(value?: any): boolean;\n }\n\n interface LoDashImplicitWrapperBase {\n /**\n * @see _.isInteger\n */\n isInteger(): boolean;\n }\n\n interface LoDashExplicitWrapperBase {\n /**\n * @see _.isInteger\n */\n isInteger(): LoDashExplicitWrapper;\n }\n\n //_.isLength\n interface LoDashStatic {\n /**\n * Checks if `value` is a valid array-like length.\n *\n * **Note:** This function is loosely based on [`ToLength`](http://ecma-international.org/ecma-262/6.0/#sec-tolength).\n *\n * @static\n * @memberOf _\n * @category Lang\n * @param {*} value The value to check.\n * @returns {boolean} Returns `true` if `value` is a valid length, else `false`.\n * @example\n *\n * _.isLength(3);\n * // => true\n *\n * _.isLength(Number.MIN_VALUE);\n * // => false\n *\n * _.isLength(Infinity);\n * // => false\n *\n * _.isLength('3');\n * // => false\n */\n isLength(value?: any): boolean;\n }\n\n interface LoDashImplicitWrapperBase {\n /**\n * @see _.isLength\n */\n isLength(): boolean;\n }\n\n interface LoDashExplicitWrapperBase {\n /**\n * @see _.isLength\n */\n isLength(): LoDashExplicitWrapper;\n }\n\n //_.isMap\n interface LoDashStatic {\n /**\n * Checks if value is classified as a Map object.\n *\n * @param value The value to check.\n * @returns Returns true if value is correctly classified, else false.\n */\n isMap(value?: any): value is Map;\n }\n\n interface LoDashImplicitWrapperBase {\n /**\n * @see _.isMap\n */\n isMap(): boolean;\n }\n\n interface LoDashExplicitWrapperBase {\n /**\n * @see _.isMap\n */\n isMap(): LoDashExplicitWrapper;\n }\n\n //_.isMatch\n type isMatchCustomizer = (value: any, other: any, indexOrKey?: number|string) => boolean;\n\n interface LoDashStatic {\n /**\n * Performs a deep comparison between `object` and `source` to determine if\n * `object` contains equivalent property values.\n *\n * **Note:** This method supports comparing the same values as `_.isEqual`.\n *\n * @static\n * @memberOf _\n * @category Lang\n * @param {Object} object The object to inspect.\n * @param {Object} source The object of property values to match.\n * @returns {boolean} Returns `true` if `object` is a match, else `false`.\n * @example\n *\n * var object = { 'user': 'fred', 'age': 40 };\n *\n * _.isMatch(object, { 'age': 40 });\n * // => true\n *\n * _.isMatch(object, { 'age': 36 });\n * // => false\n */\n isMatch(object: Object, source: Object): boolean;\n }\n\n interface LoDashImplicitObjectWrapper {\n /**\n * @see _.isMatch\n */\n isMatch(source: Object): boolean;\n }\n\n //_.isMatchWith\n type isMatchWithCustomizer = (value: any, other: any, indexOrKey?: number|string) => boolean;\n\n interface LoDashStatic {\n /**\n * This method is like `_.isMatch` except that it accepts `customizer` which\n * is invoked to compare values. If `customizer` returns `undefined` comparisons\n * are handled by the method instead. The `customizer` is invoked with three\n * arguments: (objValue, srcValue, index|key, object, source).\n *\n * @static\n * @memberOf _\n * @category Lang\n * @param {Object} object The object to inspect.\n * @param {Object} source The object of property values to match.\n * @param {Function} [customizer] The function to customize comparisons.\n * @returns {boolean} Returns `true` if `object` is a match, else `false`.\n * @example\n *\n * function isGreeting(value) {\n * return /^h(?:i|ello)$/.test(value);\n * }\n *\n * function customizer(objValue, srcValue) {\n * if (isGreeting(objValue) && isGreeting(srcValue)) {\n * return true;\n * }\n * }\n *\n * var object = { 'greeting': 'hello' };\n * var source = { 'greeting': 'hi' };\n *\n * _.isMatchWith(object, source, customizer);\n * // => true\n */\n isMatchWith(object: Object, source: Object, customizer: isMatchWithCustomizer): boolean;\n }\n\n interface LoDashImplicitObjectWrapper {\n /**\n * @see _.isMatchWith\n */\n isMatchWith(source: Object, customizer: isMatchWithCustomizer): boolean;\n }\n\n //_.isNaN\n interface LoDashStatic {\n /**\n * Checks if value is NaN.\n *\n * Note: This method is not the same as isNaN which returns true for undefined and other non-numeric values.\n *\n * @param value The value to check.\n * @return Returns true if value is NaN, else false.\n */\n isNaN(value?: any): boolean;\n }\n\n interface LoDashImplicitWrapperBase {\n /**\n * @see _.isNaN\n */\n isNaN(): boolean;\n }\n\n interface LoDashExplicitWrapperBase {\n /**\n * @see _.isNaN\n */\n isNaN(): LoDashExplicitWrapper;\n }\n\n //_.isNative\n interface LoDashStatic {\n /**\n * Checks if value is a native function.\n * @param value The value to check.\n *\n * @retrun Returns true if value is a native function, else false.\n */\n isNative(value: any): value is Function;\n }\n\n interface LoDashImplicitWrapperBase {\n /**\n * see _.isNative\n */\n isNative(): boolean;\n }\n\n interface LoDashExplicitWrapperBase {\n /**\n * see _.isNative\n */\n isNative(): LoDashExplicitWrapper;\n }\n\n //_.isNil\n interface LoDashStatic {\n /**\n * Checks if `value` is `null` or `undefined`.\n *\n * @static\n * @memberOf _\n * @category Lang\n * @param {*} value The value to check.\n * @returns {boolean} Returns `true` if `value` is nullish, else `false`.\n * @example\n *\n * _.isNil(null);\n * // => true\n *\n * _.isNil(void 0);\n * // => true\n *\n * _.isNil(NaN);\n * // => false\n */\n isNil(value: any): value is null | undefined;\n }\n\n interface LoDashImplicitWrapperBase {\n /**\n * see _.isNil\n */\n isNil(): boolean;\n }\n\n interface LoDashExplicitWrapperBase {\n /**\n * see _.isNil\n */\n isNil(): LoDashExplicitWrapper;\n }\n\n //_.isNull\n interface LoDashStatic {\n /**\n * Checks if value is null.\n *\n * @param value The value to check.\n * @return Returns true if value is null, else false.\n */\n isNull(value: any): value is null;\n }\n\n interface LoDashImplicitWrapperBase {\n /**\n * see _.isNull\n */\n isNull(): boolean;\n }\n\n interface LoDashExplicitWrapperBase {\n /**\n * see _.isNull\n */\n isNull(): LoDashExplicitWrapper;\n }\n\n //_.isNumber\n interface LoDashStatic {\n /**\n * Checks if value is classified as a Number primitive or object.\n *\n * Note: To exclude Infinity, -Infinity, and NaN, which are classified as numbers, use the _.isFinite method.\n *\n * @param value The value to check.\n * @return Returns true if value is correctly classified, else false.\n */\n isNumber(value?: any): value is number;\n }\n\n interface LoDashImplicitWrapperBase {\n /**\n * see _.isNumber\n */\n isNumber(): boolean;\n }\n\n interface LoDashExplicitWrapperBase {\n /**\n * see _.isNumber\n */\n isNumber(): LoDashExplicitWrapper;\n }\n\n //_.isObject\n interface LoDashStatic {\n /**\n * Checks if value is the language type of Object. (e.g. arrays, functions, objects, regexes, new Number(0),\n * and new String(''))\n *\n * @param value The value to check.\n * @return Returns true if value is an object, else false.\n */\n isObject(value?: any): boolean;\n }\n\n interface LoDashImplicitWrapperBase {\n /**\n * see _.isObject\n */\n isObject(): boolean;\n }\n\n interface LoDashExplicitWrapperBase {\n /**\n * see _.isObject\n */\n isObject(): LoDashExplicitWrapper;\n }\n\n //_.isObjectLike\n interface LoDashStatic {\n /**\n * Checks if `value` is object-like. A value is object-like if it's not `null`\n * and has a `typeof` result of \"object\".\n *\n * @static\n * @memberOf _\n * @category Lang\n * @param {*} value The value to check.\n * @returns {boolean} Returns `true` if `value` is object-like, else `false`.\n * @example\n *\n * _.isObjectLike({});\n * // => true\n *\n * _.isObjectLike([1, 2, 3]);\n * // => true\n *\n * _.isObjectLike(_.noop);\n * // => false\n *\n * _.isObjectLike(null);\n * // => false\n */\n isObjectLike(value?: any): boolean;\n }\n\n interface LoDashImplicitWrapperBase {\n /**\n * see _.isObjectLike\n */\n isObjectLike(): boolean;\n }\n\n interface LoDashExplicitWrapperBase {\n /**\n * see _.isObjectLike\n */\n isObjectLike(): LoDashExplicitWrapper;\n }\n\n //_.isPlainObject\n interface LoDashStatic {\n /**\n * Checks if value is a plain object, that is, an object created by the Object constructor or one with a\n * [[Prototype]] of null.\n *\n * Note: This method assumes objects created by the Object constructor have no inherited enumerable properties.\n *\n * @param value The value to check.\n * @return Returns true if value is a plain object, else false.\n */\n isPlainObject(value?: any): boolean;\n }\n\n interface LoDashImplicitWrapperBase {\n /**\n * see _.isPlainObject\n */\n isPlainObject(): boolean;\n }\n\n interface LoDashExplicitWrapperBase {\n /**\n * see _.isPlainObject\n */\n isPlainObject(): LoDashExplicitWrapper;\n }\n\n //_.isRegExp\n interface LoDashStatic {\n /**\n * Checks if value is classified as a RegExp object.\n * @param value The value to check.\n *\n * @return Returns true if value is correctly classified, else false.\n */\n isRegExp(value?: any): value is RegExp;\n }\n\n interface LoDashImplicitWrapperBase {\n /**\n * see _.isRegExp\n */\n isRegExp(): boolean;\n }\n\n interface LoDashExplicitWrapperBase {\n /**\n * see _.isRegExp\n */\n isRegExp(): LoDashExplicitWrapper;\n }\n\n //_.isSafeInteger\n interface LoDashStatic {\n /**\n * Checks if `value` is a safe integer. An integer is safe if it's an IEEE-754\n * double precision number which isn't the result of a rounded unsafe integer.\n *\n * **Note:** This method is based on [`Number.isSafeInteger`](https://mdn.io/Number/isSafeInteger).\n *\n * @static\n * @memberOf _\n * @category Lang\n * @param {*} value The value to check.\n * @returns {boolean} Returns `true` if `value` is a safe integer, else `false`.\n * @example\n *\n * _.isSafeInteger(3);\n * // => true\n *\n * _.isSafeInteger(Number.MIN_VALUE);\n * // => false\n *\n * _.isSafeInteger(Infinity);\n * // => false\n *\n * _.isSafeInteger('3');\n * // => false\n */\n isSafeInteger(value: any): boolean;\n }\n\n interface LoDashImplicitWrapperBase {\n /**\n * see _.isSafeInteger\n */\n isSafeInteger(): boolean;\n }\n\n interface LoDashExplicitWrapperBase {\n /**\n * see _.isSafeInteger\n */\n isSafeInteger(): LoDashExplicitWrapper;\n }\n\n //_.isSet\n interface LoDashStatic {\n /**\n * Checks if value is classified as a Set object.\n *\n * @param value The value to check.\n * @returns Returns true if value is correctly classified, else false.\n */\n isSet(value?: any): value is Set;\n }\n\n interface LoDashImplicitWrapperBase {\n /**\n * @see _.isSet\n */\n isSet(): boolean;\n }\n\n interface LoDashExplicitWrapperBase {\n /**\n * @see _.isSet\n */\n isSet(): LoDashExplicitWrapper;\n }\n\n //_.isString\n interface LoDashStatic {\n /**\n * Checks if value is classified as a String primitive or object.\n *\n * @param value The value to check.\n * @return Returns true if value is correctly classified, else false.\n */\n isString(value?: any): value is string;\n }\n\n interface LoDashImplicitWrapperBase {\n /**\n * see _.isString\n */\n isString(): boolean;\n }\n\n interface LoDashExplicitWrapperBase {\n /**\n * see _.isString\n */\n isString(): LoDashExplicitWrapper;\n }\n\n //_.isSymbol\n interface LoDashStatic {\n /**\n * Checks if `value` is classified as a `Symbol` primitive or object.\n *\n * @static\n * @memberOf _\n * @category Lang\n * @param {*} value The value to check.\n * @returns {boolean} Returns `true` if `value` is correctly classified, else `false`.\n * @example\n *\n * _.isSymbol(Symbol.iterator);\n * // => true\n *\n * _.isSymbol('abc');\n * // => false\n */\n isSymbol(value: any): boolean;\n }\n\n interface LoDashImplicitWrapperBase {\n /**\n * see _.isSymbol\n */\n isSymbol(): boolean;\n }\n\n interface LoDashExplicitWrapperBase {\n /**\n * see _.isSymbol\n */\n isSymbol(): LoDashExplicitWrapper;\n }\n\n //_.isTypedArray\n interface LoDashStatic {\n /**\n * Checks if value is classified as a typed array.\n *\n * @param value The value to check.\n * @return Returns true if value is correctly classified, else false.\n */\n isTypedArray(value: any): boolean;\n }\n\n interface LoDashImplicitWrapperBase {\n /**\n * see _.isTypedArray\n */\n isTypedArray(): boolean;\n }\n\n interface LoDashExplicitWrapperBase {\n /**\n * see _.isTypedArray\n */\n isTypedArray(): LoDashExplicitWrapper;\n }\n\n //_.isUndefined\n interface LoDashStatic {\n /**\n * Checks if value is undefined.\n *\n * @param value The value to check.\n * @return Returns true if value is undefined, else false.\n */\n isUndefined(value: any): value is undefined;\n }\n\n interface LoDashImplicitWrapperBase {\n /**\n * see _.isUndefined\n */\n isUndefined(): boolean;\n }\n\n interface LoDashExplicitWrapperBase {\n /**\n * see _.isUndefined\n */\n isUndefined(): LoDashExplicitWrapper;\n }\n\n //_.isWeakMap\n interface LoDashStatic {\n /**\n * Checks if value is classified as a WeakMap object.\n *\n * @param value The value to check.\n * @returns Returns true if value is correctly classified, else false.\n */\n isWeakMap(value?: any): value is WeakMap;\n }\n\n interface LoDashImplicitWrapperBase {\n /**\n * @see _.isSet\n */\n isWeakMap(): boolean;\n }\n\n interface LoDashExplicitWrapperBase {\n /**\n * @see _.isSet\n */\n isWeakMap(): LoDashExplicitWrapper;\n }\n\n //_.isWeakSet\n interface LoDashStatic {\n /**\n * Checks if value is classified as a WeakSet object.\n *\n * @param value The value to check.\n * @returns Returns true if value is correctly classified, else false.\n */\n isWeakSet(value?: any): value is WeakSet;\n }\n\n interface LoDashImplicitWrapperBase {\n /**\n * @see _.isWeakSet\n */\n isWeakSet(): boolean;\n }\n\n interface LoDashExplicitWrapperBase {\n /**\n * @see _.isWeakSet\n */\n isWeakSet(): LoDashExplicitWrapper;\n }\n\n //_.lt\n interface LoDashStatic {\n /**\n * Checks if value is less than other.\n *\n * @param value The value to compare.\n * @param other The other value to compare.\n * @return Returns true if value is less than other, else false.\n */\n lt(\n value: any,\n other: any\n ): boolean;\n }\n\n interface LoDashImplicitWrapperBase {\n /**\n * @see _.lt\n */\n lt(other: any): boolean;\n }\n\n interface LoDashExplicitWrapperBase {\n /**\n * @see _.lt\n */\n lt(other: any): LoDashExplicitWrapper;\n }\n\n //_.lte\n interface LoDashStatic {\n /**\n * Checks if value is less than or equal to other.\n *\n * @param value The value to compare.\n * @param other The other value to compare.\n * @return Returns true if value is less than or equal to other, else false.\n */\n lte(\n value: any,\n other: any\n ): boolean;\n }\n\n interface LoDashImplicitWrapperBase {\n /**\n * @see _.lte\n */\n lte(other: any): boolean;\n }\n\n interface LoDashExplicitWrapperBase {\n /**\n * @see _.lte\n */\n lte(other: any): LoDashExplicitWrapper;\n }\n\n //_.toArray\n interface LoDashStatic {\n /**\n * Converts value to an array.\n *\n * @param value The value to convert.\n * @return Returns the converted array.\n */\n toArray(value: List|Dictionary|NumericDictionary): T[];\n\n /**\n * @see _.toArray\n */\n toArray(value: TValue): TResult[];\n\n /**\n * @see _.toArray\n */\n toArray(value?: any): TResult[];\n }\n\n interface LoDashImplicitWrapper {\n /**\n * @see _.toArray\n */\n toArray(): LoDashImplicitArrayWrapper;\n }\n\n interface LoDashImplicitArrayWrapperBase {\n /**\n * @see _.toArray\n */\n toArray(): LoDashImplicitArrayWrapper;\n }\n\n interface LoDashImplicitObjectWrapperBase {\n /**\n * @see _.toArray\n */\n toArray(): LoDashImplicitArrayWrapper;\n }\n\n interface LoDashExplicitWrapper {\n /**\n * @see _.toArray\n */\n toArray(): LoDashExplicitArrayWrapper;\n }\n\n interface LoDashExplicitArrayWrapperBase {\n /**\n * @see _.toArray\n */\n toArray(): LoDashExplicitArrayWrapper;\n }\n\n interface LoDashExplicitObjectWrapperBase {\n /**\n * @see _.toArray\n */\n toArray(): LoDashExplicitArrayWrapper;\n }\n\n //_.toPlainObject\n interface LoDashStatic {\n /**\n * Converts value to a plain object flattening inherited enumerable properties of value to own properties\n * of the plain object.\n *\n * @param value The value to convert.\n * @return Returns the converted plain object.\n */\n toPlainObject(value?: any): TResult;\n }\n\n interface LoDashImplicitWrapperBase {\n /**\n * @see _.toPlainObject\n */\n toPlainObject(): LoDashImplicitObjectWrapper;\n }\n\n //_.toFinite\n interface LoDashStatic {\n /**\n * Converts `value` to a finite number.\n *\n * @static\n * @memberOf _\n * @since 4.12.0\n * @category Lang\n * @param {*} value The value to convert.\n * @returns {number} Returns the converted number.\n * @example\n *\n * _.toFinite(3.2);\n * // => 3.2\n *\n * _.toFinite(Number.MIN_VALUE);\n * // => 5e-324\n *\n * _.toFinite(Infinity);\n * // => 1.7976931348623157e+308\n *\n * _.toFinite('3.2');\n * // => 3.2\n */\n toFinite(value: any): number;\n }\n\n interface LoDashImplicitWrapperBase {\n /**\n * @see _.toFinite\n */\n toFinite(): LoDashImplicitWrapper;\n }\n\n interface LoDashExplicitWrapperBase {\n /**\n * @see _.toFinite\n */\n toFinite(): LoDashExplicitWrapper;\n }\n\n //_.toInteger\n interface LoDashStatic {\n /**\n * Converts `value` to an integer.\n *\n * **Note:** This function is loosely based on [`ToInteger`](http://www.ecma-international.org/ecma-262/6.0/#sec-tointeger).\n *\n * @static\n * @memberOf _\n * @category Lang\n * @param {*} value The value to convert.\n * @returns {number} Returns the converted integer.\n * @example\n *\n * _.toInteger(3);\n * // => 3\n *\n * _.toInteger(Number.MIN_VALUE);\n * // => 0\n *\n * _.toInteger(Infinity);\n * // => 1.7976931348623157e+308\n *\n * _.toInteger('3');\n * // => 3\n */\n toInteger(value: any): number;\n }\n\n interface LoDashImplicitWrapperBase {\n /**\n * @see _.toInteger\n */\n toInteger(): LoDashImplicitWrapper;\n }\n\n interface LoDashExplicitWrapperBase {\n /**\n * @see _.toInteger\n */\n toInteger(): LoDashExplicitWrapper;\n }\n\n //_.toLength\n interface LoDashStatic {\n /**\n * Converts `value` to an integer suitable for use as the length of an\n * array-like object.\n *\n * **Note:** This method is based on [`ToLength`](http://ecma-international.org/ecma-262/6.0/#sec-tolength).\n *\n * @static\n * @memberOf _\n * @category Lang\n * @param {*} value The value to convert.\n * @return {number} Returns the converted integer.\n * @example\n *\n * _.toLength(3);\n * // => 3\n *\n * _.toLength(Number.MIN_VALUE);\n * // => 0\n *\n * _.toLength(Infinity);\n * // => 4294967295\n *\n * _.toLength('3');\n * // => 3\n */\n toLength(value: any): number;\n }\n\n interface LoDashImplicitWrapperBase {\n /**\n * @see _.toLength\n */\n toLength(): LoDashImplicitWrapper;\n }\n\n interface LoDashExplicitWrapperBase {\n /**\n * @see _.toLength\n */\n toLength(): LoDashExplicitWrapper;\n }\n\n //_.toNumber\n interface LoDashStatic {\n /**\n * Converts `value` to a number.\n *\n * @static\n * @memberOf _\n * @category Lang\n * @param {*} value The value to process.\n * @returns {number} Returns the number.\n * @example\n *\n * _.toNumber(3);\n * // => 3\n *\n * _.toNumber(Number.MIN_VALUE);\n * // => 5e-324\n *\n * _.toNumber(Infinity);\n * // => Infinity\n *\n * _.toNumber('3');\n * // => 3\n */\n toNumber(value: any): number;\n }\n\n interface LoDashImplicitWrapperBase {\n /**\n * @see _.toNumber\n */\n toNumber(): LoDashImplicitWrapper;\n }\n\n interface LoDashExplicitWrapperBase {\n /**\n * @see _.toNumber\n */\n toNumber(): LoDashExplicitWrapper;\n }\n\n //_.toSafeInteger\n interface LoDashStatic {\n /**\n * Converts `value` to a safe integer. A safe integer can be compared and\n * represented correctly.\n *\n * @static\n * @memberOf _\n * @category Lang\n * @param {*} value The value to convert.\n * @returns {number} Returns the converted integer.\n * @example\n *\n * _.toSafeInteger(3);\n * // => 3\n *\n * _.toSafeInteger(Number.MIN_VALUE);\n * // => 0\n *\n * _.toSafeInteger(Infinity);\n * // => 9007199254740991\n *\n * _.toSafeInteger('3');\n * // => 3\n */\n toSafeInteger(value: any): number;\n }\n\n interface LoDashImplicitWrapperBase {\n /**\n * @see _.toSafeInteger\n */\n toSafeInteger(): LoDashImplicitWrapper;\n }\n\n interface LoDashExplicitWrapperBase {\n /**\n * @see _.toSafeInteger\n */\n toSafeInteger(): LoDashExplicitWrapper;\n }\n\n //_.toString DUMMY\n interface LoDashStatic {\n /**\n * Converts `value` to a string if it's not one. An empty string is returned\n * for `null` and `undefined` values. The sign of `-0` is preserved.\n *\n * @static\n * @memberOf _\n * @category Lang\n * @param {*} value The value to process.\n * @returns {string} Returns the string.\n * @example\n *\n * _.toString(null);\n * // => ''\n *\n * _.toString(-0);\n * // => '-0'\n *\n * _.toString([1, 2, 3]);\n * // => '1,2,3'\n */\n toString(value: any): string;\n }\n\n /********\n * Math *\n ********/\n\n //_.add\n interface LoDashStatic {\n /**\n * Adds two numbers.\n *\n * @param augend The first number to add.\n * @param addend The second number to add.\n * @return Returns the sum.\n */\n add(\n augend: number,\n addend: number\n ): number;\n }\n\n interface LoDashImplicitWrapper {\n /**\n * @see _.add\n */\n add(addend: number): number;\n }\n\n interface LoDashExplicitWrapper {\n /**\n * @see _.add\n */\n add(addend: number): LoDashExplicitWrapper;\n }\n\n //_.ceil\n interface LoDashStatic {\n /**\n * Calculates n rounded up to precision.\n *\n * @param n The number to round up.\n * @param precision The precision to round up to.\n * @return Returns the rounded up number.\n */\n ceil(\n n: number,\n precision?: number\n ): number;\n }\n\n interface LoDashImplicitWrapper {\n /**\n * @see _.ceil\n */\n ceil(precision?: number): number;\n }\n\n interface LoDashExplicitWrapper {\n /**\n * @see _.ceil\n */\n ceil(precision?: number): LoDashExplicitWrapper;\n }\n\n //_.divide\n interface LoDashStatic {\n /**\n * Divide two numbers.\n *\n * @param dividend The first number in a division.\n * @param divisor The second number in a division.\n * @returns Returns the quotient.\n */\n divide(\n dividend: number,\n divisor: number\n ): number;\n }\n\n interface LoDashImplicitWrapper {\n /**\n * @see _.divide\n */\n divide(divisor: number): number;\n }\n\n interface LoDashExplicitWrapper {\n /**\n * @see _.divide\n */\n divide(divisor: number): LoDashExplicitWrapper;\n }\n\n //_.floor\n interface LoDashStatic {\n /**\n * Calculates n rounded down to precision.\n *\n * @param n The number to round down.\n * @param precision The precision to round down to.\n * @return Returns the rounded down number.\n */\n floor(\n n: number,\n precision?: number\n ): number;\n }\n\n interface LoDashImplicitWrapper {\n /**\n * @see _.floor\n */\n floor(precision?: number): number;\n }\n\n interface LoDashExplicitWrapper {\n /**\n * @see _.floor\n */\n floor(precision?: number): LoDashExplicitWrapper;\n }\n\n //_.max\n interface LoDashStatic {\n /**\n * Computes the maximum value of `array`. If `array` is empty or falsey\n * `undefined` is returned.\n *\n * @static\n * @memberOf _\n * @category Math\n * @param {Array} array The array to iterate over.\n * @returns {*} Returns the maximum value.\n */\n max(\n collection: List | null | undefined\n ): T | undefined;\n }\n\n interface LoDashImplicitArrayWrapperBase {\n /**\n * @see _.max\n */\n max(): T | undefined;\n }\n\n interface LoDashImplicitObjectWrapperBase {\n /**\n * @see _.max\n */\n max(): T | undefined;\n }\n\n //_.maxBy\n interface LoDashStatic {\n /**\n * This method is like `_.max` except that it accepts `iteratee` which is\n * invoked for each element in `array` to generate the criterion by which\n * the value is ranked. The iteratee is invoked with one argument: (value).\n *\n * @static\n * @memberOf _\n * @category Math\n * @param {Array} array The array to iterate over.\n * @param {Function|Object|string} [iteratee=_.identity] The iteratee invoked per element.\n * @returns {*} Returns the maximum value.\n * @example\n *\n * var objects = [{ 'n': 1 }, { 'n': 2 }];\n *\n * _.maxBy(objects, function(o) { return o.a; });\n * // => { 'n': 2 }\n *\n * // using the `_.property` iteratee shorthand\n * _.maxBy(objects, 'n');\n * // => { 'n': 2 }\n */\n maxBy(\n collection: List | null | undefined,\n iteratee?: ListIterator\n ): T | undefined;\n\n /**\n * @see _.maxBy\n */\n maxBy(\n collection: Dictionary | null | undefined,\n iteratee?: DictionaryIterator\n ): T | undefined;\n\n /**\n * @see _.maxBy\n */\n maxBy(\n collection: List|Dictionary | null | undefined,\n iteratee?: string\n ): T | undefined;\n\n /**\n * @see _.maxBy\n */\n maxBy(\n collection: List|Dictionary | null | undefined,\n whereValue?: TObject\n ): T | undefined;\n }\n\n interface LoDashImplicitArrayWrapperBase {\n /**\n * @see _.maxBy\n */\n maxBy(\n iteratee?: ListIterator\n ): T | undefined;\n\n /**\n * @see _.maxBy\n */\n maxBy(\n iteratee?: string\n ): T | undefined;\n\n /**\n * @see _.maxBy\n */\n maxBy(\n whereValue?: TObject\n ): T | undefined;\n }\n\n interface LoDashImplicitObjectWrapperBase {\n /**\n * @see _.maxBy\n */\n maxBy(\n iteratee?: ListIterator|DictionaryIterator\n ): T | undefined;\n\n /**\n * @see _.maxBy\n */\n maxBy(\n iteratee?: string\n ): T | undefined;\n\n /**\n * @see _.maxBy\n */\n maxBy(\n whereValue?: TObject\n ): T | undefined;\n }\n\n //_.mean\n interface LoDashStatic {\n /**\n * Computes the mean of the values in `array`.\n *\n * @static\n * @memberOf _\n * @category Math\n * @param {Array} array The array to iterate over.\n * @returns {number} Returns the mean.\n * @example\n *\n * _.mean([4, 2, 8, 6]);\n * // => 5\n */\n mean(\n collection: List | null | undefined\n ): number;\n }\n\n //_.meanBy\n interface LoDashStatic {\n /**\n * Computes the mean of the provided propties of the objects in the `array`\n *\n * @static\n * @memberOf _\n * @category Math\n * @param {Array} array The array to iterate over.\n * @param {Function|Object|string} [iteratee=_.identity] The iteratee invoked per element.\n * @returns {number} Returns the mean.\n * @example\n *\n * _.mean([{ 'n': 4 }, { 'n': 2 }, { 'n': 8 }, { 'n': 6 }], 'n');\n * // => 5\n */\n meanBy(\n collection: List | null | undefined,\n iteratee?: ListIterator | string\n ): number;\n\n meanBy(\n collection: Dictionary | null | undefined,\n iteratee?: DictionaryIterator | string\n ): number;\n }\n\n interface LoDashImplicitArrayWrapperBase {\n /**\n * @see _.mean\n */\n mean(): number;\n\n /**\n * @see _.mean\n */\n mean(): number;\n }\n\n //_.min\n interface LoDashStatic {\n /**\n * Computes the minimum value of `array`. If `array` is empty or falsey\n * `undefined` is returned.\n *\n * @static\n * @memberOf _\n * @category Math\n * @param {Array} array The array to iterate over.\n * @returns {*} Returns the minimum value.\n */\n min(\n collection: List | null | undefined\n ): T | undefined;\n }\n\n interface LoDashImplicitArrayWrapperBase {\n /**\n * @see _.min\n */\n min(): T | undefined;\n }\n\n interface LoDashImplicitObjectWrapperBase {\n /**\n * @see _.min\n */\n min(): T | undefined;\n }\n\n //_.minBy\n interface LoDashStatic {\n /**\n * This method is like `_.min` except that it accepts `iteratee` which is\n * invoked for each element in `array` to generate the criterion by which\n * the value is ranked. The iteratee is invoked with one argument: (value).\n *\n * @static\n * @memberOf _\n * @category Math\n * @param {Array} array The array to iterate over.\n * @param {Function|Object|string} [iteratee=_.identity] The iteratee invoked per element.\n * @returns {*} Returns the minimum value.\n * @example\n *\n * var objects = [{ 'n': 1 }, { 'n': 2 }];\n *\n * _.minBy(objects, function(o) { return o.a; });\n * // => { 'n': 1 }\n *\n * // using the `_.property` iteratee shorthand\n * _.minBy(objects, 'n');\n * // => { 'n': 1 }\n */\n minBy(\n collection: List | null | undefined,\n iteratee?: ListIterator\n ): T | undefined;\n\n /**\n * @see _.minBy\n */\n minBy(\n collection: Dictionary | null | undefined,\n iteratee?: DictionaryIterator\n ): T | undefined;\n\n /**\n * @see _.minBy\n */\n minBy(\n collection: List|Dictionary | null | undefined,\n iteratee?: string\n ): T | undefined;\n\n /**\n * @see _.minBy\n */\n minBy(\n collection: List|Dictionary | null | undefined,\n whereValue?: TObject\n ): T | undefined;\n }\n\n interface LoDashImplicitArrayWrapperBase {\n /**\n * @see _.minBy\n */\n minBy(\n iteratee?: ListIterator\n ): T | undefined;\n\n /**\n * @see _.minBy\n */\n minBy(\n iteratee?: string\n ): T | undefined;\n\n /**\n * @see _.minBy\n */\n minBy(\n whereValue?: TObject\n ): T | undefined;\n }\n\n interface LoDashImplicitObjectWrapperBase {\n /**\n * @see _.minBy\n */\n minBy(\n iteratee?: ListIterator|DictionaryIterator\n ): T | undefined;\n\n /**\n * @see _.minBy\n */\n minBy(\n iteratee?: string\n ): T | undefined;\n\n /**\n * @see _.minBy\n */\n minBy(\n whereValue?: TObject\n ): T | undefined;\n }\n\n //_.multiply\n interface LoDashStatic {\n /**\n * Multiply two numbers.\n * @param multiplier The first number in a multiplication.\n * @param multiplicand The second number in a multiplication.\n * @returns Returns the product.\n */\n multiply(\n multiplier: number,\n multiplicand: number\n ): number;\n }\n\n interface LoDashImplicitWrapper {\n /**\n * @see _.multiply\n */\n multiply(multiplicand: number): number;\n }\n\n interface LoDashExplicitWrapper {\n /**\n * @see _.multiply\n */\n multiply(multiplicand: number): LoDashExplicitWrapper;\n }\n\n //_.round\n interface LoDashStatic {\n /**\n * Calculates n rounded to precision.\n *\n * @param n The number to round.\n * @param precision The precision to round to.\n * @return Returns the rounded number.\n */\n round(\n n: number,\n precision?: number\n ): number;\n }\n\n interface LoDashImplicitWrapper {\n /**\n * @see _.round\n */\n round(precision?: number): number;\n }\n\n interface LoDashExplicitWrapper {\n /**\n * @see _.round\n */\n round(precision?: number): LoDashExplicitWrapper;\n }\n\n //_.sum\n interface LoDashStatic {\n /**\n * Computes the sum of the values in `array`.\n *\n * @static\n * @memberOf _\n * @category Math\n * @param {Array} array The array to iterate over.\n * @returns {number} Returns the sum.\n * @example\n *\n * _.sum([4, 2, 8, 6]);\n * // => 20\n */\n sum(collection: List | null | undefined): number;\n\n /**\n * @see _.sum\n */\n sum(collection: List|Dictionary | null | undefined): number;\n }\n\n interface LoDashImplicitArrayWrapperBase {\n /**\n * @see _.sum\n */\n sum(): number;\n }\n\n interface LoDashImplicitObjectWrapperBase {\n /**\n * @see _.sum\n **/\n sum(): number;\n\n /**\n * @see _.sum\n */\n sum(): number;\n }\n\n interface LoDashExplicitArrayWrapperBase {\n /**\n * @see _.sum\n */\n sum(): LoDashExplicitWrapper;\n }\n\n interface LoDashExplicitObjectWrapperBase {\n /**\n * @see _.sum\n */\n sum(): LoDashExplicitWrapper;\n\n /**\n * @see _.sum\n */\n sum(): LoDashExplicitWrapper;\n }\n\n //_.sumBy\n interface LoDashStatic {\n /**\n * This method is like `_.sum` except that it accepts `iteratee` which is\n * invoked for each element in `array` to generate the value to be summed.\n * The iteratee is invoked with one argument: (value).\n *\n * @static\n * @memberOf _\n * @category Math\n * @param {Array} array The array to iterate over.\n * @param {Function|Object|string} [iteratee=_.identity] The iteratee invoked per element.\n * @returns {number} Returns the sum.\n * @example\n *\n * var objects = [{ 'n': 4 }, { 'n': 2 }, { 'n': 8 }, { 'n': 6 }];\n *\n * _.sumBy(objects, function(o) { return o.n; });\n * // => 20\n *\n * // using the `_.property` iteratee shorthand\n * _.sumBy(objects, 'n');\n * // => 20\n */\n sumBy(\n collection: List | null | undefined,\n iteratee: ListIterator\n ): number;\n\n /**\n * @see _.sumBy\n */\n sumBy(\n collection: List<{}> | null | undefined,\n iteratee: string\n ): number;\n\n /**\n * @see _.sumBy\n */\n sumBy(\n collection: List | null | undefined\n ): number;\n\n /**\n * @see _.sumBy\n */\n sumBy(\n collection: List<{}> | null | undefined,\n iteratee: Dictionary<{}>\n ): number;\n }\n\n interface LoDashImplicitArrayWrapperBase {\n /**\n * @see _.sumBy\n */\n sumBy(\n iteratee: ListIterator\n ): number;\n\n /**\n * @see _.sumBy\n */\n sumBy(iteratee: string): number;\n\n /**\n * @see _.sumBy\n */\n sumBy(iteratee: Dictionary<{}>): number;\n }\n\n interface LoDashImplicitObjectWrapperBase {\n /**\n * @see _.sumBy\n */\n sumBy(\n iteratee: ListIterator<{}, number>\n ): number;\n\n /**\n * @see _.sumBy\n */\n sumBy(iteratee: string): number;\n\n /**\n * @see _.sumBy\n */\n sumBy(iteratee: Dictionary<{}>): number;\n }\n\n interface LoDashExplicitArrayWrapperBase {\n /**\n * @see _.sumBy\n */\n sumBy(\n iteratee: ListIterator\n ): LoDashExplicitWrapper;\n\n /**\n * @see _.sumBy\n */\n sumBy(iteratee: string): LoDashExplicitWrapper;\n\n /**\n * @see _.sumBy\n */\n sumBy(): LoDashExplicitWrapper;\n\n /**\n * @see _.sumBy\n */\n sumBy(iteratee: Dictionary<{}>): LoDashExplicitWrapper;\n }\n\n interface LoDashExplicitObjectWrapperBase {\n /**\n * @see _.sumBy\n */\n sumBy(\n iteratee: ListIterator<{}, number>\n ): LoDashExplicitWrapper;\n\n /**\n * @see _.sumBy\n */\n sumBy(iteratee: string): LoDashExplicitWrapper;\n\n /**\n * @see _.sumBy\n */\n sumBy(iteratee: Dictionary<{}>): LoDashExplicitWrapper;\n }\n\n /**********\n * Number *\n **********/\n\n //_.subtract\n interface LoDashStatic {\n /**\n * Subtract two numbers.\n *\n * @static\n * @memberOf _\n * @category Math\n * @param {number} minuend The first number in a subtraction.\n * @param {number} subtrahend The second number in a subtraction.\n * @returns {number} Returns the difference.\n * @example\n *\n * _.subtract(6, 4);\n * // => 2\n */\n subtract(\n minuend: number,\n subtrahend: number\n ): number;\n }\n\n interface LoDashImplicitWrapper {\n /**\n * @see _.subtract\n */\n subtract(\n subtrahend: number\n ): number;\n }\n\n interface LoDashExplicitWrapper {\n /**\n * @see _.subtract\n */\n subtract(\n subtrahend: number\n ): LoDashExplicitWrapper;\n }\n\n //_.clamp\n interface LoDashStatic {\n /**\n * Clamps `number` within the inclusive `lower` and `upper` bounds.\n *\n * @static\n * @memberOf _\n * @category Number\n * @param {number} number The number to clamp.\n * @param {number} [lower] The lower bound.\n * @param {number} upper The upper bound.\n * @returns {number} Returns the clamped number.\n * @example\n *\n * _.clamp(-10, -5, 5);\n * // => -5\n *\n * _.clamp(10, -5, 5);\n * // => 5\n */\n clamp(\n number: number,\n lower: number,\n upper: number\n ): number;\n }\n\n interface LoDashImplicitWrapper {\n /**\n * @see _.clamp\n */\n clamp(\n lower: number,\n upper: number\n ): number;\n }\n\n interface LoDashExplicitWrapper {\n /**\n * @see _.clamp\n */\n clamp(\n lower: number,\n upper: number\n ): LoDashExplicitWrapper;\n }\n\n //_.inRange\n interface LoDashStatic {\n /**\n * Checks if n is between start and up to but not including, end. If end is not specified it’s set to start\n * with start then set to 0.\n *\n * @param n The number to check.\n * @param start The start of the range.\n * @param end The end of the range.\n * @return Returns true if n is in the range, else false.\n */\n inRange(\n n: number,\n start: number,\n end: number\n ): boolean;\n\n /**\n * @see _.inRange\n */\n inRange(\n n: number,\n end: number\n ): boolean;\n }\n\n interface LoDashImplicitWrapper {\n /**\n * @see _.inRange\n */\n inRange(\n start: number,\n end: number\n ): boolean;\n\n /**\n * @see _.inRange\n */\n inRange(end: number): boolean;\n }\n\n interface LoDashExplicitWrapper {\n /**\n * @see _.inRange\n */\n inRange(\n start: number,\n end: number\n ): LoDashExplicitWrapper;\n\n /**\n * @see _.inRange\n */\n inRange(end: number): LoDashExplicitWrapper;\n }\n\n //_.random\n interface LoDashStatic {\n /**\n * Produces a random number between min and max (inclusive). If only one argument is provided a number between\n * 0 and the given number is returned. If floating is true, or either min or max are floats, a floating-point\n * number is returned instead of an integer.\n *\n * @param min The minimum possible value.\n * @param max The maximum possible value.\n * @param floating Specify returning a floating-point number.\n * @return Returns the random number.\n */\n random(\n min?: number,\n max?: number,\n floating?: boolean\n ): number;\n\n /**\n * @see _.random\n */\n random(\n min?: number,\n floating?: boolean\n ): number;\n\n /**\n * @see _.random\n */\n random(floating?: boolean): number;\n }\n\n interface LoDashImplicitWrapper {\n /**\n * @see _.random\n */\n random(\n max?: number,\n floating?: boolean\n ): number;\n\n /**\n * @see _.random\n */\n random(floating?: boolean): number;\n }\n\n interface LoDashExplicitWrapper {\n /**\n * @see _.random\n */\n random(\n max?: number,\n floating?: boolean\n ): LoDashExplicitWrapper;\n\n /**\n * @see _.random\n */\n random(floating?: boolean): LoDashExplicitWrapper;\n }\n\n /**********\n * Object *\n **********/\n\n //_.assign\n interface LoDashStatic {\n /**\n * Assigns own enumerable properties of source objects to the destination\n * object. Source objects are applied from left to right. Subsequent sources\n * overwrite property assignments of previous sources.\n *\n * **Note:** This method mutates `object` and is loosely based on\n * [`Object.assign`](https://mdn.io/Object/assign).\n *\n * @static\n * @memberOf _\n * @category Object\n * @param {Object} object The destination object.\n * @param {...Object} [sources] The source objects.\n * @returns {Object} Returns `object`.\n * @example\n *\n * function Foo() {\n * this.c = 3;\n * }\n *\n * function Bar() {\n * this.e = 5;\n * }\n *\n * Foo.prototype.d = 4;\n * Bar.prototype.f = 6;\n *\n * _.assign({ 'a': 1 }, new Foo, new Bar);\n * // => { 'a': 1, 'c': 3, 'e': 5 }\n */\n assign(\n object: TObject,\n source: TSource\n ): TObject & TSource;\n\n /**\n * @see assign\n */\n assign(\n object: TObject,\n source1: TSource1,\n source2: TSource2\n ): TObject & TSource1 & TSource2;\n\n /**\n * @see assign\n */\n assign(\n object: TObject,\n source1: TSource1,\n source2: TSource2,\n source3: TSource3\n ): TObject & TSource1 & TSource2 & TSource3;\n\n /**\n * @see assign\n */\n assign(\n object: TObject,\n source1: TSource1,\n source2: TSource2,\n source3: TSource3,\n source4: TSource4\n ): TObject & TSource1 & TSource2 & TSource3 & TSource4;\n\n /**\n * @see _.assign\n */\n assign(object: TObject): TObject;\n\n /**\n * @see _.assign\n */\n assign(\n object: any,\n ...otherArgs: any[]\n ): TResult;\n }\n\n interface LoDashImplicitObjectWrapper {\n /**\n * @see _.assign\n */\n assign(\n source: TSource\n ): LoDashImplicitObjectWrapper;\n\n /**\n * @see assign\n */\n assign(\n source1: TSource1,\n source2: TSource2\n ): LoDashImplicitObjectWrapper;\n\n /**\n * @see assign\n */\n assign(\n source1: TSource1,\n source2: TSource2,\n source3: TSource3\n ): LoDashImplicitObjectWrapper;\n\n /**\n * @see assign\n */\n assign(\n source1: TSource1,\n source2: TSource2,\n source3: TSource3,\n source4: TSource4\n ): LoDashImplicitObjectWrapper;\n\n /**\n * @see _.assign\n */\n assign(): LoDashImplicitObjectWrapper;\n\n /**\n * @see _.assign\n */\n assign(...otherArgs: any[]): LoDashImplicitObjectWrapper;\n }\n\n interface LoDashExplicitObjectWrapper {\n /**\n * @see _.assign\n */\n assign(\n source: TSource\n ): LoDashExplicitObjectWrapper;\n\n /**\n * @see assign\n */\n assign(\n source1: TSource1,\n source2: TSource2\n ): LoDashExplicitObjectWrapper;\n\n /**\n * @see assign\n */\n assign(\n source1: TSource1,\n source2: TSource2,\n source3: TSource3\n ): LoDashExplicitObjectWrapper;\n\n /**\n * @see assign\n */\n assign(\n source1: TSource1,\n source2: TSource2,\n source3: TSource3,\n source4: TSource4\n ): LoDashExplicitObjectWrapper;\n\n /**\n * @see _.assign\n */\n assign(): LoDashExplicitObjectWrapper;\n\n /**\n * @see _.assign\n */\n assign(...otherArgs: any[]): LoDashExplicitObjectWrapper;\n }\n\n interface LoDashStatic {\n /**\n * This method is like `_.assign` except that it accepts `customizer` which\n * is invoked to produce the assigned values. If `customizer` returns `undefined`\n * assignment is handled by the method instead. The `customizer` is invoked\n * with five arguments: (objValue, srcValue, key, object, source).\n *\n * **Note:** This method mutates `object`.\n *\n * @static\n * @memberOf _\n * @category Object\n * @param {Object} object The destination object.\n * @param {...Object} sources The source objects.\n * @param {Function} [customizer] The function to customize assigned values.\n * @returns {Object} Returns `object`.\n * @example\n *\n * function customizer(objValue, srcValue) {\n * return _.isUndefined(objValue) ? srcValue : objValue;\n * }\n *\n * var defaults = _.partialRight(_.assignWith, customizer);\n *\n * defaults({ 'a': 1 }, { 'b': 2 }, { 'a': 3 });\n * // => { 'a': 1, 'b': 2 }\n */\n assignWith(\n object: TObject,\n source: TSource,\n customizer: AssignCustomizer\n ): TObject & TSource;\n\n /**\n * @see assignWith\n */\n assignWith(\n object: TObject,\n source1: TSource1,\n source2: TSource2,\n customizer: AssignCustomizer\n ): TObject & TSource1 & TSource2;\n\n /**\n * @see assignWith\n */\n assignWith(\n object: TObject,\n source1: TSource1,\n source2: TSource2,\n source3: TSource3,\n customizer: AssignCustomizer\n ): TObject & TSource1 & TSource2 & TSource3;\n\n /**\n * @see assignWith\n */\n assignWith(\n object: TObject,\n source1: TSource1,\n source2: TSource2,\n source3: TSource3,\n source4: TSource4,\n customizer: AssignCustomizer\n ): TObject & TSource1 & TSource2 & TSource3 & TSource4;\n\n /**\n * @see _.assignWith\n */\n assignWith(object: TObject): TObject;\n\n /**\n * @see _.assignWith\n */\n assignWith(\n object: any,\n ...otherArgs: any[]\n ): TResult;\n }\n\n interface LoDashImplicitObjectWrapper {\n /**\n * @see _.assignWith\n */\n assignWith(\n source: TSource,\n customizer: AssignCustomizer\n ): LoDashImplicitObjectWrapper;\n\n /**\n * @see assignWith\n */\n assignWith(\n source1: TSource1,\n source2: TSource2,\n customizer: AssignCustomizer\n ): LoDashImplicitObjectWrapper;\n\n /**\n * @see assignWith\n */\n assignWith(\n source1: TSource1,\n source2: TSource2,\n source3: TSource3,\n customizer: AssignCustomizer\n ): LoDashImplicitObjectWrapper;\n\n /**\n * @see assignWith\n */\n assignWith(\n source1: TSource1,\n source2: TSource2,\n source3: TSource3,\n source4: TSource4,\n customizer: AssignCustomizer\n ): LoDashImplicitObjectWrapper;\n\n /**\n * @see _.assignWith\n */\n assignWith(): LoDashImplicitObjectWrapper;\n\n /**\n * @see _.assignWith\n */\n assignWith(...otherArgs: any[]): LoDashImplicitObjectWrapper;\n }\n\n interface LoDashExplicitObjectWrapper {\n /**\n * @see _.assignWith\n */\n assignWith(\n source: TSource,\n customizer: AssignCustomizer\n ): LoDashExplicitObjectWrapper;\n\n /**\n * @see assignWith\n */\n assignWith(\n source1: TSource1,\n source2: TSource2,\n customizer: AssignCustomizer\n ): LoDashExplicitObjectWrapper;\n\n /**\n * @see assignWith\n */\n assignWith(\n source1: TSource1,\n source2: TSource2,\n source3: TSource3,\n customizer: AssignCustomizer\n ): LoDashExplicitObjectWrapper;\n\n /**\n * @see assignWith\n */\n assignWith(\n source1: TSource1,\n source2: TSource2,\n source3: TSource3,\n source4: TSource4,\n customizer: AssignCustomizer\n ): LoDashExplicitObjectWrapper;\n\n /**\n * @see _.assignWith\n */\n assignWith(): LoDashExplicitObjectWrapper;\n\n /**\n * @see _.assignWith\n */\n assignWith(...otherArgs: any[]): LoDashExplicitObjectWrapper;\n }\n\n //_.assignIn\n interface LoDashStatic {\n /**\n * This method is like `_.assign` except that it iterates over own and\n * inherited source properties.\n *\n * **Note:** This method mutates `object`.\n *\n * @static\n * @memberOf _\n * @alias extend\n * @category Object\n * @param {Object} object The destination object.\n * @param {...Object} [sources] The source objects.\n * @returns {Object} Returns `object`.\n * @example\n *\n * function Foo() {\n * this.b = 2;\n * }\n *\n * function Bar() {\n * this.d = 4;\n * }\n *\n * Foo.prototype.c = 3;\n * Bar.prototype.e = 5;\n *\n * _.assignIn({ 'a': 1 }, new Foo, new Bar);\n * // => { 'a': 1, 'b': 2, 'c': 3, 'd': 4, 'e': 5 }\n */\n assignIn(\n object: TObject,\n source: TSource\n ): TObject & TSource;\n\n /**\n * @see assignIn\n */\n assignIn(\n object: TObject,\n source1: TSource1,\n source2: TSource2\n ): TObject & TSource1 & TSource2;\n\n /**\n * @see assignIn\n */\n assignIn(\n object: TObject,\n source1: TSource1,\n source2: TSource2,\n source3: TSource3\n ): TObject & TSource1 & TSource2 & TSource3;\n\n /**\n * @see assignIn\n */\n assignIn(\n object: TObject,\n source1: TSource1,\n source2: TSource2,\n source3: TSource3,\n source4: TSource4\n ): TObject & TSource1 & TSource2 & TSource3 & TSource4;\n\n /**\n * @see _.assignIn\n */\n assignIn(object: TObject): TObject;\n\n /**\n * @see _.assignIn\n */\n assignIn(\n object: any,\n ...otherArgs: any[]\n ): TResult;\n }\n\n interface LoDashImplicitObjectWrapper {\n /**\n * @see _.assignIn\n */\n assignIn(\n source: TSource\n ): LoDashImplicitObjectWrapper;\n\n /**\n * @see assignIn\n */\n assignIn(\n source1: TSource1,\n source2: TSource2\n ): LoDashImplicitObjectWrapper;\n\n /**\n * @see assignIn\n */\n assignIn(\n source1: TSource1,\n source2: TSource2,\n source3: TSource3\n ): LoDashImplicitObjectWrapper;\n\n /**\n * @see assignIn\n */\n assignIn(\n source1: TSource1,\n source2: TSource2,\n source3: TSource3,\n source4: TSource4\n ): LoDashImplicitObjectWrapper;\n\n /**\n * @see _.assignIn\n */\n assignIn(): LoDashImplicitObjectWrapper;\n\n /**\n * @see _.assignIn\n */\n assignIn(...otherArgs: any[]): LoDashImplicitObjectWrapper;\n }\n\n interface LoDashExplicitObjectWrapper {\n /**\n * @see _.assignIn\n */\n assignIn(\n source: TSource\n ): LoDashExplicitObjectWrapper;\n\n /**\n * @see assignIn\n */\n assignIn(\n source1: TSource1,\n source2: TSource2\n ): LoDashExplicitObjectWrapper;\n\n /**\n * @see assignIn\n */\n assignIn(\n source1: TSource1,\n source2: TSource2,\n source3: TSource3\n ): LoDashExplicitObjectWrapper;\n\n /**\n * @see assignIn\n */\n assignIn(\n source1: TSource1,\n source2: TSource2,\n source3: TSource3,\n source4: TSource4\n ): LoDashExplicitObjectWrapper;\n\n /**\n * @see _.assignIn\n */\n assignIn(): LoDashExplicitObjectWrapper;\n\n /**\n * @see _.assignIn\n */\n assignIn(...otherArgs: any[]): LoDashExplicitObjectWrapper;\n }\n\n //_.assignInWith\n type AssignCustomizer = (objectValue: any, sourceValue: any, key?: string, object?: {}, source?: {}) => any;\n\n interface LoDashStatic {\n /**\n * This method is like `_.assignIn` except that it accepts `customizer` which\n * is invoked to produce the assigned values. If `customizer` returns `undefined`\n * assignment is handled by the method instead. The `customizer` is invoked\n * with five arguments: (objValue, srcValue, key, object, source).\n *\n * **Note:** This method mutates `object`.\n *\n * @static\n * @memberOf _\n * @alias extendWith\n * @category Object\n * @param {Object} object The destination object.\n * @param {...Object} sources The source objects.\n * @param {Function} [customizer] The function to customize assigned values.\n * @returns {Object} Returns `object`.\n * @example\n *\n * function customizer(objValue, srcValue) {\n * return _.isUndefined(objValue) ? srcValue : objValue;\n * }\n *\n * var defaults = _.partialRight(_.assignInWith, customizer);\n *\n * defaults({ 'a': 1 }, { 'b': 2 }, { 'a': 3 });\n * // => { 'a': 1, 'b': 2 }\n */\n assignInWith(\n object: TObject,\n source: TSource,\n customizer: AssignCustomizer\n ): TObject & TSource;\n\n /**\n * @see assignInWith\n */\n assignInWith(\n object: TObject,\n source1: TSource1,\n source2: TSource2,\n customizer: AssignCustomizer\n ): TObject & TSource1 & TSource2;\n\n /**\n * @see assignInWith\n */\n assignInWith(\n object: TObject,\n source1: TSource1,\n source2: TSource2,\n source3: TSource3,\n customizer: AssignCustomizer\n ): TObject & TSource1 & TSource2 & TSource3;\n\n /**\n * @see assignInWith\n */\n assignInWith(\n object: TObject,\n source1: TSource1,\n source2: TSource2,\n source3: TSource3,\n source4: TSource4,\n customizer: AssignCustomizer\n ): TObject & TSource1 & TSource2 & TSource3 & TSource4;\n\n /**\n * @see _.assignInWith\n */\n assignInWith(object: TObject): TObject;\n\n /**\n * @see _.assignInWith\n */\n assignInWith(\n object: any,\n ...otherArgs: any[]\n ): TResult;\n }\n\n interface LoDashImplicitObjectWrapper {\n /**\n * @see _.assignInWith\n */\n assignInWith(\n source: TSource,\n customizer: AssignCustomizer\n ): LoDashImplicitObjectWrapper;\n\n /**\n * @see assignInWith\n */\n assignInWith(\n source1: TSource1,\n source2: TSource2,\n customizer: AssignCustomizer\n ): LoDashImplicitObjectWrapper;\n\n /**\n * @see assignInWith\n */\n assignInWith(\n source1: TSource1,\n source2: TSource2,\n source3: TSource3,\n customizer: AssignCustomizer\n ): LoDashImplicitObjectWrapper;\n\n /**\n * @see assignInWith\n */\n assignInWith(\n source1: TSource1,\n source2: TSource2,\n source3: TSource3,\n source4: TSource4,\n customizer: AssignCustomizer\n ): LoDashImplicitObjectWrapper;\n\n /**\n * @see _.assignInWith\n */\n assignInWith(): LoDashImplicitObjectWrapper;\n\n /**\n * @see _.assignInWith\n */\n assignInWith(...otherArgs: any[]): LoDashImplicitObjectWrapper;\n }\n\n interface LoDashExplicitObjectWrapper {\n /**\n * @see _.assignInWith\n */\n assignInWith(\n source: TSource,\n customizer: AssignCustomizer\n ): LoDashExplicitObjectWrapper;\n\n /**\n * @see assignInWith\n */\n assignInWith(\n source1: TSource1,\n source2: TSource2,\n customizer: AssignCustomizer\n ): LoDashExplicitObjectWrapper;\n\n /**\n * @see assignInWith\n */\n assignInWith(\n source1: TSource1,\n source2: TSource2,\n source3: TSource3,\n customizer: AssignCustomizer\n ): LoDashExplicitObjectWrapper;\n\n /**\n * @see assignInWith\n */\n assignInWith(\n source1: TSource1,\n source2: TSource2,\n source3: TSource3,\n source4: TSource4,\n customizer: AssignCustomizer\n ): LoDashExplicitObjectWrapper;\n\n /**\n * @see _.assignInWith\n */\n assignInWith(): LoDashExplicitObjectWrapper;\n\n /**\n * @see _.assignInWith\n */\n assignInWith(...otherArgs: any[]): LoDashExplicitObjectWrapper;\n }\n\n //_.create\n interface LoDashStatic {\n /**\n * Creates an object that inherits from the given prototype object. If a properties object is provided its own\n * enumerable properties are assigned to the created object.\n *\n * @param prototype The object to inherit from.\n * @param properties The properties to assign to the object.\n * @return Returns the new object.\n */\n create(\n prototype: T,\n properties?: U\n ): T & U;\n }\n\n interface LoDashImplicitObjectWrapper {\n /**\n * @see _.create\n */\n create(properties?: U): LoDashImplicitObjectWrapper;\n }\n\n interface LoDashExplicitObjectWrapper {\n /**\n * @see _.create\n */\n create(properties?: U): LoDashExplicitObjectWrapper;\n }\n\n //_.defaults\n interface LoDashStatic {\n /**\n * Assigns own enumerable properties of source object(s) to the destination object for all destination\n * properties that resolve to undefined. Once a property is set, additional values of the same property are\n * ignored.\n *\n * Note: This method mutates object.\n *\n * @param object The destination object.\n * @param sources The source objects.\n * @return The destination object.\n */\n defaults(\n object: TObject,\n source: TSource\n ): TSource & TObject;\n\n /**\n * @see _.defaults\n */\n defaults(\n object: TObject,\n source1: TSource1,\n source2: TSource2\n ): TSource2 & TSource1 & TObject;\n\n /**\n * @see _.defaults\n */\n defaults(\n object: TObject,\n source1: TSource1,\n source2: TSource2,\n source3: TSource3\n ): TSource3 & TSource2 & TSource1 & TObject;\n\n /**\n * @see _.defaults\n */\n defaults(\n object: TObject,\n source1: TSource1,\n source2: TSource2,\n source3: TSource3,\n source4: TSource4\n ): TSource4 & TSource3 & TSource2 & TSource1 & TObject;\n\n /**\n * @see _.defaults\n */\n defaults(object: TObject): TObject;\n\n /**\n * @see _.defaults\n */\n defaults(\n object: any,\n ...sources: any[]\n ): TResult;\n }\n\n interface LoDashImplicitObjectWrapper {\n /**\n * @see _.defaults\n */\n defaults(\n source: TSource\n ): LoDashImplicitObjectWrapper;\n\n /**\n * @see _.defaults\n */\n defaults(\n source1: TSource1,\n source2: TSource2\n ): LoDashImplicitObjectWrapper;\n\n /**\n * @see _.defaults\n */\n defaults(\n source1: TSource1,\n source2: TSource2,\n source3: TSource3\n ): LoDashImplicitObjectWrapper;\n\n /**\n * @see _.defaults\n */\n defaults(\n source1: TSource1,\n source2: TSource2,\n source3: TSource3,\n source4: TSource4\n ): LoDashImplicitObjectWrapper;\n\n /**\n * @see _.defaults\n */\n defaults(): LoDashImplicitObjectWrapper;\n\n /**\n * @see _.defaults\n */\n defaults(...sources: any[]): LoDashImplicitObjectWrapper;\n }\n\n interface LoDashExplicitObjectWrapper {\n /**\n * @see _.defaults\n */\n defaults(\n source: TSource\n ): LoDashExplicitObjectWrapper;\n\n /**\n * @see _.defaults\n */\n defaults(\n source1: TSource1,\n source2: TSource2\n ): LoDashExplicitObjectWrapper;\n\n /**\n * @see _.defaults\n */\n defaults(\n source1: TSource1,\n source2: TSource2,\n source3: TSource3\n ): LoDashExplicitObjectWrapper;\n\n /**\n * @see _.defaults\n */\n defaults(\n source1: TSource1,\n source2: TSource2,\n source3: TSource3,\n source4: TSource4\n ): LoDashExplicitObjectWrapper;\n\n /**\n * @see _.defaults\n */\n defaults(): LoDashExplicitObjectWrapper;\n\n /**\n * @see _.defaults\n */\n defaults(...sources: any[]): LoDashExplicitObjectWrapper;\n }\n\n //_.defaultsDeep\n interface LoDashStatic {\n /**\n * This method is like _.defaults except that it recursively assigns default properties.\n * @param object The destination object.\n * @param sources The source objects.\n * @return Returns object.\n **/\n defaultsDeep(\n object: T,\n ...sources: any[]): TResult;\n }\n\n interface LoDashImplicitObjectWrapper {\n /**\n * @see _.defaultsDeep\n **/\n defaultsDeep(...sources: any[]): LoDashImplicitObjectWrapper;\n }\n\n // _.extend\n interface LoDashStatic {\n /**\n * @see _.assignIn\n */\n extend(\n object: TObject,\n source: TSource\n ): TObject & TSource;\n\n /**\n * @see _.assignIn\n */\n extend(\n object: TObject,\n source1: TSource1,\n source2: TSource2\n ): TObject & TSource1 & TSource2;\n\n /**\n * @see _.assignIn\n */\n extend(\n object: TObject,\n source1: TSource1,\n source2: TSource2,\n source3: TSource3\n ): TObject & TSource1 & TSource2 & TSource3;\n\n /**\n * @see _.assignIn\n */\n extend(\n object: TObject,\n source1: TSource1,\n source2: TSource2,\n source3: TSource3,\n source4: TSource4\n ): TObject & TSource1 & TSource2 & TSource3 & TSource4;\n\n /**\n * @see _.assignIn\n */\n extend(object: TObject): TObject;\n\n /**\n * @see _.assignIn\n */\n extend(\n object: any,\n ...otherArgs: any[]\n ): TResult;\n }\n\n interface LoDashImplicitObjectWrapper {\n /**\n * @see _.assignIn\n */\n extend(\n source: TSource\n ): LoDashImplicitObjectWrapper;\n\n /**\n * @see _.assignIn\n */\n extend(\n source1: TSource1,\n source2: TSource2\n ): LoDashImplicitObjectWrapper;\n\n /**\n * @see _.assignIn\n */\n extend(\n source1: TSource1,\n source2: TSource2,\n source3: TSource3\n ): LoDashImplicitObjectWrapper;\n\n /**\n * @see _.assignIn\n */\n extend(\n source1: TSource1,\n source2: TSource2,\n source3: TSource3,\n source4: TSource4\n ): LoDashImplicitObjectWrapper;\n\n /**\n * @see _.assignIn\n */\n extend(): LoDashImplicitObjectWrapper;\n\n /**\n * @see _.assignIn\n */\n extend(...otherArgs: any[]): LoDashImplicitObjectWrapper;\n }\n\n interface LoDashExplicitObjectWrapper {\n /**\n * @see _.assignIn\n */\n extend(\n source: TSource\n ): LoDashExplicitObjectWrapper;\n\n /**\n * @see _.assignIn\n */\n extend(\n source1: TSource1,\n source2: TSource2\n ): LoDashExplicitObjectWrapper;\n\n /**\n * @see _.assignIn\n */\n extend(\n source1: TSource1,\n source2: TSource2,\n source3: TSource3\n ): LoDashExplicitObjectWrapper;\n\n /**\n * @see _.assignIn\n */\n extend(\n source1: TSource1,\n source2: TSource2,\n source3: TSource3,\n source4: TSource4\n ): LoDashExplicitObjectWrapper;\n\n /**\n * @see _.assignIn\n */\n extend(): LoDashExplicitObjectWrapper;\n\n /**\n * @see _.assignIn\n */\n extend(...otherArgs: any[]): LoDashExplicitObjectWrapper;\n }\n\n interface LoDashStatic {\n /**\n * @see _.assignInWith\n */\n extendWith(\n object: TObject,\n source: TSource,\n customizer: AssignCustomizer\n ): TObject & TSource;\n\n /**\n * @see _.assignInWith\n */\n extendWith(\n object: TObject,\n source1: TSource1,\n source2: TSource2,\n customizer: AssignCustomizer\n ): TObject & TSource1 & TSource2;\n\n /**\n * @see _.assignInWith\n */\n extendWith(\n object: TObject,\n source1: TSource1,\n source2: TSource2,\n source3: TSource3,\n customizer: AssignCustomizer\n ): TObject & TSource1 & TSource2 & TSource3;\n\n /**\n * @see _.assignInWith\n */\n extendWith(\n object: TObject,\n source1: TSource1,\n source2: TSource2,\n source3: TSource3,\n source4: TSource4,\n customizer: AssignCustomizer\n ): TObject & TSource1 & TSource2 & TSource3 & TSource4;\n\n /**\n * @see _.assignInWith\n */\n extendWith(object: TObject): TObject;\n\n /**\n * @see _.assignInWith\n */\n extendWith(\n object: any,\n ...otherArgs: any[]\n ): TResult;\n }\n\n interface LoDashImplicitObjectWrapper {\n /**\n * @see _.assignInWith\n */\n extendWith(\n source: TSource,\n customizer: AssignCustomizer\n ): LoDashImplicitObjectWrapper;\n\n /**\n * @see _.assignInWith\n */\n extendWith(\n source1: TSource1,\n source2: TSource2,\n customizer: AssignCustomizer\n ): LoDashImplicitObjectWrapper;\n\n /**\n * @see _.assignInWith\n */\n extendWith(\n source1: TSource1,\n source2: TSource2,\n source3: TSource3,\n customizer: AssignCustomizer\n ): LoDashImplicitObjectWrapper;\n\n /**\n * @see _.assignInWith\n */\n extendWith(\n source1: TSource1,\n source2: TSource2,\n source3: TSource3,\n source4: TSource4,\n customizer: AssignCustomizer\n ): LoDashImplicitObjectWrapper;\n\n /**\n * @see _.assignInWith\n */\n extendWith(): LoDashImplicitObjectWrapper;\n\n /**\n * @see _.assignInWith\n */\n extendWith(...otherArgs: any[]): LoDashImplicitObjectWrapper;\n }\n\n interface LoDashExplicitObjectWrapper {\n /**\n * @see _.assignInWith\n */\n extendWith(\n source: TSource,\n customizer: AssignCustomizer\n ): LoDashExplicitObjectWrapper;\n\n /**\n * @see _.assignInWith\n */\n extendWith(\n source1: TSource1,\n source2: TSource2,\n customizer: AssignCustomizer\n ): LoDashExplicitObjectWrapper;\n\n /**\n * @see _.assignInWith\n */\n extendWith(\n source1: TSource1,\n source2: TSource2,\n source3: TSource3,\n customizer: AssignCustomizer\n ): LoDashExplicitObjectWrapper;\n\n /**\n * @see _.assignInWith\n */\n extendWith(\n source1: TSource1,\n source2: TSource2,\n source3: TSource3,\n source4: TSource4,\n customizer: AssignCustomizer\n ): LoDashExplicitObjectWrapper;\n\n /**\n * @see _.assignInWith\n */\n extendWith(): LoDashExplicitObjectWrapper;\n\n /**\n * @see _.assignInWith\n */\n extendWith(...otherArgs: any[]): LoDashExplicitObjectWrapper;\n }\n\n //_.findKey\n interface LoDashStatic {\n /**\n * This method is like _.find except that it returns the key of the first element predicate returns truthy for\n * instead of the element itself.\n *\n * If a property name is provided for predicate the created _.property style callback returns the property\n * value of the given element.\n *\n * If a value is also provided for thisArg the created _.matchesProperty style callback returns true for\n * elements that have a matching property value, else false.\n *\n * If an object is provided for predicate the created _.matches style callback returns true for elements that\n * have the properties of the given object, else false.\n *\n * @param object The object to search.\n * @param predicate The function invoked per iteration.\n * @param thisArg The this binding of predicate.\n * @return Returns the key of the matched element, else undefined.\n */\n findKey(\n object: TObject,\n predicate?: DictionaryIterator\n ): string | undefined;\n\n /**\n * @see _.findKey\n */\n findKey(\n object: TObject,\n predicate?: ObjectIterator\n ): string | undefined;\n\n /**\n * @see _.findKey\n */\n findKey(\n object: TObject,\n predicate?: string\n ): string | undefined;\n\n /**\n * @see _.findKey\n */\n findKey, TObject>(\n object: TObject,\n predicate?: TWhere\n ): string | undefined;\n }\n\n interface LoDashImplicitObjectWrapperBase {\n /**\n * @see _.findKey\n */\n findKey(\n predicate?: DictionaryIterator\n ): string | undefined;\n\n /**\n * @see _.findKey\n */\n findKey(\n predicate?: ObjectIterator\n ): string | undefined;\n\n /**\n * @see _.findKey\n */\n findKey(\n predicate?: string\n ): string | undefined;\n\n /**\n * @see _.findKey\n */\n findKey>(\n predicate?: TWhere\n ): string | undefined;\n }\n\n interface LoDashExplicitObjectWrapperBase {\n /**\n * @see _.findKey\n */\n findKey(\n predicate?: DictionaryIterator\n ): LoDashExplicitWrapper;\n\n /**\n * @see _.findKey\n */\n findKey(\n predicate?: ObjectIterator\n ): LoDashExplicitWrapper;\n\n /**\n * @see _.findKey\n */\n findKey(\n predicate?: string\n ): LoDashExplicitWrapper;\n\n /**\n * @see _.findKey\n */\n findKey>(\n predicate?: TWhere\n ): LoDashExplicitWrapper;\n }\n\n //_.findLastKey\n interface LoDashStatic {\n /**\n * This method is like _.findKey except that it iterates over elements of a collection in the opposite order.\n *\n * If a property name is provided for predicate the created _.property style callback returns the property\n * value of the given element.\n *\n * If a value is also provided for thisArg the created _.matchesProperty style callback returns true for\n * elements that have a matching property value, else false.\n *\n * If an object is provided for predicate the created _.matches style callback returns true for elements that\n * have the properties of the given object, else false.\n *\n * @param object The object to search.\n * @param predicate The function invoked per iteration.\n * @param thisArg The this binding of predicate.\n * @return Returns the key of the matched element, else undefined.\n */\n findLastKey(\n object: TObject,\n predicate?: DictionaryIterator\n ): string | undefined;\n\n /**\n * @see _.findLastKey\n */\n findLastKey(\n object: TObject,\n predicate?: ObjectIterator\n ): string | undefined;\n\n /**\n * @see _.findLastKey\n */\n findLastKey(\n object: TObject,\n predicate?: string\n ): string;\n\n /**\n * @see _.findLastKey\n */\n findLastKey, TObject>(\n object: TObject,\n predicate?: TWhere\n ): string | undefined;\n }\n\n interface LoDashImplicitObjectWrapperBase {\n /**\n * @see _.findLastKey\n */\n findLastKey(\n predicate?: DictionaryIterator\n ): string;\n\n /**\n * @see _.findLastKey\n */\n findLastKey(\n predicate?: ObjectIterator\n ): string | undefined;\n\n /**\n * @see _.findLastKey\n */\n findLastKey(\n predicate?: string\n ): string | undefined;\n\n /**\n * @see _.findLastKey\n */\n findLastKey>(\n predicate?: TWhere\n ): string | undefined;\n }\n\n interface LoDashExplicitObjectWrapperBase {\n /**\n * @see _.findLastKey\n */\n findLastKey(\n predicate?: DictionaryIterator\n ): LoDashExplicitWrapper;\n\n /**\n * @see _.findLastKey\n */\n findLastKey(\n predicate?: ObjectIterator\n ): LoDashExplicitWrapper;\n\n /**\n * @see _.findLastKey\n */\n findLastKey(\n predicate?: string\n ): LoDashExplicitWrapper;\n\n /**\n * @see _.findLastKey\n */\n findLastKey>(\n predicate?: TWhere\n ): LoDashExplicitWrapper;\n }\n\n //_.forIn\n interface LoDashStatic {\n /**\n * Iterates over own and inherited enumerable properties of an object invoking iteratee for each property. The\n * iteratee is bound to thisArg and invoked with three arguments: (value, key, object). Iteratee functions may\n * exit iteration early by explicitly returning false.\n *\n * @param object The object to iterate over.\n * @param iteratee The function invoked per iteration.\n * @param thisArg The this binding of iteratee.\n * @return Returns object.\n */\n forIn(\n object: Dictionary,\n iteratee?: DictionaryIterator\n ): Dictionary;\n\n /**\n * @see _.forIn\n */\n forIn(\n object: Dictionary | null | undefined,\n iteratee?: DictionaryIterator\n ): Dictionary | null | undefined;\n\n /**\n * @see _.forIn\n */\n forIn(\n object: T,\n iteratee?: ObjectIterator\n ): T;\n }\n\n interface LoDashImplicitObjectWrapperBase {\n /**\n * @see _.forIn\n */\n forIn(\n iteratee?: DictionaryIterator\n ): TWrapper;\n }\n\n interface LoDashExplicitObjectWrapperBase {\n /**\n * @see _.forIn\n */\n forIn(\n iteratee?: DictionaryIterator\n ): TWrapper;\n }\n\n //_.forInRight\n interface LoDashStatic {\n /**\n * This method is like _.forIn except that it iterates over properties of object in the opposite order.\n *\n * @param object The object to iterate over.\n * @param iteratee The function invoked per iteration.\n * @param thisArg The this binding of iteratee.\n * @return Returns object.\n */\n forInRight(\n object: Dictionary,\n iteratee?: DictionaryIterator\n ): Dictionary;\n\n /**\n * @see _.forInRight\n */\n forInRight(\n object: Dictionary | null | undefined,\n iteratee?: DictionaryIterator\n ): Dictionary | null | undefined;\n\n /**\n * @see _.forInRight\n */\n forInRight(\n object: T,\n iteratee?: ObjectIterator\n ): T;\n }\n\n interface LoDashImplicitObjectWrapperBase {\n /**\n * @see _.forInRight\n */\n forInRight(\n iteratee?: DictionaryIterator\n ): TWrapper;\n }\n\n interface LoDashExplicitObjectWrapperBase {\n /**\n * @see _.forInRight\n */\n forInRight(\n iteratee?: DictionaryIterator\n ): TWrapper;\n }\n\n //_.forOwn\n interface LoDashStatic {\n /**\n * Iterates over own enumerable properties of an object invoking iteratee for each property. The iteratee is\n * bound to thisArg and invoked with three arguments: (value, key, object). Iteratee functions may exit\n * iteration early by explicitly returning false.\n *\n * @param object The object to iterate over.\n * @param iteratee The function invoked per iteration.\n * @param thisArg The this binding of iteratee.\n * @return Returns object.\n */\n forOwn(\n object: Dictionary,\n iteratee?: DictionaryIterator\n ): Dictionary;\n\n /**\n * @see _.forOwn\n */\n forOwn(\n object: Dictionary | null | undefined,\n iteratee?: DictionaryIterator\n ): Dictionary | null | undefined;\n\n /**\n * @see _.forOwn\n */\n forOwn(\n object: T,\n iteratee?: ObjectIterator\n ): T;\n }\n\n interface LoDashImplicitObjectWrapperBase {\n /**\n * @see _.forOwn\n */\n forOwn(\n iteratee?: DictionaryIterator\n ): TWrapper;\n }\n\n interface LoDashExplicitObjectWrapperBase {\n /**\n * @see _.forOwn\n */\n forOwn(\n iteratee?: DictionaryIterator\n ): TWrapper;\n }\n\n //_.forOwnRight\n interface LoDashStatic {\n /**\n * This method is like _.forOwn except that it iterates over properties of object in the opposite order.\n *\n * @param object The object to iterate over.\n * @param iteratee The function invoked per iteration.\n * @param thisArg The this binding of iteratee.\n * @return Returns object.\n */\n forOwnRight(\n object: Dictionary,\n iteratee?: DictionaryIterator\n ): Dictionary;\n\n /**\n * @see _.forOwnRight\n */\n forOwnRight(\n object: Dictionary | null | undefined,\n iteratee?: DictionaryIterator\n ): Dictionary | null | undefined;\n\n /**\n * @see _.forOwnRight\n */\n forOwnRight(\n object: T,\n iteratee?: ObjectIterator\n ): T;\n }\n\n interface LoDashImplicitObjectWrapperBase {\n /**\n * @see _.forOwnRight\n */\n forOwnRight(\n iteratee?: DictionaryIterator\n ): TWrapper;\n }\n\n interface LoDashExplicitObjectWrapperBase {\n /**\n * @see _.forOwnRight\n */\n forOwnRight(\n iteratee?: DictionaryIterator\n ): TWrapper;\n }\n\n //_.functions\n interface LoDashStatic {\n /**\n * Creates an array of function property names from own enumerable properties\n * of `object`.\n *\n * @static\n * @memberOf _\n * @category Object\n * @param {Object} object The object to inspect.\n * @returns {Array} Returns the new array of property names.\n * @example\n *\n * function Foo() {\n * this.a = _.constant('a');\n * this.b = _.constant('b');\n * }\n *\n * Foo.prototype.c = _.constant('c');\n *\n * _.functions(new Foo);\n * // => ['a', 'b']\n */\n functions(object: any): string[];\n }\n\n interface LoDashImplicitObjectWrapper {\n /**\n * @see _.functions\n */\n functions(): _.LoDashImplicitArrayWrapper;\n }\n\n interface LoDashExplicitObjectWrapper {\n /**\n * @see _.functions\n */\n functions(): _.LoDashExplicitArrayWrapper;\n }\n\n //_.functionsIn\n interface LoDashStatic {\n /**\n * Creates an array of function property names from own and inherited\n * enumerable properties of `object`.\n *\n * @static\n * @memberOf _\n * @category Object\n * @param {Object} object The object to inspect.\n * @returns {Array} Returns the new array of property names.\n * @example\n *\n * function Foo() {\n * this.a = _.constant('a');\n * this.b = _.constant('b');\n * }\n *\n * Foo.prototype.c = _.constant('c');\n *\n * _.functionsIn(new Foo);\n * // => ['a', 'b', 'c']\n */\n functionsIn(object: any): string[];\n }\n\n interface LoDashImplicitObjectWrapper {\n /**\n * @see _.functionsIn\n */\n functionsIn(): _.LoDashImplicitArrayWrapper;\n }\n\n interface LoDashExplicitObjectWrapper {\n /**\n * @see _.functionsIn\n */\n functionsIn(): _.LoDashExplicitArrayWrapper;\n }\n\n //_.get\n interface LoDashStatic {\n /**\n * Gets the property value at path of object. If the resolved value is undefined the defaultValue is used\n * in its place.\n *\n * @param object The object to query.\n * @param path The path of the property to get.\n * @param defaultValue The value returned if the resolved value is undefined.\n * @return Returns the resolved value.\n */\n get(\n object: TObject,\n path: Many,\n defaultValue?: TResult\n ): TResult;\n\n /**\n * @see _.get\n */\n get(\n object: any,\n path: Many,\n defaultValue?: TResult\n ): TResult;\n }\n\n interface LoDashImplicitWrapper {\n /**\n * @see _.get\n */\n get(\n path: Many,\n defaultValue?: TResult\n ): TResult;\n }\n\n interface LoDashImplicitArrayWrapperBase {\n /**\n * @see _.get\n */\n get(\n path: Many,\n defaultValue?: TResult\n ): TResult;\n }\n\n interface LoDashImplicitObjectWrapperBase {\n /**\n * @see _.get\n */\n get(\n path: Many,\n defaultValue?: TResult\n ): TResult;\n }\n\n interface LoDashExplicitWrapper {\n /**\n * @see _.get\n */\n get(\n path: Many,\n defaultValue?: any\n ): TResultWrapper;\n }\n\n interface LoDashExplicitArrayWrapperBase {\n /**\n * @see _.get\n */\n get(\n path: Many,\n defaultValue?: any\n ): TResultWrapper;\n }\n\n interface LoDashExplicitObjectWrapperBase {\n /**\n * @see _.get\n */\n get(\n path: Many,\n defaultValue?: any\n ): TResultWrapper;\n }\n\n //_.has\n interface LoDashStatic {\n /**\n * Checks if `path` is a direct property of `object`.\n *\n * @static\n * @memberOf _\n * @category Object\n * @param {Object} object The object to query.\n * @param {Array|string} path The path to check.\n * @returns {boolean} Returns `true` if `path` exists, else `false`.\n * @example\n *\n * var object = { 'a': { 'b': { 'c': 3 } } };\n * var other = _.create({ 'a': _.create({ 'b': _.create({ 'c': 3 }) }) });\n *\n * _.has(object, 'a');\n * // => true\n *\n * _.has(object, 'a.b.c');\n * // => true\n *\n * _.has(object, ['a', 'b', 'c']);\n * // => true\n *\n * _.has(other, 'a');\n * // => false\n */\n has(\n object: T,\n path: Many\n ): boolean;\n }\n\n interface LoDashImplicitObjectWrapperBase {\n /**\n * @see _.has\n */\n has(path: Many): boolean;\n }\n\n interface LoDashExplicitObjectWrapperBase {\n /**\n * @see _.has\n */\n has(path: Many): LoDashExplicitWrapper;\n }\n\n //_.hasIn\n interface LoDashStatic {\n /**\n * Checks if `path` is a direct or inherited property of `object`.\n *\n * @static\n * @memberOf _\n * @category Object\n * @param {Object} object The object to query.\n * @param {Array|string} path The path to check.\n * @returns {boolean} Returns `true` if `path` exists, else `false`.\n * @example\n *\n * var object = _.create({ 'a': _.create({ 'b': _.create({ 'c': 3 }) }) });\n *\n * _.hasIn(object, 'a');\n * // => true\n *\n * _.hasIn(object, 'a.b.c');\n * // => true\n *\n * _.hasIn(object, ['a', 'b', 'c']);\n * // => true\n *\n * _.hasIn(object, 'b');\n * // => false\n */\n hasIn(\n object: T,\n path: Many\n ): boolean;\n }\n\n interface LoDashImplicitObjectWrapperBase {\n /**\n * @see _.hasIn\n */\n hasIn(path: Many): boolean;\n }\n\n interface LoDashExplicitObjectWrapperBase {\n /**\n * @see _.hasIn\n */\n hasIn(path: Many): LoDashExplicitWrapper;\n }\n\n //_.invert\n interface LoDashStatic {\n /**\n * Creates an object composed of the inverted keys and values of object. If object contains duplicate values,\n * subsequent values overwrite property assignments of previous values unless multiValue is true.\n *\n * @param object The object to invert.\n * @param multiValue Allow multiple values per key.\n * @return Returns the new inverted object.\n */\n invert(\n object: T,\n multiValue?: boolean\n ): TResult;\n\n /**\n * @see _.invert\n */\n invert(\n object: Object,\n multiValue?: boolean\n ): TResult;\n }\n\n interface LoDashImplicitObjectWrapper {\n /**\n * @see _.invert\n */\n invert(multiValue?: boolean): LoDashImplicitObjectWrapper;\n }\n\n interface LoDashExplicitObjectWrapper {\n /**\n * @see _.invert\n */\n invert(multiValue?: boolean): LoDashExplicitObjectWrapper;\n }\n\n //_.inverBy\n type InvertByIterator = (value: T) => any;\n\n interface LoDashStatic {\n /**\n * This method is like _.invert except that the inverted object is generated from the results of running each\n * element of object through iteratee. The corresponding inverted value of each inverted key is an array of\n * keys responsible for generating the inverted value. The iteratee is invoked with one argument: (value).\n *\n * @param object The object to invert.\n * @param interatee The iteratee invoked per element.\n * @return Returns the new inverted object.\n */\n invertBy(\n object: Object,\n interatee?: InvertByIterator|string\n ): Dictionary;\n\n /**\n * @see _.invertBy\n */\n invertBy(\n object: _.Dictionary|_.NumericDictionary,\n interatee?: InvertByIterator|string\n ): Dictionary;\n\n /**\n * @see _.invertBy\n */\n invertBy(\n object: Object,\n interatee?: W\n ): Dictionary;\n\n /**\n * @see _.invertBy\n */\n invertBy(\n object: _.Dictionary,\n interatee?: W\n ): Dictionary;\n }\n\n interface LoDashImplicitWrapper {\n /**\n * @see _.invertBy\n */\n invertBy(\n interatee?: InvertByIterator\n ): LoDashImplicitObjectWrapper>;\n }\n\n interface LoDashImplicitArrayWrapper {\n /**\n * @see _.invertBy\n */\n invertBy(\n interatee?: InvertByIterator|string\n ): LoDashImplicitObjectWrapper>;\n\n /**\n * @see _.invertBy\n */\n invertBy(\n interatee?: W\n ): LoDashImplicitObjectWrapper>;\n }\n\n interface LoDashImplicitObjectWrapper {\n /**\n * @see _.invertBy\n */\n invertBy(\n interatee?: InvertByIterator|string\n ): LoDashImplicitObjectWrapper>;\n\n /**\n * @see _.invertBy\n */\n invertBy(\n interatee?: W\n ): LoDashImplicitObjectWrapper>;\n }\n\n interface LoDashExplicitWrapper {\n /**\n * @see _.invertBy\n */\n invertBy(\n interatee?: InvertByIterator\n ): LoDashExplicitObjectWrapper>;\n }\n\n interface LoDashExplicitArrayWrapper {\n /**\n * @see _.invertBy\n */\n invertBy(\n interatee?: InvertByIterator|string\n ): LoDashExplicitObjectWrapper>;\n\n /**\n * @see _.invertBy\n */\n invertBy(\n interatee?: W\n ): LoDashExplicitObjectWrapper>;\n }\n\n interface LoDashExplicitObjectWrapper {\n /**\n * @see _.invertBy\n */\n invertBy(\n interatee?: InvertByIterator|string\n ): LoDashExplicitObjectWrapper>;\n\n /**\n * @see _.invertBy\n */\n invertBy(\n interatee?: W\n ): LoDashExplicitObjectWrapper>;\n }\n\n //_.keys\n interface LoDashStatic {\n /**\n * Creates an array of the own enumerable property names of object.\n *\n * Note: Non-object values are coerced to objects. See the ES spec for more details.\n *\n * @param object The object to query.\n * @return Returns the array of property names.\n */\n keys(object?: any): string[];\n }\n\n interface LoDashImplicitObjectWrapperBase {\n /**\n * @see _.keys\n */\n keys(): LoDashImplicitArrayWrapper;\n }\n\n interface LoDashExplicitObjectWrapperBase {\n /**\n * @see _.keys\n */\n keys(): LoDashExplicitArrayWrapper;\n }\n\n //_.keysIn\n interface LoDashStatic {\n /**\n * Creates an array of the own and inherited enumerable property names of object.\n *\n * Note: Non-object values are coerced to objects.\n *\n * @param object The object to query.\n * @return An array of property names.\n */\n keysIn(object?: any): string[];\n }\n\n interface LoDashImplicitObjectWrapperBase {\n /**\n * @see _.keysIn\n */\n keysIn(): LoDashImplicitArrayWrapper;\n }\n\n interface LoDashExplicitObjectWrapperBase {\n /**\n * @see _.keysIn\n */\n keysIn(): LoDashExplicitArrayWrapper;\n }\n\n //_.mapKeys\n interface LoDashStatic {\n /**\n * The opposite of _.mapValues; this method creates an object with the same values as object and keys generated\n * by running each own enumerable property of object through iteratee.\n *\n * @param object The object to iterate over.\n * @param iteratee The function invoked per iteration.\n * @param thisArg The this binding of iteratee.\n * @return Returns the new mapped object.\n */\n mapKeys(\n object: List | null | undefined,\n iteratee?: ListIterator\n ): Dictionary;\n\n /**\n * @see _.mapKeys\n */\n mapKeys(\n object: Dictionary | null | undefined,\n iteratee?: DictionaryIterator\n ): Dictionary;\n\n /**\n * @see _.mapKeys\n */\n mapKeys(\n object: List|Dictionary | null | undefined,\n iteratee?: TObject\n ): Dictionary;\n\n /**\n * @see _.mapKeys\n */\n mapKeys(\n object: List|Dictionary | null | undefined,\n iteratee?: string\n ): Dictionary;\n }\n\n interface LoDashImplicitArrayWrapperBase {\n /**\n * @see _.mapKeys\n */\n mapKeys(\n iteratee?: ListIterator\n ): LoDashImplicitObjectWrapper>;\n\n /**\n * @see _.mapKeys\n */\n mapKeys(\n iteratee?: TObject\n ): LoDashImplicitObjectWrapper>;\n\n /**\n * @see _.mapKeys\n */\n mapKeys(\n iteratee?: string\n ): LoDashImplicitObjectWrapper>;\n }\n\n interface LoDashImplicitObjectWrapperBase {\n /**\n * @see _.mapKeys\n */\n mapKeys(\n iteratee?: ListIterator|DictionaryIterator\n ): LoDashImplicitObjectWrapper>;\n\n /**\n * @see _.mapKeys\n */\n mapKeys(\n iteratee?: TObject\n ): LoDashImplicitObjectWrapper>;\n\n /**\n * @see _.mapKeys\n */\n mapKeys(\n iteratee?: string\n ): LoDashImplicitObjectWrapper>;\n }\n\n interface LoDashExplicitArrayWrapperBase {\n /**\n * @see _.mapKeys\n */\n mapKeys(\n iteratee?: ListIterator\n ): LoDashExplicitObjectWrapper>;\n\n /**\n * @see _.mapKeys\n */\n mapKeys(\n iteratee?: TObject\n ): LoDashExplicitObjectWrapper>;\n\n /**\n * @see _.mapKeys\n */\n mapKeys(\n iteratee?: string\n ): LoDashExplicitObjectWrapper>;\n }\n\n interface LoDashExplicitObjectWrapperBase {\n /**\n * @see _.mapKeys\n */\n mapKeys(\n iteratee?: ListIterator|DictionaryIterator\n ): LoDashExplicitObjectWrapper>;\n\n /**\n * @see _.mapKeys\n */\n mapKeys(\n iteratee?: TObject\n ): LoDashExplicitObjectWrapper>;\n\n /**\n * @see _.mapKeys\n */\n mapKeys(\n iteratee?: string\n ): LoDashExplicitObjectWrapper>;\n }\n\n //_.mapValues\n interface LoDashStatic {\n /**\n * Creates an object with the same keys as object and values generated by running each own\n * enumerable property of object through iteratee. The iteratee function is bound to thisArg\n * and invoked with three arguments: (value, key, object).\n *\n * If a property name is provided iteratee the created \"_.property\" style callback returns\n * the property value of the given element.\n *\n * If a value is also provided for thisArg the creted \"_.matchesProperty\" style callback returns\n * true for elements that have a matching property value, else false;.\n *\n * If an object is provided for iteratee the created \"_.matches\" style callback returns true\n * for elements that have the properties of the given object, else false.\n *\n * @param {Object} object The object to iterate over.\n * @param {Function|Object|string} [iteratee=_.identity] The function invoked per iteration.\n * @param {Object} [thisArg] The `this` binding of `iteratee`.\n * @return {Object} Returns the new mapped object.\n */\n mapValues(obj: Dictionary | null | undefined, callback: ObjectIterator): Dictionary;\n mapValues(obj: Dictionary | null | undefined, where: Dictionary): Dictionary;\n mapValues(obj: T | null | undefined, pluck: string): TMapped;\n mapValues(obj: T | null | undefined, callback: ObjectIterator): T;\n }\n\n interface LoDashImplicitObjectWrapperBase {\n /**\n * @see _.mapValues\n * TValue is the type of the property values of T.\n * TResult is the type output by the ObjectIterator function\n */\n mapValues(callback: ObjectIterator): LoDashImplicitObjectWrapper>;\n\n /**\n * @see _.mapValues\n * TResult is the type of the property specified by pluck.\n * T should be a Dictionary>\n */\n mapValues(pluck: string): LoDashImplicitObjectWrapper>;\n\n /**\n * @see _.mapValues\n * TResult is the type of the properties of each object in the values of T\n * T should be a Dictionary>\n */\n mapValues(where: Dictionary): LoDashImplicitArrayWrapper;\n }\n\n interface LoDashExplicitObjectWrapperBase {\n /**\n * @see _.mapValues\n * TValue is the type of the property values of T.\n * TResult is the type output by the ObjectIterator function\n */\n mapValues(callback: ObjectIterator): LoDashExplicitObjectWrapper>;\n\n /**\n * @see _.mapValues\n * TResult is the type of the property specified by pluck.\n * T should be a Dictionary>\n */\n mapValues(pluck: string): LoDashExplicitObjectWrapper>;\n\n /**\n * @see _.mapValues\n * TResult is the type of the properties of each object in the values of T\n * T should be a Dictionary>\n */\n mapValues(where: Dictionary): LoDashExplicitObjectWrapper;\n }\n\n //_.merge\n interface LoDashStatic {\n /**\n * Recursively merges own and inherited enumerable properties of source\n * objects into the destination object, skipping source properties that resolve\n * to `undefined`. Array and plain object properties are merged recursively.\n * Other objects and value types are overridden by assignment. Source objects\n * are applied from left to right. Subsequent sources overwrite property\n * assignments of previous sources.\n *\n * **Note:** This method mutates `object`.\n *\n * @static\n * @memberOf _\n * @category Object\n * @param {Object} object The destination object.\n * @param {...Object} [sources] The source objects.\n * @returns {Object} Returns `object`.\n * @example\n *\n * var users = {\n * 'data': [{ 'user': 'barney' }, { 'user': 'fred' }]\n * };\n *\n * var ages = {\n * 'data': [{ 'age': 36 }, { 'age': 40 }]\n * };\n *\n * _.merge(users, ages);\n * // => { 'data': [{ 'user': 'barney', 'age': 36 }, { 'user': 'fred', 'age': 40 }] }\n */\n merge(\n object: TObject,\n source: TSource\n ): TObject & TSource;\n\n /**\n * @see _.merge\n */\n merge(\n object: TObject,\n source1: TSource1,\n source2: TSource2\n ): TObject & TSource1 & TSource2;\n\n /**\n * @see _.merge\n */\n merge(\n object: TObject,\n source1: TSource1,\n source2: TSource2,\n source3: TSource3\n ): TObject & TSource1 & TSource2 & TSource3;\n\n /**\n * @see _.merge\n */\n merge(\n object: TObject,\n source1: TSource1,\n source2: TSource2,\n source3: TSource3,\n source4: TSource4\n ): TObject & TSource1 & TSource2 & TSource3 & TSource4;\n\n /**\n * @see _.merge\n */\n merge(\n object: any,\n ...otherArgs: any[]\n ): TResult;\n }\n\n interface LoDashImplicitObjectWrapper {\n /**\n * @see _.merge\n */\n merge(\n source: TSource\n ): LoDashImplicitObjectWrapper;\n\n /**\n * @see _.merge\n */\n merge(\n source1: TSource1,\n source2: TSource2\n ): LoDashImplicitObjectWrapper;\n\n /**\n * @see _.merge\n */\n merge(\n source1: TSource1,\n source2: TSource2,\n source3: TSource3\n ): LoDashImplicitObjectWrapper;\n\n /**\n * @see _.merge\n */\n merge(\n source1: TSource1,\n source2: TSource2,\n source3: TSource3,\n source4: TSource4\n ): LoDashImplicitObjectWrapper;\n\n /**\n * @see _.merge\n */\n merge(\n ...otherArgs: any[]\n ): LoDashImplicitObjectWrapper;\n }\n\n interface LoDashExplicitObjectWrapper {\n /**\n * @see _.merge\n */\n merge(\n source: TSource\n ): LoDashExplicitObjectWrapper;\n\n /**\n * @see _.merge\n */\n merge(\n source1: TSource1,\n source2: TSource2\n ): LoDashExplicitObjectWrapper;\n\n /**\n * @see _.merge\n */\n merge(\n source1: TSource1,\n source2: TSource2,\n source3: TSource3\n ): LoDashExplicitObjectWrapper;\n\n /**\n * @see _.merge\n */\n merge(\n ): LoDashExplicitObjectWrapper;\n\n /**\n * @see _.merge\n */\n merge(\n ...otherArgs: any[]\n ): LoDashExplicitObjectWrapper;\n }\n\n //_.mergeWith\n type MergeWithCustomizer = (value: any, srcValue: any, key?: string, object?: Object, source?: Object) => any;\n\n interface LoDashStatic {\n /**\n * This method is like `_.merge` except that it accepts `customizer` which\n * is invoked to produce the merged values of the destination and source\n * properties. If `customizer` returns `undefined` merging is handled by the\n * method instead. The `customizer` is invoked with seven arguments:\n * (objValue, srcValue, key, object, source, stack).\n *\n * @static\n * @memberOf _\n * @category Object\n * @param {Object} object The destination object.\n * @param {...Object} sources The source objects.\n * @param {Function} customizer The function to customize assigned values.\n * @returns {Object} Returns `object`.\n * @example\n *\n * function customizer(objValue, srcValue) {\n * if (_.isArray(objValue)) {\n * return objValue.concat(srcValue);\n * }\n * }\n *\n * var object = {\n * 'fruits': ['apple'],\n * 'vegetables': ['beet']\n * };\n *\n * var other = {\n * 'fruits': ['banana'],\n * 'vegetables': ['carrot']\n * };\n *\n * _.merge(object, other, customizer);\n * // => { 'fruits': ['apple', 'banana'], 'vegetables': ['beet', 'carrot'] }\n */\n mergeWith(\n object: TObject,\n source: TSource,\n customizer: MergeWithCustomizer\n ): TObject & TSource;\n\n /**\n * @see _.mergeWith\n */\n mergeWith(\n object: TObject,\n source1: TSource1,\n source2: TSource2,\n customizer: MergeWithCustomizer\n ): TObject & TSource1 & TSource2;\n\n /**\n * @see _.mergeWith\n */\n mergeWith(\n object: TObject,\n source1: TSource1,\n source2: TSource2,\n source3: TSource3,\n customizer: MergeWithCustomizer\n ): TObject & TSource1 & TSource2 & TSource3;\n\n /**\n * @see _.mergeWith\n */\n mergeWith(\n object: TObject,\n source1: TSource1,\n source2: TSource2,\n source3: TSource3,\n source4: TSource4,\n customizer: MergeWithCustomizer\n ): TObject & TSource1 & TSource2 & TSource3 & TSource4;\n\n /**\n * @see _.mergeWith\n */\n mergeWith(\n object: any,\n ...otherArgs: any[]\n ): TResult;\n }\n\n interface LoDashImplicitObjectWrapper {\n /**\n * @see _.mergeWith\n */\n mergeWith(\n source: TSource,\n customizer: MergeWithCustomizer\n ): LoDashImplicitObjectWrapper;\n\n /**\n * @see _.mergeWith\n */\n mergeWith(\n source1: TSource1,\n source2: TSource2,\n customizer: MergeWithCustomizer\n ): LoDashImplicitObjectWrapper;\n\n /**\n * @see _.mergeWith\n */\n mergeWith(\n source1: TSource1,\n source2: TSource2,\n source3: TSource3,\n customizer: MergeWithCustomizer\n ): LoDashImplicitObjectWrapper;\n\n /**\n * @see _.mergeWith\n */\n mergeWith(\n source1: TSource1,\n source2: TSource2,\n source3: TSource3,\n source4: TSource4,\n customizer: MergeWithCustomizer\n ): LoDashImplicitObjectWrapper;\n\n /**\n * @see _.mergeWith\n */\n mergeWith(\n ...otherArgs: any[]\n ): LoDashImplicitObjectWrapper;\n }\n\n //_.omit\n interface LoDashStatic {\n /**\n * The opposite of `_.pick`; this method creates an object composed of the\n * own and inherited enumerable properties of `object` that are not omitted.\n *\n * @static\n * @memberOf _\n * @category Object\n * @param {Object} object The source object.\n * @param {...(string|string[])} [props] The property names to omit, specified\n * individually or in arrays..\n * @returns {Object} Returns the new object.\n * @example\n *\n * var object = { 'a': 1, 'b': '2', 'c': 3 };\n *\n * _.omit(object, ['a', 'c']);\n * // => { 'b': '2' }\n */\n\n omit(\n object: T | null | undefined,\n ...predicate: Array>\n ): TResult;\n }\n\n interface LoDashImplicitObjectWrapperBase {\n /**\n * @see _.omit\n */\n omit(\n ...predicate: Array>\n ): LoDashImplicitObjectWrapper;\n }\n\n interface LoDashExplicitObjectWrapperBase {\n /**\n * @see _.omit\n */\n omit(\n ...predicate: Array>\n ): LoDashExplicitObjectWrapper;\n }\n\n //_.omitBy\n interface LoDashStatic {\n /**\n * The opposite of `_.pickBy`; this method creates an object composed of the\n * own and inherited enumerable properties of `object` that `predicate`\n * doesn't return truthy for.\n *\n * @static\n * @memberOf _\n * @category Object\n * @param {Object} object The source object.\n * @param {Function|Object|string} [predicate=_.identity] The function invoked per property.\n * @returns {Object} Returns the new object.\n * @example\n *\n * var object = { 'a': 1, 'b': '2', 'c': 3 };\n *\n * _.omitBy(object, _.isNumber);\n * // => { 'b': '2' }\n */\n omitBy(\n object: T | null | undefined,\n predicate: ObjectIterator\n ): TResult;\n }\n\n interface LoDashImplicitObjectWrapperBase {\n /**\n * @see _.omitBy\n */\n omitBy(\n predicate: ObjectIterator\n ): LoDashImplicitObjectWrapper;\n }\n\n interface LoDashExplicitObjectWrapperBase {\n /**\n * @see _.omitBy\n */\n omitBy(\n predicate: ObjectIterator\n ): LoDashExplicitObjectWrapper;\n }\n\n //_.pick\n interface LoDashStatic {\n /**\n * Creates an object composed of the picked `object` properties.\n *\n * @static\n * @memberOf _\n * @category Object\n * @param {Object} object The source object.\n * @param {...(string|string[])} [props] The property names to pick, specified\n * individually or in arrays.\n * @returns {Object} Returns the new object.\n * @example\n *\n * var object = { 'a': 1, 'b': '2', 'c': 3 };\n *\n * _.pick(object, ['a', 'c']);\n * // => { 'a': 1, 'c': 3 }\n */\n pick(\n object: T | null | undefined,\n ...predicate: Array>\n ): TResult;\n }\n\n interface LoDashImplicitObjectWrapperBase {\n /**\n * @see _.pick\n */\n pick(\n ...predicate: Array>\n ): LoDashImplicitObjectWrapper;\n }\n\n interface LoDashExplicitObjectWrapperBase {\n /**\n * @see _.pick\n */\n pick(\n ...predicate: Array>\n ): LoDashExplicitObjectWrapper;\n }\n\n //_.pickBy\n interface LoDashStatic {\n /**\n * Creates an object composed of the `object` properties `predicate` returns\n * truthy for. The predicate is invoked with one argument: (value).\n *\n * @static\n * @memberOf _\n * @category Object\n * @param {Object} object The source object.\n * @param {Function|Object|string} [predicate=_.identity] The function invoked per property.\n * @returns {Object} Returns the new object.\n * @example\n *\n * var object = { 'a': 1, 'b': '2', 'c': 3 };\n *\n * _.pickBy(object, _.isNumber);\n * // => { 'a': 1, 'c': 3 }\n */\n pickBy(\n object: T | null | undefined,\n predicate?: ObjectIterator\n ): TResult;\n }\n\n interface LoDashImplicitObjectWrapperBase {\n /**\n * @see _.pickBy\n */\n pickBy(\n predicate?: ObjectIterator\n ): LoDashImplicitObjectWrapper;\n }\n\n interface LoDashExplicitObjectWrapperBase {\n /**\n * @see _.pickBy\n */\n pickBy(\n predicate?: ObjectIterator\n ): LoDashExplicitObjectWrapper;\n }\n\n //_.result\n interface LoDashStatic {\n /**\n * This method is like _.get except that if the resolved value is a function it’s invoked with the this binding\n * of its parent object and its result is returned.\n *\n * @param object The object to query.\n * @param path The path of the property to resolve.\n * @param defaultValue The value returned if the resolved value is undefined.\n * @return Returns the resolved value.\n */\n result(\n object: TObject,\n path: Many,\n defaultValue?: TResult|((...args: any[]) => TResult)\n ): TResult;\n\n /**\n * @see _.result\n */\n result(\n object: any,\n path: Many,\n defaultValue?: TResult|((...args: any[]) => TResult)\n ): TResult;\n }\n\n interface LoDashImplicitWrapper {\n /**\n * @see _.result\n */\n result(\n path: Many,\n defaultValue?: TResult|((...args: any[]) => TResult)\n ): TResult;\n }\n\n interface LoDashImplicitArrayWrapperBase {\n /**\n * @see _.result\n */\n result(\n path: Many,\n defaultValue?: TResult|((...args: any[]) => TResult)\n ): TResult;\n }\n\n interface LoDashImplicitObjectWrapperBase {\n /**\n * @see _.result\n */\n result(\n path: Many,\n defaultValue?: TResult|((...args: any[]) => TResult)\n ): TResult;\n }\n\n interface LoDashExplicitWrapper {\n /**\n * @see _.result\n */\n result(\n path: Many,\n defaultValue?: any\n ): TResultWrapper;\n }\n\n interface LoDashExplicitArrayWrapperBase {\n /**\n * @see _.result\n */\n result(\n path: Many,\n defaultValue?: any\n ): TResultWrapper;\n }\n\n interface LoDashExplicitObjectWrapper {\n /**\n * @see _.result\n */\n result(\n path: Many,\n defaultValue?: any\n ): TResultWrapper;\n }\n\n //_.set\n interface LoDashStatic {\n /**\n * Sets the value at path of object. If a portion of path doesn’t exist it’s created. Arrays are created for\n * missing index properties while objects are created for all other missing properties. Use _.setWith to\n * customize path creation.\n *\n * @param object The object to modify.\n * @param path The path of the property to set.\n * @param value The value to set.\n * @return Returns object.\n */\n set(\n object: Object,\n path: Many,\n value: any\n ): TResult;\n\n /**\n * @see _.set\n */\n set(\n object: Object,\n path: Many,\n value: V\n ): TResult;\n\n /**\n * @see _.set\n */\n set(\n object: O,\n path: Many,\n value: V\n ): TResult;\n }\n\n interface LoDashImplicitObjectWrapper {\n /**\n * @see _.set\n */\n set(\n path: Many,\n value: any\n ): LoDashImplicitObjectWrapper;\n\n /**\n * @see _.set\n */\n set(\n path: Many,\n value: V\n ): LoDashImplicitObjectWrapper;\n }\n\n interface LoDashExplicitObjectWrapper {\n /**\n * @see _.set\n */\n set(\n path: Many,\n value: any\n ): LoDashExplicitObjectWrapper;\n\n /**\n * @see _.set\n */\n set(\n path: Many,\n value: V\n ): LoDashExplicitObjectWrapper;\n }\n\n //_.setWith\n type SetWithCustomizer = (nsValue: any, key: string, nsObject: T) => any;\n\n interface LoDashStatic {\n /**\n * This method is like _.set except that it accepts customizer which is invoked to produce the objects of\n * path. If customizer returns undefined path creation is handled by the method instead. The customizer is\n * invoked with three arguments: (nsValue, key, nsObject).\n *\n * @param object The object to modify.\n * @param path The path of the property to set.\n * @param value The value to set.\n * @parem customizer The function to customize assigned values.\n * @return Returns object.\n */\n setWith(\n object: Object,\n path: Many,\n value: any,\n customizer?: SetWithCustomizer\n ): TResult;\n\n /**\n * @see _.setWith\n */\n setWith(\n object: Object,\n path: Many,\n value: V,\n customizer?: SetWithCustomizer\n ): TResult;\n\n /**\n * @see _.setWith\n */\n setWith(\n object: O,\n path: Many,\n value: V,\n customizer?: SetWithCustomizer\n ): TResult;\n }\n\n interface LoDashImplicitObjectWrapper {\n /**\n * @see _.setWith\n */\n setWith(\n path: Many,\n value: any,\n customizer?: SetWithCustomizer\n ): LoDashImplicitObjectWrapper;\n\n /**\n * @see _.setWith\n */\n setWith(\n path: Many,\n value: V,\n customizer?: SetWithCustomizer\n ): LoDashImplicitObjectWrapper;\n }\n\n interface LoDashExplicitObjectWrapper {\n /**\n * @see _.setWith\n */\n setWith(\n path: Many,\n value: any,\n customizer?: SetWithCustomizer\n ): LoDashExplicitObjectWrapper;\n\n /**\n * @see _.setWith\n */\n setWith(\n path: Many,\n value: V,\n customizer?: SetWithCustomizer\n ): LoDashExplicitObjectWrapper;\n }\n\n //_.toPairs\n interface LoDashStatic {\n /**\n * Creates an array of own enumerable key-value pairs for object.\n *\n * @param object The object to query.\n * @return Returns the new array of key-value pairs.\n */\n toPairs(object?: T): [string, any][];\n\n toPairs(object?: T): [string, TResult][];\n }\n\n interface LoDashImplicitObjectWrapper {\n /**\n * @see _.toPairs\n */\n toPairs(): LoDashImplicitArrayWrapper<[string, TResult]>;\n }\n\n interface LoDashExplicitObjectWrapper {\n /**\n * @see _.toPairs\n */\n toPairs(): LoDashExplicitArrayWrapper<[string, TResult]>;\n }\n\n //_.toPairsIn\n interface LoDashStatic {\n /**\n * Creates an array of own and inherited enumerable key-value pairs for object.\n *\n * @param object The object to query.\n * @return Returns the new array of key-value pairs.\n */\n toPairsIn(object?: T): [string, any][];\n\n toPairsIn(object?: T): [string, TResult][];\n }\n\n interface LoDashImplicitObjectWrapper {\n /**\n * @see _.toPairsIn\n */\n toPairsIn(): LoDashImplicitArrayWrapper<[string, TResult]>;\n }\n\n interface LoDashExplicitObjectWrapper {\n /**\n * @see _.toPairsIn\n */\n toPairsIn(): LoDashExplicitArrayWrapper<[string, TResult]>;\n }\n\n //_.transform\n interface LoDashStatic {\n /**\n * An alternative to _.reduce; this method transforms object to a new accumulator object which is the result of\n * running each of its own enumerable properties through iteratee, with each invocation potentially mutating\n * the accumulator object. The iteratee is bound to thisArg and invoked with four arguments: (accumulator,\n * value, key, object). Iteratee functions may exit iteration early by explicitly returning false.\n *\n * @param object The object to iterate over.\n * @param iteratee The function invoked per iteration.\n * @param accumulator The custom accumulator value.\n * @param thisArg The this binding of iteratee.\n * @return Returns the accumulated value.\n */\n transform(\n object: T[],\n iteratee?: MemoVoidArrayIterator,\n accumulator?: TResult[]\n ): TResult[];\n\n /**\n * @see _.transform\n */\n transform(\n object: T[],\n iteratee?: MemoVoidArrayIterator>,\n accumulator?: Dictionary\n ): Dictionary;\n\n /**\n * @see _.transform\n */\n transform(\n object: Dictionary,\n iteratee?: MemoVoidDictionaryIterator>,\n accumulator?: Dictionary\n ): Dictionary;\n\n /**\n * @see _.transform\n */\n transform(\n object: Dictionary,\n iteratee?: MemoVoidDictionaryIterator,\n accumulator?: TResult[]\n ): TResult[];\n }\n\n interface LoDashImplicitArrayWrapper {\n /**\n * @see _.transform\n */\n transform(\n iteratee?: MemoVoidArrayIterator,\n accumulator?: TResult[]\n ): LoDashImplicitArrayWrapper;\n\n /**\n * @see _.transform\n */\n transform(\n iteratee?: MemoVoidArrayIterator>,\n accumulator?: Dictionary\n ): LoDashImplicitObjectWrapper>;\n }\n\n interface LoDashImplicitObjectWrapper {\n /**\n * @see _.transform\n */\n transform(\n iteratee?: MemoVoidDictionaryIterator>,\n accumulator?: Dictionary\n ): LoDashImplicitObjectWrapper>;\n\n /**\n * @see _.transform\n */\n transform(\n iteratee?: MemoVoidDictionaryIterator,\n accumulator?: TResult[]\n ): LoDashImplicitArrayWrapper;\n }\n\n //_.unset\n interface LoDashStatic {\n /**\n * Removes the property at path of object.\n *\n * Note: This method mutates object.\n *\n * @param object The object to modify.\n * @param path The path of the property to unset.\n * @return Returns true if the property is deleted, else false.\n */\n unset(\n object: T,\n path: Many\n ): boolean;\n }\n\n interface LoDashImplicitObjectWrapper {\n /**\n * @see _.unset\n */\n unset(path: Many): LoDashImplicitWrapper;\n }\n\n interface LoDashExplicitObjectWrapper {\n /**\n * @see _.unset\n */\n unset(path: Many): LoDashExplicitWrapper;\n }\n\n //_.update\n interface LoDashStatic {\n /**\n * This method is like _.set except that accepts updater to produce the value to set. Use _.updateWith to\n * customize path creation. The updater is invoked with one argument: (value).\n *\n * @param object The object to modify.\n * @param path The path of the property to set.\n * @param updater The function to produce the updated value.\n * @return Returns object.\n */\n update(\n object: Object,\n path: Many,\n updater: Function\n ): TResult;\n\n /**\n * @see _.update\n */\n update(\n object: Object,\n path: Many,\n updater: U\n ): TResult;\n\n /**\n * @see _.update\n */\n update(\n object: O,\n path: Many,\n updater: Function\n ): TResult;\n\n /**\n * @see _.update\n */\n update(\n object: O,\n path: Many,\n updater: U\n ): TResult;\n }\n\n interface LoDashImplicitObjectWrapper {\n /**\n * @see _.update\n */\n update(\n path: Many,\n updater: any\n ): LoDashImplicitObjectWrapper;\n\n /**\n * @see _.update\n */\n update(\n path: Many,\n updater: U\n ): LoDashImplicitObjectWrapper;\n }\n\n interface LoDashExplicitObjectWrapper {\n /**\n * @see _.update\n */\n update(\n path: Many,\n updater: any\n ): LoDashExplicitObjectWrapper;\n\n /**\n * @see _.update\n */\n update(\n path: Many,\n updater: U\n ): LoDashExplicitObjectWrapper;\n }\n\n //_.values\n interface LoDashStatic {\n /**\n * Creates an array of the own enumerable property values of object.\n *\n * @param object The object to query.\n * @return Returns an array of property values.\n */\n values(object?: Dictionary|NumericDictionary|List | null | undefined): T[];\n\n /**\n * @see _.values\n */\n values(object?: any): T[];\n }\n\n interface LoDashImplicitStringWrapper {\n /**\n * @see _.values\n */\n values(): LoDashImplicitArrayWrapper;\n }\n\n interface LoDashImplicitWrapper {\n /**\n * @see _.values\n */\n values(): LoDashImplicitArrayWrapper;\n }\n\n interface LoDashImplicitArrayWrapperBase {\n /**\n * @see _.values\n */\n values(): LoDashImplicitArrayWrapper;\n }\n\n interface LoDashImplicitObjectWrapperBase {\n /**\n * @see _.values\n */\n values(): LoDashImplicitArrayWrapper;\n }\n\n interface LoDashExplicitWrapper {\n /**\n * @see _.values\n */\n values(): LoDashExplicitArrayWrapper;\n }\n\n interface LoDashExplicitArrayWrapperBase {\n /**\n * @see _.values\n */\n values(): LoDashExplicitArrayWrapper;\n }\n\n interface LoDashExplicitObjectWrapperBase {\n /**\n * @see _.values\n */\n values(): LoDashExplicitArrayWrapper;\n }\n\n //_.valuesIn\n interface LoDashStatic {\n /**\n * Creates an array of the own and inherited enumerable property values of object.\n *\n * @param object The object to query.\n * @return Returns the array of property values.\n */\n valuesIn(object?: Dictionary): T[];\n\n /**\n * @see _.valuesIn\n */\n valuesIn(object?: any): T[];\n }\n\n interface LoDashImplicitObjectWrapperBase {\n /**\n * @see _.valuesIn\n */\n valuesIn(): LoDashImplicitArrayWrapper;\n }\n\n interface LoDashExplicitObjectWrapperBase {\n /**\n * @see _.valuesIn\n */\n valuesIn(): LoDashExplicitArrayWrapper;\n }\n\n /**********\n * String *\n **********/\n\n //_.camelCase\n interface LoDashStatic {\n /**\n * Converts string to camel case.\n *\n * @param string The string to convert.\n * @return Returns the camel cased string.\n */\n camelCase(string?: string): string;\n }\n\n interface LoDashImplicitWrapper {\n /**\n * @see _.camelCase\n */\n camelCase(): string;\n }\n\n interface LoDashExplicitWrapper {\n /**\n * @see _.camelCase\n */\n camelCase(): LoDashExplicitWrapper;\n }\n\n //_.capitalize\n interface LoDashStatic {\n /**\n * Converts the first character of string to upper case and the remaining to lower case.\n *\n * @param string The string to capitalize.\n * @return Returns the capitalized string.\n */\n capitalize(string?: string): string;\n }\n\n interface LoDashImplicitWrapper {\n /**\n * @see _.capitalize\n */\n capitalize(): string;\n }\n\n interface LoDashExplicitWrapper {\n /**\n * @see _.capitalize\n */\n capitalize(): LoDashExplicitWrapper;\n }\n\n //_.deburr\n interface LoDashStatic {\n /**\n * Deburrs string by converting latin-1 supplementary letters to basic latin letters and removing combining\n * diacritical marks.\n *\n * @param string The string to deburr.\n * @return Returns the deburred string.\n */\n deburr(string?: string): string;\n }\n\n interface LoDashImplicitWrapper {\n /**\n * @see _.deburr\n */\n deburr(): string;\n }\n\n interface LoDashExplicitWrapper {\n /**\n * @see _.deburr\n */\n deburr(): LoDashExplicitWrapper;\n }\n\n //_.endsWith\n interface LoDashStatic {\n /**\n * Checks if string ends with the given target string.\n *\n * @param string The string to search.\n * @param target The string to search for.\n * @param position The position to search from.\n * @return Returns true if string ends with target, else false.\n */\n endsWith(\n string?: string,\n target?: string,\n position?: number\n ): boolean;\n }\n\n interface LoDashImplicitWrapper {\n /**\n * @see _.endsWith\n */\n endsWith(\n target?: string,\n position?: number\n ): boolean;\n }\n\n interface LoDashExplicitWrapper {\n /**\n * @see _.endsWith\n */\n endsWith(\n target?: string,\n position?: number\n ): LoDashExplicitWrapper;\n }\n\n // _.escape\n interface LoDashStatic {\n /**\n * Converts the characters \"&\", \"<\", \">\", '\"', \"'\", and \"`\" in string to their corresponding HTML entities.\n *\n * Note: No other characters are escaped. To escape additional characters use a third-party library like he.\n *\n * hough the \">\" character is escaped for symmetry, characters like \">\" and \"/\" don’t need escaping in HTML\n * and have no special meaning unless they're part of a tag or unquoted attribute value. See Mathias Bynens’s\n * article (under \"semi-related fun fact\") for more details.\n *\n * Backticks are escaped because in IE < 9, they can break out of attribute values or HTML comments. See #59,\n * #102, #108, and #133 of the HTML5 Security Cheatsheet for more details.\n *\n * When working with HTML you should always quote attribute values to reduce XSS vectors.\n *\n * @param string The string to escape.\n * @return Returns the escaped string.\n */\n escape(string?: string): string;\n }\n\n interface LoDashImplicitWrapper {\n /**\n * @see _.escape\n */\n escape(): string;\n }\n\n interface LoDashExplicitWrapper {\n /**\n * @see _.escape\n */\n escape(): LoDashExplicitWrapper;\n }\n\n // _.escapeRegExp\n interface LoDashStatic {\n /**\n * Escapes the RegExp special characters \"^\", \"$\", \"\\\", \".\", \"*\", \"+\", \"?\", \"(\", \")\", \"[\", \"]\",\n * \"{\", \"}\", and \"|\" in string.\n *\n * @param string The string to escape.\n * @return Returns the escaped string.\n */\n escapeRegExp(string?: string): string;\n }\n\n interface LoDashImplicitWrapper {\n /**\n * @see _.escapeRegExp\n */\n escapeRegExp(): string;\n }\n\n interface LoDashExplicitWrapper {\n /**\n * @see _.escapeRegExp\n */\n escapeRegExp(): LoDashExplicitWrapper;\n }\n\n //_.kebabCase\n interface LoDashStatic {\n /**\n * Converts string to kebab case.\n *\n * @param string The string to convert.\n * @return Returns the kebab cased string.\n */\n kebabCase(string?: string): string;\n }\n\n interface LoDashImplicitWrapper {\n /**\n * @see _.kebabCase\n */\n kebabCase(): string;\n }\n\n interface LoDashExplicitWrapper {\n /**\n * @see _.kebabCase\n */\n kebabCase(): LoDashExplicitWrapper;\n }\n\n //_.lowerCase\n interface LoDashStatic {\n /**\n * Converts `string`, as space separated words, to lower case.\n *\n * @param string The string to convert.\n * @return Returns the lower cased string.\n */\n lowerCase(string?: string): string;\n }\n\n interface LoDashImplicitWrapper {\n /**\n * @see _.lowerCase\n */\n lowerCase(): string;\n }\n\n interface LoDashExplicitWrapper {\n /**\n * @see _.lowerCase\n */\n lowerCase(): LoDashExplicitWrapper;\n }\n\n //_.lowerFirst\n interface LoDashStatic {\n /**\n * Converts the first character of `string` to lower case.\n *\n * @param string The string to convert.\n * @return Returns the converted string.\n */\n lowerFirst(string?: string): string;\n }\n\n interface LoDashImplicitWrapper {\n /**\n * @see _.lowerFirst\n */\n lowerFirst(): string;\n }\n\n interface LoDashExplicitWrapper {\n /**\n * @see _.lowerFirst\n */\n lowerFirst(): LoDashExplicitWrapper;\n }\n\n //_.pad\n interface LoDashStatic {\n /**\n * Pads string on the left and right sides if it’s shorter than length. Padding characters are truncated if\n * they can’t be evenly divided by length.\n *\n * @param string The string to pad.\n * @param length The padding length.\n * @param chars The string used as padding.\n * @return Returns the padded string.\n */\n pad(\n string?: string,\n length?: number,\n chars?: string\n ): string;\n }\n\n interface LoDashImplicitWrapper {\n /**\n * @see _.pad\n */\n pad(\n length?: number,\n chars?: string\n ): string;\n }\n\n interface LoDashExplicitWrapper {\n /**\n * @see _.pad\n */\n pad(\n length?: number,\n chars?: string\n ): LoDashExplicitWrapper;\n }\n\n //_.padEnd\n interface LoDashStatic {\n /**\n * Pads string on the right side if it’s shorter than length. Padding characters are truncated if they exceed\n * length.\n *\n * @param string The string to pad.\n * @param length The padding length.\n * @param chars The string used as padding.\n * @return Returns the padded string.\n */\n padEnd(\n string?: string,\n length?: number,\n chars?: string\n ): string;\n }\n\n interface LoDashImplicitWrapper {\n /**\n * @see _.padEnd\n */\n padEnd(\n length?: number,\n chars?: string\n ): string;\n }\n\n interface LoDashExplicitWrapper {\n /**\n * @see _.padEnd\n */\n padEnd(\n length?: number,\n chars?: string\n ): LoDashExplicitWrapper;\n }\n\n //_.padStart\n interface LoDashStatic {\n /**\n * Pads string on the left side if it’s shorter than length. Padding characters are truncated if they exceed\n * length.\n *\n * @param string The string to pad.\n * @param length The padding length.\n * @param chars The string used as padding.\n * @return Returns the padded string.\n */\n padStart(\n string?: string,\n length?: number,\n chars?: string\n ): string;\n }\n\n interface LoDashImplicitWrapper {\n /**\n * @see _.padStart\n */\n padStart(\n length?: number,\n chars?: string\n ): string;\n }\n\n interface LoDashExplicitWrapper {\n /**\n * @see _.padStart\n */\n padStart(\n length?: number,\n chars?: string\n ): LoDashExplicitWrapper;\n }\n\n //_.parseInt\n interface LoDashStatic {\n /**\n * Converts string to an integer of the specified radix. If radix is undefined or 0, a radix of 10 is used\n * unless value is a hexadecimal, in which case a radix of 16 is used.\n *\n * Note: This method aligns with the ES5 implementation of parseInt.\n *\n * @param string The string to convert.\n * @param radix The radix to interpret value by.\n * @return Returns the converted integer.\n */\n parseInt(\n string: string,\n radix?: number\n ): number;\n }\n\n interface LoDashImplicitWrapper {\n /**\n * @see _.parseInt\n */\n parseInt(radix?: number): number;\n }\n\n interface LoDashExplicitWrapper {\n /**\n * @see _.parseInt\n */\n parseInt(radix?: number): LoDashExplicitWrapper;\n }\n\n //_.repeat\n interface LoDashStatic {\n /**\n * Repeats the given string n times.\n *\n * @param string The string to repeat.\n * @param n The number of times to repeat the string.\n * @return Returns the repeated string.\n */\n repeat(\n string?: string,\n n?: number\n ): string;\n }\n\n interface LoDashImplicitWrapper {\n /**\n * @see _.repeat\n */\n repeat(n?: number): string;\n }\n\n interface LoDashExplicitWrapper {\n /**\n * @see _.repeat\n */\n repeat(n?: number): LoDashExplicitWrapper;\n }\n\n //_.replace\n interface LoDashStatic {\n /**\n * Replaces matches for pattern in string with replacement.\n *\n * Note: This method is based on String#replace.\n *\n * @param string\n * @param pattern\n * @param replacement\n * @return Returns the modified string.\n */\n replace(\n string: string,\n pattern: RegExp|string,\n replacement: Function|string\n ): string;\n\n /**\n * @see _.replace\n */\n replace(\n pattern?: RegExp|string,\n replacement?: Function|string\n ): string;\n }\n\n interface LoDashImplicitWrapper {\n /**\n * @see _.replace\n */\n replace(\n pattern?: RegExp|string,\n replacement?: Function|string\n ): string;\n\n /**\n * @see _.replace\n */\n replace(\n replacement?: Function|string\n ): string;\n }\n\n interface LoDashImplicitObjectWrapper {\n /**\n * @see _.replace\n */\n replace(\n pattern?: RegExp|string,\n replacement?: Function|string\n ): string;\n\n /**\n * @see _.replace\n */\n replace(\n replacement?: Function|string\n ): string;\n }\n\n interface LoDashExplicitWrapper {\n /**\n * @see _.replace\n */\n replace(\n pattern?: RegExp|string,\n replacement?: Function|string\n ): LoDashExplicitWrapper;\n\n /**\n * @see _.replace\n */\n replace(\n replacement?: Function|string\n ): LoDashExplicitWrapper;\n }\n\n interface LoDashExplicitObjectWrapper {\n /**\n * @see _.replace\n */\n replace(\n pattern?: RegExp|string,\n replacement?: Function|string\n ): LoDashExplicitWrapper;\n\n /**\n * @see _.replace\n */\n replace(\n replacement?: Function|string\n ): LoDashExplicitWrapper;\n }\n\n //_.snakeCase\n interface LoDashStatic {\n /**\n * Converts string to snake case.\n *\n * @param string The string to convert.\n * @return Returns the snake cased string.\n */\n snakeCase(string?: string): string;\n }\n\n interface LoDashImplicitWrapper {\n /**\n * @see _.snakeCase\n */\n snakeCase(): string;\n }\n\n interface LoDashExplicitWrapper {\n /**\n * @see _.snakeCase\n */\n snakeCase(): LoDashExplicitWrapper;\n }\n\n //_.split\n interface LoDashStatic {\n /**\n * Splits string by separator.\n *\n * Note: This method is based on String#split.\n *\n * @param string\n * @param separator\n * @param limit\n * @return Returns the new array of string segments.\n */\n split(\n string: string,\n separator?: RegExp|string,\n limit?: number\n ): string[];\n }\n\n interface LoDashImplicitWrapper {\n /**\n * @see _.split\n */\n split(\n separator?: RegExp|string,\n limit?: number\n ): LoDashImplicitArrayWrapper;\n }\n\n interface LoDashExplicitWrapper {\n /**\n * @see _.split\n */\n split(\n separator?: RegExp|string,\n limit?: number\n ): LoDashExplicitArrayWrapper;\n }\n\n //_.startCase\n interface LoDashStatic {\n /**\n * Converts string to start case.\n *\n * @param string The string to convert.\n * @return Returns the start cased string.\n */\n startCase(string?: string): string;\n }\n\n interface LoDashImplicitWrapper {\n /**\n * @see _.startCase\n */\n startCase(): string;\n }\n\n interface LoDashExplicitWrapper {\n /**\n * @see _.startCase\n */\n startCase(): LoDashExplicitWrapper;\n }\n\n //_.startsWith\n interface LoDashStatic {\n /**\n * Checks if string starts with the given target string.\n *\n * @param string The string to search.\n * @param target The string to search for.\n * @param position The position to search from.\n * @return Returns true if string starts with target, else false.\n */\n startsWith(\n string?: string,\n target?: string,\n position?: number\n ): boolean;\n }\n\n interface LoDashImplicitWrapper {\n /**\n * @see _.startsWith\n */\n startsWith(\n target?: string,\n position?: number\n ): boolean;\n }\n\n interface LoDashExplicitWrapper {\n /**\n * @see _.startsWith\n */\n startsWith(\n target?: string,\n position?: number\n ): LoDashExplicitWrapper;\n }\n\n //_.template\n interface TemplateOptions extends TemplateSettings {\n /**\n * The sourceURL of the template's compiled source.\n */\n sourceURL?: string;\n }\n\n interface TemplateExecutor {\n (data?: Object): string;\n source: string;\n }\n\n interface LoDashStatic {\n /**\n * Creates a compiled template function that can interpolate data properties in \"interpolate\" delimiters,\n * HTML-escape interpolated data properties in \"escape\" delimiters, and execute JavaScript in \"evaluate\"\n * delimiters. Data properties may be accessed as free variables in the template. If a setting object is\n * provided it takes precedence over _.templateSettings values.\n *\n * Note: In the development build _.template utilizes\n * [sourceURLs](http://www.html5rocks.com/en/tutorials/developertools/sourcemaps/#toc-sourceurl) for easier\n * debugging.\n *\n * For more information on precompiling templates see\n * [lodash's custom builds documentation](https://lodash.com/custom-builds).\n *\n * For more information on Chrome extension sandboxes see\n * [Chrome's extensions documentation](https://developer.chrome.com/extensions/sandboxingEval).\n *\n * @param string The template string.\n * @param options The options object.\n * @param options.escape The HTML \"escape\" delimiter.\n * @param options.evaluate The \"evaluate\" delimiter.\n * @param options.imports An object to import into the template as free variables.\n * @param options.interpolate The \"interpolate\" delimiter.\n * @param options.sourceURL The sourceURL of the template's compiled source.\n * @param options.variable The data object variable name.\n * @return Returns the compiled template function.\n */\n template(\n string: string,\n options?: TemplateOptions\n ): TemplateExecutor;\n }\n\n interface LoDashImplicitWrapper {\n /**\n * @see _.template\n */\n template(options?: TemplateOptions): TemplateExecutor;\n }\n\n interface LoDashExplicitWrapper {\n /**\n * @see _.template\n */\n template(options?: TemplateOptions): LoDashExplicitObjectWrapper;\n }\n\n //_.toLower\n interface LoDashStatic {\n /**\n * Converts `string`, as a whole, to lower case.\n *\n * @param string The string to convert.\n * @return Returns the lower cased string.\n */\n toLower(string?: string): string;\n }\n\n interface LoDashImplicitWrapper {\n /**\n * @see _.toLower\n */\n toLower(): string;\n }\n\n interface LoDashExplicitWrapper {\n /**\n * @see _.toLower\n */\n toLower(): LoDashExplicitWrapper;\n }\n\n //_.toUpper\n interface LoDashStatic {\n /**\n * Converts `string`, as a whole, to upper case.\n *\n * @param string The string to convert.\n * @return Returns the upper cased string.\n */\n toUpper(string?: string): string;\n }\n\n interface LoDashImplicitWrapper {\n /**\n * @see _.toUpper\n */\n toUpper(): string;\n }\n\n interface LoDashExplicitWrapper {\n /**\n * @see _.toUpper\n */\n toUpper(): LoDashExplicitWrapper;\n }\n\n //_.trim\n interface LoDashStatic {\n /**\n * Removes leading and trailing whitespace or specified characters from string.\n *\n * @param string The string to trim.\n * @param chars The characters to trim.\n * @return Returns the trimmed string.\n */\n trim(\n string?: string,\n chars?: string\n ): string;\n }\n\n interface LoDashImplicitWrapper {\n /**\n * @see _.trim\n */\n trim(chars?: string): string;\n }\n\n interface LoDashExplicitWrapper {\n /**\n * @see _.trim\n */\n trim(chars?: string): LoDashExplicitWrapper;\n }\n\n //_.trimEnd\n interface LoDashStatic {\n /**\n * Removes trailing whitespace or specified characters from string.\n *\n * @param string The string to trim.\n * @param chars The characters to trim.\n * @return Returns the trimmed string.\n */\n trimEnd(\n string?: string,\n chars?: string\n ): string;\n }\n\n interface LoDashImplicitWrapper {\n /**\n * @see _.trimEnd\n */\n trimEnd(chars?: string): string;\n }\n\n interface LoDashExplicitWrapper {\n /**\n * @see _.trimEnd\n */\n trimEnd(chars?: string): LoDashExplicitWrapper;\n }\n\n //_.trimStart\n interface LoDashStatic {\n /**\n * Removes leading whitespace or specified characters from string.\n *\n * @param string The string to trim.\n * @param chars The characters to trim.\n * @return Returns the trimmed string.\n */\n trimStart(\n string?: string,\n chars?: string\n ): string;\n }\n\n interface LoDashImplicitWrapper {\n /**\n * @see _.trimStart\n */\n trimStart(chars?: string): string;\n }\n\n interface LoDashExplicitWrapper {\n /**\n * @see _.trimStart\n */\n trimStart(chars?: string): LoDashExplicitWrapper;\n }\n\n //_.truncate\n interface TruncateOptions {\n /** The maximum string length. */\n length?: number;\n /** The string to indicate text is omitted. */\n omission?: string;\n /** The separator pattern to truncate to. */\n separator?: string|RegExp;\n }\n\n interface LoDashStatic {\n /**\n * Truncates string if it’s longer than the given maximum string length. The last characters of the truncated\n * string are replaced with the omission string which defaults to \"…\".\n *\n * @param string The string to truncate.\n * @param options The options object or maximum string length.\n * @return Returns the truncated string.\n */\n truncate(\n string?: string,\n options?: TruncateOptions\n ): string;\n }\n\n interface LoDashImplicitWrapper {\n /**\n * @see _.truncate\n */\n truncate(options?: TruncateOptions): string;\n }\n\n interface LoDashExplicitWrapper {\n /**\n * @see _.truncate\n */\n truncate(options?: TruncateOptions): LoDashExplicitWrapper;\n }\n\n //_.unescape\n interface LoDashStatic {\n /**\n * The inverse of _.escape; this method converts the HTML entities &, <, >, ", ', and `\n * in string to their corresponding characters.\n *\n * Note: No other HTML entities are unescaped. To unescape additional HTML entities use a third-party library\n * like he.\n *\n * @param string The string to unescape.\n * @return Returns the unescaped string.\n */\n unescape(string?: string): string;\n }\n\n interface LoDashImplicitWrapper {\n /**\n * @see _.unescape\n */\n unescape(): string;\n }\n\n interface LoDashExplicitWrapper {\n /**\n * @see _.unescape\n */\n unescape(): LoDashExplicitWrapper;\n }\n\n //_.upperCase\n interface LoDashStatic {\n /**\n * Converts `string`, as space separated words, to upper case.\n *\n * @param string The string to convert.\n * @return Returns the upper cased string.\n */\n upperCase(string?: string): string;\n }\n\n interface LoDashImplicitWrapper {\n /**\n * @see _.upperCase\n */\n upperCase(): string;\n }\n\n interface LoDashExplicitWrapper {\n /**\n * @see _.upperCase\n */\n upperCase(): LoDashExplicitWrapper;\n }\n\n //_.upperFirst\n interface LoDashStatic {\n /**\n * Converts the first character of `string` to upper case.\n *\n * @param string The string to convert.\n * @return Returns the converted string.\n */\n upperFirst(string?: string): string;\n }\n\n interface LoDashImplicitWrapper {\n /**\n * @see _.upperFirst\n */\n upperFirst(): string;\n }\n\n interface LoDashExplicitWrapper {\n /**\n * @see _.upperFirst\n */\n upperFirst(): LoDashExplicitWrapper;\n }\n\n //_.words\n interface LoDashStatic {\n /**\n * Splits `string` into an array of its words.\n *\n * @param string The string to inspect.\n * @param pattern The pattern to match words.\n * @return Returns the words of `string`.\n */\n words(\n string?: string,\n pattern?: string|RegExp\n ): string[];\n }\n\n interface LoDashImplicitWrapper {\n /**\n * @see _.words\n */\n words(pattern?: string|RegExp): string[];\n }\n\n interface LoDashExplicitWrapper {\n /**\n * @see _.words\n */\n words(pattern?: string|RegExp): LoDashExplicitArrayWrapper;\n }\n\n /***********\n * Utility *\n ***********/\n\n //_.attempt\n interface LoDashStatic {\n /**\n * Attempts to invoke func, returning either the result or the caught error object. Any additional arguments\n * are provided to func when it’s invoked.\n *\n * @param func The function to attempt.\n * @return Returns the func result or error object.\n */\n attempt(func: (...args: any[]) => TResult, ...args: any[]): TResult|Error;\n }\n\n interface LoDashImplicitObjectWrapper {\n /**\n * @see _.attempt\n */\n attempt(...args: any[]): TResult|Error;\n }\n\n interface LoDashExplicitObjectWrapper {\n /**\n * @see _.attempt\n */\n attempt(...args: any[]): LoDashExplicitObjectWrapper;\n }\n\n //_.constant\n interface LoDashStatic {\n /**\n * Creates a function that returns value.\n *\n * @param value The value to return from the new function.\n * @return Returns the new function.\n */\n constant(value: T): () => T;\n }\n\n interface LoDashImplicitWrapperBase {\n /**\n * @see _.constant\n */\n constant(): LoDashImplicitObjectWrapper<() => TResult>;\n }\n\n interface LoDashExplicitWrapperBase {\n /**\n * @see _.constant\n */\n constant(): LoDashExplicitObjectWrapper<() => TResult>;\n }\n\n //_.defaultTo\n interface LoDashStatic {\n /**\n * Checks `value` to determine whether a default value should be returned in\n * its place. The `defaultValue` is returned if `value` is `NaN`, `null`,\n * or `undefined`.\n *\n * @param value The value to check.\n * @param defaultValue The default value.\n * @returns Returns the resolved value.\n */\n defaultTo(value: T | null | undefined, defaultValue: T): T;\n }\n\n interface LoDashImplicitWrapperBase {\n /**\n * @see _.defaultTo\n */\n defaultTo(value: TResult): LoDashImplicitObjectWrapper;\n }\n\n interface LoDashExplicitWrapperBase {\n /**\n * @see _.defaultTo\n */\n defaultTo(value: TResult): LoDashExplicitObjectWrapper;\n }\n\n //_.identity\n interface LoDashStatic {\n /**\n * This method returns the first argument provided to it.\n *\n * @param value Any value.\n * @return Returns value.\n */\n identity(value: T): T;\n\n /**\n * @see _.identity\n */\n identity(): undefined;\n }\n\n interface LoDashImplicitWrapper {\n /**\n * @see _.identity\n */\n identity(): T;\n }\n\n interface LoDashImplicitArrayWrapperBase {\n /**\n * @see _.identity\n */\n identity(): TArray;\n }\n\n interface LoDashImplicitObjectWrapperBase {\n /**\n * @see _.identity\n */\n identity(): TObject;\n }\n\n interface LoDashExplicitWrapper {\n /**\n * @see _.identity\n */\n identity(): LoDashExplicitWrapper;\n }\n\n interface LoDashExplicitArrayWrapperBase {\n /**\n * @see _.identity\n */\n identity(): TWrapper;\n }\n\n interface LoDashExplicitObjectWrapperBase {\n /**\n * @see _.identity\n */\n identity(): TWrapper;\n }\n\n //_.iteratee\n interface LoDashStatic {\n /**\n * Creates a function that invokes `func` with the arguments of the created\n * function. If `func` is a property name the created callback returns the\n * property value for a given element. If `func` is an object the created\n * callback returns `true` for elements that contain the equivalent object properties, otherwise it returns `false`.\n *\n * @static\n * @memberOf _\n * @category Util\n * @param {*} [func=_.identity] The value to convert to a callback.\n * @returns {Function} Returns the callback.\n * @example\n *\n * var users = [\n * { 'user': 'barney', 'age': 36 },\n * { 'user': 'fred', 'age': 40 }\n * ];\n *\n * // create custom iteratee shorthands\n * _.iteratee = _.wrap(_.iteratee, function(callback, func) {\n * var p = /^(\\S+)\\s*([<>])\\s*(\\S+)$/.exec(func);\n * return !p ? callback(func) : function(object) {\n * return (p[2] == '>' ? object[p[1]] > p[3] : object[p[1]] < p[3]);\n * };\n * });\n *\n * _.filter(users, 'age > 36');\n * // => [{ 'user': 'fred', 'age': 40 }]\n */\n iteratee(\n func: TFunction\n ): TFunction;\n\n /**\n * @see _.iteratee\n */\n iteratee(\n func: string\n ): (object: any) => TResult;\n\n /**\n * @see _.iteratee\n */\n iteratee(\n func: Object\n ): (object: any) => boolean;\n\n /**\n * @see _.iteratee\n */\n iteratee(): (value: TResult) => TResult;\n }\n\n interface LoDashImplicitWrapper {\n /**\n * @see _.iteratee\n */\n iteratee(): LoDashImplicitObjectWrapper<(object: any) => TResult>;\n }\n\n interface LoDashImplicitObjectWrapper {\n /**\n * @see _.iteratee\n */\n iteratee(): LoDashImplicitObjectWrapper<(object: any) => boolean>;\n\n /**\n * @see _.iteratee\n */\n iteratee(): LoDashImplicitObjectWrapper<(...args: any[]) => TResult>;\n }\n\n interface LoDashExplicitWrapper {\n /**\n * @see _.iteratee\n */\n iteratee(): LoDashExplicitObjectWrapper<(object: any) => TResult>;\n }\n\n interface LoDashExplicitObjectWrapper {\n /**\n * @see _.iteratee\n */\n iteratee(): LoDashExplicitObjectWrapper<(object: any) => boolean>;\n\n /**\n * @see _.iteratee\n */\n iteratee(): LoDashExplicitObjectWrapper<(...args: any[]) => TResult>;\n }\n\n //_.matches\n interface LoDashStatic {\n /**\n * Creates a function that performs a deep comparison between a given object and source, returning true if the\n * given object has equivalent property values, else false.\n *\n * Note: This method supports comparing arrays, booleans, Date objects, numbers, Object objects, regexes, and\n * strings. Objects are compared by their own, not inherited, enumerable properties. For comparing a single own\n * or inherited property value see _.matchesProperty.\n *\n * @param source The object of property values to match.\n * @return Returns the new function.\n */\n matches(source: T): (value: any) => boolean;\n\n /**\n * @see _.matches\n */\n matches(source: T): (value: V) => boolean;\n }\n\n interface LoDashImplicitWrapperBase {\n /**\n * @see _.matches\n */\n matches(): LoDashImplicitObjectWrapper<(value: V) => boolean>;\n }\n\n interface LoDashExplicitWrapperBase {\n /**\n * @see _.matches\n */\n matches(): LoDashExplicitObjectWrapper<(value: V) => boolean>;\n }\n\n //_.matchesProperty\n interface LoDashStatic {\n /**\n * Creates a function that compares the property value of path on a given object to value.\n *\n * Note: This method supports comparing arrays, booleans, Date objects, numbers, Object objects, regexes, and\n * strings. Objects are compared by their own, not inherited, enumerable properties.\n *\n * @param path The path of the property to get.\n * @param srcValue The value to match.\n * @return Returns the new function.\n */\n matchesProperty(\n path: Many,\n srcValue: T\n ): (value: any) => boolean;\n\n /**\n * @see _.matchesProperty\n */\n matchesProperty(\n path: Many,\n srcValue: T\n ): (value: V) => boolean;\n }\n\n interface LoDashImplicitWrapperBase {\n /**\n * @see _.matchesProperty\n */\n matchesProperty(\n srcValue: SrcValue\n ): LoDashImplicitObjectWrapper<(value: any) => boolean>;\n\n /**\n * @see _.matchesProperty\n */\n matchesProperty(\n srcValue: SrcValue\n ): LoDashImplicitObjectWrapper<(value: Value) => boolean>;\n }\n\n interface LoDashExplicitWrapperBase {\n /**\n * @see _.matchesProperty\n */\n matchesProperty(\n srcValue: SrcValue\n ): LoDashExplicitObjectWrapper<(value: any) => boolean>;\n\n /**\n * @see _.matchesProperty\n */\n matchesProperty(\n srcValue: SrcValue\n ): LoDashExplicitObjectWrapper<(value: Value) => boolean>;\n }\n\n //_.method\n interface LoDashStatic {\n /**\n * Creates a function that invokes the method at path on a given object. Any additional arguments are provided\n * to the invoked method.\n *\n * @param path The path of the method to invoke.\n * @param args The arguments to invoke the method with.\n * @return Returns the new function.\n */\n method(\n path: string|StringRepresentable[],\n ...args: any[]\n ): (object: TObject) => TResult;\n\n /**\n * @see _.method\n */\n method(\n path: string|StringRepresentable[],\n ...args: any[]\n ): (object: any) => TResult;\n }\n\n interface LoDashImplicitWrapper {\n /**\n * @see _.method\n */\n method(...args: any[]): LoDashImplicitObjectWrapper<(object: TObject) => TResult>;\n\n /**\n * @see _.method\n */\n method(...args: any[]): LoDashImplicitObjectWrapper<(object: any) => TResult>;\n }\n\n interface LoDashImplicitArrayWrapper {\n /**\n * @see _.method\n */\n method(...args: any[]): LoDashImplicitObjectWrapper<(object: TObject) => TResult>;\n\n /**\n * @see _.method\n */\n method(...args: any[]): LoDashImplicitObjectWrapper<(object: any) => TResult>;\n }\n\n interface LoDashExplicitWrapper {\n /**\n * @see _.method\n */\n method(...args: any[]): LoDashExplicitObjectWrapper<(object: TObject) => TResult>;\n\n /**\n * @see _.method\n */\n method(...args: any[]): LoDashExplicitObjectWrapper<(object: any) => TResult>;\n }\n\n interface LoDashExplicitArrayWrapper {\n /**\n * @see _.method\n */\n method(...args: any[]): LoDashExplicitObjectWrapper<(object: TObject) => TResult>;\n\n /**\n * @see _.method\n */\n method(...args: any[]): LoDashExplicitObjectWrapper<(object: any) => TResult>;\n }\n\n //_.methodOf\n interface LoDashStatic {\n /**\n * The opposite of _.method; this method creates a function that invokes the method at a given path on object.\n * Any additional arguments are provided to the invoked method.\n *\n * @param object The object to query.\n * @param args The arguments to invoke the method with.\n * @return Returns the new function.\n */\n methodOf(\n object: TObject,\n ...args: any[]\n ): (path: Many) => TResult;\n\n /**\n * @see _.methodOf\n */\n methodOf(\n object: {},\n ...args: any[]\n ): (path: Many) => TResult;\n }\n\n interface LoDashImplicitObjectWrapper {\n /**\n * @see _.methodOf\n */\n methodOf(\n ...args: any[]\n ): LoDashImplicitObjectWrapper<(path: Many) => TResult>;\n }\n\n interface LoDashExplicitObjectWrapper {\n /**\n * @see _.methodOf\n */\n methodOf(\n ...args: any[]\n ): LoDashExplicitObjectWrapper<(path: Many) => TResult>;\n }\n\n //_.mixin\n interface MixinOptions {\n chain?: boolean;\n }\n\n interface LoDashStatic {\n /**\n * Adds all own enumerable function properties of a source object to the destination object. If object is a\n * function then methods are added to its prototype as well.\n *\n * Note: Use _.runInContext to create a pristine lodash function to avoid conflicts caused by modifying\n * the original.\n *\n * @param object The destination object.\n * @param source The object of functions to add.\n * @param options The options object.\n * @param options.chain Specify whether the functions added are chainable.\n * @return Returns object.\n */\n mixin(\n object: TObject,\n source: Dictionary,\n options?: MixinOptions\n ): TResult;\n\n /**\n * @see _.mixin\n */\n mixin(\n source: Dictionary,\n options?: MixinOptions\n ): TResult;\n }\n\n interface LoDashImplicitObjectWrapper {\n /**\n * @see _.mixin\n */\n mixin(\n source: Dictionary,\n options?: MixinOptions\n ): LoDashImplicitObjectWrapper;\n\n /**\n * @see _.mixin\n */\n mixin(\n options?: MixinOptions\n ): LoDashImplicitObjectWrapper;\n }\n\n interface LoDashExplicitObjectWrapper {\n /**\n * @see _.mixin\n */\n mixin(\n source: Dictionary,\n options?: MixinOptions\n ): LoDashExplicitObjectWrapper;\n\n /**\n * @see _.mixin\n */\n mixin(\n options?: MixinOptions\n ): LoDashExplicitObjectWrapper;\n }\n\n //_.noConflict\n interface LoDashStatic {\n /**\n * Reverts the _ variable to its previous value and returns a reference to the lodash function.\n *\n * @return Returns the lodash function.\n */\n noConflict(): typeof _;\n }\n\n interface LoDashImplicitWrapperBase {\n /**\n * @see _.noConflict\n */\n noConflict(): typeof _;\n }\n\n interface LoDashExplicitWrapperBase {\n /**\n * @see _.noConflict\n */\n noConflict(): LoDashExplicitObjectWrapper;\n }\n\n //_.noop\n interface LoDashStatic {\n /**\n * A no-operation function that returns undefined regardless of the arguments it receives.\n *\n * @return undefined\n */\n noop(...args: any[]): void;\n }\n\n interface LoDashImplicitWrapperBase {\n /**\n * @see _.noop\n */\n noop(...args: any[]): void;\n }\n\n interface LoDashExplicitWrapperBase {\n /**\n * @see _.noop\n */\n noop(...args: any[]): _.LoDashExplicitWrapper;\n }\n\n //_.nthArg\n interface LoDashStatic {\n /**\n * Creates a function that returns its nth argument.\n *\n * @param n The index of the argument to return.\n * @return Returns the new function.\n */\n nthArg(n?: number): TResult;\n }\n\n interface LoDashImplicitWrapper {\n /**\n * @see _.nthArg\n */\n nthArg(): LoDashImplicitObjectWrapper;\n }\n\n interface LoDashExplicitWrapper {\n /**\n * @see _.nthArg\n */\n nthArg(): LoDashExplicitObjectWrapper;\n }\n\n //_.over\n interface LoDashStatic {\n /**\n * Creates a function that invokes iteratees with the arguments provided to the created function and returns\n * their results.\n *\n * @param iteratees The iteratees to invoke.\n * @return Returns the new function.\n */\n over(...iteratees: Array>): (...args: any[]) => TResult[];\n }\n\n interface LoDashImplicitArrayWrapper {\n /**\n * @see _.over\n */\n over(...iteratees: Array>): LoDashImplicitObjectWrapper<(...args: any[]) => TResult[]>;\n }\n\n interface LoDashImplicitObjectWrapper {\n /**\n * @see _.over\n */\n over(...iteratees: Array>): LoDashImplicitObjectWrapper<(...args: any[]) => TResult[]>;\n }\n\n interface LoDashExplicitArrayWrapper {\n /**\n * @see _.over\n */\n over(...iteratees: Array>): LoDashExplicitObjectWrapper<(...args: any[]) => TResult[]>;\n }\n\n interface LoDashExplicitObjectWrapper {\n /**\n * @see _.over\n */\n over(...iteratees: Array>): LoDashExplicitObjectWrapper<(...args: any[]) => TResult[]>;\n }\n\n //_.overEvery\n interface LoDashStatic {\n /**\n * Creates a function that checks if all of the predicates return truthy when invoked with the arguments\n * provided to the created function.\n *\n * @param predicates The predicates to check.\n * @return Returns the new function.\n */\n overEvery(...predicates: Array>): (...args: any[]) => boolean;\n }\n\n interface LoDashImplicitArrayWrapper {\n /**\n * @see _.overEvery\n */\n overEvery(...predicates: Array>): LoDashImplicitObjectWrapper<(...args: any[]) => boolean>;\n }\n\n interface LoDashImplicitObjectWrapper {\n /**\n * @see _.overEvery\n */\n overEvery(...predicates: Array>): LoDashImplicitObjectWrapper<(...args: any[]) => boolean>;\n }\n\n interface LoDashExplicitArrayWrapper {\n /**\n * @see _.overEvery\n */\n overEvery(...predicates: Array>): LoDashExplicitObjectWrapper<(...args: any[]) => boolean>;\n }\n\n interface LoDashExplicitObjectWrapper {\n /**\n * @see _.overEvery\n */\n overEvery(...predicates: Array>): LoDashExplicitObjectWrapper<(...args: any[]) => boolean>;\n }\n\n //_.overSome\n interface LoDashStatic {\n /**\n * Creates a function that checks if any of the predicates return truthy when invoked with the arguments\n * provided to the created function.\n *\n * @param predicates The predicates to check.\n * @return Returns the new function.\n */\n overSome(...predicates: Array>): (...args: any[]) => boolean;\n }\n\n interface LoDashImplicitArrayWrapper {\n /**\n * @see _.overSome\n */\n overSome(...predicates: Array>): LoDashImplicitObjectWrapper<(...args: any[]) => boolean>;\n }\n\n interface LoDashImplicitObjectWrapper {\n /**\n * @see _.overSome\n */\n overSome(...predicates: Array>): LoDashImplicitObjectWrapper<(...args: any[]) => boolean>;\n }\n\n interface LoDashExplicitArrayWrapper {\n /**\n * @see _.overSome\n */\n overSome(...predicates: Array>): LoDashExplicitObjectWrapper<(...args: any[]) => boolean>;\n }\n\n interface LoDashExplicitObjectWrapper {\n /**\n * @see _.overSome\n */\n overSome(...predicates: Array>): LoDashExplicitObjectWrapper<(...args: any[]) => boolean>;\n }\n\n //_.property\n interface LoDashStatic {\n /**\n * Creates a function that returns the property value at path on a given object.\n *\n * @param path The path of the property to get.\n * @return Returns the new function.\n */\n property(path: Many): (obj: TObj) => TResult;\n }\n\n interface LoDashImplicitWrapper {\n /**\n * @see _.property\n */\n property(): LoDashImplicitObjectWrapper<(obj: TObj) => TResult>;\n }\n\n interface LoDashImplicitArrayWrapper {\n /**\n * @see _.property\n */\n property(): LoDashImplicitObjectWrapper<(obj: TObj) => TResult>;\n }\n\n interface LoDashExplicitWrapper {\n /**\n * @see _.property\n */\n property(): LoDashExplicitObjectWrapper<(obj: TObj) => TResult>;\n }\n\n interface LoDashExplicitArrayWrapper {\n /**\n * @see _.property\n */\n property(): LoDashExplicitObjectWrapper<(obj: TObj) => TResult>;\n }\n\n //_.propertyOf\n interface LoDashStatic {\n /**\n * The opposite of _.property; this method creates a function that returns the property value at a given path\n * on object.\n *\n * @param object The object to query.\n * @return Returns the new function.\n */\n propertyOf(object: T): (path: Many) => any;\n }\n\n interface LoDashImplicitObjectWrapper {\n /**\n * @see _.propertyOf\n */\n propertyOf(): LoDashImplicitObjectWrapper<(path: Many) => any>;\n }\n\n interface LoDashExplicitObjectWrapper {\n /**\n * @see _.propertyOf\n */\n propertyOf(): LoDashExplicitObjectWrapper<(path: Many) => any>;\n }\n\n //_.range\n interface LoDashStatic {\n /**\n * Creates an array of numbers (positive and/or negative) progressing from start up to, but not including, end.\n * If end is not specified it’s set to start with start then set to 0. If end is less than start a zero-length\n * range is created unless a negative step is specified.\n *\n * @param start The start of the range.\n * @param end The end of the range.\n * @param step The value to increment or decrement by.\n * @return Returns a new range array.\n */\n range(\n start: number,\n end: number,\n step?: number\n ): number[];\n\n /**\n * @see _.range\n */\n range(\n end: number,\n step?: number\n ): number[];\n }\n\n interface LoDashImplicitWrapper {\n /**\n * @see _.range\n */\n range(\n end?: number,\n step?: number\n ): LoDashImplicitArrayWrapper;\n }\n\n interface LoDashExplicitWrapper {\n /**\n * @see _.range\n */\n range(\n end?: number,\n step?: number\n ): LoDashExplicitArrayWrapper;\n }\n\n //_.rangeRight\n interface LoDashStatic {\n /**\n * This method is like `_.range` except that it populates values in\n * descending order.\n *\n * @static\n * @memberOf _\n * @category Util\n * @param {number} [start=0] The start of the range.\n * @param {number} end The end of the range.\n * @param {number} [step=1] The value to increment or decrement by.\n * @returns {Array} Returns the new array of numbers.\n * @example\n *\n * _.rangeRight(4);\n * // => [3, 2, 1, 0]\n *\n * _.rangeRight(-4);\n * // => [-3, -2, -1, 0]\n *\n * _.rangeRight(1, 5);\n * // => [4, 3, 2, 1]\n *\n * _.rangeRight(0, 20, 5);\n * // => [15, 10, 5, 0]\n *\n * _.rangeRight(0, -4, -1);\n * // => [-3, -2, -1, 0]\n *\n * _.rangeRight(1, 4, 0);\n * // => [1, 1, 1]\n *\n * _.rangeRight(0);\n * // => []\n */\n rangeRight(\n start: number,\n end: number,\n step?: number\n ): number[];\n\n /**\n * @see _.rangeRight\n */\n rangeRight(\n end: number,\n step?: number\n ): number[];\n }\n\n interface LoDashImplicitWrapper {\n /**\n * @see _.rangeRight\n */\n rangeRight(\n end?: number,\n step?: number\n ): LoDashImplicitArrayWrapper;\n }\n\n interface LoDashExplicitWrapper {\n /**\n * @see _.rangeRight\n */\n rangeRight(\n end?: number,\n step?: number\n ): LoDashExplicitArrayWrapper;\n }\n\n //_.runInContext\n interface LoDashStatic {\n /**\n * Create a new pristine lodash function using the given context object.\n *\n * @param context The context object.\n * @return Returns a new lodash function.\n */\n runInContext(context?: Object): typeof _;\n }\n\n interface LoDashImplicitObjectWrapper {\n /**\n * @see _.runInContext\n */\n runInContext(): typeof _;\n }\n\n // _.stubArray\n interface LoDashStatic {\n /**\n * This method returns a new empty array.\n *\n * @returns Returns the new empty array.\n */\n stubArray(): any[];\n }\n\n interface LoDashImplicitWrapperBase {\n /**\n * @see _.stubArray\n */\n stubArray(): any[];\n }\n\n interface LoDashExplicitWrapperBase {\n /**\n * @see _.stubArray\n */\n stubArray(): _.LoDashExplicitArrayWrapper;\n }\n\n // _.stubFalse\n interface LoDashStatic {\n /**\n * This method returns `false`.\n *\n * @returns Returns `false`.\n */\n stubFalse(): boolean;\n }\n\n interface LoDashImplicitWrapperBase {\n /**\n * @see _.stubFalse\n */\n stubFalse(): boolean;\n }\n\n interface LoDashExplicitWrapperBase {\n /**\n * @see _.stubFalse\n */\n stubFalse(): _.LoDashExplicitWrapper;\n }\n\n interface LoDashStatic {\n /**\n * This method returns a new empty object.\n *\n * @returns Returns the new empty object.\n */\n stubObject(): Object;\n }\n\n interface LoDashImplicitWrapperBase {\n /**\n * @see _.stubObject\n */\n stubObject(): Object;\n }\n\n interface LoDashExplicitWrapperBase {\n /**\n * @see _.stubObject\n */\n stubObject(): _.LoDashExplicitObjectWrapper;\n }\n\n interface LoDashStatic {\n /**\n * This method returns an empty string.\n *\n * @returns Returns the empty string.\n */\n stubString(): string;\n }\n\n interface LoDashImplicitWrapperBase {\n /**\n * @see _.stubString\n */\n stubString(): string;\n }\n\n interface LoDashExplicitWrapperBase {\n /**\n * @see _.stubString\n */\n stubString(): _.LoDashExplicitWrapper;\n }\n\n interface LoDashStatic {\n /**\n * This method returns `true`.\n *\n * @returns Returns `true`.\n */\n stubTrue(): boolean;\n }\n\n interface LoDashImplicitWrapperBase {\n /**\n * @see _.stubTrue\n */\n stubTrue(): boolean;\n }\n\n interface LoDashExplicitWrapperBase {\n /**\n * @see _.stubTrue\n */\n stubTrue(): _.LoDashExplicitWrapper;\n }\n\n //_.times\n interface LoDashStatic {\n /**\n * Invokes the iteratee function n times, returning an array of the results of each invocation. The iteratee\n * is invoked with one argument; (index).\n *\n * @param n The number of times to invoke iteratee.\n * @param iteratee The function invoked per iteration.\n * @return Returns the array of results.\n */\n times(\n n: number,\n iteratee: (num: number) => TResult\n ): TResult[];\n\n /**\n * @see _.times\n */\n times(n: number): number[];\n }\n\n interface LoDashImplicitWrapper {\n /**\n * @see _.times\n */\n times(\n iteratee: (num: number) => TResult\n ): TResult[];\n\n /**\n * @see _.times\n */\n times(): number[];\n }\n\n interface LoDashExplicitWrapper {\n /**\n * @see _.times\n */\n times(\n iteratee: (num: number) => TResult\n ): LoDashExplicitArrayWrapper;\n\n /**\n * @see _.times\n */\n times(): LoDashExplicitArrayWrapper;\n }\n\n //_.toPath\n interface LoDashStatic {\n /**\n * Converts `value` to a property path array.\n *\n * @static\n * @memberOf _\n * @category Util\n * @param {*} value The value to convert.\n * @returns {Array} Returns the new property path array.\n * @example\n *\n * _.toPath('a.b.c');\n * // => ['a', 'b', 'c']\n *\n * _.toPath('a[0].b.c');\n * // => ['a', '0', 'b', 'c']\n *\n * var path = ['a', 'b', 'c'],\n * newPath = _.toPath(path);\n *\n * console.log(newPath);\n * // => ['a', 'b', 'c']\n *\n * console.log(path === newPath);\n * // => false\n */\n toPath(value: any): string[];\n }\n\n interface LoDashImplicitWrapperBase {\n /**\n * @see _.toPath\n */\n toPath(): LoDashImplicitWrapper;\n }\n\n interface LoDashExplicitWrapperBase {\n /**\n * @see _.toPath\n */\n toPath(): LoDashExplicitWrapper;\n }\n\n //_.uniqueId\n interface LoDashStatic {\n /**\n * Generates a unique ID. If prefix is provided the ID is appended to it.\n *\n * @param prefix The value to prefix the ID with.\n * @return Returns the unique ID.\n */\n uniqueId(prefix?: string): string;\n }\n\n interface LoDashImplicitWrapper {\n /**\n * @see _.uniqueId\n */\n uniqueId(): string;\n }\n\n interface LoDashExplicitWrapper {\n /**\n * @see _.uniqueId\n */\n uniqueId(): LoDashExplicitWrapper;\n }\n\n type ListIterator = (value: T, index: number, collection: List) => TResult;\n\n type ListIteratorTypeGuard = (value: T, index: number, collection: List) => value is S;\n\n type DictionaryIterator = (value: T, key: string, collection: Dictionary) => TResult;\n\n type DictionaryIteratorTypeGuard = (value: T, key: string, collection: Dictionary) => value is S;\n\n type NumericDictionaryIterator = (value: T, key: number, collection: Dictionary) => TResult;\n\n type ObjectIterator = (element: T, key: string, collection: any) => TResult;\n\n type StringIterator = (char: string, index: number, string: string) => TResult;\n\n type MemoVoidIterator = (prev: TResult, curr: T, indexOrKey: any, list: T[]) => void;\n\n type MemoIterator = (prev: TResult, curr: T, indexOrKey: any, list: T[]) => TResult;\n\n type MemoVoidArrayIterator = (acc: TResult, curr: T, index: number, arr: T[]) => void;\n type MemoVoidDictionaryIterator = (acc: TResult, curr: T, key: string, dict: Dictionary) => void;\n\n /** Common interface between Arrays and jQuery objects */\n type List = ArrayLike;\n\n interface Dictionary {\n [index: string]: T;\n }\n\n interface NumericDictionary {\n [index: number]: T;\n }\n\n interface StringRepresentable {\n toString(): string;\n }\n\n interface Cancelable {\n cancel(): void;\n flush(): void;\n }\n}\n\n// Backward compatibility with --target es5\ndeclare global {\n interface Set { }\n interface Map { }\n interface WeakSet { }\n interface WeakMap { }\n}\n" } + { "path": "/a/b/node_modules/.staging/@types/lodash-e56c4fe7/index.d.ts", "content": "\n// Stub for lodash\nexport = _;\nexport as namespace _;\ndeclare var _: _.LoDashStatic;\ndeclare namespace _ {\n interface LoDashStatic {\n someProp: string;\n }\n class SomeClass {\n someMethod(): void;\n }\n}" } ); verifyAfterPartialOrCompleteNpmInstall(2); From 680994ea42e8c5f43ca0bc2ff237966d8c0a85fd Mon Sep 17 00:00:00 2001 From: Sheetal Nandi Date: Wed, 6 Sep 2017 14:43:34 -0700 Subject: [PATCH 095/109] Better log for update graph and delay operations --- src/compiler/utilities.ts | 3 +++ src/server/editorServices.ts | 2 +- src/server/project.ts | 7 ++++--- src/server/server.ts | 2 +- src/server/utilities.ts | 18 +++++++++++++----- 5 files changed, 22 insertions(+), 10 deletions(-) diff --git a/src/compiler/utilities.ts b/src/compiler/utilities.ts index e17e0e969977b..5785692017d21 100644 --- a/src/compiler/utilities.ts +++ b/src/compiler/utilities.ts @@ -3592,7 +3592,10 @@ namespace ts { const watcher = addWatch(host, file, (fileName, cbOptional1?) => { const optionalInfo = cbOptional1 !== undefined ? ` ${cbOptional1}` : ""; log(`${watcherCaption}Trigger: ${fileName}${optionalInfo} ${info}`); + const start = timestamp(); cb(fileName, cbOptional1, optional); + const elapsed = timestamp() - start; + log(`${watcherCaption}Elapsed: ${elapsed}ms Trigger: ${fileName}${optionalInfo} ${info}`); }, optional); return { close: () => { diff --git a/src/server/editorServices.ts b/src/server/editorServices.ts index 3114bb97e8267..5c160ef007c8a 100644 --- a/src/server/editorServices.ts +++ b/src/server/editorServices.ts @@ -401,7 +401,7 @@ namespace ts.server { this.currentDirectory = this.host.getCurrentDirectory(); this.toCanonicalFileName = createGetCanonicalFileName(this.host.useCaseSensitiveFileNames); - this.throttledOperations = new ThrottledOperations(this.host); + this.throttledOperations = new ThrottledOperations(this.host, this.logger); this.typingsInstaller.attach(this); diff --git a/src/server/project.ts b/src/server/project.ts index afeb533550bd1..b8c16138e5e32 100644 --- a/src/server/project.ts +++ b/src/server/project.ts @@ -789,7 +789,8 @@ namespace ts.server { private updateGraphWorker() { const oldProgram = this.program; - this.writeLog(`Starting Update graph worker: Project: ${this.getProjectName()}`); + this.writeLog(`Starting updateGraphWorker: Project: ${this.getProjectName()}`); + const start = timestamp(); this.resolutionCache.startCachingPerDirectoryResolution(); this.program = this.languageService.getProgram(); this.resolutionCache.finishCachingPerDirectoryResolution(); @@ -843,8 +844,8 @@ namespace ts.server { scriptInfoToDetach.detachFromProject(this); } }); - - this.writeLog(`Finishing Update graph worker: Project: ${this.getProjectName()}`); + const elapsed = timestamp() - start; + this.writeLog(`Finishing updateGraphWorker: Project: ${this.getProjectName()} structureChanged: ${hasChanges} Elapsed: ${elapsed}ms`); return hasChanges; } diff --git a/src/server/server.ts b/src/server/server.ts index 0ccff7c4d2521..c999c765447fd 100644 --- a/src/server/server.ts +++ b/src/server/server.ts @@ -252,7 +252,7 @@ namespace ts.server { readonly typingSafeListLocation: string, private readonly npmLocation: string | undefined, private newLine: string) { - this.throttledOperations = new ThrottledOperations(host); + this.throttledOperations = new ThrottledOperations(host, this.logger); if (eventPort) { const s = net.connect({ port: eventPort }, () => { this.socket = s; diff --git a/src/server/utilities.ts b/src/server/utilities.ts index c596176ad8600..4b9af6481db34 100644 --- a/src/server/utilities.ts +++ b/src/server/utilities.ts @@ -173,10 +173,15 @@ namespace ts.server { export function createSortedArray(): SortedArray { return [] as SortedArray; } +} +/* @internal */ +namespace ts.server { export class ThrottledOperations { - private pendingTimeouts: Map = createMap(); - constructor(private readonly host: ServerHost) { + private readonly pendingTimeouts: Map = createMap(); + private readonly logger?: Logger | undefined; + constructor(private readonly host: ServerHost, logger: Logger) { + this.logger = logger.hasLevel(LogLevel.verbose) && logger; } public schedule(operationId: string, delay: number, cb: () => void) { @@ -187,10 +192,16 @@ namespace ts.server { } // schedule new operation, pass arguments this.pendingTimeouts.set(operationId, this.host.setTimeout(ThrottledOperations.run, delay, this, operationId, cb)); + if (this.logger) { + this.logger.info(`Scheduled: ${operationId}${pendingTimeout ? ", Cancelled earlier one" : ""}`); + } } private static run(self: ThrottledOperations, operationId: string, cb: () => void) { self.pendingTimeouts.delete(operationId); + if (self.logger) { + self.logger.info(`Running: ${operationId}`); + } cb(); } } @@ -221,10 +232,7 @@ namespace ts.server { } } } -} -/* @internal */ -namespace ts.server { export function getBaseConfigFileName(configFilePath: NormalizedPath): "tsconfig.json" | "jsconfig.json" | undefined { const base = getBaseFileName(configFilePath); return base === "tsconfig.json" || base === "jsconfig.json" ? base : undefined; From c8e711c3a37b0bdd0cd097a50fdcd8bee6c501e1 Mon Sep 17 00:00:00 2001 From: Sheetal Nandi Date: Wed, 6 Sep 2017 19:54:51 -0700 Subject: [PATCH 096/109] Invalidate resolution of the failed lookup only if its one of the default extension or is one of the failed lookup location without that default extension --- src/compiler/resolutionCache.ts | 180 ++++++++++++++++++++++---------- src/compiler/types.ts | 5 +- src/server/session.ts | 18 ++-- 3 files changed, 139 insertions(+), 64 deletions(-) diff --git a/src/compiler/resolutionCache.ts b/src/compiler/resolutionCache.ts index 56dd48aaebfab..3a989eaa13887 100644 --- a/src/compiler/resolutionCache.ts +++ b/src/compiler/resolutionCache.ts @@ -30,6 +30,10 @@ namespace ts { isInvalidated?: boolean; } + interface ResolutionWithResolvedFileName { + resolvedFileName: string | undefined; + } + export interface ResolutionCacheHost extends ModuleResolutionHost { toPath(fileName: string): Path; getCompilationSettings(): CompilerOptions; @@ -59,10 +63,15 @@ namespace ts { // The values are Map of resolutions with key being name lookedup. const resolvedModuleNames = createMap>(); const perDirectoryResolvedModuleNames = createMap>(); + const resolvedTypeReferenceDirectives = createMap>(); const perDirectoryResolvedTypeReferenceDirectives = createMap>(); + const getCurrentDirectory = memoize(() => resolutionHost.getCurrentDirectory()); + const failedLookupDefaultExtensions = [Extension.Ts, Extension.Tsx, Extension.Js, Extension.Jsx, Extension.Json]; + const customFailedLookupPaths = createMap(); + const directoryWatchesOfFailedLookups = createMap(); let rootDir: string; let rootPath: Path; @@ -85,6 +94,14 @@ namespace ts { clear }; + function getResolvedModule(resolution: ResolvedModuleWithFailedLookupLocations) { + return resolution.resolvedModule; + } + + function getResolvedTypeReferenceDirective(resolution: ResolvedTypeReferenceDirectiveWithFailedLookupLocations) { + return resolution.resolvedTypeReferenceDirective; + } + function setRootDirectory(dir: string) { Debug.assert(!resolvedModuleNames.size && !resolvedTypeReferenceDirectives.size && !directoryWatchesOfFailedLookups.size); rootDir = removeTrailingDirectorySeparator(getNormalizedAbsolutePath(dir, getCurrentDirectory())); @@ -100,6 +117,7 @@ namespace ts { function clear() { clearMap(directoryWatchesOfFailedLookups, closeFileWatcherOf); + customFailedLookupPaths.clear(); closeTypeRootsWatch(); resolvedModuleNames.clear(); resolvedTypeReferenceDirectives.clear(); @@ -160,14 +178,13 @@ namespace ts { return primaryResult; } - function resolveNamesWithLocalCache( + function resolveNamesWithLocalCache( names: string[], containingFile: string, cache: Map>, perDirectoryCache: Map>, loader: (name: string, containingFile: string, options: CompilerOptions, host: ModuleResolutionHost) => T, - getResult: (s: T) => R, - getResultFileName: (result: R) => string | undefined, + getResolutionFromNameResolutionWithFailedLookupLocations: (s: T) => R, logChanges: boolean): R[] { const path = resolutionHost.toPath(containingFile); @@ -208,7 +225,7 @@ namespace ts { } Debug.assert(resolution !== undefined && !resolution.isInvalidated); seenNamesInFile.set(name, true); - resolvedModules.push(getResult(resolution)); + resolvedModules.push(getResolutionFromNameResolutionWithFailedLookupLocations(resolution)); } // Stop watching and remove the unused name @@ -228,15 +245,15 @@ namespace ts { if (!oldResolution || !newResolution || oldResolution.isInvalidated) { return false; } - const oldResult = getResult(oldResolution); - const newResult = getResult(newResolution); + const oldResult = getResolutionFromNameResolutionWithFailedLookupLocations(oldResolution); + const newResult = getResolutionFromNameResolutionWithFailedLookupLocations(newResolution); if (oldResult === newResult) { return true; } if (!oldResult || !newResult) { return false; } - return getResultFileName(oldResult) === getResultFileName(newResult); + return oldResult.resolvedFileName === newResult.resolvedFileName; } } @@ -244,7 +261,7 @@ namespace ts { return resolveNamesWithLocalCache( typeDirectiveNames, containingFile, resolvedTypeReferenceDirectives, perDirectoryResolvedTypeReferenceDirectives, - resolveTypeReferenceDirective, m => m.resolvedTypeReferenceDirective, r => r.resolvedFileName, + resolveTypeReferenceDirective, getResolvedTypeReferenceDirective, /*logChanges*/ false ); } @@ -253,7 +270,7 @@ namespace ts { return resolveNamesWithLocalCache( moduleNames, containingFile, resolvedModuleNames, perDirectoryResolvedModuleNames, - resolveModuleName, m => m.resolvedModule, r => r.resolvedFileName, + resolveModuleName, getResolvedModule, logChanges ); } @@ -300,13 +317,24 @@ namespace ts { return { dir, dirPath }; } + function isPathWithDefaultFailedLookupExtension(path: Path) { + return fileExtensionIsOneOf(path, failedLookupDefaultExtensions); + } + function watchFailedLookupLocationOfResolution( resolution: T, startIndex?: number ) { if (resolution && resolution.failedLookupLocations) { for (let i = startIndex || 0; i < resolution.failedLookupLocations.length; i++) { const failedLookupLocation = resolution.failedLookupLocations[i]; - const { dir, dirPath } = getDirectoryToWatchFailedLookupLocation(failedLookupLocation, resolutionHost.toPath(failedLookupLocation)); + const failedLookupLocationPath = resolutionHost.toPath(failedLookupLocation); + // If the failed lookup location path is not one of the supported extensions, + // store it in the custom path + if (!isPathWithDefaultFailedLookupExtension(failedLookupLocationPath)) { + const refCount = customFailedLookupPaths.get(failedLookupLocationPath) || 0; + customFailedLookupPaths.set(failedLookupLocationPath, refCount + 1); + } + const { dir, dirPath } = getDirectoryToWatchFailedLookupLocation(failedLookupLocation, failedLookupLocationPath); const dirWatcher = directoryWatchesOfFailedLookups.get(dirPath); if (dirWatcher) { dirWatcher.refCount++; @@ -330,7 +358,17 @@ namespace ts { if (resolution && resolution.failedLookupLocations) { for (let i = startIndex; i < resolution.failedLookupLocations.length; i++) { const failedLookupLocation = resolution.failedLookupLocations[i]; - const { dirPath } = getDirectoryToWatchFailedLookupLocation(failedLookupLocation, resolutionHost.toPath(failedLookupLocation)); + const failedLookupLocationPath = resolutionHost.toPath(failedLookupLocation); + const refCount = customFailedLookupPaths.get(failedLookupLocationPath); + if (refCount) { + if (refCount === 1) { + customFailedLookupPaths.delete(failedLookupLocationPath); + } + else { + customFailedLookupPaths.set(failedLookupLocationPath, refCount - 1); + } + } + const { dirPath } = getDirectoryToWatchFailedLookupLocation(failedLookupLocation, failedLookupLocationPath); const dirWatcher = directoryWatchesOfFailedLookups.get(dirPath); // Do not close the watcher yet since it might be needed by other failed lookup locations. dirWatcher.refCount--; @@ -349,72 +387,106 @@ namespace ts { // If the files are added to project root or node_modules directory, always run through the invalidation process // Otherwise run through invalidation only if adding to the immediate directory if (dirPath === rootPath || isNodeModulesDirectory(dirPath) || getDirectoryPath(fileOrFolderPath) === dirPath) { - const isChangedFailedLookupLocation: (location: string) => boolean = dirPath === fileOrFolderPath ? - // If the file watched directory is created/deleted invalidate any resolution has failed lookup in this directory - location => isInDirectoryPath(dirPath, resolutionHost.toPath(location)) : - // Otherwise only the resolutions referencing the file or folder added - location => resolutionHost.toPath(location) === fileOrFolderPath; - if (invalidateResolutionOfFailedLookupLocation(isChangedFailedLookupLocation)) { + let isChangedFailedLookupLocation: (location: string) => boolean; + if (dirPath === fileOrFolderPath) { + // Watching directory is created + // Invalidate any resolution has failed lookup in this directory + isChangedFailedLookupLocation = location => isInDirectoryPath(dirPath, resolutionHost.toPath(location)); + } + else { + // Some file or folder in the watching directory is created + // Return early if it does not have any of the watching extension or not the custom failed lookup path + if (!isPathWithDefaultFailedLookupExtension(fileOrFolderPath) && !customFailedLookupPaths.has(fileOrFolderPath)) { + return; + } + // Resolution need to be invalidated if failed lookup location is same as the file or folder getting created + isChangedFailedLookupLocation = location => resolutionHost.toPath(location) === fileOrFolderPath; + } + const hasChangedFailedLookupLocation = (resolution: NameResolutionWithFailedLookupLocations) => some(resolution.failedLookupLocations, isChangedFailedLookupLocation); + if (invalidateResolutionOfFailedLookupLocation(hasChangedFailedLookupLocation)) { resolutionHost.onInvalidatedResolution(); } } }, WatchDirectoryFlags.Recursive); } - function invalidateResolutionCacheOfDeletedFile( - deletedFilePath: Path, + function invalidateResolutionCache( cache: Map>, - getResult: (s: T) => R, - getResultFileName: (result: R) => string | undefined) { - cache.forEach((value, path) => { - if (path === deletedFilePath) { - cache.delete(path); - if (value) { - value.forEach(stopWatchFailedLookupLocationOfResolution); + ignoreFile: (resolutions: Map, containingFilePath: Path) => boolean, + isInvalidatedResolution: (resolution: T) => boolean + ) { + const seen = createMap>(); + cache.forEach((resolutions, containingFilePath) => { + if (!ignoreFile(resolutions, containingFilePath as Path) && resolutions) { + const dirPath = getDirectoryPath(containingFilePath); + let seenInDir = seen.get(dirPath); + if (!seenInDir) { + seenInDir = createMap(); + seen.set(dirPath, seenInDir); } - } - else if (value) { - value.forEach(resolution => { - if (resolution && !resolution.isInvalidated) { - const result = getResult(resolution); - if (result) { - if (resolutionHost.toPath(getResultFileName(result)) === deletedFilePath) { - resolution.isInvalidated = true; - (filesWithInvalidatedResolutions || (filesWithInvalidatedResolutions = createMap())).set(path, true); - } - } + resolutions.forEach((resolution, name) => { + if (seenInDir.has(name)) { + return; + } + seenInDir.set(name, true); + if (!resolution.isInvalidated && isInvalidatedResolution(resolution)) { + // Mark the file as needing re-evaluation of module resolution instead of using it blindly. + resolution.isInvalidated = true; + (filesWithInvalidatedResolutions || (filesWithInvalidatedResolutions = createMap())).set(containingFilePath, true); } }); } }); } + function invalidateResolutionCacheOfDeletedFile( + deletedFilePath: Path, + cache: Map>, + getResolutionFromNameResolutionWithFailedLookupLocations: (s: T) => R, + ) { + invalidateResolutionCache( + cache, + // Ignore file thats same as deleted file path, and handle it here + (resolutions, containingFilePath) => { + if (containingFilePath !== deletedFilePath) { + return false; + } + + // Deleted file, stop watching failed lookups for all the resolutions in the file + cache.delete(containingFilePath); + resolutions.forEach(stopWatchFailedLookupLocationOfResolution); + return true; + }, + // Resolution is invalidated if the resulting file name is same as the deleted file path + resolution => { + const result = getResolutionFromNameResolutionWithFailedLookupLocations(resolution); + return result && resolutionHost.toPath(result.resolvedFileName) === deletedFilePath; + } + ); + } + function invalidateResolutionOfFile(filePath: Path) { - invalidateResolutionCacheOfDeletedFile(filePath, resolvedModuleNames, m => m.resolvedModule, r => r.resolvedFileName); - invalidateResolutionCacheOfDeletedFile(filePath, resolvedTypeReferenceDirectives, m => m.resolvedTypeReferenceDirective, r => r.resolvedFileName); + invalidateResolutionCacheOfDeletedFile(filePath, resolvedModuleNames, getResolvedModule); + invalidateResolutionCacheOfDeletedFile(filePath, resolvedTypeReferenceDirectives, getResolvedTypeReferenceDirective); } function invalidateResolutionCacheOfFailedLookupLocation( cache: Map>, - isChangedFailedLookupLocation: (location: string) => boolean + hasChangedFailedLookupLocation: (resolution: T) => boolean ) { - cache.forEach((value, containingFile) => { - if (value) { - value.forEach(resolution => { - if (resolution && !resolution.isInvalidated && some(resolution.failedLookupLocations, isChangedFailedLookupLocation)) { - // Mark the file as needing re-evaluation of module resolution instead of using it blindly. - resolution.isInvalidated = true; - (filesWithInvalidatedResolutions || (filesWithInvalidatedResolutions = createMap())).set(containingFile, true); - } - }); - } - }); + invalidateResolutionCache( + cache, + // Do not ignore any file + returnFalse, + // Resolution is invalidated if the resulting file name is same as the deleted file path + hasChangedFailedLookupLocation + ); } - function invalidateResolutionOfFailedLookupLocation(isChangedFailedLookupLocation: (location: string) => boolean) { + function invalidateResolutionOfFailedLookupLocation(hasChangedFailedLookupLocation: (resolution: NameResolutionWithFailedLookupLocations) => boolean) { const invalidatedFilesCount = filesWithInvalidatedResolutions && filesWithInvalidatedResolutions.size; - invalidateResolutionCacheOfFailedLookupLocation(resolvedModuleNames, isChangedFailedLookupLocation); - invalidateResolutionCacheOfFailedLookupLocation(resolvedTypeReferenceDirectives, isChangedFailedLookupLocation); + invalidateResolutionCacheOfFailedLookupLocation(resolvedModuleNames, hasChangedFailedLookupLocation); + invalidateResolutionCacheOfFailedLookupLocation(resolvedTypeReferenceDirectives, hasChangedFailedLookupLocation); return filesWithInvalidatedResolutions && filesWithInvalidatedResolutions.size !== invalidatedFilesCount; } diff --git a/src/compiler/types.ts b/src/compiler/types.ts index a105e5fcbfefc..d033de6a65c8c 100644 --- a/src/compiler/types.ts +++ b/src/compiler/types.ts @@ -4010,7 +4010,8 @@ namespace ts { Tsx = ".tsx", Dts = ".d.ts", Js = ".js", - Jsx = ".jsx" + Jsx = ".jsx", + Json = ".json" } export interface ResolvedModuleWithFailedLookupLocations { @@ -4025,7 +4026,7 @@ namespace ts { // True if the type declaration file was found in a primary lookup location primary: boolean; // The location of the .d.ts file we located, or undefined if resolution failed - resolvedFileName?: string; + resolvedFileName: string | undefined; } export interface ResolvedTypeReferenceDirectiveWithFailedLookupLocations { diff --git a/src/server/session.ts b/src/server/session.ts index 92f4b34a46e1d..6bcfef554d3af 100644 --- a/src/server/session.ts +++ b/src/server/session.ts @@ -469,12 +469,14 @@ namespace ts.server { index++; if (checkSpec.project.containsFile(checkSpec.fileName, requireOpen)) { this.syntacticCheck(checkSpec.fileName, checkSpec.project); - next.immediate(() => { - this.semanticCheck(checkSpec.fileName, checkSpec.project); - if (checkList.length > index) { - next.delay(followMs, checkOne); - } - }); + if (this.changeSeq === seq) { + next.immediate(() => { + this.semanticCheck(checkSpec.fileName, checkSpec.project); + if (checkList.length > index) { + next.delay(followMs, checkOne); + } + }); + } } } }; @@ -1287,11 +1289,11 @@ namespace ts.server { const start = scriptInfo.lineOffsetToPosition(args.line, args.offset); const end = scriptInfo.lineOffsetToPosition(args.endLine, args.endOffset); if (start >= 0) { + this.changeSeq++; this.projectService.applyChangesToFile(scriptInfo, [{ span: { start, length: end - start }, newText: args.insertString }]); - this.changeSeq++; } } @@ -1698,8 +1700,8 @@ namespace ts.server { return this.requiredResponse(converted); }, [CommandNames.ApplyChangedToOpenFiles]: (request: protocol.ApplyChangedToOpenFilesRequest) => { - this.projectService.applyChangesInOpenFiles(request.arguments.openFiles, request.arguments.changedFiles, request.arguments.closedFiles); this.changeSeq++; + this.projectService.applyChangesInOpenFiles(request.arguments.openFiles, request.arguments.changedFiles, request.arguments.closedFiles); // TODO: report errors return this.requiredResponse(/*response*/ true); }, From 29e93c38a5a0e81c1bffe57bad4529cef36fbcac Mon Sep 17 00:00:00 2001 From: Sheetal Nandi Date: Thu, 7 Sep 2017 09:12:36 -0700 Subject: [PATCH 097/109] Update the test cases for project changed event since it doesnt apply to the edits to open files --- .../unittests/tsserverProjectSystem.ts | 41 +++++++++++-------- 1 file changed, 23 insertions(+), 18 deletions(-) diff --git a/src/harness/unittests/tsserverProjectSystem.ts b/src/harness/unittests/tsserverProjectSystem.ts index 3537fde9a49e1..55d0e26ad1def 100644 --- a/src/harness/unittests/tsserverProjectSystem.ts +++ b/src/harness/unittests/tsserverProjectSystem.ts @@ -4953,18 +4953,23 @@ namespace ts.projectSystem { } function verifyProjectChangedEvent(filesToEmit: FileOrFolder[], filesToReload?: FileOrFolder[], additionalChangedFiles?: FileOrFolder[]) { - const changedFiles = mapDefined(additionalChangedFiles ? filesToEmit.concat(additionalChangedFiles) : filesToEmit, f => f !== configFile ? f.path : undefined); host.reloadFS(filesToReload || files); host.runQueuedTimeoutCallbacks(); - const project = projectService.configuredProjects.get(configFile.path); - verifyProjectChangedEventHandler([{ - eventName: server.ProjectChangedEvent, - data: { - project, - changedFiles, - filesToEmit: mapDefined(filesToEmit, f => f !== libFile && f !== configFile ? f.path : undefined) - } - }]); + if (filesToEmit.length) { + const project = projectService.configuredProjects.get(configFile.path); + const changedFiles = mapDefined(additionalChangedFiles ? filesToEmit.concat(additionalChangedFiles) : filesToEmit, f => f !== configFile ? f.path : undefined); + verifyProjectChangedEventHandler([{ + eventName: server.ProjectChangedEvent, + data: { + project, + changedFiles, + filesToEmit: mapDefined(filesToEmit, f => f !== libFile && f !== configFile ? f.path : undefined) + } + }]); + } + else { + verifyProjectChangedEventHandler([]); + } } function updateContentOfOpenFile(file: FileOrFolder, newContent: string) { @@ -5000,15 +5005,15 @@ namespace ts.projectSystem { // Change file1Consumer1 content to `export let y = Foo();` updateContentOfOpenFile(file1Consumer1, "export let y = Foo();"); - verifyProjectChangedEvent([file1Consumer1]); + verifyProjectChangedEvent([]); // Change the content of moduleFile1 to `export var T: number;export function Foo() { };` moduleFile1.content = `export var T: number;export function Foo() { };`; - verifyProjectChangedEvent([moduleFile1, file1Consumer2]); + verifyProjectChangedEvent([moduleFile1, file1Consumer2, file1Consumer1]); // Add the import statements back to file1Consumer1 updateContentOfOpenFile(file1Consumer1, `import {Foo} from "./moduleFile1";let y = Foo();`); - verifyProjectChangedEvent([file1Consumer1]); + verifyProjectChangedEvent([]); // Change the content of moduleFile1 to `export var T: number;export var T2: string;export function Foo() { };` moduleFile1.content = `export var T: number;export var T2: string;export function Foo() { };`; @@ -5095,11 +5100,11 @@ namespace ts.projectSystem { }); updateContentOfOpenFile(file1Consumer1, file1Consumer1.content + "export var T: number;"); - verifyProjectChangedEvent([file1Consumer1, file1Consumer1Consumer1]); + verifyProjectChangedEvent([]); // Doesnt change the shape of file1Consumer1 moduleFile1.content = `export var T: number;export function Foo() { };`; - verifyProjectChangedEvent([moduleFile1, file1Consumer1, file1Consumer2]); + verifyProjectChangedEvent([moduleFile1, file1Consumer1, file1Consumer2, file1Consumer1Consumer1]); // Change both files before the timeout updateContentOfOpenFile(file1Consumer1, file1Consumer1.content + "export var T2: number;"); @@ -5120,12 +5125,12 @@ namespace ts.projectSystem { /// export var t2 = 10;` }; - const { configFile, verifyProjectChangedEvent, updateContentOfOpenFile } = getInitialState({ + const { configFile, verifyProjectChangedEvent } = getInitialState({ getAdditionalFileOrFolder: () => [file1, file2], firstReloadFileList: [file1.path, libFile.path, file2.path, configFilePath] }); - updateContentOfOpenFile(file1, file1.content + "export var t3 = 10;"); + file2.content += "export var t3 = 10;"; verifyProjectChangedEvent([file1, file2], [file1, file2, libFile, configFile]); }); @@ -5157,7 +5162,7 @@ namespace ts.projectSystem { }); updateContentOfOpenFile(referenceFile1, referenceFile1.content + "export var yy = Foo();"); - verifyProjectChangedEvent([referenceFile1], [libFile, referenceFile1, configFile]); + verifyProjectChangedEvent([], [libFile, referenceFile1, configFile]); // Create module File2 and see both files are saved verifyProjectChangedEvent([referenceFile1, moduleFile2], [libFile, moduleFile2, referenceFile1, configFile]); From b179cd1e1cc74efb6d6202ca0d70c19adc423713 Mon Sep 17 00:00:00 2001 From: Sheetal Nandi Date: Thu, 7 Sep 2017 14:48:16 -0700 Subject: [PATCH 098/109] Return configured project being closed by config file --- src/server/editorServices.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/server/editorServices.ts b/src/server/editorServices.ts index 5c160ef007c8a..4e9ef31ba09ff 100644 --- a/src/server/editorServices.ts +++ b/src/server/editorServices.ts @@ -1979,11 +1979,12 @@ namespace ts.server { } } - private closeConfiguredProject(configFile: NormalizedPath): void { + private closeConfiguredProject(configFile: NormalizedPath) { const configuredProject = this.findConfiguredProjectByProjectName(configFile); if (configuredProject && configuredProject.deleteOpenRef() === 0) { this.removeProject(configuredProject); } + return configuredProject; } closeExternalProject(uncheckedFileName: string, suppressRefresh = false): void { From 67f9533c67a429d480d66d16be3a278b3e5604fa Mon Sep 17 00:00:00 2001 From: Sheetal Nandi Date: Thu, 7 Sep 2017 17:27:00 -0700 Subject: [PATCH 099/109] Limit the resolution invalidation to max number of files as invalidation for larger cache might take more time than to just recalculate resolutions --- src/compiler/resolutionCache.ts | 197 +++++++++--------- src/compiler/watchedProgram.ts | 2 +- .../unittests/tsserverProjectSystem.ts | 79 +++++++ src/server/editorServices.ts | 12 +- src/server/project.ts | 31 +-- src/server/scriptInfo.ts | 2 +- 6 files changed, 211 insertions(+), 112 deletions(-) diff --git a/src/compiler/resolutionCache.ts b/src/compiler/resolutionCache.ts index 3a989eaa13887..54122440024b3 100644 --- a/src/compiler/resolutionCache.ts +++ b/src/compiler/resolutionCache.ts @@ -12,6 +12,7 @@ namespace ts { resolveTypeReferenceDirectives(typeDirectiveNames: string[], containingFile: string): ResolvedTypeReferenceDirective[]; invalidateResolutionOfFile(filePath: Path): void; + removeResolutionsOfFile(filePath: Path): void; createHasInvalidatedResolution(): HasInvalidatedResolution; startCachingPerDirectoryResolution(): void; @@ -25,7 +26,7 @@ namespace ts { clear(): void; } - interface NameResolutionWithFailedLookupLocations { + interface ResolutionWithFailedLookupLocations { readonly failedLookupLocations: ReadonlyArray; isInvalidated?: boolean; } @@ -45,6 +46,7 @@ namespace ts { projectName?: string; getGlobalCache?(): string | undefined; writeLog(s: string): void; + maxNumberOfFilesToIterateForInvalidation?: number; } interface DirectoryWatchesOfFailedLookup { @@ -54,9 +56,17 @@ namespace ts { refCount: number; } + /*@internal*/ + export const maxNumberOfFilesToIterateForInvalidation = 256; + + interface GetResolutionWithResolvedFileName { + (resolution: T): R; + } + export function createResolutionCache(resolutionHost: ResolutionCacheHost): ResolutionCache { let filesWithChangedSetOfUnresolvedImports: Path[] | undefined; let filesWithInvalidatedResolutions: Map | undefined; + let allFilesHaveInvalidatedResolution = false; // The resolvedModuleNames and resolvedTypeReferenceDirectives are the cache of resolutions per file. // The key in the map is source file's path. @@ -86,6 +96,7 @@ namespace ts { finishCachingPerDirectoryResolution, resolveModuleNames, resolveTypeReferenceDirectives, + removeResolutionsOfFile, invalidateResolutionOfFile, createHasInvalidatedResolution, setRootDirectory, @@ -121,6 +132,7 @@ namespace ts { closeTypeRootsWatch(); resolvedModuleNames.clear(); resolvedTypeReferenceDirectives.clear(); + allFilesHaveInvalidatedResolution = false; Debug.assert(perDirectoryResolvedModuleNames.size === 0 && perDirectoryResolvedTypeReferenceDirectives.size === 0); } @@ -135,6 +147,11 @@ namespace ts { } function createHasInvalidatedResolution(): HasInvalidatedResolution { + if (allFilesHaveInvalidatedResolution) { + // Any file asked would have invalidated resolution + filesWithInvalidatedResolutions = undefined; + return returnTrue; + } const collected = filesWithInvalidatedResolutions; filesWithInvalidatedResolutions = undefined; return path => collected && collected.has(path); @@ -145,6 +162,7 @@ namespace ts { } function finishCachingPerDirectoryResolution() { + allFilesHaveInvalidatedResolution = false; directoryWatchesOfFailedLookups.forEach((watcher, path) => { if (watcher.refCount === 0) { directoryWatchesOfFailedLookups.delete(path); @@ -178,13 +196,13 @@ namespace ts { return primaryResult; } - function resolveNamesWithLocalCache( + function resolveNamesWithLocalCache( names: string[], containingFile: string, cache: Map>, perDirectoryCache: Map>, loader: (name: string, containingFile: string, options: CompilerOptions, host: ModuleResolutionHost) => T, - getResolutionFromNameResolutionWithFailedLookupLocations: (s: T) => R, + getResolutionWithResolvedFileName: GetResolutionWithResolvedFileName, logChanges: boolean): R[] { const path = resolutionHost.toPath(containingFile); @@ -203,7 +221,7 @@ namespace ts { for (const name of names) { let resolution = resolutionsInFile.get(name); // Resolution is valid if it is present and not invalidated - if (!resolution || resolution.isInvalidated) { + if (allFilesHaveInvalidatedResolution || !resolution || resolution.isInvalidated) { const existingResolution = resolution; const resolutionInDirectory = perDirectoryResolution.get(name); if (resolutionInDirectory) { @@ -225,7 +243,7 @@ namespace ts { } Debug.assert(resolution !== undefined && !resolution.isInvalidated); seenNamesInFile.set(name, true); - resolvedModules.push(getResolutionFromNameResolutionWithFailedLookupLocations(resolution)); + resolvedModules.push(getResolutionWithResolvedFileName(resolution)); } // Stop watching and remove the unused name @@ -245,8 +263,8 @@ namespace ts { if (!oldResolution || !newResolution || oldResolution.isInvalidated) { return false; } - const oldResult = getResolutionFromNameResolutionWithFailedLookupLocations(oldResolution); - const newResult = getResolutionFromNameResolutionWithFailedLookupLocations(newResolution); + const oldResult = getResolutionWithResolvedFileName(oldResolution); + const newResult = getResolutionWithResolvedFileName(newResolution); if (oldResult === newResult) { return true; } @@ -321,9 +339,7 @@ namespace ts { return fileExtensionIsOneOf(path, failedLookupDefaultExtensions); } - function watchFailedLookupLocationOfResolution( - resolution: T, startIndex?: number - ) { + function watchFailedLookupLocationOfResolution(resolution: ResolutionWithFailedLookupLocations, startIndex?: number) { if (resolution && resolution.failedLookupLocations) { for (let i = startIndex || 0; i < resolution.failedLookupLocations.length; i++) { const failedLookupLocation = resolution.failedLookupLocations[i]; @@ -346,15 +362,11 @@ namespace ts { } } - function stopWatchFailedLookupLocationOfResolution( - resolution: T - ) { + function stopWatchFailedLookupLocationOfResolution(resolution: ResolutionWithFailedLookupLocations) { stopWatchFailedLookupLocationOfResolutionFrom(resolution, 0); } - function stopWatchFailedLookupLocationOfResolutionFrom( - resolution: T, startIndex: number - ) { + function stopWatchFailedLookupLocationOfResolutionFrom(resolution: ResolutionWithFailedLookupLocations, startIndex: number) { if (resolution && resolution.failedLookupLocations) { for (let i = startIndex; i < resolution.failedLookupLocations.length; i++) { const failedLookupLocation = resolution.failedLookupLocations[i]; @@ -387,107 +399,106 @@ namespace ts { // If the files are added to project root or node_modules directory, always run through the invalidation process // Otherwise run through invalidation only if adding to the immediate directory if (dirPath === rootPath || isNodeModulesDirectory(dirPath) || getDirectoryPath(fileOrFolderPath) === dirPath) { - let isChangedFailedLookupLocation: (location: string) => boolean; - if (dirPath === fileOrFolderPath) { - // Watching directory is created - // Invalidate any resolution has failed lookup in this directory - isChangedFailedLookupLocation = location => isInDirectoryPath(dirPath, resolutionHost.toPath(location)); - } - else { - // Some file or folder in the watching directory is created - // Return early if it does not have any of the watching extension or not the custom failed lookup path - if (!isPathWithDefaultFailedLookupExtension(fileOrFolderPath) && !customFailedLookupPaths.has(fileOrFolderPath)) { - return; - } - // Resolution need to be invalidated if failed lookup location is same as the file or folder getting created - isChangedFailedLookupLocation = location => resolutionHost.toPath(location) === fileOrFolderPath; - } - const hasChangedFailedLookupLocation = (resolution: NameResolutionWithFailedLookupLocations) => some(resolution.failedLookupLocations, isChangedFailedLookupLocation); - if (invalidateResolutionOfFailedLookupLocation(hasChangedFailedLookupLocation)) { + if (invalidateResolutionOfFailedLookupLocation(fileOrFolderPath, dirPath === fileOrFolderPath)) { resolutionHost.onInvalidatedResolution(); } } }, WatchDirectoryFlags.Recursive); } - function invalidateResolutionCache( + function removeResolutionsOfFileFromCache(cache: Map>, filePath: Path) { + // Deleted file, stop watching failed lookups for all the resolutions in the file + const resolutions = cache.get(filePath); + if (resolutions) { + resolutions.forEach(stopWatchFailedLookupLocationOfResolution); + cache.delete(filePath); + } + } + + function removeResolutionsOfFile(filePath: Path) { + removeResolutionsOfFileFromCache(resolvedModuleNames, filePath); + removeResolutionsOfFileFromCache(resolvedTypeReferenceDirectives, filePath); + } + + function invalidateResolutionCache( cache: Map>, - ignoreFile: (resolutions: Map, containingFilePath: Path) => boolean, - isInvalidatedResolution: (resolution: T) => boolean + isInvalidatedResolution: (resolution: T, getResolutionWithResolvedFileName: GetResolutionWithResolvedFileName) => boolean, + getResolutionWithResolvedFileName: GetResolutionWithResolvedFileName ) { const seen = createMap>(); cache.forEach((resolutions, containingFilePath) => { - if (!ignoreFile(resolutions, containingFilePath as Path) && resolutions) { - const dirPath = getDirectoryPath(containingFilePath); - let seenInDir = seen.get(dirPath); - if (!seenInDir) { - seenInDir = createMap(); - seen.set(dirPath, seenInDir); - } - resolutions.forEach((resolution, name) => { - if (seenInDir.has(name)) { - return; - } - seenInDir.set(name, true); - if (!resolution.isInvalidated && isInvalidatedResolution(resolution)) { - // Mark the file as needing re-evaluation of module resolution instead of using it blindly. - resolution.isInvalidated = true; - (filesWithInvalidatedResolutions || (filesWithInvalidatedResolutions = createMap())).set(containingFilePath, true); - } - }); + const dirPath = getDirectoryPath(containingFilePath); + let seenInDir = seen.get(dirPath); + if (!seenInDir) { + seenInDir = createMap(); + seen.set(dirPath, seenInDir); } + resolutions.forEach((resolution, name) => { + if (seenInDir.has(name)) { + return; + } + seenInDir.set(name, true); + if (!resolution.isInvalidated && isInvalidatedResolution(resolution, getResolutionWithResolvedFileName)) { + // Mark the file as needing re-evaluation of module resolution instead of using it blindly. + resolution.isInvalidated = true; + (filesWithInvalidatedResolutions || (filesWithInvalidatedResolutions = createMap())).set(containingFilePath, true); + } + }); }); } - function invalidateResolutionCacheOfDeletedFile( - deletedFilePath: Path, - cache: Map>, - getResolutionFromNameResolutionWithFailedLookupLocations: (s: T) => R, + function hasReachedResolutionIterationLimit() { + const maxSize = resolutionHost.maxNumberOfFilesToIterateForInvalidation || maxNumberOfFilesToIterateForInvalidation; + return resolvedModuleNames.size > maxSize || resolvedTypeReferenceDirectives.size > maxSize; + } + + function invalidateResolutions( + isInvalidatedResolution: (resolution: ResolutionWithFailedLookupLocations, getResolutionWithResolvedFileName: GetResolutionWithResolvedFileName) => boolean, ) { - invalidateResolutionCache( - cache, - // Ignore file thats same as deleted file path, and handle it here - (resolutions, containingFilePath) => { - if (containingFilePath !== deletedFilePath) { - return false; - } + // If more than maxNumberOfFilesToIterateForInvalidation present, + // just invalidated all files and recalculate the resolutions for files instead + if (hasReachedResolutionIterationLimit()) { + allFilesHaveInvalidatedResolution = true; + return; + } + invalidateResolutionCache(resolvedModuleNames, isInvalidatedResolution, getResolvedModule); + invalidateResolutionCache(resolvedTypeReferenceDirectives, isInvalidatedResolution, getResolvedTypeReferenceDirective); + } - // Deleted file, stop watching failed lookups for all the resolutions in the file - cache.delete(containingFilePath); - resolutions.forEach(stopWatchFailedLookupLocationOfResolution); - return true; - }, + function invalidateResolutionOfFile(filePath: Path) { + removeResolutionsOfFile(filePath); + invalidateResolutions( // Resolution is invalidated if the resulting file name is same as the deleted file path - resolution => { - const result = getResolutionFromNameResolutionWithFailedLookupLocations(resolution); - return result && resolutionHost.toPath(result.resolvedFileName) === deletedFilePath; + (resolution, getResolutionWithResolvedFileName) => { + const result = getResolutionWithResolvedFileName(resolution); + return result && resolutionHost.toPath(result.resolvedFileName) === filePath; } ); } - function invalidateResolutionOfFile(filePath: Path) { - invalidateResolutionCacheOfDeletedFile(filePath, resolvedModuleNames, getResolvedModule); - invalidateResolutionCacheOfDeletedFile(filePath, resolvedTypeReferenceDirectives, getResolvedTypeReferenceDirective); - } - - function invalidateResolutionCacheOfFailedLookupLocation( - cache: Map>, - hasChangedFailedLookupLocation: (resolution: T) => boolean - ) { - invalidateResolutionCache( - cache, - // Do not ignore any file - returnFalse, + function invalidateResolutionOfFailedLookupLocation(fileOrFolderPath: Path, isCreatingWatchedDirectory: boolean) { + let isChangedFailedLookupLocation: (location: string) => boolean; + if (isCreatingWatchedDirectory) { + // Watching directory is created + // Invalidate any resolution has failed lookup in this directory + isChangedFailedLookupLocation = location => isInDirectoryPath(fileOrFolderPath, resolutionHost.toPath(location)); + } + else { + // Some file or folder in the watching directory is created + // Return early if it does not have any of the watching extension or not the custom failed lookup path + if (!isPathWithDefaultFailedLookupExtension(fileOrFolderPath) && !customFailedLookupPaths.has(fileOrFolderPath)) { + return false; + } + // Resolution need to be invalidated if failed lookup location is same as the file or folder getting created + isChangedFailedLookupLocation = location => resolutionHost.toPath(location) === fileOrFolderPath; + } + const hasChangedFailedLookupLocation = (resolution: ResolutionWithFailedLookupLocations) => some(resolution.failedLookupLocations, isChangedFailedLookupLocation); + const invalidatedFilesCount = filesWithInvalidatedResolutions && filesWithInvalidatedResolutions.size; + invalidateResolutions( // Resolution is invalidated if the resulting file name is same as the deleted file path hasChangedFailedLookupLocation ); - } - - function invalidateResolutionOfFailedLookupLocation(hasChangedFailedLookupLocation: (resolution: NameResolutionWithFailedLookupLocations) => boolean) { - const invalidatedFilesCount = filesWithInvalidatedResolutions && filesWithInvalidatedResolutions.size; - invalidateResolutionCacheOfFailedLookupLocation(resolvedModuleNames, hasChangedFailedLookupLocation); - invalidateResolutionCacheOfFailedLookupLocation(resolvedTypeReferenceDirectives, hasChangedFailedLookupLocation); - return filesWithInvalidatedResolutions && filesWithInvalidatedResolutions.size !== invalidatedFilesCount; + return allFilesHaveInvalidatedResolution || filesWithInvalidatedResolutions && filesWithInvalidatedResolutions.size !== invalidatedFilesCount; } function closeTypeRootsWatch() { diff --git a/src/compiler/watchedProgram.ts b/src/compiler/watchedProgram.ts index 0dba848eb1c90..b9961a385dd86 100644 --- a/src/compiler/watchedProgram.ts +++ b/src/compiler/watchedProgram.ts @@ -501,7 +501,7 @@ namespace ts { } else if (hostSourceFileInfo.sourceFile === oldSourceFile) { sourceFilesCache.delete(oldSourceFile.path); - resolutionCache.invalidateResolutionOfFile(oldSourceFile.path); + resolutionCache.removeResolutionsOfFile(oldSourceFile.path); } } } diff --git a/src/harness/unittests/tsserverProjectSystem.ts b/src/harness/unittests/tsserverProjectSystem.ts index 55d0e26ad1def..e9561c25ff95c 100644 --- a/src/harness/unittests/tsserverProjectSystem.ts +++ b/src/harness/unittests/tsserverProjectSystem.ts @@ -5168,6 +5168,85 @@ namespace ts.projectSystem { verifyProjectChangedEvent([referenceFile1, moduleFile2], [libFile, moduleFile2, referenceFile1, configFile]); }); }); + + describe("resolution when resolution cache size", () => { + function verifyWithMaxCacheLimit(limitHit: boolean) { + const file1: FileOrFolder = { + path: "/a/b/project/file1.ts", + content: 'import a from "file2"' + }; + const file2: FileOrFolder = { + path: "/a/b/node_modules/file2.d.ts", + content: "export class a { }" + }; + const file3: FileOrFolder = { + path: "/a/b/project/file3.ts", + content: "export class c { }" + }; + const configFile: FileOrFolder = { + path: "/a/b/project/tsconfig.json", + content: JSON.stringify({ compilerOptions: { typeRoots: [] } }) + }; + + const projectFiles = [file1, file3, libFile, configFile]; + const watchedRecursiveDirectories = ["/a/b/project", "/a/b/node_modules", "/a/node_modules", "/node_modules"]; + const host = createServerHost(projectFiles); + const { session, verifyInitialOpen, verifyProjectChangedEventHandler } = createSession(host); + const projectService = session.getProjectService(); + verifyInitialOpen(file1); + checkNumberOfProjects(projectService, { configuredProjects: 1 }); + const project = projectService.configuredProjects.get(configFile.path); + verifyProject(); + if (limitHit) { + (project as ResolutionCacheHost).maxNumberOfFilesToIterateForInvalidation = 1; + } + + file3.content += "export class d {}"; + host.reloadFS(projectFiles); + host.checkTimeoutQueueLengthAndRun(2); + + // Since this is first event + verifyProject(); + verifyProjectChangedEventHandler([{ + eventName: server.ProjectChangedEvent, + data: { + project, + changedFiles: [libFile.path, file1.path, file3.path], + filesToEmit: [file1.path, file3.path] + } + }]); + + projectFiles.push(file2); + host.reloadFS(projectFiles); + host.runQueuedTimeoutCallbacks(); + watchedRecursiveDirectories.length = 2; + verifyProject(); + + const changedFiles = limitHit ? [file1.path, file2.path, file3.path, libFile.path] : [file1.path, file2.path]; + verifyProjectChangedEventHandler([{ + eventName: server.ProjectChangedEvent, + data: { + project, + changedFiles, + filesToEmit: changedFiles + } + }]); + + function verifyProject() { + checkProjectActualFiles(project, map(projectFiles, file => file.path)); + checkWatchedDirectories(host, [], /*recursive*/ false); + checkWatchedDirectories(host, watchedRecursiveDirectories, /*recursive*/ true); + } + } + + it("limit not hit", () => { + verifyWithMaxCacheLimit(/*limitHit*/ false); + }); + + it("limit hit", () => { + verifyWithMaxCacheLimit(/*limitHit*/ true); + }); + }); } describe("when event handler is set in the session", () => { diff --git a/src/server/editorServices.ts b/src/server/editorServices.ts index 4e9ef31ba09ff..ae9757d8a9bec 100644 --- a/src/server/editorServices.ts +++ b/src/server/editorServices.ts @@ -876,6 +876,8 @@ namespace ts.server { unorderedRemoveItem(this.openFiles, info); + const fileExists = this.host.fileExists(info.fileName); + // collect all projects that should be removed let projectsToRemove: Project[]; for (const p of info.containingProjects) { @@ -896,7 +898,7 @@ namespace ts.server { (projectsToRemove || (projectsToRemove = [])).push(p); } else { - p.removeFile(info); + p.removeFile(info, fileExists, /*detachFromProject*/ true); } } @@ -926,7 +928,7 @@ namespace ts.server { // If the current info is being just closed - add the watcher file to track changes // But if file was deleted, handle that part - if (this.host.fileExists(info.fileName)) { + if (fileExists) { this.watchClosedScriptInfo(info); } else { @@ -1447,7 +1449,7 @@ namespace ts.server { path = normalizedPathToPath(normalizedPath, this.currentDirectory, this.toCanonicalFileName); const existingValue = projectRootFilesMap.get(path); if (isScriptInfo(existingValue)) { - project.removeFile(existingValue); + project.removeFile(existingValue, /*fileExists*/ false, /*detachFromProject*/ true); } projectRootFilesMap.set(path, normalizedPath); scriptInfo = normalizedPath; @@ -1476,7 +1478,7 @@ namespace ts.server { projectRootFilesMap.forEach((value, path) => { if (!newRootScriptInfoMap.has(path)) { if (isScriptInfo(value)) { - project.removeFile(value); + project.removeFile(value, project.fileExists(path), /*detachFromProject*/ true); } else { projectRootFilesMap.delete(path); @@ -1806,7 +1808,7 @@ namespace ts.server { this.removeProject(inferredProject); } else { - inferredProject.removeFile(info); + inferredProject.removeFile(info, /*fileExists*/ true, /*detachFromProject*/ true); } } } diff --git a/src/server/project.ts b/src/server/project.ts index b8c16138e5e32..1a89fcb770421 100644 --- a/src/server/project.ts +++ b/src/server/project.ts @@ -667,11 +667,17 @@ namespace ts.server { this.markAsDirty(); } - removeFile(info: ScriptInfo, detachFromProject = true) { + removeFile(info: ScriptInfo, fileExists: boolean, detachFromProject: boolean) { if (this.isRoot(info)) { this.removeRoot(info); } - this.resolutionCache.invalidateResolutionOfFile(info.path); + if (fileExists) { + // If file is present, just remove the resolutions for the file + this.resolutionCache.removeResolutionsOfFile(info.path); + } + else { + this.resolutionCache.invalidateResolutionOfFile(info.path); + } this.cachedUnresolvedImportsPerFile.remove(info.path); if (detachFromProject) { @@ -807,10 +813,7 @@ namespace ts.server { continue; } // new program does not contain this file - detach it from the project - const scriptInfoToDetach = this.projectService.getScriptInfo(f.fileName); - if (scriptInfoToDetach) { - scriptInfoToDetach.detachFromProject(this); - } + this.detachScriptInfoFromProject(f.fileName); } } @@ -838,17 +841,21 @@ namespace ts.server { const scriptInfo = this.projectService.getOrCreateScriptInfoNotOpenedByClient(inserted, this.partialSystem); scriptInfo.attachToProject(this); }, - removed => { - const scriptInfoToDetach = this.projectService.getScriptInfo(removed); - if (scriptInfoToDetach) { - scriptInfoToDetach.detachFromProject(this); - } - }); + removed => this.detachScriptInfoFromProject(removed) + ); const elapsed = timestamp() - start; this.writeLog(`Finishing updateGraphWorker: Project: ${this.getProjectName()} structureChanged: ${hasChanges} Elapsed: ${elapsed}ms`); return hasChanges; } + private detachScriptInfoFromProject(uncheckedFileName: string) { + const scriptInfoToDetach = this.projectService.getScriptInfo(uncheckedFileName); + if (scriptInfoToDetach) { + scriptInfoToDetach.detachFromProject(this); + this.resolutionCache.removeResolutionsOfFile(scriptInfoToDetach.path); + } + } + private addMissingFileWatcher(missingFilePath: Path) { const fileWatcher = this.projectService.watchFile( this.projectService.host, diff --git a/src/server/scriptInfo.ts b/src/server/scriptInfo.ts index bc3e11baa0e45..3dfa0cc7e5f65 100644 --- a/src/server/scriptInfo.ts +++ b/src/server/scriptInfo.ts @@ -282,7 +282,7 @@ namespace ts.server { } const isInfoRoot = p.isRoot(this); // detach is unnecessary since we'll clean the list of containing projects anyways - p.removeFile(this, /*detachFromProjects*/ false); + p.removeFile(this, /*fileExists*/ false, /*detachFromProjects*/ false); // If the info was for the external or configured project's root, // add missing file as the root if (isInfoRoot && p.projectKind !== ProjectKind.Inferred) { From de28d02626ca2c91fd5ac8d4f57482eac5a4b6c4 Mon Sep 17 00:00:00 2001 From: Sheetal Nandi Date: Fri, 8 Sep 2017 15:14:03 -0700 Subject: [PATCH 100/109] Add test case to verify correct resolution file is picked when current resolution file is not removed but higher precedence file is created --- src/compiler/program.ts | 4 +- .../unittests/tsserverProjectSystem.ts | 53 +++++++++++++++++++ src/harness/virtualFileSystemWithWatch.ts | 4 +- 3 files changed, 57 insertions(+), 4 deletions(-) diff --git a/src/compiler/program.ts b/src/compiler/program.ts index f74675dbf3126..bd702edb27c44 100644 --- a/src/compiler/program.ts +++ b/src/compiler/program.ts @@ -825,7 +825,7 @@ namespace ts { const moduleName = moduleNames[i]; // If we want to reuse resolutions more aggressively, we can refine this to check for whether the // text of the corresponding modulenames has changed. - if (file === oldSourceFile) { + if (file === oldSourceFile && !hasInvalidatedResolution(oldSourceFile.path)) { const oldResolvedModule = oldSourceFile && oldSourceFile.resolvedModules.get(moduleName); if (oldResolvedModule) { if (isTraceEnabled(options, host)) { @@ -846,7 +846,7 @@ namespace ts { trace(host, Diagnostics.Module_0_was_resolved_as_locally_declared_ambient_module_in_file_1, moduleName, containingFile); } } - else if (!hasInvalidatedResolution(oldProgramState.file.path)) { + else { resolvesToAmbientModuleInNonModifiedFile = moduleNameResolvesToAmbientModuleInNonModifiedFile(moduleName, oldProgramState); } diff --git a/src/harness/unittests/tsserverProjectSystem.ts b/src/harness/unittests/tsserverProjectSystem.ts index e9561c25ff95c..7d49e82151f22 100644 --- a/src/harness/unittests/tsserverProjectSystem.ts +++ b/src/harness/unittests/tsserverProjectSystem.ts @@ -2306,6 +2306,59 @@ namespace ts.projectSystem { }); assert.isFalse(hasErrorMsg); }); + + it("Changed module resolution reflected when specifying files list", () => { + const file1: FileOrFolder = { + path: "/a/b/file1.ts", + content: 'import classc from "file2"' + }; + const file2a: FileOrFolder = { + path: "/a/file2.ts", + content: "export classc { method2a() { return 10; } }" + }; + const file2: FileOrFolder = { + path: "/a/b/file2.ts", + content: "export classc { method2() { return 10; } }" + }; + const configFile: FileOrFolder = { + path: "/a/b/tsconfig.json", + content: JSON.stringify({ files: [file1.path], compilerOptions: { module: "amd" } }) + }; + const files = [file1, file2a, configFile, libFile]; + const host = createServerHost(files); + const projectService = createProjectService(host); + projectService.openClientFile(file1.path); + checkNumberOfProjects(projectService, { configuredProjects: 1 }); + const project = projectService.configuredProjects.get(configFile.path); + assert.isDefined(project); + checkProjectActualFiles(project, map(files, file => file.path)); + checkWatchedFiles(host, mapDefined(files, file => file === file1 ? undefined : file.path)); + checkWatchedDirectories(host, [], /*recursive*/ false); + const watchedRecursiveDirectories = getTypeRootsFromLocation("/a/b"); + watchedRecursiveDirectories.push("/a/b"); + checkWatchedDirectories(host, watchedRecursiveDirectories, /*recursive*/ true); + + files.push(file2); + host.reloadFS(files); + host.runQueuedTimeoutCallbacks(); + watchedRecursiveDirectories.pop(); + checkNumberOfProjects(projectService, { configuredProjects: 1 }); + assert.strictEqual(projectService.configuredProjects.get(configFile.path), project); + checkProjectActualFiles(project, mapDefined(files, file => file === file2a ? undefined : file.path)); + checkWatchedFiles(host, mapDefined(files, file => file === file1 ? undefined : file.path)); + checkWatchedDirectories(host, [], /*recursive*/ false); + checkWatchedDirectories(host, watchedRecursiveDirectories, /*recursive*/ true); + + // On next file open the files file2a should be closed and not watched any more + projectService.openClientFile(file2.path); + checkNumberOfProjects(projectService, { configuredProjects: 1 }); + assert.strictEqual(projectService.configuredProjects.get(configFile.path), project); + checkProjectActualFiles(project, mapDefined(files, file => file === file2a ? undefined : file.path)); + checkWatchedFiles(host, [libFile.path, configFile.path]); + checkWatchedDirectories(host, [], /*recursive*/ false); + checkWatchedDirectories(host, watchedRecursiveDirectories, /*recursive*/ true); + + }); }); describe("Proper errors", () => { diff --git a/src/harness/virtualFileSystemWithWatch.ts b/src/harness/virtualFileSystemWithWatch.ts index 724326d3eb63a..1eee6c57f127b 100644 --- a/src/harness/virtualFileSystemWithWatch.ts +++ b/src/harness/virtualFileSystemWithWatch.ts @@ -130,9 +130,9 @@ namespace ts.TestFSWithWatch { } export function checkFileNames(caption: string, actualFileNames: string[], expectedFileNames: string[]) { - assert.equal(actualFileNames.length, expectedFileNames.length, `${caption}: incorrect actual number of files, expected ${JSON.stringify(expectedFileNames)}, got ${actualFileNames}`); + assert.equal(actualFileNames.length, expectedFileNames.length, `${caption}: incorrect actual number of files, expected ${expectedFileNames}, got ${actualFileNames}`); for (const f of expectedFileNames) { - assert.isTrue(contains(actualFileNames, f), `${caption}: expected to find ${f} in ${JSON.stringify(actualFileNames)}`); + assert.isTrue(contains(actualFileNames, f), `${caption}: expected to find ${f} in ${actualFileNames}`); } } From 5739b6897a3bad40ff5551dd4df39ea4fe32924b Mon Sep 17 00:00:00 2001 From: Sheetal Nandi Date: Fri, 8 Sep 2017 18:02:10 -0700 Subject: [PATCH 101/109] Do not create map just to store empty reference files. Also update file as changed if file text is same but it had invalidated resolution --- src/compiler/builder.ts | 62 ++++++++++++++++--- src/compiler/utilities.ts | 48 +++++++------- .../unittests/tsserverProjectSystem.ts | 2 +- 3 files changed, 76 insertions(+), 36 deletions(-) diff --git a/src/compiler/builder.ts b/src/compiler/builder.ts index 68fb36b9144f8..40f3cdadb6447 100644 --- a/src/compiler/builder.ts +++ b/src/compiler/builder.ts @@ -48,6 +48,7 @@ namespace ts { addScriptInfo(program: Program, sourceFile: SourceFile): void; removeScriptInfo(path: Path): void; updateScriptInfo(program: Program, sourceFile: SourceFile): void; + updaterScriptInfoWithSameVersion(program: Program, sourceFile: SourceFile): boolean; /** * Gets the files affected by the script info which has updated shape from the known one */ @@ -143,11 +144,15 @@ namespace ts { } function updateExistingFileInfo(program: Program, existingInfo: FileInfo, sourceFile: SourceFile, hasInvalidatedResolution: HasInvalidatedResolution) { - if (existingInfo.version !== sourceFile.version || hasInvalidatedResolution(sourceFile.path)) { + if (existingInfo.version !== sourceFile.version) { registerChangedFile(sourceFile.path, sourceFile.fileName); existingInfo.version = sourceFile.version; emitHandler.updateScriptInfo(program, sourceFile); } + else if (hasInvalidatedResolution(sourceFile.path) && + emitHandler.updaterScriptInfoWithSameVersion(program, sourceFile)) { + registerChangedFile(sourceFile.path, sourceFile.fileName); + } } function ensureProgramGraph(program: Program) { @@ -338,8 +343,8 @@ namespace ts { /** * Gets the referenced files for a file from the program with values for the keys as referenced file's path to be true */ - function getReferencedFiles(program: Program, sourceFile: SourceFile): Map { - const referencedFiles = createMap(); + function getReferencedFiles(program: Program, sourceFile: SourceFile): Map | undefined { + let referencedFiles: Map | undefined; // We need to use a set here since the code can contain the same import twice, // but that will only be one dependency. @@ -351,7 +356,7 @@ namespace ts { if (symbol && symbol.declarations && symbol.declarations[0]) { const declarationSourceFile = getSourceFileOfNode(symbol.declarations[0]); if (declarationSourceFile) { - referencedFiles.set(declarationSourceFile.path, true); + addReferencedFile(declarationSourceFile.path); } } } @@ -362,7 +367,7 @@ namespace ts { if (sourceFile.referencedFiles && sourceFile.referencedFiles.length > 0) { for (const referencedFile of sourceFile.referencedFiles) { const referencedPath = toPath(referencedFile.fileName, sourceFileDirectory, getCanonicalFileName); - referencedFiles.set(referencedPath, true); + addReferencedFile(referencedPath); } } @@ -375,11 +380,18 @@ namespace ts { const fileName = resolvedTypeReferenceDirective.resolvedFileName; const typeFilePath = toPath(fileName, sourceFileDirectory, getCanonicalFileName); - referencedFiles.set(typeFilePath, true); + addReferencedFile(typeFilePath); }); } return referencedFiles; + + function addReferencedFile(referencedPath: Path) { + if (!referencedFiles) { + referencedFiles = createMap(); + } + referencedFiles.set(referencedPath, true); + } } /** @@ -402,6 +414,7 @@ namespace ts { addScriptInfo: noop, removeScriptInfo: noop, updateScriptInfo: noop, + updaterScriptInfoWithSameVersion: returnFalse, getFilesAffectedByUpdatedShape }; @@ -421,12 +434,45 @@ namespace ts { return { addScriptInfo: setReferences, removeScriptInfo, - updateScriptInfo: setReferences, + updateScriptInfo: updateReferences, + updaterScriptInfoWithSameVersion: updateReferencesTrackingChangedReferences, getFilesAffectedByUpdatedShape }; function setReferences(program: Program, sourceFile: SourceFile) { - references.set(sourceFile.path, getReferencedFiles(program, sourceFile)); + const newReferences = getReferencedFiles(program, sourceFile); + if (newReferences) { + references.set(sourceFile.path, getReferencedFiles(program, sourceFile)); + } + } + + function updateReferences(program: Program, sourceFile: SourceFile) { + const newReferences = getReferencedFiles(program, sourceFile); + if (newReferences) { + references.set(sourceFile.path, newReferences); + } + else { + references.delete(sourceFile.path); + } + } + + function updateReferencesTrackingChangedReferences(program: Program, sourceFile: SourceFile) { + const newReferences = getReferencedFiles(program, sourceFile); + if (!newReferences) { + // Changed if we had references + return references.delete(sourceFile.path); + } + + const oldReferences = references.get(sourceFile.path); + references.set(sourceFile.path, newReferences); + if (!oldReferences || oldReferences.size !== newReferences.size) { + return true; + } + + // If there are any new references that werent present previously there is change + return forEachEntry(newReferences, (_true, referencedPath) => !oldReferences.delete(referencedPath)) || + // Otherwise its changed if there are more references previously than now + !!oldReferences.size; } function removeScriptInfo(removedFilePath: Path) { diff --git a/src/compiler/utilities.ts b/src/compiler/utilities.ts index 5785692017d21..868e9e9420ed9 100644 --- a/src/compiler/utilities.ts +++ b/src/compiler/utilities.ts @@ -3513,34 +3513,28 @@ namespace ts { * Mutates the map with newMap such that keys in map will be same as newMap. */ export function mutateMap(map: Map, newMap: ReadonlyMap, options: MutateMapOptions) { - // If there are new values update them - if (newMap) { - const { createNewValue, onDeleteValue, onExistingValue } = options; - // Needs update - map.forEach((existingValue, key) => { - const valueInNewMap = newMap.get(key); - // Not present any more in new map, remove it - if (valueInNewMap === undefined) { - map.delete(key); - onDeleteValue(existingValue, key); - } - // If present notify about existing values - else if (onExistingValue) { - onExistingValue(existingValue, valueInNewMap, key); - } - }); + const { createNewValue, onDeleteValue, onExistingValue } = options; + // Needs update + map.forEach((existingValue, key) => { + const valueInNewMap = newMap.get(key); + // Not present any more in new map, remove it + if (valueInNewMap === undefined) { + map.delete(key); + onDeleteValue(existingValue, key); + } + // If present notify about existing values + else if (onExistingValue) { + onExistingValue(existingValue, valueInNewMap, key); + } + }); - // Add new values that are not already present - newMap.forEach((valueInNewMap, key) => { - if (!map.has(key)) { - // New values - map.set(key, createNewValue(key, valueInNewMap)); - } - }); - } - else { - clearMap(map, options.onDeleteValue); - } + // Add new values that are not already present + newMap.forEach((valueInNewMap, key) => { + if (!map.has(key)) { + // New values + map.set(key, createNewValue(key, valueInNewMap)); + } + }); } /** diff --git a/src/harness/unittests/tsserverProjectSystem.ts b/src/harness/unittests/tsserverProjectSystem.ts index 7d49e82151f22..7752d26e208db 100644 --- a/src/harness/unittests/tsserverProjectSystem.ts +++ b/src/harness/unittests/tsserverProjectSystem.ts @@ -5275,7 +5275,7 @@ namespace ts.projectSystem { watchedRecursiveDirectories.length = 2; verifyProject(); - const changedFiles = limitHit ? [file1.path, file2.path, file3.path, libFile.path] : [file1.path, file2.path]; + const changedFiles = [file1.path, file2.path]; verifyProjectChangedEventHandler([{ eventName: server.ProjectChangedEvent, data: { From b536f9dadedd2ea279ba6dabe007cd3033984e6d Mon Sep 17 00:00:00 2001 From: Sheetal Nandi Date: Tue, 12 Sep 2017 12:09:06 -0700 Subject: [PATCH 102/109] Should not remove the reused resolutions in the file when file contents have not changed. --- src/compiler/program.ts | 8 +++++--- src/compiler/resolutionCache.ts | 11 ++++++----- src/compiler/types.ts | 2 +- src/compiler/watchedProgram.ts | 4 ++-- src/server/project.ts | 4 ++-- src/services/services.ts | 2 +- src/services/types.ts | 2 +- 7 files changed, 18 insertions(+), 15 deletions(-) diff --git a/src/compiler/program.ts b/src/compiler/program.ts index f1683d1e4db3a..5ff1c1e2b3b88 100644 --- a/src/compiler/program.ts +++ b/src/compiler/program.ts @@ -595,11 +595,11 @@ namespace ts { let _compilerOptionsObjectLiteralSyntax: ObjectLiteralExpression; let moduleResolutionCache: ModuleResolutionCache; - let resolveModuleNamesWorker: (moduleNames: string[], containingFile: string) => ResolvedModuleFull[]; + let resolveModuleNamesWorker: (moduleNames: string[], containingFile: string, reusedNames?: string[]) => ResolvedModuleFull[]; const hasInvalidatedResolution = host.hasInvalidatedResolution || returnFalse; const hasChangedAutomaticTypeDirectiveNames = host.hasChangedAutomaticTypeDirectiveNames && host.hasChangedAutomaticTypeDirectiveNames.bind(host) || returnFalse; if (host.resolveModuleNames) { - resolveModuleNamesWorker = (moduleNames, containingFile) => host.resolveModuleNames(checkAllDefined(moduleNames), containingFile).map(resolved => { + resolveModuleNamesWorker = (moduleNames, containingFile, reusedNames) => host.resolveModuleNames(checkAllDefined(moduleNames), containingFile, reusedNames).map(resolved => { // An older host may have omitted extension, in which case we should infer it from the file extension of resolvedFileName. if (!resolved || (resolved as ResolvedModuleFull).extension !== undefined) { return resolved as ResolvedModuleFull; @@ -820,6 +820,7 @@ namespace ts { * * ResolvedModuleFull instance: can be reused. */ let result: ResolvedModuleFull[]; + let reusedNames: string[]; /** A transient placeholder used to mark predicted resolution in the result list. */ const predictedToResolveToAmbientModuleMarker: ResolvedModuleFull = {}; @@ -835,6 +836,7 @@ namespace ts { trace(host, Diagnostics.Reusing_resolution_of_module_0_to_file_1_from_old_program, moduleName, containingFile); } (result || (result = new Array(moduleNames.length)))[i] = oldResolvedModule; + (reusedNames || (reusedNames = [])).push(moduleName); continue; } } @@ -863,7 +865,7 @@ namespace ts { } const resolutions = unknownModuleNames && unknownModuleNames.length - ? resolveModuleNamesWorker(unknownModuleNames, containingFile) + ? resolveModuleNamesWorker(unknownModuleNames, containingFile, reusedNames) : emptyArray; // Combine results of resolutions and predicted results diff --git a/src/compiler/resolutionCache.ts b/src/compiler/resolutionCache.ts index 71ef37a9a372c..c69caf15f07a5 100644 --- a/src/compiler/resolutionCache.ts +++ b/src/compiler/resolutionCache.ts @@ -8,7 +8,7 @@ namespace ts { startRecordingFilesWithChangedResolutions(): void; finishRecordingFilesWithChangedResolutions(): Path[]; - resolveModuleNames(moduleNames: string[], containingFile: string, logChanges: boolean): ResolvedModuleFull[]; + resolveModuleNames(moduleNames: string[], containingFile: string, reusedNames: string[] | undefined, logChanges: boolean): ResolvedModuleFull[]; resolveTypeReferenceDirectives(typeDirectiveNames: string[], containingFile: string): ResolvedTypeReferenceDirective[]; invalidateResolutionOfFile(filePath: Path): void; @@ -207,6 +207,7 @@ namespace ts { perDirectoryCache: Map>, loader: (name: string, containingFile: string, options: CompilerOptions, host: ModuleResolutionHost) => T, getResolutionWithResolvedFileName: GetResolutionWithResolvedFileName, + reusedNames: string[] | undefined, logChanges: boolean): R[] { const path = resolutionHost.toPath(containingFile); @@ -253,7 +254,7 @@ namespace ts { // Stop watching and remove the unused name resolutionsInFile.forEach((resolution, name) => { - if (!seenNamesInFile.has(name)) { + if (!seenNamesInFile.has(name) && !contains(reusedNames, name)) { stopWatchFailedLookupLocationOfResolution(resolution); resolutionsInFile.delete(name); } @@ -285,16 +286,16 @@ namespace ts { typeDirectiveNames, containingFile, resolvedTypeReferenceDirectives, perDirectoryResolvedTypeReferenceDirectives, resolveTypeReferenceDirective, getResolvedTypeReferenceDirective, - /*logChanges*/ false + /*reusedNames*/ undefined, /*logChanges*/ false ); } - function resolveModuleNames(moduleNames: string[], containingFile: string, logChanges: boolean): ResolvedModuleFull[] { + function resolveModuleNames(moduleNames: string[], containingFile: string, reusedNames: string[] | undefined, logChanges: boolean): ResolvedModuleFull[] { return resolveNamesWithLocalCache( moduleNames, containingFile, resolvedModuleNames, perDirectoryResolvedModuleNames, resolveModuleName, getResolvedModule, - logChanges + reusedNames, logChanges ); } diff --git a/src/compiler/types.ts b/src/compiler/types.ts index ef15e3fdd1ef5..d473f2441bd34 100644 --- a/src/compiler/types.ts +++ b/src/compiler/types.ts @@ -4138,7 +4138,7 @@ namespace ts { * If resolveModuleNames is implemented then implementation for members from ModuleResolutionHost can be just * 'throw new Error("NotImplemented")' */ - resolveModuleNames?(moduleNames: string[], containingFile: string): ResolvedModule[]; + resolveModuleNames?(moduleNames: string[], containingFile: string, reusedNames?: string[]): ResolvedModule[]; /** * This method is a companion for 'resolveModuleNames' and is used to resolve 'types' references to actual type declaration files */ diff --git a/src/compiler/watchedProgram.ts b/src/compiler/watchedProgram.ts index 67a1a943bf612..7d7b4b1b1d3d4 100644 --- a/src/compiler/watchedProgram.ts +++ b/src/compiler/watchedProgram.ts @@ -396,8 +396,8 @@ namespace ts { return partialSystem.getDirectories(path); } - function resolveModuleNames(moduleNames: string[], containingFile: string) { - return resolutionCache.resolveModuleNames(moduleNames, containingFile, /*logChanges*/ false); + function resolveModuleNames(moduleNames: string[], containingFile: string, reusedNames?: string[]) { + return resolutionCache.resolveModuleNames(moduleNames, containingFile, reusedNames, /*logChanges*/ false); } function resolveTypeReferenceDirectives(typeDirectiveNames: string[], containingFile: string) { diff --git a/src/server/project.ts b/src/server/project.ts index c52f520b8a42c..4a66a4064e0a8 100644 --- a/src/server/project.ts +++ b/src/server/project.ts @@ -325,8 +325,8 @@ namespace ts.server { return !this.isWatchedMissingFile(path) && this.partialSystem.fileExists(file); } - resolveModuleNames(moduleNames: string[], containingFile: string): ResolvedModuleFull[] { - return this.resolutionCache.resolveModuleNames(moduleNames, containingFile, /*logChanges*/ true); + resolveModuleNames(moduleNames: string[], containingFile: string, reusedNames?: string[]): ResolvedModuleFull[] { + return this.resolutionCache.resolveModuleNames(moduleNames, containingFile, reusedNames, /*logChanges*/ true); } resolveTypeReferenceDirectives(typeDirectiveNames: string[], containingFile: string): ResolvedTypeReferenceDirective[] { diff --git a/src/services/services.ts b/src/services/services.ts index e8f073f4a17d8..08d5b007e877c 100644 --- a/src/services/services.ts +++ b/src/services/services.ts @@ -1177,7 +1177,7 @@ namespace ts { } if (host.resolveModuleNames) { - compilerHost.resolveModuleNames = (moduleNames, containingFile) => host.resolveModuleNames(moduleNames, containingFile); + compilerHost.resolveModuleNames = (moduleNames, containingFile, reusedNames) => host.resolveModuleNames(moduleNames, containingFile, reusedNames); } if (host.resolveTypeReferenceDirectives) { compilerHost.resolveTypeReferenceDirectives = (typeReferenceDirectiveNames, containingFile) => { diff --git a/src/services/types.ts b/src/services/types.ts index e63651b3371f8..1fa06796065b7 100644 --- a/src/services/types.ts +++ b/src/services/types.ts @@ -183,7 +183,7 @@ namespace ts { * if implementation is omitted then language service will use built-in module resolution logic and get answers to * host specific questions using 'getScriptSnapshot'. */ - resolveModuleNames?(moduleNames: string[], containingFile: string): ResolvedModule[]; + resolveModuleNames?(moduleNames: string[], containingFile: string, reusedNames?: string[]): ResolvedModule[]; resolveTypeReferenceDirectives?(typeDirectiveNames: string[], containingFile: string): ResolvedTypeReferenceDirective[]; hasInvalidatedResolution?: HasInvalidatedResolution; hasChangedAutomaticTypeDirectiveNames?(): boolean; From 4f7c0e5e1ce6085cd71976cd4a4bf301f328f5c4 Mon Sep 17 00:00:00 2001 From: Sheetal Nandi Date: Tue, 12 Sep 2017 18:11:45 -0700 Subject: [PATCH 103/109] Simplify event sent on background project update since its anyways just to update the error list --- src/compiler/builder.ts | 27 -- .../unittests/tsserverProjectSystem.ts | 245 ++++++++---------- src/server/editorServices.ts | 40 ++- src/server/project.ts | 6 - src/server/protocol.ts | 24 +- src/server/session.ts | 22 +- 6 files changed, 137 insertions(+), 227 deletions(-) diff --git a/src/compiler/builder.ts b/src/compiler/builder.ts index b4ebbc76bd727..e7bcb28d820b1 100644 --- a/src/compiler/builder.ts +++ b/src/compiler/builder.ts @@ -35,8 +35,6 @@ namespace ts { /** Emit the changed files and clear the cache of the changed files */ emitChangedFiles(program: Program): EmitOutputDetailed[]; - /** Get the changed files since last query and then clear the cache of changed files */ - getChangedProgramFiles(program: Program): ChangedProgramFiles; /** When called gets the semantic diagnostics for the program. It also caches the diagnostics and manage them */ getSemanticDiagnostics(program: Program, cancellationToken?: CancellationToken): Diagnostic[]; @@ -103,7 +101,6 @@ namespace ts { getFilesAffectedBy, emitFile, emitChangedFiles, - getChangedProgramFiles, getSemanticDiagnostics, clear }; @@ -273,30 +270,6 @@ namespace ts { return diagnostics || emptyArray; } - function getChangedProgramFiles(program: Program): ChangedProgramFiles { - ensureProgramGraph(program); - - let filesToEmit: string[]; - const changedFiles = createMap(); - enumerateChangedFilesEmitOutput(program, /*emitOnlyDtsFiles*/ true, - // All the changed files are required to get diagnostics - (changedFileName, changedFilePath) => addFileForDiagnostics(changedFileName, changedFilePath), - // Emitted file is for emit as well as diagnostic - (_emitOutput, sourceFile) => { - (filesToEmit || (filesToEmit = [])).push(sourceFile.fileName); - addFileForDiagnostics(sourceFile.fileName, sourceFile.path); - }); - changedFileNames.clear(); - return { - filesToEmit: filesToEmit || emptyArray, - changedFiles: arrayFrom(changedFiles.values()) - }; - - function addFileForDiagnostics(fileName: string, path: Path) { - changedFiles.set(path, fileName); - } - } - function clear() { isModuleEmit = undefined; emitHandler = undefined; diff --git a/src/harness/unittests/tsserverProjectSystem.ts b/src/harness/unittests/tsserverProjectSystem.ts index f253eca704fa7..d4126d603f2f5 100644 --- a/src/harness/unittests/tsserverProjectSystem.ts +++ b/src/harness/unittests/tsserverProjectSystem.ts @@ -2158,7 +2158,7 @@ namespace ts.projectSystem { const session = createSession(host, { canUseEvents: true, eventHandler: e => { - if (e.eventName === server.ConfigFileDiagEvent || e.eventName === server.ProjectChangedEvent || e.eventName === server.ProjectInfoTelemetryEvent) { + if (e.eventName === server.ConfigFileDiagEvent || e.eventName === server.ProjectsUpdatedInBackgroundEvent || e.eventName === server.ProjectInfoTelemetryEvent) { return; } assert.equal(e.eventName, server.ProjectLanguageServiceStateEvent); @@ -4819,7 +4819,7 @@ namespace ts.projectSystem { }); }); - describe("ProjectChangedEvent", () => { + describe("ProjectsChangedInBackground", () => { function verifyFiles(caption: string, actual: ReadonlyArray, expected: ReadonlyArray) { assert.equal(actual.length, expected.length, `Incorrect number of ${caption}. Actual: ${actual} Expected: ${expected}`); const seen = createMap(); @@ -4830,7 +4830,7 @@ namespace ts.projectSystem { }); } - function createVerifyInitialOpen(session: TestSession, verifyProjectChangedEventHandler: (events: server.ProjectChangedEvent[]) => void) { + function createVerifyInitialOpen(session: TestSession, verifyProjectsUpdatedInBackgroundEventHandler: (events: server.ProjectsUpdatedInBackgroundEvent[]) => void) { return (file: FileOrFolder) => { session.executeCommandSeq({ command: server.CommandNames.Open, @@ -4838,17 +4838,17 @@ namespace ts.projectSystem { file: file.path } }); - verifyProjectChangedEventHandler([]); + verifyProjectsUpdatedInBackgroundEventHandler([]); }; } - interface ProjectChangeEventVerifier { + interface ProjectsUpdatedInBackgroundEventVerifier { session: TestSession; - verifyProjectChangedEventHandler(events: server.ProjectChangedEvent[]): void; + verifyProjectsUpdatedInBackgroundEventHandler(events: server.ProjectsUpdatedInBackgroundEvent[]): void; verifyInitialOpen(file: FileOrFolder): void; } - function verifyProjectChangedEvent(createSession: (host: TestServerHost) => ProjectChangeEventVerifier) { + function verifyProjectsUpdatedInBackgroundEvent(createSession: (host: TestServerHost) => ProjectsUpdatedInBackgroundEventVerifier) { it("when adding new file", () => { const commonFile1: FileOrFolder = { path: "/a/b/file1.ts", @@ -4866,32 +4866,26 @@ namespace ts.projectSystem { path: "/a/b/tsconfig.json", content: `{}` }; + const openFiles = [commonFile1.path]; const host = createServerHost([commonFile1, libFile, configFile]); - const { session, verifyProjectChangedEventHandler, verifyInitialOpen } = createSession(host, ); - const projectService = session.getProjectService(); + const { verifyProjectsUpdatedInBackgroundEventHandler, verifyInitialOpen } = createSession(host, ); verifyInitialOpen(commonFile1); host.reloadFS([commonFile1, libFile, configFile, commonFile2]); host.runQueuedTimeoutCallbacks(); - // Since this is first event - const project = projectService.configuredProjects.get(configFile.path); - verifyProjectChangedEventHandler([{ - eventName: server.ProjectChangedEvent, + verifyProjectsUpdatedInBackgroundEventHandler([{ + eventName: server.ProjectsUpdatedInBackgroundEvent, data: { - project, - changedFiles: [libFile.path, commonFile1.path, commonFile2.path], - filesToEmit: [commonFile1.path, commonFile2.path] + openFiles } }]); host.reloadFS([commonFile1, commonFile2, libFile, configFile, commonFile3]); host.runQueuedTimeoutCallbacks(); - verifyProjectChangedEventHandler([{ - eventName: server.ProjectChangedEvent, + verifyProjectsUpdatedInBackgroundEventHandler([{ + eventName: server.ProjectsUpdatedInBackgroundEvent, data: { - project, - changedFiles: [commonFile3.path], - filesToEmit: [commonFile3.path] + openFiles } }]); }); @@ -4914,36 +4908,30 @@ namespace ts.projectSystem { content: "export let y = 1" }; + const openFiles = [f1.path]; const files = [f1, config, libFile]; const host = createServerHost(files); - const { session, verifyInitialOpen, verifyProjectChangedEventHandler } = createSession(host); - const projectService = session.getProjectService(); + const { verifyInitialOpen, verifyProjectsUpdatedInBackgroundEventHandler } = createSession(host); verifyInitialOpen(f1); files.push(f2); host.reloadFS(files); host.runQueuedTimeoutCallbacks(); - // Since this is first event - const project = projectService.configuredProjects.get(config.path); - verifyProjectChangedEventHandler([{ - eventName: server.ProjectChangedEvent, + verifyProjectsUpdatedInBackgroundEventHandler([{ + eventName: server.ProjectsUpdatedInBackgroundEvent, data: { - project, - changedFiles: [libFile.path, f1.path, f2.path], - filesToEmit: [f1.path, f2.path] + openFiles } }]); f2.content = "export let x = 11"; host.reloadFS(files); host.runQueuedTimeoutCallbacks(); - verifyProjectChangedEventHandler([{ - eventName: server.ProjectChangedEvent, + verifyProjectsUpdatedInBackgroundEventHandler([{ + eventName: server.ProjectsUpdatedInBackgroundEvent, data: { - project, - changedFiles: [f2.path], - filesToEmit: [f2.path] + openFiles } }]); } @@ -4970,14 +4958,12 @@ namespace ts.projectSystem { interface InitialStateParams { /** custom config file options */ configObj?: any; - /** list of files emitted/changed on first update graph */ - firstCompilationEmitFiles?: string[]; /** Additional files and folders to add */ getAdditionalFileOrFolder?(): FileOrFolder[]; /** initial list of files to reload in fs and first file in this list being the file to open */ firstReloadFileList?: string[]; } - function getInitialState({ configObj = {}, getAdditionalFileOrFolder, firstReloadFileList, firstCompilationEmitFiles }: InitialStateParams = {}) { + function getInitialState({ configObj = {}, getAdditionalFileOrFolder, firstReloadFileList }: InitialStateParams = {}) { const moduleFile1: FileOrFolder = { path: moduleFile1Path, content: "export function Foo() { };", @@ -5015,20 +5001,19 @@ namespace ts.projectSystem { const host = createServerHost([filesToReload[0], configFile]); // Initial project creation - const { session, verifyProjectChangedEventHandler, verifyInitialOpen } = createSession(host); - const projectService = session.getProjectService(); + const { session, verifyProjectsUpdatedInBackgroundEventHandler, verifyInitialOpen } = createSession(host); + const openFiles = [filesToReload[0].path]; verifyInitialOpen(filesToReload[0]); // Since this is first event, it will have all the files - const firstFilesExpected = firstCompilationEmitFiles && getFiles(firstCompilationEmitFiles) || filesToReload; - verifyProjectChangedEvent(firstFilesExpected, filesToReload); + verifyProjectsUpdatedInBackgroundEvent(filesToReload); return { moduleFile1, file1Consumer1, file1Consumer2, moduleFile2, globalFile3, configFile, files, updateContentOfOpenFile, - verifyProjectChangedEvent, - verifyAffectedAllFiles, + verifyNoProjectsUpdatedInBackgroundEvent, + verifyProjectsUpdatedInBackgroundEvent }; function getFiles(filelist: string[]) { @@ -5039,28 +5024,21 @@ namespace ts.projectSystem { return find(files, file => file.path === fileName); } - function verifyAffectedAllFiles() { - verifyProjectChangedEvent(filter(files, f => f !== libFile)); + function verifyNoProjectsUpdatedInBackgroundEvent(filesToReload?: FileOrFolder[]) { + host.reloadFS(filesToReload || files); + host.runQueuedTimeoutCallbacks(); + verifyProjectsUpdatedInBackgroundEventHandler([]); } - function verifyProjectChangedEvent(filesToEmit: FileOrFolder[], filesToReload?: FileOrFolder[], additionalChangedFiles?: FileOrFolder[]) { + function verifyProjectsUpdatedInBackgroundEvent(filesToReload?: FileOrFolder[]) { host.reloadFS(filesToReload || files); host.runQueuedTimeoutCallbacks(); - if (filesToEmit.length) { - const project = projectService.configuredProjects.get(configFile.path); - const changedFiles = mapDefined(additionalChangedFiles ? filesToEmit.concat(additionalChangedFiles) : filesToEmit, f => f !== configFile ? f.path : undefined); - verifyProjectChangedEventHandler([{ - eventName: server.ProjectChangedEvent, - data: { - project, - changedFiles, - filesToEmit: mapDefined(filesToEmit, f => f !== libFile && f !== configFile ? f.path : undefined) - } - }]); - } - else { - verifyProjectChangedEventHandler([]); - } + verifyProjectsUpdatedInBackgroundEventHandler([{ + eventName: server.ProjectsUpdatedInBackgroundEvent, + data: { + openFiles + } + }]); } function updateContentOfOpenFile(file: FileOrFolder, newContent: string) { @@ -5080,35 +5058,35 @@ namespace ts.projectSystem { } it("should contains only itself if a module file's shape didn't change, and all files referencing it if its shape changed", () => { - const { moduleFile1, file1Consumer1, file1Consumer2, verifyProjectChangedEvent } = getInitialState(); + const { moduleFile1, verifyProjectsUpdatedInBackgroundEvent } = getInitialState(); // Change the content of moduleFile1 to `export var T: number;export function Foo() { };` moduleFile1.content = `export var T: number;export function Foo() { };`; - verifyProjectChangedEvent([moduleFile1, file1Consumer1, file1Consumer2]); + verifyProjectsUpdatedInBackgroundEvent(); // Change the content of moduleFile1 to `export var T: number;export function Foo() { console.log('hi'); };` moduleFile1.content = `export var T: number;export function Foo() { console.log('hi'); };`; - verifyProjectChangedEvent([moduleFile1]); + verifyProjectsUpdatedInBackgroundEvent(); }); it("should be up-to-date with the reference map changes", () => { - const { moduleFile1, file1Consumer1, file1Consumer2, updateContentOfOpenFile, verifyProjectChangedEvent } = getInitialState(); + const { moduleFile1, file1Consumer1, updateContentOfOpenFile, verifyProjectsUpdatedInBackgroundEvent, verifyNoProjectsUpdatedInBackgroundEvent } = getInitialState(); // Change file1Consumer1 content to `export let y = Foo();` updateContentOfOpenFile(file1Consumer1, "export let y = Foo();"); - verifyProjectChangedEvent([]); + verifyNoProjectsUpdatedInBackgroundEvent(); // Change the content of moduleFile1 to `export var T: number;export function Foo() { };` moduleFile1.content = `export var T: number;export function Foo() { };`; - verifyProjectChangedEvent([moduleFile1, file1Consumer2, file1Consumer1]); + verifyProjectsUpdatedInBackgroundEvent(); // Add the import statements back to file1Consumer1 updateContentOfOpenFile(file1Consumer1, `import {Foo} from "./moduleFile1";let y = Foo();`); - verifyProjectChangedEvent([]); + verifyNoProjectsUpdatedInBackgroundEvent(); // Change the content of moduleFile1 to `export var T: number;export var T2: string;export function Foo() { };` moduleFile1.content = `export var T: number;export var T2: string;export function Foo() { };`; - verifyProjectChangedEvent([moduleFile1, file1Consumer2, file1Consumer1]); + verifyProjectsUpdatedInBackgroundEvent(); // Multiple file edits in one go: @@ -5116,69 +5094,68 @@ namespace ts.projectSystem { // Change the content of moduleFile1 to `export var T: number;export function Foo() { };` updateContentOfOpenFile(file1Consumer1, `export let y = Foo();`); moduleFile1.content = `export var T: number;export function Foo() { };`; - verifyProjectChangedEvent([moduleFile1, file1Consumer1, file1Consumer2]); + verifyProjectsUpdatedInBackgroundEvent(); }); it("should be up-to-date with deleted files", () => { - const { moduleFile1, file1Consumer1, file1Consumer2, files, verifyProjectChangedEvent } = getInitialState(); + const { moduleFile1, file1Consumer2, files, verifyProjectsUpdatedInBackgroundEvent } = getInitialState(); // Change the content of moduleFile1 to `export var T: number;export function Foo() { };` moduleFile1.content = `export var T: number;export function Foo() { };`; // Delete file1Consumer2 const filesToLoad = filter(files, file => file !== file1Consumer2); - verifyProjectChangedEvent([moduleFile1, file1Consumer1], filesToLoad, [file1Consumer2]); + verifyProjectsUpdatedInBackgroundEvent(filesToLoad); }); it("should be up-to-date with newly created files", () => { - const { moduleFile1, file1Consumer1, file1Consumer2, files, verifyProjectChangedEvent, } = getInitialState(); + const { moduleFile1, files, verifyProjectsUpdatedInBackgroundEvent, } = getInitialState(); const file1Consumer3: FileOrFolder = { path: "/a/b/file1Consumer3.ts", content: `import {Foo} from "./moduleFile1"; let y = Foo();` }; moduleFile1.content = `export var T: number;export function Foo() { };`; - verifyProjectChangedEvent([moduleFile1, file1Consumer1, file1Consumer3, file1Consumer2], files.concat(file1Consumer3)); + verifyProjectsUpdatedInBackgroundEvent(files.concat(file1Consumer3)); }); it("should detect changes in non-root files", () => { - const { moduleFile1, file1Consumer1, verifyProjectChangedEvent } = getInitialState({ + const { moduleFile1, verifyProjectsUpdatedInBackgroundEvent } = getInitialState({ configObj: { files: [file1Consumer1Path] }, - firstCompilationEmitFiles: [file1Consumer1Path, moduleFile1Path, libFile.path] }); moduleFile1.content = `export var T: number;export function Foo() { };`; - verifyProjectChangedEvent([moduleFile1, file1Consumer1]); + verifyProjectsUpdatedInBackgroundEvent(); // change file1 internal, and verify only file1 is affected moduleFile1.content += "var T1: number;"; - verifyProjectChangedEvent([moduleFile1]); + verifyProjectsUpdatedInBackgroundEvent(); }); it("should return all files if a global file changed shape", () => { - const { globalFile3, verifyAffectedAllFiles } = getInitialState(); + const { globalFile3, verifyProjectsUpdatedInBackgroundEvent } = getInitialState(); globalFile3.content += "var T2: string;"; - verifyAffectedAllFiles(); + verifyProjectsUpdatedInBackgroundEvent(); }); it("should always return the file itself if '--isolatedModules' is specified", () => { - const { moduleFile1, verifyProjectChangedEvent } = getInitialState({ + const { moduleFile1, verifyProjectsUpdatedInBackgroundEvent } = getInitialState({ configObj: { compilerOptions: { isolatedModules: true } } }); moduleFile1.content = `export var T: number;export function Foo() { };`; - verifyProjectChangedEvent([moduleFile1]); + verifyProjectsUpdatedInBackgroundEvent(); }); it("should always return the file itself if '--out' or '--outFile' is specified", () => { const outFilePath = "/a/b/out.js"; - const { moduleFile1, verifyProjectChangedEvent } = getInitialState({ + const { moduleFile1, verifyProjectsUpdatedInBackgroundEvent } = getInitialState({ configObj: { compilerOptions: { module: "system", outFile: outFilePath } } }); moduleFile1.content = `export var T: number;export function Foo() { };`; - verifyProjectChangedEvent([moduleFile1]); + verifyProjectsUpdatedInBackgroundEvent(); }); it("should return cascaded affected file list", () => { @@ -5186,21 +5163,21 @@ namespace ts.projectSystem { path: "/a/b/file1Consumer1Consumer1.ts", content: `import {y} from "./file1Consumer1";` }; - const { moduleFile1, file1Consumer1, file1Consumer2, updateContentOfOpenFile, verifyProjectChangedEvent } = getInitialState({ + const { moduleFile1, file1Consumer1, updateContentOfOpenFile, verifyNoProjectsUpdatedInBackgroundEvent, verifyProjectsUpdatedInBackgroundEvent } = getInitialState({ getAdditionalFileOrFolder: () => [file1Consumer1Consumer1] }); updateContentOfOpenFile(file1Consumer1, file1Consumer1.content + "export var T: number;"); - verifyProjectChangedEvent([]); + verifyNoProjectsUpdatedInBackgroundEvent(); // Doesnt change the shape of file1Consumer1 moduleFile1.content = `export var T: number;export function Foo() { };`; - verifyProjectChangedEvent([moduleFile1, file1Consumer1, file1Consumer2, file1Consumer1Consumer1]); + verifyProjectsUpdatedInBackgroundEvent(); // Change both files before the timeout updateContentOfOpenFile(file1Consumer1, file1Consumer1.content + "export var T2: number;"); moduleFile1.content = `export var T2: number;export function Foo() { };`; - verifyProjectChangedEvent([moduleFile1, file1Consumer1, file1Consumer2, file1Consumer1Consumer1]); + verifyProjectsUpdatedInBackgroundEvent(); }); it("should work fine for files with circular references", () => { @@ -5216,13 +5193,13 @@ namespace ts.projectSystem { /// export var t2 = 10;` }; - const { configFile, verifyProjectChangedEvent } = getInitialState({ + const { configFile, verifyProjectsUpdatedInBackgroundEvent } = getInitialState({ getAdditionalFileOrFolder: () => [file1, file2], firstReloadFileList: [file1.path, libFile.path, file2.path, configFilePath] }); file2.content += "export var t3 = 10;"; - verifyProjectChangedEvent([file1, file2], [file1, file2, libFile, configFile]); + verifyProjectsUpdatedInBackgroundEvent([file1, file2, libFile, configFile]); }); it("should detect removed code file", () => { @@ -5232,12 +5209,12 @@ namespace ts.projectSystem { /// export var x = Foo();` }; - const { configFile, verifyProjectChangedEvent, moduleFile1 } = getInitialState({ + const { configFile, verifyProjectsUpdatedInBackgroundEvent } = getInitialState({ getAdditionalFileOrFolder: () => [referenceFile1], firstReloadFileList: [referenceFile1.path, libFile.path, moduleFile1Path, configFilePath] }); - verifyProjectChangedEvent([referenceFile1], [libFile, referenceFile1, configFile], [moduleFile1]); + verifyProjectsUpdatedInBackgroundEvent([libFile, referenceFile1, configFile]); }); it("should detect non-existing code file", () => { @@ -5247,16 +5224,16 @@ namespace ts.projectSystem { /// export var x = Foo();` }; - const { configFile, moduleFile2, updateContentOfOpenFile, verifyProjectChangedEvent } = getInitialState({ + const { configFile, moduleFile2, updateContentOfOpenFile, verifyNoProjectsUpdatedInBackgroundEvent, verifyProjectsUpdatedInBackgroundEvent } = getInitialState({ getAdditionalFileOrFolder: () => [referenceFile1], firstReloadFileList: [referenceFile1.path, libFile.path, configFilePath] }); updateContentOfOpenFile(referenceFile1, referenceFile1.content + "export var yy = Foo();"); - verifyProjectChangedEvent([], [libFile, referenceFile1, configFile]); + verifyNoProjectsUpdatedInBackgroundEvent([libFile, referenceFile1, configFile]); // Create module File2 and see both files are saved - verifyProjectChangedEvent([referenceFile1, moduleFile2], [libFile, moduleFile2, referenceFile1, configFile]); + verifyProjectsUpdatedInBackgroundEvent([libFile, moduleFile2, referenceFile1, configFile]); }); }); @@ -5280,9 +5257,10 @@ namespace ts.projectSystem { }; const projectFiles = [file1, file3, libFile, configFile]; + const openFiles = [file1.path]; const watchedRecursiveDirectories = ["/a/b/project", "/a/b/node_modules", "/a/node_modules", "/node_modules"]; const host = createServerHost(projectFiles); - const { session, verifyInitialOpen, verifyProjectChangedEventHandler } = createSession(host); + const { session, verifyInitialOpen, verifyProjectsUpdatedInBackgroundEventHandler } = createSession(host); const projectService = session.getProjectService(); verifyInitialOpen(file1); checkNumberOfProjects(projectService, { configuredProjects: 1 }); @@ -5298,12 +5276,10 @@ namespace ts.projectSystem { // Since this is first event verifyProject(); - verifyProjectChangedEventHandler([{ - eventName: server.ProjectChangedEvent, + verifyProjectsUpdatedInBackgroundEventHandler([{ + eventName: server.ProjectsUpdatedInBackgroundEvent, data: { - project, - changedFiles: [libFile.path, file1.path, file3.path], - filesToEmit: [file1.path, file3.path] + openFiles } }]); @@ -5313,13 +5289,10 @@ namespace ts.projectSystem { watchedRecursiveDirectories.length = 2; verifyProject(); - const changedFiles = [file1.path, file2.path]; - verifyProjectChangedEventHandler([{ - eventName: server.ProjectChangedEvent, + verifyProjectsUpdatedInBackgroundEventHandler([{ + eventName: server.ProjectsUpdatedInBackgroundEvent, data: { - project, - changedFiles, - filesToEmit: changedFiles + openFiles } }]); @@ -5341,13 +5314,13 @@ namespace ts.projectSystem { } describe("when event handler is set in the session", () => { - verifyProjectChangedEvent(createSessionWithProjectChangedEventHandler); + verifyProjectsUpdatedInBackgroundEvent(createSessionWithProjectChangedEventHandler); - function createSessionWithProjectChangedEventHandler(host: TestServerHost): ProjectChangeEventVerifier { - const projectChangedEvents: server.ProjectChangedEvent[] = []; + function createSessionWithProjectChangedEventHandler(host: TestServerHost): ProjectsUpdatedInBackgroundEventVerifier { + const projectChangedEvents: server.ProjectsUpdatedInBackgroundEvent[] = []; const session = createSession(host, { eventHandler: e => { - if (e.eventName === server.ProjectChangedEvent) { + if (e.eventName === server.ProjectsUpdatedInBackgroundEvent) { projectChangedEvents.push(e); } } @@ -5355,34 +5328,24 @@ namespace ts.projectSystem { return { session, - verifyProjectChangedEventHandler, - verifyInitialOpen: createVerifyInitialOpen(session, verifyProjectChangedEventHandler) + verifyProjectsUpdatedInBackgroundEventHandler, + verifyInitialOpen: createVerifyInitialOpen(session, verifyProjectsUpdatedInBackgroundEventHandler) }; - function eventToString(event: server.ProjectChangedEvent) { - const eventToModify = event && { - eventName: event.eventName, - data: { - project: event.data.project.getProjectName(), - changedFiles: event.data.changedFiles, - filesToEmit: event.data.filesToEmit - } - }; - return JSON.stringify(eventToModify); + function eventToString(event: server.ProjectsUpdatedInBackgroundEvent) { + return JSON.stringify(event && { eventName: event.eventName, data: event.data }); } - function eventsToString(events: ReadonlyArray) { + function eventsToString(events: ReadonlyArray) { return "[" + map(events, eventToString).join(",") + "]"; } - function verifyProjectChangedEventHandler(expectedEvents: ReadonlyArray) { + function verifyProjectsUpdatedInBackgroundEventHandler(expectedEvents: ReadonlyArray) { assert.equal(projectChangedEvents.length, expectedEvents.length, `Incorrect number of events Actual: ${eventsToString(projectChangedEvents)} Expected: ${eventsToString(expectedEvents)}`); forEach(projectChangedEvents, (actualEvent, i) => { const expectedEvent = expectedEvents[i]; assert.strictEqual(actualEvent.eventName, expectedEvent.eventName); - assert.strictEqual(actualEvent.data.project, expectedEvent.data.project); - verifyFiles("changedFiles", actualEvent.data.changedFiles, expectedEvent.data.changedFiles); - verifyFiles("filesToEmit", actualEvent.data.filesToEmit, expectedEvent.data.filesToEmit); + verifyFiles("openFiles", actualEvent.data.openFiles, expectedEvent.data.openFiles); }); // Verified the events, reset them @@ -5392,41 +5355,37 @@ namespace ts.projectSystem { }); describe("when event handler is not set but session is created with canUseEvents = true", () => { - verifyProjectChangedEvent(createSessionThatUsesEvents); + verifyProjectsUpdatedInBackgroundEvent(createSessionThatUsesEvents); - function createSessionThatUsesEvents(host: TestServerHost): ProjectChangeEventVerifier { + function createSessionThatUsesEvents(host: TestServerHost): ProjectsUpdatedInBackgroundEventVerifier { const session = createSession(host, { canUseEvents: true }); return { session, - verifyProjectChangedEventHandler, - verifyInitialOpen: createVerifyInitialOpen(session, verifyProjectChangedEventHandler) + verifyProjectsUpdatedInBackgroundEventHandler, + verifyInitialOpen: createVerifyInitialOpen(session, verifyProjectsUpdatedInBackgroundEventHandler) }; - function verifyProjectChangedEventHandler(expected: ReadonlyArray) { - const expectedEvents: protocol.ProjectChangedEventBody[] = map(expected, e => { + function verifyProjectsUpdatedInBackgroundEventHandler(expected: ReadonlyArray) { + const expectedEvents: protocol.ProjectsUpdatedInBackgroundEventBody[] = map(expected, e => { return { - projectName: e.data.project.getProjectName(), - changedFiles: e.data.changedFiles, - fileNamesToEmit: e.data.filesToEmit + openFiles: e.data.openFiles }; }); const outputEventRegex = /Content\-Length: [\d]+\r\n\r\n/; - const events: protocol.ProjectStructureChangedEvent[] = filter( + const events: protocol.ProjectsUpdatedInBackgroundEvent[] = filter( map( host.getOutput(), s => convertToObject( ts.parseJsonText("json.json", s.replace(outputEventRegex, "")), [] ) ), - e => e.event === server.ProjectChangedEvent + e => e.event === server.ProjectsUpdatedInBackgroundEvent ); assert.equal(events.length, expectedEvents.length, `Incorrect number of events Actual: ${map(events, e => e.body)} Expected: ${expectedEvents}`); forEach(events, (actualEvent, i) => { const expectedEvent = expectedEvents[i]; - assert.strictEqual(actualEvent.body.projectName, expectedEvent.projectName); - verifyFiles("changedFiles", actualEvent.body.changedFiles, expectedEvent.changedFiles); - verifyFiles("fileNamesToEmit", actualEvent.body.fileNamesToEmit, expectedEvent.fileNamesToEmit); + verifyFiles("openFiles", actualEvent.body.openFiles, expectedEvent.openFiles); }); // Verified the events, reset them diff --git a/src/server/editorServices.ts b/src/server/editorServices.ts index 84f8b5c13f83c..f7f935049f3fa 100644 --- a/src/server/editorServices.ts +++ b/src/server/editorServices.ts @@ -9,14 +9,14 @@ namespace ts.server { export const maxProgramSizeForNonTsFiles = 20 * 1024 * 1024; - export const ProjectChangedEvent = "projectChanged"; + export const ProjectsUpdatedInBackgroundEvent = "projectsUpdatedInBackground"; export const ConfigFileDiagEvent = "configFileDiag"; export const ProjectLanguageServiceStateEvent = "projectLanguageServiceState"; export const ProjectInfoTelemetryEvent = "projectInfo"; - export interface ProjectChangedEvent { - eventName: typeof ProjectChangedEvent; - data: { project: Project; filesToEmit: string[]; changedFiles: string[] }; + export interface ProjectsUpdatedInBackgroundEvent { + eventName: typeof ProjectsUpdatedInBackgroundEvent; + data: { openFiles: string[]; }; } export interface ConfigFileDiagEvent { @@ -76,7 +76,7 @@ namespace ts.server { readonly dts: number; } - export type ProjectServiceEvent = ProjectChangedEvent | ConfigFileDiagEvent | ProjectLanguageServiceStateEvent | ProjectInfoTelemetryEvent; + export type ProjectServiceEvent = ProjectsUpdatedInBackgroundEvent | ConfigFileDiagEvent | ProjectLanguageServiceStateEvent | ProjectInfoTelemetryEvent; export interface ProjectServiceEventHandler { (event: ProjectServiceEvent): void; @@ -517,9 +517,14 @@ namespace ts.server { if (this.pendingProjectUpdates.size !== 0) { this.delayInferredProjectsRefresh(); } - else if (this.pendingInferredProjectUpdate) { - this.pendingInferredProjectUpdate = false; - this.refreshInferredProjects(); + else { + if (this.pendingInferredProjectUpdate) { + this.pendingInferredProjectUpdate = false; + this.refreshInferredProjects(); + } + // Send the event to notify that there were background project updates + // send current list of open files + this.sendProjectsUpdatedInBackgroundEvent(); } }); } @@ -531,27 +536,18 @@ namespace ts.server { if (this.pendingProjectUpdates.delete(projectName)) { project.updateGraph(); } - // Send the update event to notify about the project changes - this.sendProjectChangedEvent(project); }); } - private sendProjectChangedEvent(project: Project) { - if (project.isClosed() || !this.eventHandler || !project.languageServiceEnabled) { - return; - } - - const { filesToEmit, changedFiles } = project.getChangedFiles(); - if (changedFiles.length === 0) { + private sendProjectsUpdatedInBackgroundEvent() { + if (!this.eventHandler) { return; } - const event: ProjectChangedEvent = { - eventName: ProjectChangedEvent, + const event: ProjectsUpdatedInBackgroundEvent = { + eventName: ProjectsUpdatedInBackgroundEvent, data: { - project, - filesToEmit: filesToEmit as string[], - changedFiles: changedFiles as string[] + openFiles: this.openFiles.map(f => f.fileName) } }; this.eventHandler(event); diff --git a/src/server/project.ts b/src/server/project.ts index 4a66a4064e0a8..3e2643fdb6a8d 100644 --- a/src/server/project.ts +++ b/src/server/project.ts @@ -457,12 +457,6 @@ namespace ts.server { return !emitSkipped; } - getChangedFiles() { - Debug.assert(this.languageServiceEnabled); - this.ensureBuilder(); - return this.builder.getChangedProgramFiles(this.program); - } - enableLanguageService() { if (this.languageServiceEnabled) { return; diff --git a/src/server/protocol.ts b/src/server/protocol.ts index cf91905280bac..3d07392bbe69f 100644 --- a/src/server/protocol.ts +++ b/src/server/protocol.ts @@ -2040,27 +2040,17 @@ namespace ts.server.protocol { languageServiceEnabled: boolean; } - export type ProjectChangedEventName = "projectChanged"; - export interface ProjectStructureChangedEvent extends Event { - event: ProjectChangedEventName; - body: ProjectChangedEventBody; + export type ProjectsUpdatedInBackgroundEventName = "projectsUpdatedInBackground"; + export interface ProjectsUpdatedInBackgroundEvent extends Event { + event: ProjectsUpdatedInBackgroundEventName; + body: ProjectsUpdatedInBackgroundEventBody; } - export interface ProjectChangedEventBody { + export interface ProjectsUpdatedInBackgroundEventBody { /** - * Project name that has changes + * Current set of open files */ - projectName: string; - - /** - * Minimum set of file names to emit - */ - fileNamesToEmit: string[]; - - /** - * List of files that have changed/added/removed or could have been affected by the changed files - */ - changedFiles: string[]; + openFiles: string[]; } /** diff --git a/src/server/session.ts b/src/server/session.ts index cad460bba7b8b..3e7cbf0402488 100644 --- a/src/server/session.ts +++ b/src/server/session.ts @@ -332,9 +332,9 @@ namespace ts.server { private defaultEventHandler(event: ProjectServiceEvent) { switch (event.eventName) { - case ProjectChangedEvent: - const { project, filesToEmit, changedFiles } = event.data; - this.projectChangedEvent(project, filesToEmit, changedFiles); + case ProjectsUpdatedInBackgroundEvent: + const { openFiles } = event.data; + this.projectsUpdatedInBackgroundEvent(openFiles); break; case ConfigFileDiagEvent: const { triggerFile, configFileName: configFile, diagnostics } = event.data; @@ -364,21 +364,19 @@ namespace ts.server { } } - private projectChangedEvent(project: Project, fileNamesToEmit: string[], changedFiles: string[]): void { - this.projectService.logger.info(`got project changed event, updating diagnostics for ${changedFiles}`); - if (changedFiles.length) { - const checkList = this.createCheckList(changedFiles, project); + private projectsUpdatedInBackgroundEvent(openFiles: string[]): void { + this.projectService.logger.info(`got projects updated in background, updating diagnostics for ${openFiles}`); + if (openFiles.length) { + const checkList = this.createCheckList(openFiles); // For now only queue error checking for open files. We can change this to include non open files as well this.errorCheck.startNew(next => this.updateErrorCheck(next, checkList, 100, /*requireOpen*/ true)); // Send project changed event - this.event({ - projectName: project.getProjectName(), - changedFiles, - fileNamesToEmit - }, "projectChanged"); + this.event({ + openFiles + }, "projectsUpdatedInBackground"); } } From 14febe211308082ae0875bc74da95a3ee4bb6c1a Mon Sep 17 00:00:00 2001 From: Sheetal Nandi Date: Tue, 26 Sep 2017 10:46:32 -0700 Subject: [PATCH 104/109] Rename watchedProgram.ts to watch.ts --- src/compiler/tsc.ts | 2 +- src/compiler/tsconfig.json | 2 +- src/compiler/{watchedProgram.ts => watch.ts} | 0 src/harness/unittests/tscWatchMode.ts | 2 +- 4 files changed, 3 insertions(+), 3 deletions(-) rename src/compiler/{watchedProgram.ts => watch.ts} (100%) diff --git a/src/compiler/tsc.ts b/src/compiler/tsc.ts index 57c906c7fb659..680daeeb4856f 100644 --- a/src/compiler/tsc.ts +++ b/src/compiler/tsc.ts @@ -1,5 +1,5 @@ /// -/// +/// /// namespace ts { diff --git a/src/compiler/tsconfig.json b/src/compiler/tsconfig.json index f527610cbd75e..dee33cbf16301 100644 --- a/src/compiler/tsconfig.json +++ b/src/compiler/tsconfig.json @@ -39,7 +39,7 @@ "program.ts", "builder.ts", "resolutionCache.ts", - "watchedProgram.ts", + "watch.ts", "commandLineParser.ts", "tsc.ts", "diagnosticInformationMap.generated.ts" diff --git a/src/compiler/watchedProgram.ts b/src/compiler/watch.ts similarity index 100% rename from src/compiler/watchedProgram.ts rename to src/compiler/watch.ts diff --git a/src/harness/unittests/tscWatchMode.ts b/src/harness/unittests/tscWatchMode.ts index def410c6b6b13..5267bbcf0478c 100644 --- a/src/harness/unittests/tscWatchMode.ts +++ b/src/harness/unittests/tscWatchMode.ts @@ -1,5 +1,5 @@ /// -/// +/// /// namespace ts.tscWatch { From 38f3a2b700a89df96b9e119d9ecf573e133e890a Mon Sep 17 00:00:00 2001 From: Sheetal Nandi Date: Tue, 26 Sep 2017 11:05:52 -0700 Subject: [PATCH 105/109] Renamed PartialSystem as DirectoryStructureHost and CachedPartialSystem as CachedDirectoryStructureHost --- src/compiler/core.ts | 7 ++---- src/compiler/resolutionCache.ts | 10 ++++----- src/compiler/sys.ts | 7 ++---- src/compiler/watch.ts | 36 +++++++++++++++---------------- src/server/editorServices.ts | 26 +++++++++++----------- src/server/project.ts | 38 ++++++++++++++++----------------- src/server/scriptInfo.ts | 2 +- 7 files changed, 60 insertions(+), 66 deletions(-) diff --git a/src/compiler/core.ts b/src/compiler/core.ts index 11f9d0a9e2463..9579dec51cd72 100644 --- a/src/compiler/core.ts +++ b/src/compiler/core.ts @@ -2668,21 +2668,18 @@ namespace ts { export function assertTypeIsNever(_: never): void { } - export interface CachedHost { + export interface CachedDirectoryStructureHost extends DirectoryStructureHost { addOrDeleteFileOrFolder(fileOrFolder: string, fileOrFolderPath: Path): void; addOrDeleteFile(fileName: string, filePath: Path, eventKind: FileWatcherEventKind): void; clearCache(): void; } - export interface CachedPartialSystem extends PartialSystem, CachedHost { - } - interface MutableFileSystemEntries { readonly files: string[]; readonly directories: string[]; } - export function createCachedPartialSystem(host: PartialSystem): CachedPartialSystem { + export function createCachedDirectoryStructureHost(host: DirectoryStructureHost): CachedDirectoryStructureHost { const cachedReadDirectoryResult = createMap(); const getCurrentDirectory = memoize(() => host.getCurrentDirectory()); const getCanonicalFileName = createGetCanonicalFileName(host.useCaseSensitiveFileNames); diff --git a/src/compiler/resolutionCache.ts b/src/compiler/resolutionCache.ts index a320e754d8538..814d5053341a2 100644 --- a/src/compiler/resolutionCache.ts +++ b/src/compiler/resolutionCache.ts @@ -42,7 +42,7 @@ namespace ts { onInvalidatedResolution(): void; watchTypeRootsDirectory(directory: string, cb: DirectoryWatcherCallback, flags: WatchDirectoryFlags): FileWatcher; onChangedAutomaticTypeDirectiveNames(): void; - getCachedPartialSystem?(): CachedPartialSystem; + getCachedDirectoryStructureHost?(): CachedDirectoryStructureHost; projectName?: string; getGlobalCache?(): string | undefined; writeLog(s: string): void; @@ -396,9 +396,9 @@ namespace ts { function createDirectoryWatcher(directory: string, dirPath: Path) { return resolutionHost.watchDirectoryOfFailedLookupLocation(directory, fileOrFolder => { const fileOrFolderPath = resolutionHost.toPath(fileOrFolder); - if (resolutionHost.getCachedPartialSystem) { + if (resolutionHost.getCachedDirectoryStructureHost) { // Since the file existance changed, update the sourceFiles cache - resolutionHost.getCachedPartialSystem().addOrDeleteFileOrFolder(fileOrFolder, fileOrFolderPath); + resolutionHost.getCachedDirectoryStructureHost().addOrDeleteFileOrFolder(fileOrFolder, fileOrFolderPath); } // If the files are added to project root or node_modules directory, always run through the invalidation process @@ -515,9 +515,9 @@ namespace ts { // Create new watch and recursive info return resolutionHost.watchTypeRootsDirectory(typeRoot, fileOrFolder => { const fileOrFolderPath = resolutionHost.toPath(fileOrFolder); - if (resolutionHost.getCachedPartialSystem) { + if (resolutionHost.getCachedDirectoryStructureHost) { // Since the file existance changed, update the sourceFiles cache - resolutionHost.getCachedPartialSystem().addOrDeleteFileOrFolder(fileOrFolder, fileOrFolderPath); + resolutionHost.getCachedDirectoryStructureHost().addOrDeleteFileOrFolder(fileOrFolder, fileOrFolderPath); } // For now just recompile diff --git a/src/compiler/sys.ts b/src/compiler/sys.ts index 5f3b8bd06d167..822de1eef52f0 100644 --- a/src/compiler/sys.ts +++ b/src/compiler/sys.ts @@ -33,7 +33,7 @@ namespace ts { /** * Partial interface of the System thats needed to support the caching of directory structure */ - export interface PartialSystem { + export interface DirectoryStructureHost { newLine: string; useCaseSensitiveFileNames: boolean; write(s: string): void; @@ -48,11 +48,8 @@ namespace ts { exit(exitCode?: number): void; } - export interface System extends PartialSystem { + export interface System extends DirectoryStructureHost { args: string[]; - newLine: string; - useCaseSensitiveFileNames: boolean; - write(s: string): void; getFileSize?(path: string): number; /** * @pollingInterval - this parameter is used in polling-based watchers and ignored in watchers that diff --git a/src/compiler/watch.ts b/src/compiler/watch.ts index 939e50ce2abef..2d1d6e8bf5880 100644 --- a/src/compiler/watch.ts +++ b/src/compiler/watch.ts @@ -4,7 +4,7 @@ namespace ts { export type DiagnosticReporter = (diagnostic: Diagnostic) => void; - export type ParseConfigFile = (configFileName: string, optionsToExtend: CompilerOptions, system: PartialSystem, reportDiagnostic: DiagnosticReporter, reportWatchDiagnostic: DiagnosticReporter) => ParsedCommandLine; + export type ParseConfigFile = (configFileName: string, optionsToExtend: CompilerOptions, system: DirectoryStructureHost, reportDiagnostic: DiagnosticReporter, reportWatchDiagnostic: DiagnosticReporter) => ParsedCommandLine; export interface WatchingSystemHost { // FS system to use system: System; @@ -18,7 +18,7 @@ namespace ts { // Callbacks to do custom action before creating program and after creating program beforeCompile(compilerOptions: CompilerOptions): void; - afterCompile(host: PartialSystem, program: Program, builder: Builder): void; + afterCompile(host: DirectoryStructureHost, program: Program, builder: Builder): void; } const defaultFormatDiagnosticsHost: FormatDiagnosticsHost = sys ? { @@ -61,7 +61,7 @@ namespace ts { system.write(ts.formatDiagnosticsWithColorAndContext([diagnostic], host) + host.getNewLine()); } - export function parseConfigFile(configFileName: string, optionsToExtend: CompilerOptions, system: PartialSystem, reportDiagnostic: DiagnosticReporter, reportWatchDiagnostic: DiagnosticReporter): ParsedCommandLine { + export function parseConfigFile(configFileName: string, optionsToExtend: CompilerOptions, system: DirectoryStructureHost, reportDiagnostic: DiagnosticReporter, reportWatchDiagnostic: DiagnosticReporter): ParsedCommandLine { let configFileText: string; try { configFileText = system.readFile(configFileName); @@ -89,7 +89,7 @@ namespace ts { return configParseResult; } - function reportEmittedFiles(files: string[], system: PartialSystem): void { + function reportEmittedFiles(files: string[], system: DirectoryStructureHost): void { if (!files || files.length === 0) { return; } @@ -100,7 +100,7 @@ namespace ts { } } - export function handleEmitOutputAndReportErrors(system: PartialSystem, program: Program, + export function handleEmitOutputAndReportErrors(system: DirectoryStructureHost, program: Program, emittedFiles: string[], emitSkipped: boolean, diagnostics: Diagnostic[], reportDiagnostic: DiagnosticReporter ): ExitStatus { @@ -141,7 +141,7 @@ namespace ts { afterCompile: compileWatchedProgram, }; - function compileWatchedProgram(host: PartialSystem, program: Program, builder: Builder) { + function compileWatchedProgram(host: DirectoryStructureHost, program: Program, builder: Builder) { // First get and report any syntactic errors. let diagnostics = program.getSyntacticDiagnostics().slice(); let reportSemanticDiagnostics = false; @@ -256,14 +256,14 @@ namespace ts { watchingHost = watchingHost || createWatchingSystemHost(compilerOptions.pretty); const { system, parseConfigFile, reportDiagnostic, reportWatchDiagnostic, beforeCompile, afterCompile } = watchingHost; - const partialSystem = configFileName ? createCachedPartialSystem(system) : system; + const directoryStructureHost = configFileName ? createCachedDirectoryStructureHost(system) : system; if (configFileName) { watchFile(system, configFileName, scheduleProgramReload, writeLog); } - const getCurrentDirectory = memoize(() => partialSystem.getCurrentDirectory()); + const getCurrentDirectory = memoize(() => directoryStructureHost.getCurrentDirectory()); const realpath = system.realpath && ((path: string) => system.realpath(path)); - const getCachedPartialSystem = configFileName && (() => partialSystem as CachedPartialSystem); + const getCachedDirectoryStructureHost = configFileName && (() => directoryStructureHost as CachedDirectoryStructureHost); const getCanonicalFileName = createGetCanonicalFileName(system.useCaseSensitiveFileNames); let newLine = getNewLineCharacter(compilerOptions, system); @@ -294,7 +294,7 @@ namespace ts { getCompilationSettings: () => compilerOptions, watchDirectoryOfFailedLookupLocation: watchDirectory, watchTypeRootsDirectory: watchDirectory, - getCachedPartialSystem, + getCachedDirectoryStructureHost, onInvalidatedResolution: scheduleProgramUpdate, onChangedAutomaticTypeDirectiveNames, writeLog @@ -361,7 +361,7 @@ namespace ts { missingFilePathsRequestedForRelease = undefined; } - afterCompile(partialSystem, program, builder); + afterCompile(directoryStructureHost, program, builder); reportWatchDiagnostic(createCompilerDiagnostic(Diagnostics.Compilation_complete_Watching_for_file_changes)); } @@ -376,11 +376,11 @@ namespace ts { return !isString(hostSourceFileInfo); } - return partialSystem.fileExists(fileName); + return directoryStructureHost.fileExists(fileName); } function directoryExists(directoryName: string) { - return partialSystem.directoryExists(directoryName); + return directoryStructureHost.directoryExists(directoryName); } function readFile(fileName: string) { @@ -392,7 +392,7 @@ namespace ts { } function getDirectories(path: string) { - return partialSystem.getDirectories(path); + return directoryStructureHost.getDirectories(path); } function resolveModuleNames(moduleNames: string[], containingFile: string, reusedNames?: string[]) { @@ -541,7 +541,7 @@ namespace ts { writeLog(`Reloading config file: ${configFileName}`); needsReload = false; - const cachedHost = partialSystem as CachedPartialSystem; + const cachedHost = directoryStructureHost as CachedDirectoryStructureHost; cachedHost.clearCache(); const configParseResult = parseConfigFile(configFileName, optionsToExtendForConfigFile, cachedHost, reportDiagnostic, reportWatchDiagnostic); rootFileNames = configParseResult.fileNames; @@ -586,7 +586,7 @@ namespace ts { function updateCachedSystemWithFile(fileName: string, path: Path, eventKind: FileWatcherEventKind) { if (configFileName) { - (partialSystem as CachedPartialSystem).addOrDeleteFile(fileName, path, eventKind); + (directoryStructureHost as CachedDirectoryStructureHost).addOrDeleteFile(fileName, path, eventKind); } } @@ -639,7 +639,7 @@ namespace ts { const fileOrFolderPath = toPath(fileOrFolder); // Since the file existance changed, update the sourceFiles cache - (partialSystem as CachedPartialSystem).addOrDeleteFileOrFolder(fileOrFolder, fileOrFolderPath); + (directoryStructureHost as CachedDirectoryStructureHost).addOrDeleteFileOrFolder(fileOrFolder, fileOrFolderPath); removeSourceFile(fileOrFolderPath); // If the the added or created file or folder is not supported file name, ignore the file @@ -651,7 +651,7 @@ namespace ts { // Reload is pending, do the reload if (!needsReload) { - const result = getFileNamesFromConfigSpecs(configFileSpecs, getDirectoryPath(configFileName), compilerOptions, partialSystem); + const result = getFileNamesFromConfigSpecs(configFileSpecs, getDirectoryPath(configFileName), compilerOptions, directoryStructureHost); if (!configFileSpecs.filesSpecs && result.fileNames.length === 0) { reportDiagnostic(getErrorForNoInputFiles(configFileSpecs, configFileName)); } diff --git a/src/server/editorServices.ts b/src/server/editorServices.ts index b0d9e2ccf0424..c93098c32c206 100644 --- a/src/server/editorServices.ts +++ b/src/server/editorServices.ts @@ -755,7 +755,7 @@ namespace ts.server { directory, fileOrFolder => { const fileOrFolderPath = this.toPath(fileOrFolder); - project.getCachedPartialSystem().addOrDeleteFileOrFolder(fileOrFolder, fileOrFolderPath); + project.getCachedDirectoryStructureHost().addOrDeleteFileOrFolder(fileOrFolder, fileOrFolderPath); const configFilename = project.getConfigFilePath(); // If the the added or created file or folder is not supported file name, ignore the file @@ -768,7 +768,7 @@ namespace ts.server { // Reload is pending, do the reload if (!project.pendingReload) { const configFileSpecs = project.configFileSpecs; - const result = getFileNamesFromConfigSpecs(configFileSpecs, getDirectoryPath(configFilename), project.getCompilationSettings(), project.getCachedPartialSystem(), this.hostConfiguration.extraFileExtensions); + const result = getFileNamesFromConfigSpecs(configFileSpecs, getDirectoryPath(configFilename), project.getCompilationSettings(), project.getCachedDirectoryStructureHost(), this.hostConfiguration.extraFileExtensions); project.updateErrorOnNoInputFiles(result.fileNames.length !== 0); this.updateNonInferredProjectFiles(project, result.fileNames, fileNamePropertyReader); this.delayUpdateProjectGraphAndInferredProjectsRefresh(project); @@ -1292,7 +1292,7 @@ namespace ts.server { return findProjectByName(projectFileName, this.externalProjects); } - private convertConfigFileContentToProjectOptions(configFilename: string, cachedPartialSystem: CachedPartialSystem) { + private convertConfigFileContentToProjectOptions(configFilename: string, cachedDirectoryStructureHost: CachedDirectoryStructureHost) { configFilename = normalizePath(configFilename); const configFileContent = this.host.readFile(configFilename); @@ -1304,7 +1304,7 @@ namespace ts.server { const errors = result.parseDiagnostics; const parsedCommandLine = parseJsonSourceFileConfigFileContent( result, - cachedPartialSystem, + cachedDirectoryStructureHost, getDirectoryPath(configFilename), /*existingOptions*/ {}, configFilename, @@ -1429,8 +1429,8 @@ namespace ts.server { } private createConfiguredProject(configFileName: NormalizedPath) { - const cachedPartialSystem = createCachedPartialSystem(this.host); - const { projectOptions, configFileErrors, configFileSpecs } = this.convertConfigFileContentToProjectOptions(configFileName, cachedPartialSystem); + const cachedDirectoryStructureHost = createCachedDirectoryStructureHost(this.host); + const { projectOptions, configFileErrors, configFileSpecs } = this.convertConfigFileContentToProjectOptions(configFileName, cachedDirectoryStructureHost); this.logger.info(`Opened configuration file ${configFileName}`); const languageServiceEnabled = !this.exceededTotalSizeLimitForNonTsFiles(configFileName, projectOptions.compilerOptions, projectOptions.files, fileNamePropertyReader); const project = new ConfiguredProject( @@ -1441,7 +1441,7 @@ namespace ts.server { projectOptions.compilerOptions, languageServiceEnabled, projectOptions.compileOnSave === undefined ? false : projectOptions.compileOnSave, - cachedPartialSystem); + cachedDirectoryStructureHost); project.configFileSpecs = configFileSpecs; // TODO: We probably should also watch the configFiles that are extended @@ -1488,7 +1488,7 @@ namespace ts.server { else { const scriptKind = propertyReader.getScriptKind(f); const hasMixedContent = propertyReader.hasMixedContent(f, this.hostConfiguration.extraFileExtensions); - scriptInfo = this.getOrCreateScriptInfoNotOpenedByClientForNormalizedPath(normalizedPath, scriptKind, hasMixedContent, project.partialSystem); + scriptInfo = this.getOrCreateScriptInfoNotOpenedByClientForNormalizedPath(normalizedPath, scriptKind, hasMixedContent, project.directoryStructureHost); path = scriptInfo.path; // If this script info is not already a root add it if (!project.isRoot(scriptInfo)) { @@ -1539,7 +1539,7 @@ namespace ts.server { /* @internal */ reloadConfiguredProject(project: ConfiguredProject) { // At this point, there is no reason to not have configFile in the host - const host = project.getCachedPartialSystem(); + const host = project.getCachedDirectoryStructureHost(); // Clear the cache since we are reloading the project from disk host.clearCache(); @@ -1637,7 +1637,7 @@ namespace ts.server { } /*@internal*/ - getOrCreateScriptInfoNotOpenedByClient(uncheckedFileName: string, hostToQueryFileExistsOn: PartialSystem) { + getOrCreateScriptInfoNotOpenedByClient(uncheckedFileName: string, hostToQueryFileExistsOn: DirectoryStructureHost) { return this.getOrCreateScriptInfoNotOpenedByClientForNormalizedPath( toNormalizedPath(uncheckedFileName), /*scriptKind*/ undefined, /*hasMixedContent*/ undefined, hostToQueryFileExistsOn @@ -1669,15 +1669,15 @@ namespace ts.server { } } - getOrCreateScriptInfoNotOpenedByClientForNormalizedPath(fileName: NormalizedPath, scriptKind?: ScriptKind, hasMixedContent?: boolean, hostToQueryFileExistsOn?: PartialSystem) { + getOrCreateScriptInfoNotOpenedByClientForNormalizedPath(fileName: NormalizedPath, scriptKind?: ScriptKind, hasMixedContent?: boolean, hostToQueryFileExistsOn?: DirectoryStructureHost) { return this.getOrCreateScriptInfoForNormalizedPath(fileName, /*openedByClient*/ false, /*fileContent*/ undefined, scriptKind, hasMixedContent, hostToQueryFileExistsOn); } - getOrCreateScriptInfoOpenedByClientForNormalizedPath(fileName: NormalizedPath, fileContent?: string, scriptKind?: ScriptKind, hasMixedContent?: boolean, hostToQueryFileExistsOn?: PartialSystem) { + getOrCreateScriptInfoOpenedByClientForNormalizedPath(fileName: NormalizedPath, fileContent?: string, scriptKind?: ScriptKind, hasMixedContent?: boolean, hostToQueryFileExistsOn?: DirectoryStructureHost) { return this.getOrCreateScriptInfoForNormalizedPath(fileName, /*openedByClient*/ true, fileContent, scriptKind, hasMixedContent, hostToQueryFileExistsOn); } - getOrCreateScriptInfoForNormalizedPath(fileName: NormalizedPath, openedByClient: boolean, fileContent?: string, scriptKind?: ScriptKind, hasMixedContent?: boolean, hostToQueryFileExistsOn?: PartialSystem) { + getOrCreateScriptInfoForNormalizedPath(fileName: NormalizedPath, openedByClient: boolean, fileContent?: string, scriptKind?: ScriptKind, hasMixedContent?: boolean, hostToQueryFileExistsOn?: DirectoryStructureHost) { Debug.assert(fileContent === undefined || openedByClient, "ScriptInfo needs to be opened by client to be able to set its user defined content"); const path = normalizedPathToPath(fileName, this.currentDirectory, this.toCanonicalFileName); let info = this.getScriptInfoForPath(path); diff --git a/src/server/project.ts b/src/server/project.ts index a094054c821a9..746e6bf3fbe8d 100644 --- a/src/server/project.ts +++ b/src/server/project.ts @@ -201,7 +201,7 @@ namespace ts.server { languageServiceEnabled: boolean, private compilerOptions: CompilerOptions, public compileOnSaveEnabled: boolean, - /*@internal*/public partialSystem: PartialSystem) { + /*@internal*/public directoryStructureHost: DirectoryStructureHost) { if (!this.compilerOptions) { this.compilerOptions = getDefaultCompilerOptions(); @@ -236,7 +236,7 @@ namespace ts.server { } getNewLine() { - return this.partialSystem.newLine; + return this.directoryStructureHost.newLine; } getProjectVersion() { @@ -263,7 +263,7 @@ namespace ts.server { } private getScriptInfoLSHost(fileName: string) { - const scriptInfo = this.projectService.getOrCreateScriptInfoNotOpenedByClient(fileName, this.partialSystem); + const scriptInfo = this.projectService.getOrCreateScriptInfoNotOpenedByClient(fileName, this.directoryStructureHost); if (scriptInfo) { const existingValue = this.rootFilesMap.get(scriptInfo.path); if (existingValue !== undefined && existingValue !== scriptInfo) { @@ -298,7 +298,7 @@ namespace ts.server { } getCurrentDirectory(): string { - return this.partialSystem.getCurrentDirectory(); + return this.directoryStructureHost.getCurrentDirectory(); } getDefaultLibFileName() { @@ -307,22 +307,22 @@ namespace ts.server { } useCaseSensitiveFileNames() { - return this.partialSystem.useCaseSensitiveFileNames; + return this.directoryStructureHost.useCaseSensitiveFileNames; } readDirectory(path: string, extensions?: ReadonlyArray, exclude?: ReadonlyArray, include?: ReadonlyArray, depth?: number): string[] { - return this.partialSystem.readDirectory(path, extensions, exclude, include, depth); + return this.directoryStructureHost.readDirectory(path, extensions, exclude, include, depth); } readFile(fileName: string): string | undefined { - return this.partialSystem.readFile(fileName); + return this.directoryStructureHost.readFile(fileName); } fileExists(file: string): boolean { // As an optimization, don't hit the disks for files we already know don't exist // (because we're watching for their creation). const path = this.toPath(file); - return !this.isWatchedMissingFile(path) && this.partialSystem.fileExists(file); + return !this.isWatchedMissingFile(path) && this.directoryStructureHost.fileExists(file); } resolveModuleNames(moduleNames: string[], containingFile: string, reusedNames?: string[]): ResolvedModuleFull[] { @@ -334,11 +334,11 @@ namespace ts.server { } directoryExists(path: string): boolean { - return this.partialSystem.directoryExists(path); + return this.directoryStructureHost.directoryExists(path); } getDirectories(path: string): string[] { - return this.partialSystem.getDirectories(path); + return this.directoryStructureHost.getDirectories(path); } /*@internal*/ @@ -518,7 +518,7 @@ namespace ts.server { this.resolutionCache.clear(); this.resolutionCache = undefined; this.cachedUnresolvedImportsPerFile = undefined; - this.partialSystem = undefined; + this.directoryStructureHost = undefined; // Clean up file watchers waiting for missing files if (this.missingFilesMap) { @@ -835,7 +835,7 @@ namespace ts.server { // by the LSHost for files in the program when the program is retrieved above but // the program doesn't contain external files so this must be done explicitly. inserted => { - const scriptInfo = this.projectService.getOrCreateScriptInfoNotOpenedByClient(inserted, this.partialSystem); + const scriptInfo = this.projectService.getOrCreateScriptInfoNotOpenedByClient(inserted, this.directoryStructureHost); scriptInfo.attachToProject(this); }, removed => this.detachScriptInfoFromProject(removed) @@ -859,7 +859,7 @@ namespace ts.server { missingFilePath, (fileName, eventKind) => { if (this.projectKind === ProjectKind.Configured) { - (this.partialSystem as CachedPartialSystem).addOrDeleteFile(fileName, missingFilePath, eventKind); + (this.directoryStructureHost as CachedDirectoryStructureHost).addOrDeleteFile(fileName, missingFilePath, eventKind); } if (eventKind === FileWatcherEventKind.Created && this.missingFilesMap.has(missingFilePath)) { @@ -882,7 +882,7 @@ namespace ts.server { getScriptInfoForNormalizedPath(fileName: NormalizedPath) { const scriptInfo = this.projectService.getOrCreateScriptInfoNotOpenedByClientForNormalizedPath( - fileName, /*scriptKind*/ undefined, /*hasMixedContent*/ undefined, this.partialSystem + fileName, /*scriptKind*/ undefined, /*hasMixedContent*/ undefined, this.directoryStructureHost ); if (scriptInfo && !scriptInfo.isAttached(this)) { return Errors.ThrowProjectDoesNotContainDocument(fileName, this); @@ -1134,8 +1134,8 @@ namespace ts.server { compilerOptions: CompilerOptions, languageServiceEnabled: boolean, public compileOnSaveEnabled: boolean, - cachedPartialSystem: PartialSystem) { - super(configFileName, ProjectKind.Configured, projectService, documentRegistry, hasExplicitListOfFiles, languageServiceEnabled, compilerOptions, compileOnSaveEnabled, cachedPartialSystem); + cachedDirectoryStructureHost: CachedDirectoryStructureHost) { + super(configFileName, ProjectKind.Configured, projectService, documentRegistry, hasExplicitListOfFiles, languageServiceEnabled, compilerOptions, compileOnSaveEnabled, cachedDirectoryStructureHost); this.canonicalConfigFilePath = asNormalizedPath(projectService.toCanonicalFileName(configFileName)); this.enablePlugins(); this.resolutionCache.setRootDirectory(getDirectoryPath(configFileName)); @@ -1155,8 +1155,8 @@ namespace ts.server { } /*@internal*/ - getCachedPartialSystem() { - return this.partialSystem as CachedPartialSystem; + getCachedDirectoryStructureHost() { + return this.directoryStructureHost as CachedDirectoryStructureHost; } getConfigFilePath() { @@ -1342,7 +1342,7 @@ namespace ts.server { } getEffectiveTypeRoots() { - return getEffectiveTypeRoots(this.getCompilationSettings(), this.partialSystem) || []; + return getEffectiveTypeRoots(this.getCompilationSettings(), this.directoryStructureHost) || []; } /*@internal*/ diff --git a/src/server/scriptInfo.ts b/src/server/scriptInfo.ts index e1e05b36f2d3b..3971b29fe7ef9 100644 --- a/src/server/scriptInfo.ts +++ b/src/server/scriptInfo.ts @@ -292,7 +292,7 @@ namespace ts.server { detachAllProjects() { for (const p of this.containingProjects) { if (p.projectKind === ProjectKind.Configured) { - (p.partialSystem as CachedPartialSystem).addOrDeleteFile(this.fileName, this.path, FileWatcherEventKind.Deleted); + (p.directoryStructureHost as CachedDirectoryStructureHost).addOrDeleteFile(this.fileName, this.path, FileWatcherEventKind.Deleted); } const isInfoRoot = p.isRoot(this); // detach is unnecessary since we'll clean the list of containing projects anyways From 68d360585a6b09d93b39774fa54dc3622caee88f Mon Sep 17 00:00:00 2001 From: Sheetal Nandi Date: Tue, 26 Sep 2017 13:34:56 -0700 Subject: [PATCH 106/109] PR feedback --- src/compiler/builder.ts | 49 +++-- src/compiler/commandLineParser.ts | 2 +- src/compiler/core.ts | 22 +-- src/compiler/program.ts | 84 +-------- src/compiler/resolutionCache.ts | 167 +++++++++++------- src/compiler/sys.ts | 29 ++- src/compiler/tsconfig.json | 1 + src/compiler/types.ts | 6 +- src/compiler/utilities.ts | 72 +------- src/compiler/watch.ts | 79 +++------ src/compiler/watchUtilities.ts | 136 ++++++++++++++ .../unittests/reuseProgramStructure.ts | 2 +- .../unittests/tsserverProjectSystem.ts | 6 +- src/harness/virtualFileSystemWithWatch.ts | 78 ++++---- src/server/editorServices.ts | 32 ++-- src/server/project.ts | 102 ++++++----- src/server/scriptInfo.ts | 23 ++- src/services/jsTyping.ts | 2 +- src/services/services.ts | 8 +- src/services/types.ts | 3 +- 20 files changed, 472 insertions(+), 431 deletions(-) create mode 100644 src/compiler/watchUtilities.ts diff --git a/src/compiler/builder.ts b/src/compiler/builder.ts index e7bcb28d820b1..9436be6e8a183 100644 --- a/src/compiler/builder.ts +++ b/src/compiler/builder.ts @@ -43,10 +43,23 @@ namespace ts { } interface EmitHandler { - addScriptInfo(program: Program, sourceFile: SourceFile): void; - removeScriptInfo(path: Path): void; - updateScriptInfo(program: Program, sourceFile: SourceFile): void; - updaterScriptInfoWithSameVersion(program: Program, sourceFile: SourceFile): boolean; + /** + * Called when sourceFile is added to the program + */ + onAddSourceFile(program: Program, sourceFile: SourceFile): void; + /** + * Called when sourceFile is removed from the program + */ + onRemoveSourceFile(path: Path): void; + /** + * Called when sourceFile is changed + */ + onUpdateSourceFile(program: Program, sourceFile: SourceFile): void; + /** + * Called when source file has not changed but has some of the resolutions invalidated + * If returned true, builder will mark the file as changed (noting that something associated with file has changed) + */ + onUpdateSourceFileWithSameVersion(program: Program, sourceFile: SourceFile): boolean; /** * Gets the files affected by the script info which has updated shape from the known one */ @@ -135,23 +148,23 @@ namespace ts { function addNewFileInfo(program: Program, sourceFile: SourceFile): FileInfo { registerChangedFile(sourceFile.path, sourceFile.fileName); - emitHandler.addScriptInfo(program, sourceFile); + emitHandler.onAddSourceFile(program, sourceFile); return { fileName: sourceFile.fileName, version: sourceFile.version, signature: undefined }; } function removeExistingFileInfo(existingFileInfo: FileInfo, path: Path) { registerChangedFile(path, existingFileInfo.fileName); - emitHandler.removeScriptInfo(path); + emitHandler.onRemoveSourceFile(path); } function updateExistingFileInfo(program: Program, existingInfo: FileInfo, sourceFile: SourceFile, hasInvalidatedResolution: HasInvalidatedResolution) { if (existingInfo.version !== sourceFile.version) { registerChangedFile(sourceFile.path, sourceFile.fileName); existingInfo.version = sourceFile.version; - emitHandler.updateScriptInfo(program, sourceFile); + emitHandler.onUpdateSourceFile(program, sourceFile); } else if (hasInvalidatedResolution(sourceFile.path) && - emitHandler.updaterScriptInfoWithSameVersion(program, sourceFile)) { + emitHandler.onUpdateSourceFileWithSameVersion(program, sourceFile)) { registerChangedFile(sourceFile.path, sourceFile.fileName); } } @@ -388,10 +401,10 @@ namespace ts { function getNonModuleEmitHandler(): EmitHandler { return { - addScriptInfo: noop, - removeScriptInfo: noop, - updateScriptInfo: noop, - updaterScriptInfoWithSameVersion: returnFalse, + onAddSourceFile: noop, + onRemoveSourceFile: noop, + onUpdateSourceFile: noop, + onUpdateSourceFileWithSameVersion: returnFalse, getFilesAffectedByUpdatedShape }; @@ -409,17 +422,17 @@ namespace ts { function getModuleEmitHandler(): EmitHandler { const references = createMap>(); return { - addScriptInfo: setReferences, - removeScriptInfo, - updateScriptInfo: updateReferences, - updaterScriptInfoWithSameVersion: updateReferencesTrackingChangedReferences, + onAddSourceFile: setReferences, + onRemoveSourceFile, + onUpdateSourceFile: updateReferences, + onUpdateSourceFileWithSameVersion: updateReferencesTrackingChangedReferences, getFilesAffectedByUpdatedShape }; function setReferences(program: Program, sourceFile: SourceFile) { const newReferences = getReferencedFiles(program, sourceFile); if (newReferences) { - references.set(sourceFile.path, getReferencedFiles(program, sourceFile)); + references.set(sourceFile.path, newReferences); } } @@ -452,7 +465,7 @@ namespace ts { !!oldReferences.size; } - function removeScriptInfo(removedFilePath: Path) { + function onRemoveSourceFile(removedFilePath: Path) { // Remove existing references references.forEach((referencesInFile, filePath) => { if (referencesInFile.has(removedFilePath)) { diff --git a/src/compiler/commandLineParser.ts b/src/compiler/commandLineParser.ts index d49d993bce151..fae45709a3c8d 100644 --- a/src/compiler/commandLineParser.ts +++ b/src/compiler/commandLineParser.ts @@ -1683,7 +1683,7 @@ namespace ts { return undefined; } let extendedConfigPath = toPath(extendedConfig, basePath, getCanonicalFileName); - if (!host.fileExists(extendedConfigPath) && !endsWith(extendedConfigPath, ".json")) { + if (!host.fileExists(extendedConfigPath) && !endsWith(extendedConfigPath, Extension.Json)) { extendedConfigPath = `${extendedConfigPath}.json` as Path; if (!host.fileExists(extendedConfigPath)) { errors.push(createDiagnostic(Diagnostics.File_0_does_not_exist, extendedConfig)); diff --git a/src/compiler/core.ts b/src/compiler/core.ts index 9579dec51cd72..8d5883b7755b9 100644 --- a/src/compiler/core.ts +++ b/src/compiler/core.ts @@ -2262,7 +2262,7 @@ namespace ts { return ScriptKind.TS; case Extension.Tsx: return ScriptKind.TSX; - case ".json": + case Extension.Json: return ScriptKind.JSON; default: return ScriptKind.Unknown; @@ -2669,7 +2669,7 @@ namespace ts { export function assertTypeIsNever(_: never): void { } export interface CachedDirectoryStructureHost extends DirectoryStructureHost { - addOrDeleteFileOrFolder(fileOrFolder: string, fileOrFolderPath: Path): void; + addOrDeleteFileOrDirectory(fileOrDirectory: string, fileOrDirectoryPath: Path): void; addOrDeleteFile(fileName: string, filePath: Path, eventKind: FileWatcherEventKind): void; clearCache(): void; } @@ -2695,7 +2695,7 @@ namespace ts { getCurrentDirectory, getDirectories, readDirectory, - addOrDeleteFileOrFolder, + addOrDeleteFileOrDirectory, addOrDeleteFile, clearCache, exit: code => host.exit(code) @@ -2824,23 +2824,23 @@ namespace ts { } } - function addOrDeleteFileOrFolder(fileOrFolder: string, fileOrFolderPath: Path) { - const existingResult = getCachedFileSystemEntries(fileOrFolderPath); + function addOrDeleteFileOrDirectory(fileOrDirectory: string, fileOrDirectoryPath: Path) { + const existingResult = getCachedFileSystemEntries(fileOrDirectoryPath); if (existingResult) { // This was a folder already present, remove it if this doesnt exist any more - if (!host.directoryExists(fileOrFolder)) { - cachedReadDirectoryResult.delete(fileOrFolderPath); + if (!host.directoryExists(fileOrDirectory)) { + cachedReadDirectoryResult.delete(fileOrDirectoryPath); } } else { // This was earlier a file (hence not in cached directory contents) // or we never cached the directory containing it - const parentResult = getCachedFileSystemEntriesForBaseDir(fileOrFolderPath); + const parentResult = getCachedFileSystemEntriesForBaseDir(fileOrDirectoryPath); if (parentResult) { - const baseName = getBaseNameOfFileName(fileOrFolder); + const baseName = getBaseNameOfFileName(fileOrDirectory); if (parentResult) { - updateFilesOfFileSystemEntry(parentResult, baseName, host.fileExists(fileOrFolderPath)); - updateFileSystemEntry(parentResult.directories, baseName, host.directoryExists(fileOrFolderPath)); + updateFilesOfFileSystemEntry(parentResult, baseName, host.fileExists(fileOrDirectoryPath)); + updateFileSystemEntry(parentResult.directories, baseName, host.directoryExists(fileOrDirectoryPath)); } } } diff --git a/src/compiler/program.ts b/src/compiler/program.ts index 01ec2e62b2ec9..674d0b6cfacab 100644 --- a/src/compiler/program.ts +++ b/src/compiler/program.ts @@ -395,6 +395,9 @@ namespace ts { allDiagnostics?: Diagnostic[]; } + /** + * Determines if program structure is upto date or needs to be recreated + */ export function isProgramUptoDate( program: Program | undefined, rootFileNames: string[], @@ -402,10 +405,10 @@ namespace ts { getSourceVersion: (path: Path) => string, fileExists: (fileName: string) => boolean, hasInvalidatedResolution: HasInvalidatedResolution, - hasChangedAutomaticTypeDirectiveNames: () => boolean, + hasChangedAutomaticTypeDirectiveNames: boolean, ): boolean { // If we haven't create a program yet or has changed automatic type directives, then it is not up-to-date - if (!program || hasChangedAutomaticTypeDirectiveNames()) { + if (!program || hasChangedAutomaticTypeDirectiveNames) { return false; } @@ -416,7 +419,7 @@ namespace ts { // If any file is not up-to-date, then the whole program is not up-to-date if (program.getSourceFiles().some(sourceFileNotUptoDate)) { - return false; + return false; } // If any of the missing file paths are now created @@ -464,78 +467,6 @@ namespace ts { ); } - /** - * Updates the existing missing file watches with the new set of missing files after new program is created - */ - export function updateMissingFilePathsWatch( - program: Program, - missingFileWatches: Map, - createMissingFileWatch: (missingFilePath: Path) => FileWatcher, - ) { - const missingFilePaths = program.getMissingFilePaths(); - const newMissingFilePathMap = arrayToSet(missingFilePaths); - // Update the missing file paths watcher - mutateMap( - missingFileWatches, - newMissingFilePathMap, - { - // Watch the missing files - createNewValue: createMissingFileWatch, - // Files that are no longer missing (e.g. because they are no longer required) - // should no longer be watched. - onDeleteValue: closeFileWatcher - } - ); - } - - export interface WildcardDirectoryWatcher { - watcher: FileWatcher; - flags: WatchDirectoryFlags; - } - - /** - * Updates the existing wild card directory watches with the new set of wild card directories from the config file - * after new program is created because the config file was reloaded or program was created first time from the config file - * Note that there is no need to call this function when the program is updated with additional files without reloading config files, - * as wildcard directories wont change unless reloading config file - */ - export function updateWatchingWildcardDirectories( - existingWatchedForWildcards: Map, - wildcardDirectories: Map, - watchDirectory: (directory: string, flags: WatchDirectoryFlags) => FileWatcher - ) { - mutateMap( - existingWatchedForWildcards, - wildcardDirectories, - { - // Create new watch and recursive info - createNewValue: createWildcardDirectoryWatcher, - // Close existing watch thats not needed any more - onDeleteValue: closeFileWatcherOf, - // Close existing watch that doesnt match in the flags - onExistingValue: updateWildcardDirectoryWatcher - } - ); - - function createWildcardDirectoryWatcher(directory: string, flags: WatchDirectoryFlags): WildcardDirectoryWatcher { - // Create new watch and recursive info - return { - watcher: watchDirectory(directory, flags), - flags - }; - } - - function updateWildcardDirectoryWatcher(existingWatcher: WildcardDirectoryWatcher, flags: WatchDirectoryFlags, directory: string) { - // Watcher needs to be updated if the recursive flags dont match - if (existingWatcher.flags === flags) { - return; - } - - existingWatcher.watcher.close(); - existingWatchedForWildcards.set(directory, createWildcardDirectoryWatcher(directory, flags)); - } - } - /** * Create a new 'Program' instance. A Program is an immutable collection of 'SourceFile's and a 'CompilerOptions' * that represent a compilation unit. @@ -599,7 +530,6 @@ namespace ts { let moduleResolutionCache: ModuleResolutionCache; let resolveModuleNamesWorker: (moduleNames: string[], containingFile: string, reusedNames?: string[]) => ResolvedModuleFull[]; const hasInvalidatedResolution = host.hasInvalidatedResolution || returnFalse; - const hasChangedAutomaticTypeDirectiveNames = host.hasChangedAutomaticTypeDirectiveNames && host.hasChangedAutomaticTypeDirectiveNames.bind(host) || returnFalse; if (host.resolveModuleNames) { resolveModuleNamesWorker = (moduleNames, containingFile, reusedNames) => host.resolveModuleNames(checkAllDefined(moduleNames), containingFile, reusedNames).map(resolved => { // An older host may have omitted extension, in which case we should infer it from the file extension of resolvedFileName. @@ -1105,7 +1035,7 @@ namespace ts { return oldProgram.structureIsReused; } - if (hasChangedAutomaticTypeDirectiveNames()) { + if (host.hasChangedAutomaticTypeDirectiveNames) { return oldProgram.structureIsReused = StructureIsReused.SafeModules; } diff --git a/src/compiler/resolutionCache.ts b/src/compiler/resolutionCache.ts index 814d5053341a2..aecc698989134 100644 --- a/src/compiler/resolutionCache.ts +++ b/src/compiler/resolutionCache.ts @@ -1,5 +1,6 @@ /// /// +/// /*@internal*/ namespace ts { @@ -18,8 +19,6 @@ namespace ts { startCachingPerDirectoryResolution(): void; finishCachingPerDirectoryResolution(): void; - setRootDirectory(dir: string): void; - updateTypeRootsWatch(): void; closeTypeRootsWatch(): void; @@ -35,6 +34,12 @@ namespace ts { resolvedFileName: string | undefined; } + interface ResolvedModuleWithFailedLookupLocations extends ts.ResolvedModuleWithFailedLookupLocations, ResolutionWithFailedLookupLocations { + } + + interface ResolvedTypeReferenceDirectiveWithFailedLookupLocations extends ts.ResolvedTypeReferenceDirectiveWithFailedLookupLocations, ResolutionWithFailedLookupLocations { + } + export interface ResolutionCacheHost extends ModuleResolutionHost { toPath(fileName: string): Path; getCompilationSettings(): CompilerOptions; @@ -67,7 +72,7 @@ namespace ts { (resolution: T): R; } - export function createResolutionCache(resolutionHost: ResolutionCacheHost): ResolutionCache { + export function createResolutionCache(resolutionHost: ResolutionCacheHost, rootDirForResolution: string): ResolutionCache { let filesWithChangedSetOfUnresolvedImports: Path[] | undefined; let filesWithInvalidatedResolutions: Map | undefined; let allFilesHaveInvalidatedResolution = false; @@ -83,12 +88,18 @@ namespace ts { const getCurrentDirectory = memoize(() => resolutionHost.getCurrentDirectory()); + /** + * These are the extensions that failed lookup files will have by default, + * any other extension of failed lookup will be store that path in custom failed lookup path + * This helps in not having to comb through all resolutions when files are added/removed + * Note that .d.ts file also has .d.ts extension hence will be part of default extensions + */ const failedLookupDefaultExtensions = [Extension.Ts, Extension.Tsx, Extension.Js, Extension.Jsx, Extension.Json]; const customFailedLookupPaths = createMap(); const directoryWatchesOfFailedLookups = createMap(); - let rootDir: string; - let rootPath: Path; + const rootDir = rootDirForResolution && removeTrailingDirectorySeparator(getNormalizedAbsolutePath(rootDirForResolution, getCurrentDirectory())); + const rootPath = rootDir && resolutionHost.toPath(rootDir); // TypeRoot watches for the types that get added as part of getAutomaticTypeDirectiveNames const typeRootsWatches = createMap(); @@ -103,7 +114,6 @@ namespace ts { removeResolutionsOfFile, invalidateResolutionOfFile, createHasInvalidatedResolution, - setRootDirectory, updateTypeRootsWatch, closeTypeRootsWatch, clear @@ -117,12 +127,6 @@ namespace ts { return resolution.resolvedTypeReferenceDirective; } - function setRootDirectory(dir: string) { - Debug.assert(!resolvedModuleNames.size && !resolvedTypeReferenceDirectives.size && !directoryWatchesOfFailedLookups.size); - rootDir = removeTrailingDirectorySeparator(getNormalizedAbsolutePath(dir, getCurrentDirectory())); - rootPath = resolutionHost.toPath(rootDir); - } - function isInDirectoryPath(dir: Path, file: Path) { if (dir === undefined || file.length <= dir.length) { return false; @@ -238,9 +242,17 @@ namespace ts { perDirectoryResolution.set(name, resolution); } resolutionsInFile.set(name, resolution); - const diffIndex = existingResolution && existingResolution.failedLookupLocations && resolution.failedLookupLocations && findDiffIndex(resolution.failedLookupLocations, existingResolution.failedLookupLocations); - watchFailedLookupLocationOfResolution(resolution, diffIndex || 0); - stopWatchFailedLookupLocationOfResolutionFrom(existingResolution, diffIndex || 0); + if (resolution.failedLookupLocations) { + if (existingResolution && existingResolution.failedLookupLocations) { + watchAndStopWatchDiffFailedLookupLocations(resolution, existingResolution); + } + else { + watchFailedLookupLocationOfResolution(resolution, 0); + } + } + else if (existingResolution) { + stopWatchFailedLookupLocationOfResolution(existingResolution); + } if (logChanges && filesWithChangedSetOfUnresolvedImports && !resolutionIsEqualTo(existingResolution, resolution)) { filesWithChangedSetOfUnresolvedImports.push(path); // reset log changes to avoid recording the same file multiple times @@ -344,68 +356,91 @@ namespace ts { return fileExtensionIsOneOf(path, failedLookupDefaultExtensions); } - function watchFailedLookupLocationOfResolution(resolution: ResolutionWithFailedLookupLocations, startIndex?: number) { - if (resolution && resolution.failedLookupLocations) { - for (let i = startIndex || 0; i < resolution.failedLookupLocations.length; i++) { - const failedLookupLocation = resolution.failedLookupLocations[i]; - const failedLookupLocationPath = resolutionHost.toPath(failedLookupLocation); - // If the failed lookup location path is not one of the supported extensions, - // store it in the custom path - if (!isPathWithDefaultFailedLookupExtension(failedLookupLocationPath)) { - const refCount = customFailedLookupPaths.get(failedLookupLocationPath) || 0; - customFailedLookupPaths.set(failedLookupLocationPath, refCount + 1); - } - const { dir, dirPath } = getDirectoryToWatchFailedLookupLocation(failedLookupLocation, failedLookupLocationPath); - const dirWatcher = directoryWatchesOfFailedLookups.get(dirPath); - if (dirWatcher) { - dirWatcher.refCount++; - } - else { - directoryWatchesOfFailedLookups.set(dirPath, { watcher: createDirectoryWatcher(dir, dirPath), refCount: 1 }); - } + function watchAndStopWatchDiffFailedLookupLocations(resolution: ResolutionWithFailedLookupLocations, existingResolution: ResolutionWithFailedLookupLocations) { + const failedLookupLocations = resolution.failedLookupLocations; + const existingFailedLookupLocations = existingResolution.failedLookupLocations; + for (let index = 0; index < failedLookupLocations.length; index++) { + if (index === existingFailedLookupLocations.length) { + // Additional failed lookup locations, watch from this index + watchFailedLookupLocationOfResolution(resolution, index); + return; + } + else if (failedLookupLocations[index] !== existingFailedLookupLocations[index]) { + // Different failed lookup locations, + // Watch new resolution failed lookup locations from this index and + // stop watching existing resolutions from this index + watchFailedLookupLocationOfResolution(resolution, index); + stopWatchFailedLookupLocationOfResolutionFrom(existingResolution, index); + return; + } + } + + // All new failed lookup locations are already watched (and are same), + // Stop watching failed lookup locations of existing resolution after failed lookup locations length + stopWatchFailedLookupLocationOfResolutionFrom(existingResolution, failedLookupLocations.length); + } + + function watchFailedLookupLocationOfResolution({ failedLookupLocations }: ResolutionWithFailedLookupLocations, startIndex: number) { + for (let i = startIndex; i < failedLookupLocations.length; i++) { + const failedLookupLocation = failedLookupLocations[i]; + const failedLookupLocationPath = resolutionHost.toPath(failedLookupLocation); + // If the failed lookup location path is not one of the supported extensions, + // store it in the custom path + if (!isPathWithDefaultFailedLookupExtension(failedLookupLocationPath)) { + const refCount = customFailedLookupPaths.get(failedLookupLocationPath) || 0; + customFailedLookupPaths.set(failedLookupLocationPath, refCount + 1); + } + const { dir, dirPath } = getDirectoryToWatchFailedLookupLocation(failedLookupLocation, failedLookupLocationPath); + const dirWatcher = directoryWatchesOfFailedLookups.get(dirPath); + if (dirWatcher) { + dirWatcher.refCount++; + } + else { + directoryWatchesOfFailedLookups.set(dirPath, { watcher: createDirectoryWatcher(dir, dirPath), refCount: 1 }); } } } function stopWatchFailedLookupLocationOfResolution(resolution: ResolutionWithFailedLookupLocations) { - stopWatchFailedLookupLocationOfResolutionFrom(resolution, 0); + if (resolution.failedLookupLocations) { + stopWatchFailedLookupLocationOfResolutionFrom(resolution, 0); + } } - function stopWatchFailedLookupLocationOfResolutionFrom(resolution: ResolutionWithFailedLookupLocations, startIndex: number) { - if (resolution && resolution.failedLookupLocations) { - for (let i = startIndex; i < resolution.failedLookupLocations.length; i++) { - const failedLookupLocation = resolution.failedLookupLocations[i]; - const failedLookupLocationPath = resolutionHost.toPath(failedLookupLocation); - const refCount = customFailedLookupPaths.get(failedLookupLocationPath); - if (refCount) { - if (refCount === 1) { - customFailedLookupPaths.delete(failedLookupLocationPath); - } - else { - customFailedLookupPaths.set(failedLookupLocationPath, refCount - 1); - } + function stopWatchFailedLookupLocationOfResolutionFrom({ failedLookupLocations }: ResolutionWithFailedLookupLocations, startIndex: number) { + for (let i = startIndex; i < failedLookupLocations.length; i++) { + const failedLookupLocation = failedLookupLocations[i]; + const failedLookupLocationPath = resolutionHost.toPath(failedLookupLocation); + const refCount = customFailedLookupPaths.get(failedLookupLocationPath); + if (refCount) { + if (refCount === 1) { + customFailedLookupPaths.delete(failedLookupLocationPath); + } + else { + Debug.assert(refCount > 1); + customFailedLookupPaths.set(failedLookupLocationPath, refCount - 1); } - const { dirPath } = getDirectoryToWatchFailedLookupLocation(failedLookupLocation, failedLookupLocationPath); - const dirWatcher = directoryWatchesOfFailedLookups.get(dirPath); - // Do not close the watcher yet since it might be needed by other failed lookup locations. - dirWatcher.refCount--; } + const { dirPath } = getDirectoryToWatchFailedLookupLocation(failedLookupLocation, failedLookupLocationPath); + const dirWatcher = directoryWatchesOfFailedLookups.get(dirPath); + // Do not close the watcher yet since it might be needed by other failed lookup locations. + dirWatcher.refCount--; } } function createDirectoryWatcher(directory: string, dirPath: Path) { - return resolutionHost.watchDirectoryOfFailedLookupLocation(directory, fileOrFolder => { - const fileOrFolderPath = resolutionHost.toPath(fileOrFolder); + return resolutionHost.watchDirectoryOfFailedLookupLocation(directory, fileOrDirectory => { + const fileOrDirectoryPath = resolutionHost.toPath(fileOrDirectory); if (resolutionHost.getCachedDirectoryStructureHost) { // Since the file existance changed, update the sourceFiles cache - resolutionHost.getCachedDirectoryStructureHost().addOrDeleteFileOrFolder(fileOrFolder, fileOrFolderPath); + resolutionHost.getCachedDirectoryStructureHost().addOrDeleteFileOrDirectory(fileOrDirectory, fileOrDirectoryPath); } // If the files are added to project root or node_modules directory, always run through the invalidation process // Otherwise run through invalidation only if adding to the immediate directory if (!allFilesHaveInvalidatedResolution && - dirPath === rootPath || isNodeModulesDirectory(dirPath) || getDirectoryPath(fileOrFolderPath) === dirPath) { - if (invalidateResolutionOfFailedLookupLocation(fileOrFolderPath, dirPath === fileOrFolderPath)) { + dirPath === rootPath || isNodeModulesDirectory(dirPath) || getDirectoryPath(fileOrDirectoryPath) === dirPath) { + if (invalidateResolutionOfFailedLookupLocation(fileOrDirectoryPath, dirPath === fileOrDirectoryPath)) { resolutionHost.onInvalidatedResolution(); } } @@ -482,21 +517,21 @@ namespace ts { ); } - function invalidateResolutionOfFailedLookupLocation(fileOrFolderPath: Path, isCreatingWatchedDirectory: boolean) { + function invalidateResolutionOfFailedLookupLocation(fileOrDirectoryPath: Path, isCreatingWatchedDirectory: boolean) { let isChangedFailedLookupLocation: (location: string) => boolean; if (isCreatingWatchedDirectory) { // Watching directory is created // Invalidate any resolution has failed lookup in this directory - isChangedFailedLookupLocation = location => isInDirectoryPath(fileOrFolderPath, resolutionHost.toPath(location)); + isChangedFailedLookupLocation = location => isInDirectoryPath(fileOrDirectoryPath, resolutionHost.toPath(location)); } else { - // Some file or folder in the watching directory is created + // Some file or directory in the watching directory is created // Return early if it does not have any of the watching extension or not the custom failed lookup path - if (!isPathWithDefaultFailedLookupExtension(fileOrFolderPath) && !customFailedLookupPaths.has(fileOrFolderPath)) { + if (!isPathWithDefaultFailedLookupExtension(fileOrDirectoryPath) && !customFailedLookupPaths.has(fileOrDirectoryPath)) { return false; } - // Resolution need to be invalidated if failed lookup location is same as the file or folder getting created - isChangedFailedLookupLocation = location => resolutionHost.toPath(location) === fileOrFolderPath; + // Resolution need to be invalidated if failed lookup location is same as the file or directory getting created + isChangedFailedLookupLocation = location => resolutionHost.toPath(location) === fileOrDirectoryPath; } const hasChangedFailedLookupLocation = (resolution: ResolutionWithFailedLookupLocations) => some(resolution.failedLookupLocations, isChangedFailedLookupLocation); const invalidatedFilesCount = filesWithInvalidatedResolutions && filesWithInvalidatedResolutions.size; @@ -513,11 +548,11 @@ namespace ts { function createTypeRootsWatch(_typeRootPath: string, typeRoot: string): FileWatcher { // Create new watch and recursive info - return resolutionHost.watchTypeRootsDirectory(typeRoot, fileOrFolder => { - const fileOrFolderPath = resolutionHost.toPath(fileOrFolder); + return resolutionHost.watchTypeRootsDirectory(typeRoot, fileOrDirectory => { + const fileOrDirectoryPath = resolutionHost.toPath(fileOrDirectory); if (resolutionHost.getCachedDirectoryStructureHost) { // Since the file existance changed, update the sourceFiles cache - resolutionHost.getCachedDirectoryStructureHost().addOrDeleteFileOrFolder(fileOrFolder, fileOrFolderPath); + resolutionHost.getCachedDirectoryStructureHost().addOrDeleteFileOrDirectory(fileOrDirectory, fileOrDirectoryPath); } // For now just recompile diff --git a/src/compiler/sys.ts b/src/compiler/sys.ts index 822de1eef52f0..fa3ec22f586cd 100644 --- a/src/compiler/sys.ts +++ b/src/compiler/sys.ts @@ -78,8 +78,7 @@ namespace ts { close(): void; } - export interface DirectoryWatcher extends FileWatcher { - directoryName: string; + interface DirectoryWatcher extends FileWatcher { referenceCount: number; } @@ -187,7 +186,7 @@ namespace ts { fileWatcherCallbacks.remove(filePath, callback); } - function fileEventHandler(eventName: string, relativeFileName: string, baseDirPath: string) { + function fileEventHandler(eventName: string, relativeFileName: string | undefined, baseDirPath: string) { // When files are deleted from disk, the triggered "rename" event would have a relativefileName of "undefined" const fileName = !isString(relativeFileName) ? undefined @@ -255,19 +254,25 @@ namespace ts { } function fsWatchDirectory(directoryName: string, callback: (eventName: string, relativeFileName: string) => void, recursive?: boolean): FileWatcher { - // Node 4.0 `fs.watch` function supports the "recursive" option on both OSX and Windows - // (ref: https://github.com/nodejs/node/pull/2649 and https://github.com/Microsoft/TypeScript/issues/4643) let options: any; + /** Watcher for the directory depending on whether it is missing or present */ let watcher = !directoryExists(directoryName) ? watchMissingDirectory() : watchPresentDirectory(); return { close: () => { + // Close the watcher (either existing directory watcher or missing directory watcher) watcher.close(); } }; + /** + * Watch the directory that is currently present + * and when the watched directory is deleted, switch to missing directory watcher + */ function watchPresentDirectory(): FileWatcher { + // Node 4.0 `fs.watch` function supports the "recursive" option on both OSX and Windows + // (ref: https://github.com/nodejs/node/pull/2649 and https://github.com/Microsoft/TypeScript/issues/4643) if (options === undefined) { if (isNode4OrLater && (process.platform === "win32" || process.platform === "darwin")) { options = { persistent: true, recursive: !!recursive }; @@ -283,14 +288,20 @@ namespace ts { callback ); dirWatcher.on("error", () => { - // Deleting file - watcher = watchMissingDirectory(); - // Call the callback for current directory - callback("rename", ""); + if (!directoryExists(directoryName)) { + // Deleting directory + watcher = watchMissingDirectory(); + // Call the callback for current directory + callback("rename", ""); + } }); return dirWatcher; } + /** + * Watch the directory that is missing + * and switch to existing directory when the directory is created + */ function watchMissingDirectory(): FileWatcher { return fsWatchFile(directoryName, (_fileName, eventKind) => { if (eventKind === FileWatcherEventKind.Created && directoryExists(directoryName)) { diff --git a/src/compiler/tsconfig.json b/src/compiler/tsconfig.json index dee33cbf16301..07f69ddfe2830 100644 --- a/src/compiler/tsconfig.json +++ b/src/compiler/tsconfig.json @@ -36,6 +36,7 @@ "sourcemap.ts", "declarationEmitter.ts", "emitter.ts", + "watchUtilities.ts", "program.ts", "builder.ts", "resolutionCache.ts", diff --git a/src/compiler/types.ts b/src/compiler/types.ts index 2d955da1252b7..e62981866b367 100644 --- a/src/compiler/types.ts +++ b/src/compiler/types.ts @@ -4099,8 +4099,6 @@ namespace ts { readonly resolvedModule: ResolvedModuleFull | undefined; /* @internal */ readonly failedLookupLocations: ReadonlyArray; - /*@internal*/ - isInvalidated?: boolean; } export interface ResolvedTypeReferenceDirective { @@ -4114,8 +4112,6 @@ namespace ts { export interface ResolvedTypeReferenceDirectiveWithFailedLookupLocations { readonly resolvedTypeReferenceDirective: ResolvedTypeReferenceDirective; readonly failedLookupLocations: ReadonlyArray; - /*@internal*/ - isInvalidated?: boolean; } export interface HasInvalidatedResolution { @@ -4150,7 +4146,7 @@ namespace ts { getEnvironmentVariable?(name: string): string; onReleaseOldSourceFile?(oldSourceFile: SourceFile, oldOptions: CompilerOptions): void; hasInvalidatedResolution?: HasInvalidatedResolution; - hasChangedAutomaticTypeDirectiveNames?(): boolean; + hasChangedAutomaticTypeDirectiveNames?: boolean; } /* @internal */ diff --git a/src/compiler/utilities.ts b/src/compiler/utilities.ts index 87bc601bd7e5a..fcd390cded6d5 100644 --- a/src/compiler/utilities.ts +++ b/src/compiler/utilities.ts @@ -3521,7 +3521,7 @@ namespace ts { /** * clears already present map by calling onDeleteExistingValue callback before deleting that key/value */ - export function clearMap(map: Map, onDeleteValue: (existingValue: T, key: string) => void) { + export function clearMap(map: Map, onDeleteValue: (valueInMap: T, key: string) => void) { // Remove all map.forEach(onDeleteValue); map.clear(); @@ -3568,76 +3568,6 @@ namespace ts { }); } - /** - * Find the index where arrayA and arrayB differ - */ - export function findDiffIndex(arrayA: ReadonlyArray, arrayB: ReadonlyArray) { - for (let i = 0; i < arrayA.length; i++) { - if (i === arrayB.length || arrayA[i] !== arrayB[i]) { - return i; - } - } - return arrayA.length; - } - - export function addFileWatcher(host: System, file: string, cb: FileWatcherCallback): FileWatcher { - return host.watchFile(file, cb); - } - - export function addFileWatcherWithLogging(host: System, file: string, cb: FileWatcherCallback, log: (s: string) => void): FileWatcher { - const watcherCaption = `FileWatcher:: `; - return createWatcherWithLogging(addFileWatcher, watcherCaption, log, host, file, cb); - } - - export type FilePathWatcherCallback = (fileName: string, eventKind: FileWatcherEventKind, filePath: Path) => void; - export function addFilePathWatcher(host: System, file: string, cb: FilePathWatcherCallback, path: Path): FileWatcher { - return host.watchFile(file, (fileName, eventKind) => cb(fileName, eventKind, path)); - } - - export function addFilePathWatcherWithLogging(host: System, file: string, cb: FilePathWatcherCallback, path: Path, log: (s: string) => void): FileWatcher { - const watcherCaption = `FileWatcher:: `; - return createWatcherWithLogging(addFileWatcher, watcherCaption, log, host, file, cb, path); - } - - export function addDirectoryWatcher(host: System, directory: string, cb: DirectoryWatcherCallback, flags: WatchDirectoryFlags): FileWatcher { - const recursive = (flags & WatchDirectoryFlags.Recursive) !== 0; - return host.watchDirectory(directory, cb, recursive); - } - - export function addDirectoryWatcherWithLogging(host: System, directory: string, cb: DirectoryWatcherCallback, flags: WatchDirectoryFlags, log: (s: string) => void): FileWatcher { - const watcherCaption = `DirectoryWatcher ${(flags & WatchDirectoryFlags.Recursive) !== 0 ? "recursive" : ""}:: `; - return createWatcherWithLogging(addDirectoryWatcher, watcherCaption, log, host, directory, cb, flags); - } - - type WatchCallback = (fileName: string, cbOptional1?: T, optional?: U) => void; - type AddWatch = (host: System, file: string, cb: WatchCallback, optional?: U) => FileWatcher; - function createWatcherWithLogging(addWatch: AddWatch, watcherCaption: string, log: (s: string) => void, host: System, file: string, cb: WatchCallback, optional?: U): FileWatcher { - const info = `PathInfo: ${file}`; - log(`${watcherCaption}Added: ${info}`); - const watcher = addWatch(host, file, (fileName, cbOptional1?) => { - const optionalInfo = cbOptional1 !== undefined ? ` ${cbOptional1}` : ""; - log(`${watcherCaption}Trigger: ${fileName}${optionalInfo} ${info}`); - const start = timestamp(); - cb(fileName, cbOptional1, optional); - const elapsed = timestamp() - start; - log(`${watcherCaption}Elapsed: ${elapsed}ms Trigger: ${fileName}${optionalInfo} ${info}`); - }, optional); - return { - close: () => { - log(`${watcherCaption}Close: ${info}`); - watcher.close(); - } - }; - } - - export function closeFileWatcher(watcher: FileWatcher) { - watcher.close(); - } - - export function closeFileWatcherOf(objWithWatcher: T) { - objWithWatcher.watcher.close(); - } - /** Calls `callback` on `directory` and every ancestor directory it has, returning the first defined result. */ export function forEachAncestorDirectory(directory: string, callback: (directory: string) => T): T { while (true) { diff --git a/src/compiler/watch.ts b/src/compiler/watch.ts index 2d1d6e8bf5880..8b098f4109865 100644 --- a/src/compiler/watch.ts +++ b/src/compiler/watch.ts @@ -245,7 +245,7 @@ namespace ts { const sourceFilesCache = createMap(); // 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 hasChangedCompilerOptions = false; // True if the compiler options have changed between compilations - let changedAutomaticTypeDirectiveNames = false; // True if the automatic type directives have changed + let hasChangedAutomaticTypeDirectiveNames = false; // True if the automatic type directives have changed const loggingEnabled = compilerOptions.diagnostics || compilerOptions.extendedDiagnostics; const writeLog: (s: string) => void = loggingEnabled ? s => system.write(s) : noop; @@ -269,26 +269,25 @@ namespace ts { const compilerHost: CompilerHost & ResolutionCacheHost = { // Members for CompilerHost - getSourceFile: getVersionedSourceFile, + getSourceFile: (fileName, languageVersion, onError?, shouldCreateNewSourceFile?) => getVersionedSourceFileByPath(fileName, toPath(fileName), languageVersion, onError, shouldCreateNewSourceFile), getSourceFileByPath: getVersionedSourceFileByPath, getDefaultLibLocation, getDefaultLibFileName: options => combinePaths(getDefaultLibLocation(), getDefaultLibFileName(options)), - writeFile: (_fileName, _data, _writeByteOrderMark, _onError?, _sourceFiles?) => { }, + writeFile: notImplemented, getCurrentDirectory, useCaseSensitiveFileNames: () => system.useCaseSensitiveFileNames, getCanonicalFileName, getNewLine: () => newLine, fileExists, - readFile, - trace, - directoryExists, + readFile: fileName => system.readFile(fileName), + trace: s => system.write(s + newLine), + directoryExists: directoryName => directoryStructureHost.directoryExists(directoryName), getEnvironmentVariable: name => system.getEnvironmentVariable ? system.getEnvironmentVariable(name) : "", - getDirectories, + getDirectories: path => directoryStructureHost.getDirectories(path), realpath, - resolveTypeReferenceDirectives, - resolveModuleNames, + resolveTypeReferenceDirectives: (typeDirectiveNames, containingFile) => resolutionCache.resolveTypeReferenceDirectives(typeDirectiveNames, containingFile), + resolveModuleNames: (moduleNames, containingFile, reusedNames?) => resolutionCache.resolveModuleNames(moduleNames, containingFile, reusedNames, /*logChanges*/ false), onReleaseOldSourceFile, - hasChangedAutomaticTypeDirectiveNames, // Members for ResolutionCacheHost toPath, getCompilationSettings: () => compilerOptions, @@ -296,12 +295,14 @@ namespace ts { watchTypeRootsDirectory: watchDirectory, getCachedDirectoryStructureHost, onInvalidatedResolution: scheduleProgramUpdate, - onChangedAutomaticTypeDirectiveNames, + onChangedAutomaticTypeDirectiveNames: () => { + hasChangedAutomaticTypeDirectiveNames = true; + scheduleProgramUpdate(); + }, writeLog }; // Cache for the module resolution - const resolutionCache = createResolutionCache(compilerHost); - resolutionCache.setRootDirectory(configFileName ? + const resolutionCache = createResolutionCache(compilerHost, configFileName ? getDirectoryPath(getNormalizedAbsolutePath(configFileName, getCurrentDirectory())) : getCurrentDirectory() ); @@ -337,6 +338,7 @@ namespace ts { // Compile the program resolutionCache.startCachingPerDirectoryResolution(); compilerHost.hasInvalidatedResolution = hasInvalidatedResolution; + compilerHost.hasChangedAutomaticTypeDirectiveNames = hasChangedAutomaticTypeDirectiveNames; program = createProgram(rootFileNames, compilerOptions, compilerHost, program); resolutionCache.finishCachingPerDirectoryResolution(); builder.onProgramUpdateGraph(program, hasInvalidatedResolution); @@ -379,38 +381,10 @@ namespace ts { return directoryStructureHost.fileExists(fileName); } - function directoryExists(directoryName: string) { - return directoryStructureHost.directoryExists(directoryName); - } - - function readFile(fileName: string) { - return system.readFile(fileName); - } - - function trace(s: string) { - return system.write(s + newLine); - } - - function getDirectories(path: string) { - return directoryStructureHost.getDirectories(path); - } - - function resolveModuleNames(moduleNames: string[], containingFile: string, reusedNames?: string[]) { - return resolutionCache.resolveModuleNames(moduleNames, containingFile, reusedNames, /*logChanges*/ false); - } - - function resolveTypeReferenceDirectives(typeDirectiveNames: string[], containingFile: string) { - return resolutionCache.resolveTypeReferenceDirectives(typeDirectiveNames, containingFile); - } - function getDefaultLibLocation(): string { return getDirectoryPath(normalizePath(system.getExecutingFilePath())); } - function getVersionedSourceFile(fileName: string, languageVersion: ScriptTarget, onError?: (message: string) => void, shouldCreateNewSourceFile?: boolean): SourceFile { - return getVersionedSourceFileByPath(fileName, toPath(fileName), languageVersion, onError, shouldCreateNewSourceFile); - } - function getVersionedSourceFileByPath(fileName: string, path: Path, languageVersion: ScriptTarget, onError?: (message: string) => void, shouldCreateNewSourceFile?: boolean): SourceFile { const hostSourceFile = sourceFilesCache.get(path); // No source file on the host @@ -594,15 +568,6 @@ namespace ts { return watchDirectoryWorker(system, directory, cb, flags, writeLog); } - function onChangedAutomaticTypeDirectiveNames() { - changedAutomaticTypeDirectiveNames = true; - scheduleProgramUpdate(); - } - - function hasChangedAutomaticTypeDirectiveNames() { - return changedAutomaticTypeDirectiveNames; - } - function watchMissingFilePath(missingFilePath: Path) { return watchFilePath(system, missingFilePath, onMissingFileChange, missingFilePath, writeLog); } @@ -633,19 +598,19 @@ namespace ts { function watchWildcardDirectory(directory: string, flags: WatchDirectoryFlags) { return watchDirectory( directory, - fileOrFolder => { + fileOrDirectory => { Debug.assert(!!configFileName); - const fileOrFolderPath = toPath(fileOrFolder); + const fileOrDirectoryPath = toPath(fileOrDirectory); // Since the file existance changed, update the sourceFiles cache - (directoryStructureHost as CachedDirectoryStructureHost).addOrDeleteFileOrFolder(fileOrFolder, fileOrFolderPath); - removeSourceFile(fileOrFolderPath); + (directoryStructureHost as CachedDirectoryStructureHost).addOrDeleteFileOrDirectory(fileOrDirectory, fileOrDirectoryPath); + removeSourceFile(fileOrDirectoryPath); - // If the the added or created file or folder is not supported file name, ignore the file + // If the the added or created file or directory is not supported file name, ignore the file // But when watched directory is added/removed, we need to reload the file list - if (fileOrFolderPath !== directory && !isSupportedSourceFileName(fileOrFolder, compilerOptions)) { - writeLog(`Project: ${configFileName} Detected file add/remove of non supported extension: ${fileOrFolder}`); + if (fileOrDirectoryPath !== directory && !isSupportedSourceFileName(fileOrDirectory, compilerOptions)) { + writeLog(`Project: ${configFileName} Detected file add/remove of non supported extension: ${fileOrDirectory}`); return; } diff --git a/src/compiler/watchUtilities.ts b/src/compiler/watchUtilities.ts new file mode 100644 index 0000000000000..de4152939545e --- /dev/null +++ b/src/compiler/watchUtilities.ts @@ -0,0 +1,136 @@ +/// + +namespace ts { + /** + * Updates the existing missing file watches with the new set of missing files after new program is created + */ + export function updateMissingFilePathsWatch( + program: Program, + missingFileWatches: Map, + createMissingFileWatch: (missingFilePath: Path) => FileWatcher, + ) { + const missingFilePaths = program.getMissingFilePaths(); + const newMissingFilePathMap = arrayToSet(missingFilePaths); + // Update the missing file paths watcher + mutateMap( + missingFileWatches, + newMissingFilePathMap, + { + // Watch the missing files + createNewValue: createMissingFileWatch, + // Files that are no longer missing (e.g. because they are no longer required) + // should no longer be watched. + onDeleteValue: closeFileWatcher + } + ); + } + + export interface WildcardDirectoryWatcher { + watcher: FileWatcher; + flags: WatchDirectoryFlags; + } + + /** + * Updates the existing wild card directory watches with the new set of wild card directories from the config file + * after new program is created because the config file was reloaded or program was created first time from the config file + * Note that there is no need to call this function when the program is updated with additional files without reloading config files, + * as wildcard directories wont change unless reloading config file + */ + export function updateWatchingWildcardDirectories( + existingWatchedForWildcards: Map, + wildcardDirectories: Map, + watchDirectory: (directory: string, flags: WatchDirectoryFlags) => FileWatcher + ) { + mutateMap( + existingWatchedForWildcards, + wildcardDirectories, + { + // Create new watch and recursive info + createNewValue: createWildcardDirectoryWatcher, + // Close existing watch thats not needed any more + onDeleteValue: closeFileWatcherOf, + // Close existing watch that doesnt match in the flags + onExistingValue: updateWildcardDirectoryWatcher + } + ); + + function createWildcardDirectoryWatcher(directory: string, flags: WatchDirectoryFlags): WildcardDirectoryWatcher { + // Create new watch and recursive info + return { + watcher: watchDirectory(directory, flags), + flags + }; + } + + function updateWildcardDirectoryWatcher(existingWatcher: WildcardDirectoryWatcher, flags: WatchDirectoryFlags, directory: string) { + // Watcher needs to be updated if the recursive flags dont match + if (existingWatcher.flags === flags) { + return; + } + + existingWatcher.watcher.close(); + existingWatchedForWildcards.set(directory, createWildcardDirectoryWatcher(directory, flags)); + } + } +} + +/* @internal */ +namespace ts { + export function addFileWatcher(host: System, file: string, cb: FileWatcherCallback): FileWatcher { + return host.watchFile(file, cb); + } + + export function addFileWatcherWithLogging(host: System, file: string, cb: FileWatcherCallback, log: (s: string) => void): FileWatcher { + const watcherCaption = `FileWatcher:: `; + return createWatcherWithLogging(addFileWatcher, watcherCaption, log, host, file, cb); + } + + export type FilePathWatcherCallback = (fileName: string, eventKind: FileWatcherEventKind, filePath: Path) => void; + export function addFilePathWatcher(host: System, file: string, cb: FilePathWatcherCallback, path: Path): FileWatcher { + return host.watchFile(file, (fileName, eventKind) => cb(fileName, eventKind, path)); + } + + export function addFilePathWatcherWithLogging(host: System, file: string, cb: FilePathWatcherCallback, path: Path, log: (s: string) => void): FileWatcher { + const watcherCaption = `FileWatcher:: `; + return createWatcherWithLogging(addFileWatcher, watcherCaption, log, host, file, cb, path); + } + + export function addDirectoryWatcher(host: System, directory: string, cb: DirectoryWatcherCallback, flags: WatchDirectoryFlags): FileWatcher { + const recursive = (flags & WatchDirectoryFlags.Recursive) !== 0; + return host.watchDirectory(directory, cb, recursive); + } + + export function addDirectoryWatcherWithLogging(host: System, directory: string, cb: DirectoryWatcherCallback, flags: WatchDirectoryFlags, log: (s: string) => void): FileWatcher { + const watcherCaption = `DirectoryWatcher ${(flags & WatchDirectoryFlags.Recursive) !== 0 ? "recursive" : ""}:: `; + return createWatcherWithLogging(addDirectoryWatcher, watcherCaption, log, host, directory, cb, flags); + } + + type WatchCallback = (fileName: string, cbOptional1?: T, optional?: U) => void; + type AddWatch = (host: System, file: string, cb: WatchCallback, optional?: U) => FileWatcher; + function createWatcherWithLogging(addWatch: AddWatch, watcherCaption: string, log: (s: string) => void, host: System, file: string, cb: WatchCallback, optional?: U): FileWatcher { + const info = `PathInfo: ${file}`; + log(`${watcherCaption}Added: ${info}`); + const watcher = addWatch(host, file, (fileName, cbOptional1?) => { + const optionalInfo = cbOptional1 !== undefined ? ` ${cbOptional1}` : ""; + log(`${watcherCaption}Trigger: ${fileName}${optionalInfo} ${info}`); + const start = timestamp(); + cb(fileName, cbOptional1, optional); + const elapsed = timestamp() - start; + log(`${watcherCaption}Elapsed: ${elapsed}ms Trigger: ${fileName}${optionalInfo} ${info}`); + }, optional); + return { + close: () => { + log(`${watcherCaption}Close: ${info}`); + watcher.close(); + } + }; + } + + export function closeFileWatcher(watcher: FileWatcher) { + watcher.close(); + } + + export function closeFileWatcherOf(objWithWatcher: T) { + objWithWatcher.watcher.close(); + } +} diff --git a/src/harness/unittests/reuseProgramStructure.ts b/src/harness/unittests/reuseProgramStructure.ts index 53a26c2ae9558..5c2b9edd9d0ba 100644 --- a/src/harness/unittests/reuseProgramStructure.ts +++ b/src/harness/unittests/reuseProgramStructure.ts @@ -885,7 +885,7 @@ namespace ts { program, newRootFileNames, newOptions, path => program.getSourceFileByPath(path).version, /*fileExists*/ returnFalse, /*hasInvalidatedResolution*/ returnFalse, - /*hasChangedAutomaticTypeDirectiveNames*/ returnFalse + /*hasChangedAutomaticTypeDirectiveNames*/ false ); assert.isTrue(actual); } diff --git a/src/harness/unittests/tsserverProjectSystem.ts b/src/harness/unittests/tsserverProjectSystem.ts index cac52d34a7ea1..7e1205f76cf5c 100644 --- a/src/harness/unittests/tsserverProjectSystem.ts +++ b/src/harness/unittests/tsserverProjectSystem.ts @@ -967,7 +967,7 @@ namespace ts.projectSystem { checkProjectActualFiles(projectService.inferredProjects[0], [file2.path, file3.path, libFile.path]); }); - it("should resuse same project if file is opened from the configured project that has no open files", () => { + it("should reuse same project if file is opened from the configured project that has no open files", () => { const file1 = { path: "/a/b/main.ts", content: "let x =1;" @@ -4639,7 +4639,7 @@ namespace ts.projectSystem { verifyProjectAndWatchedDirectories(); callsTrackingHost.verifyNoHostCalls(); - function getFilePathIfOpen(f: FileOrFolder) { + function getFilePathIfNotOpen(f: FileOrFolder) { const path = toCanonical(f.path); const info = projectService.getScriptInfoForPath(toCanonical(f.path)); return info && info.isScriptOpen() ? undefined : path; @@ -4647,7 +4647,7 @@ namespace ts.projectSystem { function verifyProjectAndWatchedDirectories() { checkProjectActualFiles(project, map(projectFiles, f => f.path)); - checkWatchedFiles(host, mapDefined(projectFiles, getFilePathIfOpen)); + checkWatchedFiles(host, mapDefined(projectFiles, getFilePathIfNotOpen)); checkWatchedDirectories(host, watchingRecursiveDirectories, /*recursive*/ true); checkWatchedDirectories(host, [], /*recursive*/ false); } diff --git a/src/harness/virtualFileSystemWithWatch.ts b/src/harness/virtualFileSystemWithWatch.ts index e1e0e6f4c7e5a..c16f57235e412 100644 --- a/src/harness/virtualFileSystemWithWatch.ts +++ b/src/harness/virtualFileSystemWithWatch.ts @@ -246,17 +246,17 @@ namespace ts.TestFSWithWatch { const mapNewLeaves = createMap(); const isNewFs = this.fs.size === 0; // always inject safelist file in the list of files - for (const fileOrFolder of fileOrFolderList.concat(this.withSafeList ? safeList : [])) { - const path = this.toFullPath(fileOrFolder.path); + for (const fileOrDirectory of fileOrFolderList.concat(this.withSafeList ? safeList : [])) { + const path = this.toFullPath(fileOrDirectory.path); mapNewLeaves.set(path, true); // If its a change const currentEntry = this.fs.get(path); if (currentEntry) { if (isFile(currentEntry)) { - if (isString(fileOrFolder.content)) { + if (isString(fileOrDirectory.content)) { // Update file - if (currentEntry.content !== fileOrFolder.content) { - currentEntry.content = fileOrFolder.content; + if (currentEntry.content !== fileOrDirectory.content) { + currentEntry.content = fileOrDirectory.content; this.invokeFileWatcher(currentEntry.fullPath, FileWatcherEventKind.Changed); } } @@ -266,7 +266,7 @@ namespace ts.TestFSWithWatch { } else { // Folder - if (isString(fileOrFolder.content)) { + if (isString(fileOrDirectory.content)) { // TODO: Changing from folder => file } else { @@ -275,32 +275,32 @@ namespace ts.TestFSWithWatch { } } else { - this.ensureFileOrFolder(fileOrFolder); + this.ensureFileOrFolder(fileOrDirectory); } } if (!isNewFs) { - this.fs.forEach((fileOrFolder, path) => { + this.fs.forEach((fileOrDirectory, path) => { // If this entry is not from the new file or folder if (!mapNewLeaves.get(path)) { // Leaf entries that arent in new list => remove these - if (isFile(fileOrFolder) || isFolder(fileOrFolder) && fileOrFolder.entries.length === 0) { - this.removeFileOrFolder(fileOrFolder, folder => !mapNewLeaves.get(folder.path)); + if (isFile(fileOrDirectory) || isFolder(fileOrDirectory) && fileOrDirectory.entries.length === 0) { + this.removeFileOrFolder(fileOrDirectory, folder => !mapNewLeaves.get(folder.path)); } } }); } } - ensureFileOrFolder(fileOrFolder: FileOrFolder) { - if (isString(fileOrFolder.content)) { - const file = this.toFile(fileOrFolder); + ensureFileOrFolder(fileOrDirectory: FileOrFolder) { + if (isString(fileOrDirectory.content)) { + const file = this.toFile(fileOrDirectory); Debug.assert(!this.fs.get(file.path)); const baseFolder = this.ensureFolder(getDirectoryPath(file.fullPath)); this.addFileOrFolderInFolder(baseFolder, file); } else { - const fullPath = getNormalizedAbsolutePath(fileOrFolder.path, this.currentDirectory); + const fullPath = getNormalizedAbsolutePath(fileOrDirectory.path, this.currentDirectory); this.ensureFolder(fullPath); } } @@ -326,44 +326,44 @@ namespace ts.TestFSWithWatch { return folder; } - private addFileOrFolderInFolder(folder: Folder, fileOrFolder: File | Folder) { - folder.entries.push(fileOrFolder); - this.fs.set(fileOrFolder.path, fileOrFolder); + private addFileOrFolderInFolder(folder: Folder, fileOrDirectory: File | Folder) { + folder.entries.push(fileOrDirectory); + this.fs.set(fileOrDirectory.path, fileOrDirectory); - if (isFile(fileOrFolder)) { - this.invokeFileWatcher(fileOrFolder.fullPath, FileWatcherEventKind.Created); + if (isFile(fileOrDirectory)) { + this.invokeFileWatcher(fileOrDirectory.fullPath, FileWatcherEventKind.Created); } - this.invokeDirectoryWatcher(folder.fullPath, fileOrFolder.fullPath); + this.invokeDirectoryWatcher(folder.fullPath, fileOrDirectory.fullPath); } - private removeFileOrFolder(fileOrFolder: File | Folder, isRemovableLeafFolder: (folder: Folder) => boolean) { - const basePath = getDirectoryPath(fileOrFolder.path); + private removeFileOrFolder(fileOrDirectory: File | Folder, isRemovableLeafFolder: (folder: Folder) => boolean) { + const basePath = getDirectoryPath(fileOrDirectory.path); const baseFolder = this.fs.get(basePath) as Folder; - if (basePath !== fileOrFolder.path) { + if (basePath !== fileOrDirectory.path) { Debug.assert(!!baseFolder); - filterMutate(baseFolder.entries, entry => entry !== fileOrFolder); + filterMutate(baseFolder.entries, entry => entry !== fileOrDirectory); } - this.fs.delete(fileOrFolder.path); + this.fs.delete(fileOrDirectory.path); - if (isFile(fileOrFolder)) { - this.invokeFileWatcher(fileOrFolder.fullPath, FileWatcherEventKind.Deleted); + if (isFile(fileOrDirectory)) { + this.invokeFileWatcher(fileOrDirectory.fullPath, FileWatcherEventKind.Deleted); } else { - Debug.assert(fileOrFolder.entries.length === 0); - const relativePath = this.getRelativePathToDirectory(fileOrFolder.fullPath, fileOrFolder.fullPath); + Debug.assert(fileOrDirectory.entries.length === 0); + const relativePath = this.getRelativePathToDirectory(fileOrDirectory.fullPath, fileOrDirectory.fullPath); // Invoke directory and recursive directory watcher for the folder // Here we arent invoking recursive directory watchers for the base folders // since that is something we would want to do for both file as well as folder we are deleting - invokeWatcherCallbacks(this.watchedDirectories.get(fileOrFolder.path), cb => this.directoryCallback(cb, relativePath)); - invokeWatcherCallbacks(this.watchedDirectoriesRecursive.get(fileOrFolder.path), cb => this.directoryCallback(cb, relativePath)); + invokeWatcherCallbacks(this.watchedDirectories.get(fileOrDirectory.path), cb => this.directoryCallback(cb, relativePath)); + invokeWatcherCallbacks(this.watchedDirectoriesRecursive.get(fileOrDirectory.path), cb => this.directoryCallback(cb, relativePath)); } - if (basePath !== fileOrFolder.path) { + if (basePath !== fileOrDirectory.path) { if (baseFolder.entries.length === 0 && isRemovableLeafFolder(baseFolder)) { this.removeFileOrFolder(baseFolder, isRemovableLeafFolder); } else { - this.invokeRecursiveDirectoryWatcher(baseFolder.fullPath, fileOrFolder.fullPath); + this.invokeRecursiveDirectoryWatcher(baseFolder.fullPath, fileOrDirectory.fullPath); } } } @@ -402,13 +402,13 @@ namespace ts.TestFSWithWatch { } } - private toFile(fileOrFolder: FileOrFolder): File { - const fullPath = getNormalizedAbsolutePath(fileOrFolder.path, this.currentDirectory); + private toFile(fileOrDirectory: FileOrFolder): File { + const fullPath = getNormalizedAbsolutePath(fileOrDirectory.path, this.currentDirectory); return { path: this.toPath(fullPath), - content: fileOrFolder.content, + content: fileOrDirectory.content, fullPath, - fileSize: fileOrFolder.fileSize + fileSize: fileOrDirectory.fileSize }; } @@ -477,7 +477,7 @@ namespace ts.TestFSWithWatch { }); } - watchDirectory(directoryName: string, cb: DirectoryWatcherCallback, recursive: boolean): DirectoryWatcher { + watchDirectory(directoryName: string, cb: DirectoryWatcherCallback, recursive: boolean): FileWatcher { const path = this.toFullPath(directoryName); const map = recursive ? this.watchedDirectoriesRecursive : this.watchedDirectories; const callback: TestDirectoryWatcher = { @@ -486,8 +486,6 @@ namespace ts.TestFSWithWatch { }; map.add(path, callback); return { - referenceCount: 0, - directoryName, close: () => map.remove(path, callback) }; } diff --git a/src/server/editorServices.ts b/src/server/editorServices.ts index c93098c32c206..25a285929bef2 100644 --- a/src/server/editorServices.ts +++ b/src/server/editorServices.ts @@ -349,7 +349,7 @@ namespace ts.server { private readonly projectToSizeMap: Map = createMap(); /** * This is a map of config file paths existance that doesnt need query to disk - * - The entry can be present because there is inferred project that needs to watch addition of config file to folder + * - The entry can be present because there is inferred project that needs to watch addition of config file to directory * In this case the exists could be true/false based on config file is present or not * - Or it is present if we have configured project open with config file at that location * In this case the exists property is always true @@ -753,15 +753,15 @@ namespace ts.server { return this.watchDirectory( this.host, directory, - fileOrFolder => { - const fileOrFolderPath = this.toPath(fileOrFolder); - project.getCachedDirectoryStructureHost().addOrDeleteFileOrFolder(fileOrFolder, fileOrFolderPath); + fileOrDirectory => { + const fileOrDirectoryPath = this.toPath(fileOrDirectory); + project.getCachedDirectoryStructureHost().addOrDeleteFileOrDirectory(fileOrDirectory, fileOrDirectoryPath); const configFilename = project.getConfigFilePath(); - // If the the added or created file or folder is not supported file name, ignore the file + // If the the added or created file or directory is not supported file name, ignore the file // But when watched directory is added/removed, we need to reload the file list - if (fileOrFolderPath !== directory && !isSupportedSourceFileName(fileOrFolder, project.getCompilationSettings(), this.hostConfiguration.extraFileExtensions)) { - this.logger.info(`Project: ${configFilename} Detected file add/remove of non supported extension: ${fileOrFolder}`); + if (fileOrDirectoryPath !== directory && !isSupportedSourceFileName(fileOrDirectory, project.getCompilationSettings(), this.hostConfiguration.extraFileExtensions)) { + this.logger.info(`Project: ${configFilename} Detected file add/remove of non supported extension: ${fileOrDirectory}`); return; } @@ -833,7 +833,7 @@ namespace ts.server { switch (project.projectKind) { case ProjectKind.External: unorderedRemoveItem(this.externalProjects, project); - this.projectToSizeMap.delete((project as ExternalProject).externalProjectName); + this.projectToSizeMap.delete(project.getProjectName()); break; case ProjectKind.Configured: this.configuredProjects.delete((project).canonicalConfigFilePath); @@ -852,7 +852,7 @@ namespace ts.server { const project = this.getOrCreateInferredProjectForProjectRootPathIfEnabled(info, projectRootPath) || this.getOrCreateSingleInferredProjectIfEnabled() || - this.createInferredProject(); + this.createInferredProject(getDirectoryPath(info.path)); project.addRoot(info); project.updateGraph(); @@ -1374,7 +1374,7 @@ namespace ts.server { this.addFilesToNonInferredProjectAndUpdateGraph(project, files, externalFilePropertyReader, typeAcquisition); this.externalProjects.push(project); - this.sendProjectTelemetry(project.externalProjectName, project); + this.sendProjectTelemetry(projectFileName, project); return project; } @@ -1461,7 +1461,7 @@ namespace ts.server { this.addFilesToNonInferredProjectAndUpdateGraph(project, filesToAdd, fileNamePropertyReader, projectOptions.typeAcquisition); this.configuredProjects.set(project.canonicalConfigFilePath, project); this.setConfigFileExistenceByNewConfiguredProject(project); - this.sendProjectTelemetry(project.getConfigFilePath(), project, projectOptions); + this.sendProjectTelemetry(configFileName, project, projectOptions); return project; } @@ -1584,7 +1584,7 @@ namespace ts.server { return project; } } - return this.createInferredProject(/*isSingleInferredProject*/ false, projectRootPath); + return this.createInferredProject(projectRootPath, /*isSingleInferredProject*/ false, projectRootPath); } // we don't have an explicit root path, so we should try to find an inferred project @@ -1621,12 +1621,12 @@ namespace ts.server { return this.inferredProjects[0]; } - return this.createInferredProject(/*isSingleInferredProject*/ true); + return this.createInferredProject(/*rootDirectoryForResolution*/ undefined, /*isSingleInferredProject*/ true); } - private createInferredProject(isSingleInferredProject?: boolean, projectRootPath?: string): InferredProject { + private createInferredProject(rootDirectoryForResolution: string | undefined, isSingleInferredProject?: boolean, projectRootPath?: string): InferredProject { const compilerOptions = projectRootPath && this.compilerOptionsForInferredProjectsPerProjectRoot.get(projectRootPath) || this.compilerOptionsForInferredProjects; - const project = new InferredProject(this, this.documentRegistry, compilerOptions, projectRootPath); + const project = new InferredProject(this, this.documentRegistry, compilerOptions, projectRootPath, rootDirectoryForResolution); if (isSingleInferredProject) { this.inferredProjects.unshift(project); } @@ -1669,10 +1669,12 @@ namespace ts.server { } } + /*@internal*/ getOrCreateScriptInfoNotOpenedByClientForNormalizedPath(fileName: NormalizedPath, scriptKind?: ScriptKind, hasMixedContent?: boolean, hostToQueryFileExistsOn?: DirectoryStructureHost) { return this.getOrCreateScriptInfoForNormalizedPath(fileName, /*openedByClient*/ false, /*fileContent*/ undefined, scriptKind, hasMixedContent, hostToQueryFileExistsOn); } + /*@internal*/ getOrCreateScriptInfoOpenedByClientForNormalizedPath(fileName: NormalizedPath, fileContent?: string, scriptKind?: ScriptKind, hasMixedContent?: boolean, hostToQueryFileExistsOn?: DirectoryStructureHost) { return this.getOrCreateScriptInfoForNormalizedPath(fileName, /*openedByClient*/ true, fileContent, scriptKind, hasMixedContent, hostToQueryFileExistsOn); } diff --git a/src/server/project.ts b/src/server/project.ts index 746e6bf3fbe8d..76d4499f68807 100644 --- a/src/server/project.ts +++ b/src/server/project.ts @@ -162,7 +162,8 @@ namespace ts.server { */ private projectStateVersion = 0; - private changedAutomaticTypeDirectiveNames = false; + /*@internal*/ + hasChangedAutomaticTypeDirectiveNames = false; private typingFiles: SortedReadonlyArray; @@ -201,7 +202,8 @@ namespace ts.server { languageServiceEnabled: boolean, private compilerOptions: CompilerOptions, public compileOnSaveEnabled: boolean, - /*@internal*/public directoryStructureHost: DirectoryStructureHost) { + /*@internal*/public directoryStructureHost: DirectoryStructureHost, + rootDirectoryForResolution: string | undefined) { if (!this.compilerOptions) { this.compilerOptions = getDefaultCompilerOptions(); @@ -224,7 +226,7 @@ namespace ts.server { } this.languageService = createLanguageService(this, this.documentRegistry); - this.resolutionCache = createResolutionCache(this); + this.resolutionCache = createResolutionCache(this, rootDirectoryForResolution); if (!languageServiceEnabled) { this.disableLanguageService(); } @@ -244,29 +246,26 @@ namespace ts.server { } getScriptFileNames() { - const result: string[] = []; - if (this.rootFiles) { - this.rootFilesMap.forEach((value, _path) => { - const f: ScriptInfo = isScriptInfo(value) && value; - if (this.languageServiceEnabled || (f && f.isScriptOpen())) { - // if language service is disabled - process only files that are open - result.push(f ? f.fileName : value as NormalizedPath); - } - }); - if (this.typingFiles) { - for (const f of this.typingFiles) { - result.push(f); - } - } + if (!this.rootFiles) { + return ts.emptyArray; } - return result; + + let result: string[] | undefined; + this.rootFilesMap.forEach(value => { + if (this.languageServiceEnabled || (isScriptInfo(value) && value.isScriptOpen())) { + // if language service is disabled - process only files that are open + (result || (result = [])).push(isScriptInfo(value) ? value.fileName : value); + } + }); + + return addRange(result, this.typingFiles) || ts.emptyArray; } - private getScriptInfoLSHost(fileName: string) { + private getOrCreateScriptInfoAndAttachToProject(fileName: string) { const scriptInfo = this.projectService.getOrCreateScriptInfoNotOpenedByClient(fileName, this.directoryStructureHost); if (scriptInfo) { const existingValue = this.rootFilesMap.get(scriptInfo.path); - if (existingValue !== undefined && existingValue !== scriptInfo) { + if (existingValue !== scriptInfo && existingValue !== undefined) { // This was missing path earlier but now the file exists. Update the root this.rootFiles.push(scriptInfo); this.rootFilesMap.set(scriptInfo.path, scriptInfo); @@ -277,17 +276,17 @@ namespace ts.server { } getScriptKind(fileName: string) { - const info = this.getScriptInfoLSHost(fileName); + const info = this.getOrCreateScriptInfoAndAttachToProject(fileName); return info && info.scriptKind; } getScriptVersion(filename: string) { - const info = this.getScriptInfoLSHost(filename); + const info = this.getOrCreateScriptInfoAndAttachToProject(filename); return info && info.getLatestVersion(); } getScriptSnapshot(filename: string): IScriptSnapshot { - const scriptInfo = this.getScriptInfoLSHost(filename); + const scriptInfo = this.getOrCreateScriptInfoAndAttachToProject(filename); if (scriptInfo) { return scriptInfo.getSnapshot(); } @@ -377,15 +376,10 @@ namespace ts.server { /*@internal*/ onChangedAutomaticTypeDirectiveNames() { - this.changedAutomaticTypeDirectiveNames = true; + this.hasChangedAutomaticTypeDirectiveNames = true; this.projectService.delayUpdateProjectGraphAndInferredProjectsRefresh(this); } - /*@internal*/ - hasChangedAutomaticTypeDirectiveNames() { - return this.changedAutomaticTypeDirectiveNames; - } - /*@internal*/ getGlobalCache() { return this.getTypeAcquisition().enable ? this.projectService.typingsInstaller.globalTypingsCacheLocation : undefined; @@ -802,7 +796,7 @@ 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 hasChanges = !oldProgram || (this.program !== oldProgram && !(oldProgram.structureIsReused & StructureIsReused.Completely)); - this.changedAutomaticTypeDirectiveNames = false; + this.hasChangedAutomaticTypeDirectiveNames = false; if (hasChanges) { if (oldProgram) { for (const f of oldProgram.getSourceFiles()) { @@ -1035,7 +1029,12 @@ namespace ts.server { super.setCompilerOptions(newOptions); } - constructor(projectService: ProjectService, documentRegistry: DocumentRegistry, compilerOptions: CompilerOptions, public readonly projectRootPath?: string | undefined) { + constructor( + projectService: ProjectService, + documentRegistry: DocumentRegistry, + compilerOptions: CompilerOptions, + public readonly projectRootPath: string | undefined, + rootDirectoryForResolution: string | undefined) { super(InferredProject.newName(), ProjectKind.Inferred, projectService, @@ -1044,7 +1043,8 @@ namespace ts.server { /*languageServiceEnabled*/ true, compilerOptions, /*compileOnSaveEnabled*/ false, - projectService.host); + projectService.host, + rootDirectoryForResolution); } addRoot(info: ScriptInfo) { @@ -1053,13 +1053,6 @@ namespace ts.server { this.toggleJsInferredProject(/*isJsInferredProject*/ true); } super.addRoot(info); - // For first root set the resolution cache root dir - if (this.getRootFiles.length === 1) { - const rootDirPath = this.getProjectRootPath(); - if (rootDirPath) { - this.resolutionCache.setRootDirectory(rootDirPath); - } - } } removeRoot(info: ScriptInfo) { @@ -1081,11 +1074,9 @@ namespace ts.server { } getProjectRootPath() { - // Single inferred project does not have a project root. - if (this.projectService.useSingleInferredProject) { - return undefined; - } - return this.projectRootPath || getDirectoryPath(this.getRootFiles()[0]); + return this.projectRootPath || + // Single inferred project does not have a project root. + !this.projectService.useSingleInferredProject && getDirectoryPath(this.getRootFiles()[0]); } close() { @@ -1135,10 +1126,18 @@ namespace ts.server { languageServiceEnabled: boolean, public compileOnSaveEnabled: boolean, cachedDirectoryStructureHost: CachedDirectoryStructureHost) { - super(configFileName, ProjectKind.Configured, projectService, documentRegistry, hasExplicitListOfFiles, languageServiceEnabled, compilerOptions, compileOnSaveEnabled, cachedDirectoryStructureHost); + super(configFileName, + ProjectKind.Configured, + projectService, + documentRegistry, + hasExplicitListOfFiles, + languageServiceEnabled, + compilerOptions, + compileOnSaveEnabled, + cachedDirectoryStructureHost, + getDirectoryPath(configFileName)); this.canonicalConfigFilePath = asNormalizedPath(projectService.toCanonicalFileName(configFileName)); this.enablePlugins(); - this.resolutionCache.setRootDirectory(getDirectoryPath(configFileName)); } /** @@ -1370,8 +1369,15 @@ namespace ts.server { languageServiceEnabled: boolean, public compileOnSaveEnabled: boolean, private readonly projectFilePath?: string) { - super(externalProjectName, ProjectKind.External, projectService, documentRegistry, /*hasExplicitListOfFiles*/ true, languageServiceEnabled, compilerOptions, compileOnSaveEnabled, projectService.host); - this.resolutionCache.setRootDirectory(this.getProjectRootPath()); + super(externalProjectName, + ProjectKind.External, + projectService, + documentRegistry, + /*hasExplicitListOfFiles*/ true, + languageServiceEnabled, compilerOptions, + compileOnSaveEnabled, + projectService.host, + getDirectoryPath(projectFilePath || normalizeSlashes(externalProjectName))); } getExcludedFiles() { diff --git a/src/server/scriptInfo.ts b/src/server/scriptInfo.ts index 3971b29fe7ef9..245b27dd58ae0 100644 --- a/src/server/scriptInfo.ts +++ b/src/server/scriptInfo.ts @@ -4,17 +4,36 @@ namespace ts.server { /* @internal */ export class TextStorage { - // Generated only on demand and removes the text if any edits + /** + * Generated only on demand (based on edits, or information requested) + * The property text is set to undefined when edits happen on the cache + */ private svc: ScriptVersionCache | undefined; private svcVersion = 0; - // Store text when there is no svc or svc has no change, on edit, remove the text + /** + * Stores the text when there are no changes to the script version cache + * The script version cache is generated on demand and text is still retained. + * Only on edits to the script version cache, the text will be set to undefined + */ private text: string; + /** + * Line map for the text when there is no script version cache present + */ private lineMap: number[]; private textVersion = 0; + /** + * True if the text is for the file thats open in the editor + */ public isOpen: boolean; + /** + * True if the text present is the text from the file on the disk + */ private ownFileText: boolean; + /** + * True when reloading contents of file from the disk is pending + */ private pendingReloadFromDisk: boolean; constructor(private readonly host: ServerHost, private readonly fileName: NormalizedPath) { diff --git a/src/services/jsTyping.ts b/src/services/jsTyping.ts index c775a995fd666..f1859a90b98fa 100644 --- a/src/services/jsTyping.ts +++ b/src/services/jsTyping.ts @@ -207,7 +207,7 @@ namespace ts.JsTyping { } // depth of 2, so we access `node_modules/foo` but not `node_modules/foo/bar` - const fileNames = host.readDirectory(packagesFolderPath, [".json"], /*excludes*/ undefined, /*includes*/ undefined, /*depth*/ 2); + const fileNames = host.readDirectory(packagesFolderPath, [Extension.Json], /*excludes*/ undefined, /*includes*/ undefined, /*depth*/ 2); if (log) log(`Searching for typing names in ${packagesFolderPath}; all files: ${JSON.stringify(fileNames)}`); const packageNames: string[] = []; for (const fileName of fileNames) { diff --git a/src/services/services.ts b/src/services/services.ts index d91ebbd95ecbc..5057ced0c903d 100644 --- a/src/services/services.ts +++ b/src/services/services.ts @@ -1101,13 +1101,11 @@ namespace ts { } function synchronizeHostData(): void { - const hasChangedAutomaticTypeDirectiveNames = host.hasChangedAutomaticTypeDirectiveNames && host.hasChangedAutomaticTypeDirectiveNames.bind(host) || returnFalse; - // perform fast check if host supports it if (host.getProjectVersion) { const hostProjectVersion = host.getProjectVersion(); if (hostProjectVersion) { - if (lastProjectVersion === hostProjectVersion && !hasChangedAutomaticTypeDirectiveNames()) { + if (lastProjectVersion === hostProjectVersion && !host.hasChangedAutomaticTypeDirectiveNames) { return; } @@ -1129,7 +1127,7 @@ namespace ts { const hasInvalidatedResolution: HasInvalidatedResolution = host.hasInvalidatedResolution || returnFalse; // If the program is already up-to-date, we can reuse it - if (isProgramUptoDate(program, rootFileNames, hostCache.compilationSettings(), path => hostCache.getVersion(path), fileExists, hasInvalidatedResolution, hasChangedAutomaticTypeDirectiveNames)) { + if (isProgramUptoDate(program, rootFileNames, hostCache.compilationSettings(), path => hostCache.getVersion(path), fileExists, hasInvalidatedResolution, host.hasChangedAutomaticTypeDirectiveNames)) { return; } @@ -1170,7 +1168,7 @@ namespace ts { }, onReleaseOldSourceFile, hasInvalidatedResolution, - hasChangedAutomaticTypeDirectiveNames + hasChangedAutomaticTypeDirectiveNames: host.hasChangedAutomaticTypeDirectiveNames }; if (host.trace) { compilerHost.trace = message => host.trace(message); diff --git a/src/services/types.ts b/src/services/types.ts index bd1358b09c124..4c77cd14d9c9a 100644 --- a/src/services/types.ts +++ b/src/services/types.ts @@ -186,7 +186,7 @@ namespace ts { resolveModuleNames?(moduleNames: string[], containingFile: string, reusedNames?: string[]): ResolvedModule[]; resolveTypeReferenceDirectives?(typeDirectiveNames: string[], containingFile: string): ResolvedTypeReferenceDirective[]; hasInvalidatedResolution?: HasInvalidatedResolution; - hasChangedAutomaticTypeDirectiveNames?(): boolean; + hasChangedAutomaticTypeDirectiveNames?: boolean; directoryExists?(directoryName: string): boolean; /* @@ -278,6 +278,7 @@ namespace ts { getApplicableRefactors(fileName: string, positionOrRaneg: number | TextRange): ApplicableRefactorInfo[]; getEditsForRefactor(fileName: string, formatOptions: FormatCodeSettings, positionOrRange: number | TextRange, refactorName: string, actionName: string): RefactorEditInfo | undefined; + getEmitOutput(fileName: string, emitOnlyDtsFiles?: boolean): EmitOutput; getEmitOutput(fileName: string, emitOnlyDtsFiles?: boolean, isDetailed?: boolean): EmitOutput | EmitOutputDetailed; getProgram(): Program; From 835153ba50f08801ea507686721eead349a560a8 Mon Sep 17 00:00:00 2001 From: Sheetal Nandi Date: Mon, 2 Oct 2017 12:22:05 -0700 Subject: [PATCH 107/109] PR feedback --- src/compiler/builder.ts | 2 +- src/compiler/program.ts | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/compiler/builder.ts b/src/compiler/builder.ts index 9436be6e8a183..b2cdad3598a95 100644 --- a/src/compiler/builder.ts +++ b/src/compiler/builder.ts @@ -521,7 +521,7 @@ namespace ts { } // Return array of values that needs emit - return arrayFrom(seenFileNamesMap.values()); + return flatMapIter(seenFileNamesMap.values(), value => value); } } } diff --git a/src/compiler/program.ts b/src/compiler/program.ts index d335ef3df85ba..ee1e2ea28fe7e 100755 --- a/src/compiler/program.ts +++ b/src/compiler/program.ts @@ -764,8 +764,7 @@ namespace ts { for (let i = 0; i < moduleNames.length; i++) { const moduleName = moduleNames[i]; - // If we want to reuse resolutions more aggressively, we can refine this to check for whether the - // text of the corresponding modulenames has changed. + // If the source file is unchanged and doesnt have invalidated resolution, reuse the module resolutions if (file === oldSourceFile && !hasInvalidatedResolution(oldSourceFile.path)) { const oldResolvedModule = oldSourceFile && oldSourceFile.resolvedModules.get(moduleName); if (oldResolvedModule) { From 7f969e8138a468e932b54f15f26fd957a8a3a6f3 Mon Sep 17 00:00:00 2001 From: Sheetal Nandi Date: Mon, 2 Oct 2017 16:14:42 -0700 Subject: [PATCH 108/109] Making APIs as internal so that we can enable them after we have figured out final details --- src/compiler/builder.ts | 29 ++-- src/compiler/program.ts | 1 + src/compiler/types.ts | 2 + src/compiler/watch.ts | 3 +- src/server/builder.ts | 359 ---------------------------------------- src/server/project.ts | 6 +- 6 files changed, 22 insertions(+), 378 deletions(-) delete mode 100644 src/server/builder.ts diff --git a/src/compiler/builder.ts b/src/compiler/builder.ts index b2cdad3598a95..baa88e0d20f86 100644 --- a/src/compiler/builder.ts +++ b/src/compiler/builder.ts @@ -18,18 +18,12 @@ namespace ts { text: string; } - export interface ChangedProgramFiles { - /** Minimal set of list of files that require emit */ - readonly filesToEmit: ReadonlyArray; - /** File paths of source files changed/added/removed or affected by changed files */ - readonly changedFiles: ReadonlyArray; - } - + /* @internal */ export interface Builder { /** - * This is the callback when file infos in the builder are updated + * Call this to feed new program */ - onProgramUpdateGraph(program: Program, hasInvalidatedResolution: HasInvalidatedResolution): void; + updateProgram(newProgram: Program): void; getFilesAffectedBy(program: Program, path: Path): string[]; emitFile(program: Program, path: Path): EmitOutput; @@ -97,6 +91,7 @@ namespace ts { signature: string; } + /* @internal */ export function createBuilder( getCanonicalFileName: (fileName: string) => string, getEmitOutput: (program: Program, sourceFile: SourceFile, emitOnlyDtsFiles: boolean, isDetailed: boolean) => EmitOutput | EmitOutputDetailed, @@ -110,7 +105,7 @@ namespace ts { const changedFileNames = createMap(); let emitHandler: EmitHandler; return { - onProgramUpdateGraph, + updateProgram, getFilesAffectedBy, emitFile, emitChangedFiles, @@ -118,7 +113,7 @@ namespace ts { clear }; - function createProgramGraph(program: Program, hasInvalidatedResolution: HasInvalidatedResolution) { + function createProgramGraph(program: Program) { const currentIsModuleEmit = program.getCompilerOptions().module !== ModuleKind.None; if (isModuleEmit !== currentIsModuleEmit) { isModuleEmit = currentIsModuleEmit; @@ -135,7 +130,7 @@ namespace ts { // Remove existing file info onDeleteValue: removeExistingFileInfo, // We will update in place instead of deleting existing value and adding new one - onExistingValue: (existingInfo, sourceFile) => updateExistingFileInfo(program, existingInfo, sourceFile, hasInvalidatedResolution) + onExistingValue: (existingInfo, sourceFile) => updateExistingFileInfo(program, existingInfo, sourceFile) } ); } @@ -157,13 +152,13 @@ namespace ts { emitHandler.onRemoveSourceFile(path); } - function updateExistingFileInfo(program: Program, existingInfo: FileInfo, sourceFile: SourceFile, hasInvalidatedResolution: HasInvalidatedResolution) { + function updateExistingFileInfo(program: Program, existingInfo: FileInfo, sourceFile: SourceFile) { if (existingInfo.version !== sourceFile.version) { registerChangedFile(sourceFile.path, sourceFile.fileName); existingInfo.version = sourceFile.version; emitHandler.onUpdateSourceFile(program, sourceFile); } - else if (hasInvalidatedResolution(sourceFile.path) && + else if (program.hasInvalidatedResolution(sourceFile.path) && emitHandler.onUpdateSourceFileWithSameVersion(program, sourceFile)) { registerChangedFile(sourceFile.path, sourceFile.fileName); } @@ -171,13 +166,13 @@ namespace ts { function ensureProgramGraph(program: Program) { if (!emitHandler) { - createProgramGraph(program, returnFalse); + createProgramGraph(program); } } - function onProgramUpdateGraph(program: Program, hasInvalidatedResolution: HasInvalidatedResolution) { + function updateProgram(newProgram: Program) { if (emitHandler) { - createProgramGraph(program, hasInvalidatedResolution); + createProgramGraph(newProgram); } } diff --git a/src/compiler/program.ts b/src/compiler/program.ts index ee1e2ea28fe7e..36bb0a737298e 100755 --- a/src/compiler/program.ts +++ b/src/compiler/program.ts @@ -663,6 +663,7 @@ namespace ts { getSourceFileFromReference, sourceFileToPackageName, redirectTargetsSet, + hasInvalidatedResolution }; verifyCompilerOptions(); diff --git a/src/compiler/types.ts b/src/compiler/types.ts index d56aa12c4ad74..f09041a3d2cdb 100644 --- a/src/compiler/types.ts +++ b/src/compiler/types.ts @@ -2525,6 +2525,8 @@ namespace ts { /* @internal */ sourceFileToPackageName: Map; /** Set of all source files that some other source file redirects to. */ /* @internal */ redirectTargetsSet: Map; + /** Returns true when file in the program had invalidated resolution at the time of program creation. */ + hasInvalidatedResolution: HasInvalidatedResolution; } /* @internal */ diff --git a/src/compiler/watch.ts b/src/compiler/watch.ts index 8b098f4109865..2900737b09f8b 100644 --- a/src/compiler/watch.ts +++ b/src/compiler/watch.ts @@ -2,6 +2,7 @@ /// /// +/* @internal */ namespace ts { export type DiagnosticReporter = (diagnostic: Diagnostic) => void; export type ParseConfigFile = (configFileName: string, optionsToExtend: CompilerOptions, system: DirectoryStructureHost, reportDiagnostic: DiagnosticReporter, reportWatchDiagnostic: DiagnosticReporter) => ParsedCommandLine; @@ -341,7 +342,7 @@ namespace ts { compilerHost.hasChangedAutomaticTypeDirectiveNames = hasChangedAutomaticTypeDirectiveNames; program = createProgram(rootFileNames, compilerOptions, compilerHost, program); resolutionCache.finishCachingPerDirectoryResolution(); - builder.onProgramUpdateGraph(program, hasInvalidatedResolution); + builder.updateProgram(program); // Update watches updateMissingFilePathsWatch(program, missingFilesMap || (missingFilesMap = createMap()), watchMissingFilePath); diff --git a/src/server/builder.ts b/src/server/builder.ts deleted file mode 100644 index 5cf65611fb35d..0000000000000 --- a/src/server/builder.ts +++ /dev/null @@ -1,359 +0,0 @@ -/// -/// -/// - -namespace ts.server { - - export function shouldEmitFile(scriptInfo: ScriptInfo) { - return !scriptInfo.hasMixedContent && !scriptInfo.isDynamic; - } - - /** - * An abstract file info that maintains a shape signature. - */ - export class BuilderFileInfo { - - private lastCheckedShapeSignature: string; - - constructor(public readonly scriptInfo: ScriptInfo, public readonly project: Project) { - } - - public isExternalModuleOrHasOnlyAmbientExternalModules() { - const sourceFile = this.getSourceFile(); - return isExternalModule(sourceFile) || this.containsOnlyAmbientModules(sourceFile); - } - - /** - * For script files that contains only ambient external modules, although they are not actually external module files, - * they can only be consumed via importing elements from them. Regular script files cannot consume them. Therefore, - * there are no point to rebuild all script files if these special files have changed. However, if any statement - * in the file is not ambient external module, we treat it as a regular script file. - */ - private containsOnlyAmbientModules(sourceFile: SourceFile) { - for (const statement of sourceFile.statements) { - if (statement.kind !== SyntaxKind.ModuleDeclaration || (statement).name.kind !== SyntaxKind.StringLiteral) { - return false; - } - } - return true; - } - - private computeHash(text: string): string { - return this.project.projectService.host.createHash(text); - } - - private getSourceFile(): SourceFile { - return this.project.getSourceFile(this.scriptInfo.path); - } - - /** - * @return {boolean} indicates if the shape signature has changed since last update. - */ - public updateShapeSignature() { - const sourceFile = this.getSourceFile(); - if (!sourceFile) { - return true; - } - - const lastSignature = this.lastCheckedShapeSignature; - if (sourceFile.isDeclarationFile) { - this.lastCheckedShapeSignature = this.computeHash(sourceFile.text); - } - else { - const emitOutput = this.project.getFileEmitOutput(this.scriptInfo, /*emitOnlyDtsFiles*/ true); - if (emitOutput.outputFiles && emitOutput.outputFiles.length > 0) { - this.lastCheckedShapeSignature = this.computeHash(emitOutput.outputFiles[0].text); - } - } - return !lastSignature || this.lastCheckedShapeSignature !== lastSignature; - } - } - - export interface Builder { - readonly project: Project; - getFilesAffectedBy(scriptInfo: ScriptInfo): string[]; - onProjectUpdateGraph(): void; - emitFile(scriptInfo: ScriptInfo, writeFile: (path: string, data: string, writeByteOrderMark?: boolean) => void): boolean; - clear(): void; - } - - abstract class AbstractBuilder implements Builder { - - /** - * stores set of files from the project. - * NOTE: this field is created on demand and should not be accessed directly. - * Use 'getFileInfos' instead. - */ - private fileInfos_doNotAccessDirectly: Map; - - constructor(public readonly project: Project, private ctor: { new (scriptInfo: ScriptInfo, project: Project): T }) { - } - - private getFileInfos() { - return this.fileInfos_doNotAccessDirectly || (this.fileInfos_doNotAccessDirectly = createMap()); - } - - protected hasFileInfos() { - return !!this.fileInfos_doNotAccessDirectly; - } - - public clear() { - // drop the existing list - it will be re-created as necessary - this.fileInfos_doNotAccessDirectly = undefined; - } - - protected getFileInfo(path: Path): T { - return this.getFileInfos().get(path); - } - - protected getOrCreateFileInfo(path: Path): T { - let fileInfo = this.getFileInfo(path); - if (!fileInfo) { - const scriptInfo = this.project.getScriptInfo(path); - fileInfo = new this.ctor(scriptInfo, this.project); - this.setFileInfo(path, fileInfo); - } - return fileInfo; - } - - protected getFileInfoPaths(): Path[] { - return arrayFrom(this.getFileInfos().keys() as Iterator); - } - - protected setFileInfo(path: Path, info: T) { - this.getFileInfos().set(path, info); - } - - protected removeFileInfo(path: Path) { - this.getFileInfos().delete(path); - } - - protected forEachFileInfo(action: (fileInfo: T) => any) { - this.getFileInfos().forEach(action); - } - - abstract getFilesAffectedBy(scriptInfo: ScriptInfo): string[]; - abstract onProjectUpdateGraph(): void; - protected abstract ensureFileInfoIfInProject(scriptInfo: ScriptInfo): void; - - /** - * @returns {boolean} whether the emit was conducted or not - */ - emitFile(scriptInfo: ScriptInfo, writeFile: (path: string, data: string, writeByteOrderMark?: boolean) => void): boolean { - this.ensureFileInfoIfInProject(scriptInfo); - const fileInfo = this.getFileInfo(scriptInfo.path); - if (!fileInfo) { - return false; - } - - const { emitSkipped, outputFiles } = this.project.getFileEmitOutput(fileInfo.scriptInfo, /*emitOnlyDtsFiles*/ false); - if (!emitSkipped) { - const projectRootPath = this.project.getProjectRootPath(); - for (const outputFile of outputFiles) { - const outputFileAbsoluteFileName = getNormalizedAbsolutePath(outputFile.name, projectRootPath ? projectRootPath : getDirectoryPath(scriptInfo.fileName)); - writeFile(outputFileAbsoluteFileName, outputFile.text, outputFile.writeByteOrderMark); - } - } - return !emitSkipped; - } - } - - class NonModuleBuilder extends AbstractBuilder { - - constructor(public readonly project: Project) { - super(project, BuilderFileInfo); - } - - protected ensureFileInfoIfInProject(scriptInfo: ScriptInfo) { - if (this.project.containsScriptInfo(scriptInfo)) { - this.getOrCreateFileInfo(scriptInfo.path); - } - } - - onProjectUpdateGraph() { - if (this.hasFileInfos()) { - this.forEachFileInfo(fileInfo => { - if (!this.project.containsScriptInfo(fileInfo.scriptInfo)) { - // This file was deleted from this project - this.removeFileInfo(fileInfo.scriptInfo.path); - } - }); - } - } - - /** - * Note: didn't use path as parameter because the returned file names will be directly - * consumed by the API user, which will use it to interact with file systems. Path - * should only be used internally, because the case sensitivity is not trustable. - */ - getFilesAffectedBy(scriptInfo: ScriptInfo): string[] { - const info = this.getOrCreateFileInfo(scriptInfo.path); - const singleFileResult = scriptInfo.hasMixedContent || scriptInfo.isDynamic ? [] : [scriptInfo.fileName]; - if (info.updateShapeSignature()) { - const options = this.project.getCompilerOptions(); - // If `--out` or `--outFile` is specified, any new emit will result in re-emitting the entire project, - // so returning the file itself is good enough. - if (options && (options.out || options.outFile)) { - return singleFileResult; - } - return this.project.getAllEmittableFiles(); - } - return singleFileResult; - } - } - - class ModuleBuilderFileInfo extends BuilderFileInfo { - references = createSortedArray(); - readonly referencedBy = createSortedArray(); - scriptVersionForReferences: string; - - static compareFileInfos(lf: ModuleBuilderFileInfo, rf: ModuleBuilderFileInfo): Comparison { - return compareStrings(lf.scriptInfo.fileName, rf.scriptInfo.fileName); - } - - addReferencedBy(fileInfo: ModuleBuilderFileInfo): void { - insertSorted(this.referencedBy, fileInfo, ModuleBuilderFileInfo.compareFileInfos); - } - - removeReferencedBy(fileInfo: ModuleBuilderFileInfo): void { - removeSorted(this.referencedBy, fileInfo, ModuleBuilderFileInfo.compareFileInfos); - } - - removeFileReferences() { - for (const reference of this.references) { - reference.removeReferencedBy(this); - } - clear(this.references); - } - } - - class ModuleBuilder extends AbstractBuilder { - - constructor(public readonly project: Project) { - super(project, ModuleBuilderFileInfo); - } - - private projectVersionForDependencyGraph: string; - - public clear() { - this.projectVersionForDependencyGraph = undefined; - super.clear(); - } - - private getReferencedFileInfos(fileInfo: ModuleBuilderFileInfo): SortedArray { - if (!fileInfo.isExternalModuleOrHasOnlyAmbientExternalModules()) { - return createSortedArray(); - } - - const referencedFilePaths = this.project.getReferencedFiles(fileInfo.scriptInfo.path); - return toSortedArray(referencedFilePaths.map(f => this.getOrCreateFileInfo(f)), ModuleBuilderFileInfo.compareFileInfos); - } - - protected ensureFileInfoIfInProject(_scriptInfo: ScriptInfo) { - this.ensureProjectDependencyGraphUpToDate(); - } - - onProjectUpdateGraph() { - // Update the graph only if we have computed graph earlier - if (this.hasFileInfos()) { - this.ensureProjectDependencyGraphUpToDate(); - } - } - - private ensureProjectDependencyGraphUpToDate() { - if (!this.projectVersionForDependencyGraph || this.project.getProjectVersion() !== this.projectVersionForDependencyGraph) { - const currentScriptInfos = this.project.getScriptInfos(); - for (const scriptInfo of currentScriptInfos) { - const fileInfo = this.getOrCreateFileInfo(scriptInfo.path); - this.updateFileReferences(fileInfo); - } - this.forEachFileInfo(fileInfo => { - if (!this.project.containsScriptInfo(fileInfo.scriptInfo)) { - // This file was deleted from this project - fileInfo.removeFileReferences(); - this.removeFileInfo(fileInfo.scriptInfo.path); - } - }); - this.projectVersionForDependencyGraph = this.project.getProjectVersion(); - } - } - - private updateFileReferences(fileInfo: ModuleBuilderFileInfo) { - // Only need to update if the content of the file changed. - if (fileInfo.scriptVersionForReferences === fileInfo.scriptInfo.getLatestVersion()) { - return; - } - - const newReferences = this.getReferencedFileInfos(fileInfo); - const oldReferences = fileInfo.references; - enumerateInsertsAndDeletes(newReferences, oldReferences, - /*inserted*/ newReference => newReference.addReferencedBy(fileInfo), - /*deleted*/ oldReference => { - // New reference is greater then current reference. That means - // the current reference doesn't exist anymore after parsing. So delete - // references. - oldReference.removeReferencedBy(fileInfo); - }, - /*compare*/ ModuleBuilderFileInfo.compareFileInfos); - - fileInfo.references = newReferences; - fileInfo.scriptVersionForReferences = fileInfo.scriptInfo.getLatestVersion(); - } - - getFilesAffectedBy(scriptInfo: ScriptInfo): string[] { - this.ensureProjectDependencyGraphUpToDate(); - - const singleFileResult = scriptInfo.hasMixedContent || scriptInfo.isDynamic ? [] : [scriptInfo.fileName]; - const fileInfo = this.getFileInfo(scriptInfo.path); - if (!fileInfo || !fileInfo.updateShapeSignature()) { - return singleFileResult; - } - - if (!fileInfo.isExternalModuleOrHasOnlyAmbientExternalModules()) { - return this.project.getAllEmittableFiles(); - } - - const options = this.project.getCompilerOptions(); - if (options && (options.isolatedModules || options.out || options.outFile)) { - return singleFileResult; - } - - // Now we need to if each file in the referencedBy list has a shape change as well. - // Because if so, its own referencedBy files need to be saved as well to make the - // emitting result consistent with files on disk. - - // Use slice to clone the array to avoid manipulating in place - const queue = fileInfo.referencedBy.slice(0); - const fileNameSet = createMap(); - fileNameSet.set(scriptInfo.fileName, scriptInfo); - while (queue.length > 0) { - const processingFileInfo = queue.pop(); - if (processingFileInfo.updateShapeSignature() && processingFileInfo.referencedBy.length > 0) { - for (const potentialFileInfo of processingFileInfo.referencedBy) { - if (!fileNameSet.has(potentialFileInfo.scriptInfo.fileName)) { - queue.push(potentialFileInfo); - } - } - } - fileNameSet.set(processingFileInfo.scriptInfo.fileName, processingFileInfo.scriptInfo); - } - const result: string[] = []; - fileNameSet.forEach((scriptInfo, fileName) => { - if (shouldEmitFile(scriptInfo)) { - result.push(fileName); - } - }); - return result; - } - } - - export function createBuilder(project: Project): Builder { - const moduleKind = project.getCompilerOptions().module; - switch (moduleKind) { - case ModuleKind.None: - return new NonModuleBuilder(project); - default: - return new ModuleBuilder(project); - } - } -} diff --git a/src/server/project.ts b/src/server/project.ts index f4a83c94449d3..27f0744537ac3 100644 --- a/src/server/project.ts +++ b/src/server/project.ts @@ -193,6 +193,7 @@ namespace ts.server { return result.module; } + /*@internal*/ constructor( /*@internal*/readonly projectName: string, readonly projectKind: ProjectKind, @@ -758,7 +759,7 @@ namespace ts.server { hasChanges = this.updateGraphWorker() || hasChanges; } if (this.builder) { - this.builder.onProgramUpdateGraph(this.program, this.hasInvalidatedResolution); + this.builder.updateProgram(this.program); } } else { @@ -1030,6 +1031,7 @@ namespace ts.server { super.setCompilerOptions(newOptions); } + /*@internal*/ constructor( projectService: ProjectService, documentRegistry: DocumentRegistry, @@ -1119,6 +1121,7 @@ namespace ts.server { private projectErrors: Diagnostic[]; + /*@internal*/ constructor(configFileName: NormalizedPath, projectService: ProjectService, documentRegistry: DocumentRegistry, @@ -1363,6 +1366,7 @@ namespace ts.server { export class ExternalProject extends Project { excludedFiles: ReadonlyArray = []; private typeAcquisition: TypeAcquisition; + /*@internal*/ constructor(public externalProjectName: string, projectService: ProjectService, documentRegistry: DocumentRegistry, From 8ac01d76f531661229e183a3490797f2a09ad4bb Mon Sep 17 00:00:00 2001 From: Sheetal Nandi Date: Mon, 2 Oct 2017 17:16:49 -0700 Subject: [PATCH 109/109] Separate namespace declarations in builder of intenal and exported interfaces and functions --- src/compiler/builder.ts | 55 +++++++++++++++++++++-------------------- 1 file changed, 28 insertions(+), 27 deletions(-) diff --git a/src/compiler/builder.ts b/src/compiler/builder.ts index baa88e0d20f86..38969b85bacd0 100644 --- a/src/compiler/builder.ts +++ b/src/compiler/builder.ts @@ -18,7 +18,34 @@ namespace ts { text: string; } - /* @internal */ + export function getFileEmitOutput(program: Program, sourceFile: SourceFile, emitOnlyDtsFiles: boolean, isDetailed: boolean, + cancellationToken?: CancellationToken, customTransformers?: CustomTransformers): EmitOutput | EmitOutputDetailed { + const outputFiles: OutputFile[] = []; + let emittedSourceFiles: SourceFile[]; + const emitResult = program.emit(sourceFile, writeFile, cancellationToken, emitOnlyDtsFiles, customTransformers); + if (!isDetailed) { + return { outputFiles, emitSkipped: emitResult.emitSkipped }; + } + + return { + outputFiles, + emitSkipped: emitResult.emitSkipped, + diagnostics: emitResult.diagnostics, + sourceMaps: emitResult.sourceMaps, + emittedSourceFiles + }; + + function writeFile(fileName: string, text: string, writeByteOrderMark: boolean, _onError: (message: string) => void, sourceFiles: SourceFile[]) { + outputFiles.push({ name: fileName, writeByteOrderMark, text }); + if (isDetailed) { + emittedSourceFiles = addRange(emittedSourceFiles, sourceFiles); + } + } + } +} + +/* @internal */ +namespace ts { export interface Builder { /** * Call this to feed new program @@ -60,38 +87,12 @@ namespace ts { getFilesAffectedByUpdatedShape(program: Program, sourceFile: SourceFile, singleFileResult: string[]): string[]; } - export function getFileEmitOutput(program: Program, sourceFile: SourceFile, emitOnlyDtsFiles: boolean, isDetailed: boolean, - cancellationToken?: CancellationToken, customTransformers?: CustomTransformers): EmitOutput | EmitOutputDetailed { - const outputFiles: OutputFile[] = []; - let emittedSourceFiles: SourceFile[]; - const emitResult = program.emit(sourceFile, writeFile, cancellationToken, emitOnlyDtsFiles, customTransformers); - if (!isDetailed) { - return { outputFiles, emitSkipped: emitResult.emitSkipped }; - } - - return { - outputFiles, - emitSkipped: emitResult.emitSkipped, - diagnostics: emitResult.diagnostics, - sourceMaps: emitResult.sourceMaps, - emittedSourceFiles - }; - - function writeFile(fileName: string, text: string, writeByteOrderMark: boolean, _onError: (message: string) => void, sourceFiles: SourceFile[]) { - outputFiles.push({ name: fileName, writeByteOrderMark, text }); - if (isDetailed) { - emittedSourceFiles = addRange(emittedSourceFiles, sourceFiles); - } - } - } - interface FileInfo { fileName: string; version: string; signature: string; } - /* @internal */ export function createBuilder( getCanonicalFileName: (fileName: string) => string, getEmitOutput: (program: Program, sourceFile: SourceFile, emitOnlyDtsFiles: boolean, isDetailed: boolean) => EmitOutput | EmitOutputDetailed,