From 412e31b8bc99472c17641bb12f65efd349a93cb8 Mon Sep 17 00:00:00 2001 From: Sheetal Nandi Date: Wed, 11 Oct 2017 17:10:45 -0700 Subject: [PATCH 01/51] Adding test case where opened file included in project is not added to ref count of configured project --- .../unittests/tsserverProjectSystem.ts | 88 +++++++++++++++++++ 1 file changed, 88 insertions(+) diff --git a/src/harness/unittests/tsserverProjectSystem.ts b/src/harness/unittests/tsserverProjectSystem.ts index 236f52db1347a..40eef501e52b1 100644 --- a/src/harness/unittests/tsserverProjectSystem.ts +++ b/src/harness/unittests/tsserverProjectSystem.ts @@ -335,6 +335,10 @@ namespace ts.projectSystem { return countWhere(recursiveWatchedDirs, dir => file.length > dir.length && startsWith(file, dir) && file[dir.length] === directorySeparator); } + function checkOpenFiles(projectService: server.ProjectService, expectedFiles: FileOrFolder[]) { + checkFileNames("Open files", projectService.openFiles.map(info => info.fileName), expectedFiles.map(file => file.path)); + } + /** * Test server cancellation token used to mock host token cancellation requests. * The cancelAfterRequest constructor param specifies how many isCancellationRequested() calls @@ -2109,6 +2113,90 @@ namespace ts.projectSystem { assert.equal(project1.openRefCount, 1, "Open ref count in project1 - 5"); }); + it("Open ref of configured project when open file gets added to the project as part of configured file update", () => { + const file1 = { + path: "/a/b/src/file1.ts", + content: "let x = 1;" + }; + const file2 = { + path: "/a/b/src/file2.ts", + content: "let y = 1;" + }; + const file3 = { + path: "/a/b/file3.ts", + content: "let z = 1;" + }; + const file4 = { + path: "/a/file4.ts", + content: "let z = 1;" + }; + const configFile = { + path: "/a/b/tsconfig.json", + content: JSON.stringify({ files: ["src/file1.ts", "file3.ts"] }) + }; + + const files = [file1, file2, file3, file4]; + const host = createServerHost(files.concat(configFile)); + const projectService = createProjectService(host); + + projectService.openClientFile(file1.path); + projectService.openClientFile(file2.path); + projectService.openClientFile(file3.path); + projectService.openClientFile(file4.path); + + const infos = files.map(file => projectService.getScriptInfoForPath(file.path as Path)); + checkOpenFiles(projectService, files); + checkNumberOfProjects(projectService, { configuredProjects: 1, inferredProjects: 2 }); + const configProject1 = projectService.configuredProjects.get(configFile.path); + assert.equal(configProject1.openRefCount, 2); + checkProjectActualFiles(configProject1, [file1.path, file3.path, configFile.path]); + const inferredProject1 = projectService.inferredProjects[0]; + checkProjectActualFiles(inferredProject1, [file2.path]); + const inferredProject2 = projectService.inferredProjects[1]; + checkProjectActualFiles(inferredProject2, [file4.path]); + + configFile.content = "{}"; + host.reloadFS(files.concat(configFile)); + host.runQueuedTimeoutCallbacks(); + + verifyScriptInfos(); + checkOpenFiles(projectService, files); + verifyConfiguredProjectStateAfterUpdate(3); + checkNumberOfInferredProjects(projectService, 1); + const inferredProject3 = projectService.inferredProjects[0]; + checkProjectActualFiles(inferredProject3, [file4.path]); + assert.strictEqual(inferredProject2, inferredProject3); + + projectService.closeClientFile(file1.path); + projectService.closeClientFile(file2.path); + projectService.closeClientFile(file4.path); + + verifyScriptInfos(); + checkOpenFiles(projectService, [file3]); + verifyConfiguredProjectStateAfterUpdate(1); + checkNumberOfInferredProjects(projectService, 0); + + projectService.openClientFile(file4.path); + //verifyScriptInfos(); + checkOpenFiles(projectService, [file3, file4]); + //verifyConfiguredProjectStateAfterUpdate(1); + checkNumberOfInferredProjects(projectService, 1); + const inferredProject4 = projectService.inferredProjects[0]; + checkProjectActualFiles(inferredProject4, [file4.path]); + + function verifyScriptInfos() { + infos.forEach(info => assert.strictEqual(projectService.getScriptInfoForPath(info.path), info)); + } + + function verifyConfiguredProjectStateAfterUpdate(_openRefCount: number) { + checkNumberOfConfiguredProjects(projectService, 1); + const configProject2 = projectService.configuredProjects.get(configFile.path); + assert.strictEqual(configProject1, configProject2); + checkProjectActualFiles(configProject2, [file1.path, file2.path, file3.path, configFile.path]); + //assert.equal(configProject2.openRefCount, openRefCount); + } + }); + it("language service disabled state is updated in external projects", () => { const f1 = { path: "/a/app.js", From b68a6363480409067e244da35b1372868947e17b Mon Sep 17 00:00:00 2001 From: Sheetal Nandi Date: Thu, 12 Oct 2017 11:48:25 -0700 Subject: [PATCH 02/51] Fix the way configured project's reference is managed so that the open file --- .../unittests/tsserverProjectSystem.ts | 186 +++++++++++++++--- src/server/editorServices.ts | 55 +++--- src/server/project.ts | 48 +++-- .../reference/api/tsserverlibrary.d.ts | 10 +- 4 files changed, 224 insertions(+), 75 deletions(-) diff --git a/src/harness/unittests/tsserverProjectSystem.ts b/src/harness/unittests/tsserverProjectSystem.ts index 40eef501e52b1..dfed47084e07e 100644 --- a/src/harness/unittests/tsserverProjectSystem.ts +++ b/src/harness/unittests/tsserverProjectSystem.ts @@ -1053,16 +1053,19 @@ namespace ts.projectSystem { projectService.openClientFile(file1.path); checkNumberOfConfiguredProjects(projectService, 1); const project = projectService.configuredProjects.get(configFile.path); + assert.isTrue(project.hasOpenRef()); // file1 projectService.closeClientFile(file1.path); checkNumberOfConfiguredProjects(projectService, 1); assert.strictEqual(projectService.configuredProjects.get(configFile.path), project); - assert.equal(project.openRefCount, 0); + assert.isFalse(project.hasOpenRef()); // No open files + assert.isFalse(project.isClosed()); projectService.openClientFile(file2.path); checkNumberOfConfiguredProjects(projectService, 1); assert.strictEqual(projectService.configuredProjects.get(configFile.path), project); - assert.equal(project.openRefCount, 1); + assert.isTrue(project.hasOpenRef()); // file2 + assert.isFalse(project.isClosed()); }); 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", () => { @@ -1084,14 +1087,18 @@ namespace ts.projectSystem { projectService.openClientFile(file1.path); checkNumberOfConfiguredProjects(projectService, 1); const project = projectService.configuredProjects.get(configFile.path); + assert.isTrue(project.hasOpenRef()); // file1 projectService.closeClientFile(file1.path); checkNumberOfConfiguredProjects(projectService, 1); assert.strictEqual(projectService.configuredProjects.get(configFile.path), project); - assert.equal(project.openRefCount, 0); + assert.isFalse(project.hasOpenRef()); // No files + assert.isFalse(project.isClosed()); projectService.openClientFile(libFile.path); checkNumberOfConfiguredProjects(projectService, 0); + assert.isFalse(project.hasOpenRef()); // No files + project closed + assert.isTrue(project.isClosed()); }); it("should not close external project with no open files", () => { @@ -2078,55 +2085,64 @@ namespace ts.projectSystem { projectService.openClientFile(file2.path); checkNumberOfProjects(projectService, { configuredProjects: 1 }); const project1 = projectService.configuredProjects.get(tsconfig1.path); - assert.equal(project1.openRefCount, 1, "Open ref count in project1 - 1"); + assert.isTrue(project1.hasOpenRef(), "Has open ref count in project1 - 1"); // file2 assert.equal(project1.getScriptInfo(file2.path).containingProjects.length, 1, "containing projects count"); + assert.isFalse(project1.isClosed()); projectService.openClientFile(file1.path); checkNumberOfProjects(projectService, { configuredProjects: 2 }); - assert.equal(project1.openRefCount, 2, "Open ref count in project1 - 2"); + assert.isTrue(project1.hasOpenRef(), "Has open ref count in project1 - 2"); // file2 assert.strictEqual(projectService.configuredProjects.get(tsconfig1.path), project1); + assert.isFalse(project1.isClosed()); const project2 = projectService.configuredProjects.get(tsconfig2.path); - assert.equal(project2.openRefCount, 1, "Open ref count in project2 - 2"); + assert.isTrue(project2.hasOpenRef(), "Has open ref count in project2 - 2"); // file1 + assert.isFalse(project2.isClosed()); assert.equal(project1.getScriptInfo(file1.path).containingProjects.length, 2, `${file1.path} containing projects count`); assert.equal(project1.getScriptInfo(file2.path).containingProjects.length, 1, `${file2.path} containing projects count`); projectService.closeClientFile(file2.path); 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.isFalse(project1.hasOpenRef(), "Has open ref count in project1 - 3"); // No files + assert.isTrue(project2.hasOpenRef(), "Has open ref count in project2 - 3"); // file1 assert.strictEqual(projectService.configuredProjects.get(tsconfig1.path), project1); assert.strictEqual(projectService.configuredProjects.get(tsconfig2.path), project2); + assert.isFalse(project1.isClosed()); + assert.isFalse(project2.isClosed()); projectService.closeClientFile(file1.path); 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.isFalse(project1.hasOpenRef(), "Has open ref count in project1 - 4"); // No files + assert.isFalse(project2.hasOpenRef(), "Has open ref count in project2 - 4"); // No files assert.strictEqual(projectService.configuredProjects.get(tsconfig1.path), project1); assert.strictEqual(projectService.configuredProjects.get(tsconfig2.path), project2); + assert.isFalse(project1.isClosed()); + assert.isFalse(project2.isClosed()); 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"); + assert.isTrue(project1.hasOpenRef(), "Has open ref count in project1 - 5"); // file2 + assert.isFalse(project1.isClosed()); + assert.isTrue(project2.isClosed()); }); it("Open ref of configured project when open file gets added to the project as part of configured file update", () => { - const file1 = { + const file1: FileOrFolder = { path: "/a/b/src/file1.ts", content: "let x = 1;" }; - const file2 = { + const file2: FileOrFolder = { path: "/a/b/src/file2.ts", content: "let y = 1;" }; - const file3 = { + const file3: FileOrFolder = { path: "/a/b/file3.ts", content: "let z = 1;" }; - const file4 = { + const file4: FileOrFolder = { path: "/a/file4.ts", content: "let z = 1;" }; @@ -2148,7 +2164,7 @@ namespace ts.projectSystem { checkOpenFiles(projectService, files); checkNumberOfProjects(projectService, { configuredProjects: 1, inferredProjects: 2 }); const configProject1 = projectService.configuredProjects.get(configFile.path); - assert.equal(configProject1.openRefCount, 2); + assert.isTrue(configProject1.hasOpenRef()); // file1 and file3 checkProjectActualFiles(configProject1, [file1.path, file3.path, configFile.path]); const inferredProject1 = projectService.inferredProjects[0]; checkProjectActualFiles(inferredProject1, [file2.path]); @@ -2161,11 +2177,11 @@ namespace ts.projectSystem { verifyScriptInfos(); checkOpenFiles(projectService, files); - verifyConfiguredProjectStateAfterUpdate(3); + verifyConfiguredProjectStateAfterUpdate(/*hasOpenRef*/ true); // file1, file2, file3 checkNumberOfInferredProjects(projectService, 1); const inferredProject3 = projectService.inferredProjects[0]; checkProjectActualFiles(inferredProject3, [file4.path]); - assert.strictEqual(inferredProject2, inferredProject3); + assert.strictEqual(inferredProject3, inferredProject2); projectService.closeClientFile(file1.path); projectService.closeClientFile(file2.path); @@ -2173,30 +2189,122 @@ namespace ts.projectSystem { verifyScriptInfos(); checkOpenFiles(projectService, [file3]); - verifyConfiguredProjectStateAfterUpdate(1); + verifyConfiguredProjectStateAfterUpdate(/*hasOpenRef*/ true); // file3 checkNumberOfInferredProjects(projectService, 0); projectService.openClientFile(file4.path); - //verifyScriptInfos(); + verifyScriptInfos(); checkOpenFiles(projectService, [file3, file4]); - //verifyConfiguredProjectStateAfterUpdate(1); + verifyConfiguredProjectStateAfterUpdate(/*hasOpenRef*/ true); // file3 checkNumberOfInferredProjects(projectService, 1); const inferredProject4 = projectService.inferredProjects[0]; checkProjectActualFiles(inferredProject4, [file4.path]); + projectService.closeClientFile(file3.path); + verifyScriptInfos(); + checkOpenFiles(projectService, [file4]); + verifyConfiguredProjectStateAfterUpdate(/*hasOpenRef*/ false); // No open files + checkNumberOfInferredProjects(projectService, 1); + const inferredProject5 = projectService.inferredProjects[0]; + checkProjectActualFiles(inferredProject4, [file4.path]); + assert.strictEqual(inferredProject5, inferredProject4); + + const file5: FileOrFolder = { + path: "/file5.ts", + content: "let zz = 1;" + }; + host.reloadFS(files.concat(configFile, file5)); + projectService.openClientFile(file5.path); + verifyScriptInfosAreUndefined([file1, file2, file3]); + assert.strictEqual(projectService.getScriptInfoForPath(file4.path as Path), find(infos, info => info.path === file4.path)); + assert.isDefined(projectService.getScriptInfoForPath(file5.path as Path)); + checkOpenFiles(projectService, [file4, file5]); + checkNumberOfConfiguredProjects(projectService, 0); + function verifyScriptInfos() { infos.forEach(info => assert.strictEqual(projectService.getScriptInfoForPath(info.path), info)); } - function verifyConfiguredProjectStateAfterUpdate(_openRefCount: number) { + function verifyScriptInfosAreUndefined(files: FileOrFolder[]) { + for (const file of files) { + assert.isUndefined(projectService.getScriptInfoForPath(file.path as Path)); + } + } + + function verifyConfiguredProjectStateAfterUpdate(hasOpenRef: boolean) { checkNumberOfConfiguredProjects(projectService, 1); const configProject2 = projectService.configuredProjects.get(configFile.path); - assert.strictEqual(configProject1, configProject2); + assert.strictEqual(configProject2, configProject1); checkProjectActualFiles(configProject2, [file1.path, file2.path, file3.path, configFile.path]); - //assert.equal(configProject2.openRefCount, openRefCount); + assert.equal(configProject2.hasOpenRef(), hasOpenRef); } }); + it("Open ref of configured project when open file gets added to the project as part of configured file update buts its open file references are all closed when the update happens", () => { + const file1: FileOrFolder = { + path: "/a/b/src/file1.ts", + content: "let x = 1;" + }; + const file2: FileOrFolder = { + path: "/a/b/src/file2.ts", + content: "let y = 1;" + }; + const file3: FileOrFolder = { + path: "/a/b/file3.ts", + content: "let z = 1;" + }; + const file4: FileOrFolder = { + path: "/a/file4.ts", + content: "let z = 1;" + }; + const configFile = { + path: "/a/b/tsconfig.json", + content: JSON.stringify({ files: ["src/file1.ts", "file3.ts"] }) + }; + + const files = [file1, file2, file3]; + const hostFiles = files.concat(file4, configFile); + const host = createServerHost(hostFiles); + const projectService = createProjectService(host); + + projectService.openClientFile(file1.path); + projectService.openClientFile(file2.path); + projectService.openClientFile(file3.path); + + checkNumberOfProjects(projectService, { configuredProjects: 1, inferredProjects: 1 }); + const configuredProject = projectService.configuredProjects.get(configFile.path); + assert.isTrue(configuredProject.hasOpenRef()); // file1 and file3 + checkProjectActualFiles(configuredProject, [file1.path, file3.path, configFile.path]); + const inferredProject1 = projectService.inferredProjects[0]; + checkProjectActualFiles(inferredProject1, [file2.path]); + + projectService.closeClientFile(file1.path); + projectService.closeClientFile(file3.path); + assert.isFalse(configuredProject.hasOpenRef()); // No files + + configFile.content = "{}"; + host.reloadFS(files.concat(configFile)); + // Time out is not yet run so there is project update pending + assert.isTrue(configuredProject.hasOpenRef()); // Pending update and file2 might get into the project + + projectService.openClientFile(file4.path); + + checkNumberOfProjects(projectService, { configuredProjects: 1, inferredProjects: 2 }); + assert.strictEqual(projectService.configuredProjects.get(configFile.path), configuredProject); + assert.isTrue(configuredProject.hasOpenRef()); // Pending update and F2 might get into the project + assert.strictEqual(projectService.inferredProjects[0], inferredProject1); + const inferredProject2 = projectService.inferredProjects[1]; + checkProjectActualFiles(inferredProject2, [file4.path]); + + host.runQueuedTimeoutCallbacks(); + checkNumberOfProjects(projectService, { configuredProjects: 1, inferredProjects: 1 }); + assert.strictEqual(projectService.configuredProjects.get(configFile.path), configuredProject); + assert.isTrue(configuredProject.hasOpenRef()); // file2 + checkProjectActualFiles(configuredProject, [file1.path, file2.path, file3.path, configFile.path]); + assert.strictEqual(projectService.inferredProjects[0], inferredProject2); + checkProjectActualFiles(inferredProject2, [file4.path]); + }); + it("language service disabled state is updated in external projects", () => { const f1 = { path: "/a/app.js", @@ -2265,18 +2373,36 @@ namespace ts.projectSystem { projectService.openClientFile(f1.path); projectService.checkNumberOfProjects({ configuredProjects: 1 }); const project = projectService.configuredProjects.get(config.path); + assert.isTrue(project.hasOpenRef()); // f1 + assert.isFalse(project.isClosed()); projectService.closeClientFile(f1.path); projectService.checkNumberOfProjects({ configuredProjects: 1 }); assert.strictEqual(projectService.configuredProjects.get(config.path), project); - assert.equal(project.openRefCount, 0); + assert.isFalse(project.hasOpenRef()); // No files + assert.isFalse(project.isClosed()); for (const f of [f1, f2, f3]) { - // There shouldnt be any script info as we closed the file that resulted in creation of it + // All the script infos should be present and contain the project since it is still alive. const scriptInfo = projectService.getScriptInfoForNormalizedPath(server.toNormalizedPath(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}'`); } + + const f4 = { + path: "/aa.js", + content: "var x = 1" + }; + host.reloadFS([f1, f2, f3, config, f4]); + projectService.openClientFile(f4.path); + projectService.checkNumberOfProjects({ inferredProjects: 1 }); + assert.isFalse(project.hasOpenRef()); // No files + assert.isTrue(project.isClosed()); + + for (const f of [f1, f2, f3]) { + // All the script infos should not be present since the project is closed and orphan script infos are collected + assert.isUndefined(projectService.getScriptInfoForNormalizedPath(server.toNormalizedPath(f.path))); + } }); it("language service disabled events are triggered", () => { @@ -2910,17 +3036,19 @@ namespace ts.projectSystem { projectService.openClientFile(f.path); projectService.checkNumberOfProjects({ configuredProjects: 1 }); const project = projectService.configuredProjects.get(config.path); - assert.equal(project.openRefCount, 1); + assert.isTrue(project.hasOpenRef()); // f projectService.closeClientFile(f.path); projectService.checkNumberOfProjects({ configuredProjects: 1 }); assert.strictEqual(projectService.configuredProjects.get(config.path), project); - assert.equal(project.openRefCount, 0); + assert.isFalse(project.hasOpenRef()); // No files + assert.isFalse(project.isClosed()); projectService.openClientFile(f.path); projectService.checkNumberOfProjects({ configuredProjects: 1 }); assert.strictEqual(projectService.configuredProjects.get(config.path), project); - assert.equal(project.openRefCount, 1); + assert.isTrue(project.hasOpenRef()); // f + assert.isFalse(project.isClosed()); }); }); diff --git a/src/server/editorServices.ts b/src/server/editorServices.ts index 70598313a7365..a017ac5e45e6d 100644 --- a/src/server/editorServices.ts +++ b/src/server/editorServices.ts @@ -554,6 +554,11 @@ namespace ts.server { }); } + /*@internal*/ + hasPendingProjectUpdate(project: Project) { + return this.pendingProjectUpdates.has(project.getProjectName()); + } + private sendProjectsUpdatedInBackgroundEvent() { if (!this.eventHandler) { return; @@ -795,8 +800,14 @@ namespace ts.server { ); } + /** Gets the config file existence info for the configured project */ + /*@internal*/ + getConfigFileExistenceInfo(project: ConfiguredProject) { + return this.configFileExistenceInfoCache.get(project.canonicalConfigFilePath); + } + private onConfigChangedForConfiguredProject(project: ConfiguredProject, eventKind: FileWatcherEventKind) { - const configFileExistenceInfo = this.configFileExistenceInfoCache.get(project.canonicalConfigFilePath); + const configFileExistenceInfo = this.getConfigFileExistenceInfo(project); 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 @@ -898,18 +909,6 @@ namespace ts.server { return project; } - private addToListOfOpenFiles(info: ScriptInfo) { - 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) { - ((p)).addOpenRef(); - } - } - - this.openFiles.push(info); - } - /** * Remove this file from the set of open, non-configured files. * @param info The file that has been closed or newly configured @@ -932,10 +931,8 @@ namespace ts.server { if (info.hasMixedContent) { info.registerFileUpdate(); } - // Delete the reference to the open configured projects but - // do not remove the project so that we can reuse this project + // 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 @@ -1025,7 +1022,7 @@ namespace ts.server { } private setConfigFileExistenceByNewConfiguredProject(project: ConfiguredProject) { - const configFileExistenceInfo = this.configFileExistenceInfoCache.get(project.canonicalConfigFilePath); + const configFileExistenceInfo = this.getConfigFileExistenceInfo(project); if (configFileExistenceInfo) { Debug.assert(configFileExistenceInfo.exists); // close existing watcher @@ -1054,7 +1051,7 @@ namespace ts.server { } private setConfigFileExistenceInfoByClosedConfiguredProject(closedProject: ConfiguredProject) { - const configFileExistenceInfo = this.configFileExistenceInfoCache.get(closedProject.canonicalConfigFilePath); + const configFileExistenceInfo = this.getConfigFileExistenceInfo(closedProject); Debug.assert(!!configFileExistenceInfo); if (configFileExistenceInfo.openFilesImpactedByConfigFile.size) { const configFileName = closedProject.getConfigFilePath(); @@ -1943,7 +1940,8 @@ namespace ts.server { this.assignOrphanScriptInfoToInferredProject(info, projectRootPath); } - this.addToListOfOpenFiles(info); + Debug.assert(!info.isOrphan()); + this.openFiles.push(info); if (sendConfigFileDiagEvent) { configFileErrors = project.getAllProjectErrors(); @@ -2043,11 +2041,14 @@ namespace ts.server { } } - private closeConfiguredProject(configFile: NormalizedPath): boolean { + private closeConfiguredProjectReferencedFromExternalProject(configFile: NormalizedPath): boolean { const configuredProject = this.findConfiguredProjectByProjectName(configFile); - if (configuredProject && configuredProject.deleteOpenRef() === 0) { - this.removeProject(configuredProject); - return true; + if (configuredProject) { + configuredProject.deleteExternalProjectReference(); + if (!configuredProject.hasOpenRef()) { + this.removeProject(configuredProject); + return true; + } } return false; } @@ -2058,7 +2059,7 @@ namespace ts.server { if (configFiles) { let shouldRefreshInferredProjects = false; for (const configFile of configFiles) { - if (this.closeConfiguredProject(configFile)) { + if (this.closeConfiguredProjectReferencedFromExternalProject(configFile)) { shouldRefreshInferredProjects = true; } } @@ -2253,7 +2254,7 @@ namespace ts.server { const newConfig = tsConfigFiles[iNew]; const oldConfig = oldConfigFiles[iOld]; if (oldConfig < newConfig) { - this.closeConfiguredProject(oldConfig); + this.closeConfiguredProjectReferencedFromExternalProject(oldConfig); iOld++; } else if (oldConfig > newConfig) { @@ -2268,7 +2269,7 @@ namespace ts.server { } for (let i = iOld; i < oldConfigFiles.length; i++) { // projects for all remaining old config files should be closed - this.closeConfiguredProject(oldConfigFiles[i]); + this.closeConfiguredProjectReferencedFromExternalProject(oldConfigFiles[i]); } } } @@ -2283,7 +2284,7 @@ namespace ts.server { } 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 - project.addOpenRef(); + project.addExternalProjectReference(); } } } diff --git a/src/server/project.ts b/src/server/project.ts index 7653fbf93cfa4..395da1bd6436d 100644 --- a/src/server/project.ts +++ b/src/server/project.ts @@ -892,9 +892,7 @@ namespace ts.server { } getScriptInfoForNormalizedPath(fileName: NormalizedPath) { - const scriptInfo = this.projectService.getOrCreateScriptInfoNotOpenedByClientForNormalizedPath( - fileName, /*scriptKind*/ undefined, /*hasMixedContent*/ undefined, this.directoryStructureHost - ); + const scriptInfo = this.projectService.getScriptInfoForNormalizedPath(fileName); if (scriptInfo && !scriptInfo.isAttached(this)) { return Errors.ThrowProjectDoesNotContainDocument(fileName, this); } @@ -902,7 +900,7 @@ namespace ts.server { } getScriptInfo(uncheckedFileName: string) { - return this.getScriptInfoForNormalizedPath(toNormalizedPath(uncheckedFileName)); + return this.projectService.getScriptInfo(uncheckedFileName); } filesToString(writeProjectFileNames: boolean) { @@ -1130,8 +1128,8 @@ namespace ts.server { private plugins: PluginModule[] = []; - /** Used for configured projects which may have multiple open roots */ - openRefCount = 0; + /** Ref count to the project when opened from external project */ + private externalProjectRefCount = 0; private projectErrors: Diagnostic[]; @@ -1342,17 +1340,43 @@ namespace ts.server { super.close(); } - addOpenRef() { - this.openRefCount++; + /* @internal */ + addExternalProjectReference() { + this.externalProjectRefCount++; } - deleteOpenRef() { - this.openRefCount--; - return this.openRefCount; + /* @internal */ + deleteExternalProjectReference() { + this.externalProjectRefCount--; } + /** Returns true if the project is needed by any of the open script info/external project */ + /* @internal */ hasOpenRef() { - return !!this.openRefCount; + if (!!this.externalProjectRefCount) { + return true; + } + + // Closed project doesnt have any reference + if (this.isClosed()) { + return false; + } + + const configFileExistenceInfo = this.projectService.getConfigFileExistenceInfo(this); + if (this.projectService.hasPendingProjectUpdate(this)) { + // If there is pending update for this project, + // we dont know if this project would be needed by any of the open files impacted by this config file + // In that case keep the project alive if there are open files impacted by this project + return !!configFileExistenceInfo.openFilesImpactedByConfigFile.size; + } + + // If there is no pending update for this project, + // We know exact set of open files that get impacted by this configured project as the files in the project + // The project is referenced only if open files impacted by this project are present in this project + return forEachEntry( + configFileExistenceInfo.openFilesImpactedByConfigFile, + (_value, infoPath) => this.containsScriptInfo(this.projectService.getScriptInfoForPath(infoPath as Path)) + ) || false; } getEffectiveTypeRoots() { diff --git a/tests/baselines/reference/api/tsserverlibrary.d.ts b/tests/baselines/reference/api/tsserverlibrary.d.ts index 151c948602de4..204c718fabdb0 100644 --- a/tests/baselines/reference/api/tsserverlibrary.d.ts +++ b/tests/baselines/reference/api/tsserverlibrary.d.ts @@ -7211,8 +7211,8 @@ declare namespace ts.server { private directoriesWatchedForWildcards; readonly canonicalConfigFilePath: NormalizedPath; private plugins; - /** Used for configured projects which may have multiple open roots */ - openRefCount: number; + /** Ref count to the project when opened from external project */ + private externalProjectRefCount; private projectErrors; /** * If the project has reload from disk pending, it reloads (and then updates graph as part of that) instead of just updating the graph @@ -7236,9 +7236,6 @@ declare namespace ts.server { getTypeAcquisition(): TypeAcquisition; getExternalFiles(): SortedReadonlyArray; close(): void; - addOpenRef(): void; - deleteOpenRef(): number; - hasOpenRef(): boolean; getEffectiveTypeRoots(): string[]; } /** @@ -7468,7 +7465,6 @@ declare namespace ts.server { */ private onConfigFileChangeForOpenScriptInfo(configFileName, eventKind); private removeProject(project); - private addToListOfOpenFiles(info); /** * Remove this file from the set of open, non-configured files. * @param info The file that has been closed or newly configured @@ -7576,7 +7572,7 @@ declare namespace ts.server { */ closeClientFile(uncheckedFileName: string): void; private collectChanges(lastKnownProjectVersions, currentProjects, result); - private closeConfiguredProject(configFile); + private closeConfiguredProjectReferencedFromExternalProject(configFile); closeExternalProject(uncheckedFileName: string, suppressRefresh?: boolean): void; openExternalProjects(projects: protocol.ExternalProject[]): void; /** Makes a filename safe to insert in a RegExp */ From 50628e73c57185b59e12fecde15ce1654fa95e56 Mon Sep 17 00:00:00 2001 From: Sheetal Nandi Date: Mon, 16 Oct 2017 16:53:33 -0700 Subject: [PATCH 03/51] Do not watch root folders for failed lookup locations and effective type roots Fixes #19170 --- src/compiler/resolutionCache.ts | 72 +++++++-- src/harness/unittests/tscWatchMode.ts | 4 +- .../unittests/tsserverProjectSystem.ts | 142 +++++++++++------- 3 files changed, 148 insertions(+), 70 deletions(-) diff --git a/src/compiler/resolutionCache.ts b/src/compiler/resolutionCache.ts index 84fffbe6f5ea5..b988da0fd5f13 100644 --- a/src/compiler/resolutionCache.ts +++ b/src/compiler/resolutionCache.ts @@ -64,6 +64,7 @@ namespace ts { interface DirectoryOfFailedLookupWatch { dir: string; dirPath: Path; + ignore?: true; } export const maxNumberOfFilesToIterateForInvalidation = 256; @@ -319,6 +320,33 @@ namespace ts { return endsWith(dirPath, "/node_modules"); } + function isDirectoryAtleastAtLevelFromFSRoot(dirPath: Path, minLevels: number) { + for (let searchIndex = getRootLength(dirPath); minLevels > 0; minLevels--) { + searchIndex = dirPath.indexOf(directorySeparator, searchIndex) + 1; + if (searchIndex === 0) { + // Folder isnt at expected minimun levels + return false; + } + } + return true; + } + + function canWatchDirectory(dirPath: Path) { + return isDirectoryAtleastAtLevelFromFSRoot(dirPath, + // When root is "/" do not watch directories like: + // "/", "/user", "/user/username", "/user/username/folderAtRoot" + // When root is "c:/" do not watch directories like: + // "c:/", "c:/folderAtRoot" + dirPath.charCodeAt(0) === CharacterCodes.slash ? 3 : 1); + } + + function filterFSRootDirectoriesToWatch(watchPath: DirectoryOfFailedLookupWatch, dirPath: Path): DirectoryOfFailedLookupWatch { + if (!canWatchDirectory(dirPath)) { + watchPath.ignore = true; + } + return watchPath; + } + function getDirectoryToWatchFailedLookupLocation(failedLookupLocation: string, failedLookupLocationPath: Path): DirectoryOfFailedLookupWatch { if (isInDirectoryPath(rootPath, failedLookupLocationPath)) { return { dir: rootDir, dirPath: rootPath }; @@ -335,7 +363,7 @@ namespace ts { // If the directory is node_modules use it to watch if (isNodeModulesDirectory(dirPath)) { - return { dir, dirPath }; + return filterFSRootDirectoriesToWatch({ dir, dirPath }, getDirectoryPath(dirPath)); } // Use some ancestor of the root directory @@ -350,7 +378,7 @@ namespace ts { } } - return { dir, dirPath }; + return filterFSRootDirectoriesToWatch({ dir, dirPath }, dirPath); } function isPathWithDefaultFailedLookupExtension(path: Path) { @@ -391,13 +419,15 @@ namespace ts { 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 }); + const { dir, dirPath, ignore } = getDirectoryToWatchFailedLookupLocation(failedLookupLocation, failedLookupLocationPath); + if (!ignore) { + const dirWatcher = directoryWatchesOfFailedLookups.get(dirPath); + if (dirWatcher) { + dirWatcher.refCount++; + } + else { + directoryWatchesOfFailedLookups.set(dirPath, { watcher: createDirectoryWatcher(dir, dirPath), refCount: 1 }); + } } } } @@ -422,10 +452,12 @@ namespace ts { 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, ignore } = getDirectoryToWatchFailedLookupLocation(failedLookupLocation, failedLookupLocationPath); + if (!ignore) { + const dirWatcher = directoryWatchesOfFailedLookups.get(dirPath); + // Do not close the watcher yet since it might be needed by other failed lookup locations. + dirWatcher.refCount--; + } } } @@ -577,7 +609,8 @@ namespace ts { } // 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 }); + // But filter directories that are at root level to say directory doesnt exist, so that we arent watching them + const typeRoots = getEffectiveTypeRoots(options, { directoryExists: directoryExistsForTypeRootWatch, getCurrentDirectory }); if (typeRoots) { mutateMap( typeRootsWatches, @@ -592,5 +625,16 @@ namespace ts { closeTypeRootsWatch(); } } + + /** + * Use this function to return if directory exists to get type roots to watch + * If we return directory exists then only the paths will be added to type roots + * Hence return true for all directories except root directories which are filtered from watching + */ + function directoryExistsForTypeRootWatch(nodeTypesDirectory: string) { + const dir = getDirectoryPath(getDirectoryPath(nodeTypesDirectory)); + const dirPath = resolutionHost.toPath(dir); + return dirPath === rootPath || canWatchDirectory(dirPath); + } } } diff --git a/src/harness/unittests/tscWatchMode.ts b/src/harness/unittests/tscWatchMode.ts index b25a7b1eb53c2..24052bd1add2e 100644 --- a/src/harness/unittests/tscWatchMode.ts +++ b/src/harness/unittests/tscWatchMode.ts @@ -254,7 +254,7 @@ namespace ts.tscWatch { checkProgramRootFiles(watch(), [file1.path, file2.path]); checkWatchedFiles(host, [configFile.path, file1.path, file2.path, libFile.path]); const configDir = getDirectoryPath(configFile.path); - checkWatchedDirectories(host, projectSystem.getTypeRootsFromLocation(configDir).concat(configDir), /*recursive*/ true); + checkWatchedDirectories(host, [configDir, combinePaths(configDir, projectSystem.nodeModulesAtTypes)], /*recursive*/ true); }); // TODO: if watching for config file creation @@ -269,7 +269,7 @@ namespace ts.tscWatch { const host = createWatchedSystem([commonFile1, libFile, configFile]); const watch = createWatchModeWithConfigFile(configFile.path, host); const configDir = getDirectoryPath(configFile.path); - checkWatchedDirectories(host, projectSystem.getTypeRootsFromLocation(configDir).concat(configDir), /*recursive*/ true); + checkWatchedDirectories(host, [configDir, combinePaths(configDir, projectSystem.nodeModulesAtTypes)], /*recursive*/ true); checkProgramRootFiles(watch(), [commonFile1.path]); diff --git a/src/harness/unittests/tsserverProjectSystem.ts b/src/harness/unittests/tsserverProjectSystem.ts index 4929cbfbaa517..175b579103f57 100644 --- a/src/harness/unittests/tsserverProjectSystem.ts +++ b/src/harness/unittests/tsserverProjectSystem.ts @@ -323,14 +323,31 @@ namespace ts.projectSystem { checkFileNames(`${server.ProjectKind[project.projectKind]} project, rootFileNames`, project.getRootFiles(), expectedFiles); } - function getNodeModuleDirectories(dir: string) { + function mapCombinedPathsInAncestor(dir: string, path2: string, mapAncestor: (ancestor: string) => boolean) { + dir = normalizePath(dir); const result: string[] = []; forEachAncestorDirectory(dir, ancestor => { - result.push(combinePaths(ancestor, "node_modules")); + if (mapAncestor(ancestor)) { + result.push(combinePaths(ancestor, path2)); + } }); return result; } + function getRootsToWatchWithAncestorDirectory(dir: string, path2: string) { + return mapCombinedPathsInAncestor(dir, path2, ancestor => ancestor.split(directorySeparator).length > 4); + } + + const nodeModules = "node_modules"; + function getNodeModuleDirectories(dir: string) { + return getRootsToWatchWithAncestorDirectory(dir, nodeModules); + } + + export const nodeModulesAtTypes = "node_modules/@types"; + export function getTypeRootsFromLocation(currentDirectory: string) { + return getRootsToWatchWithAncestorDirectory(currentDirectory, nodeModulesAtTypes); + } + function getNumberOfWatchesInvokedForRecursiveWatches(recursiveWatchedDirs: string[], file: string) { return countWhere(recursiveWatchedDirs, dir => file.length > dir.length && startsWith(file, dir) && file[dir.length] === directorySeparator); } @@ -413,15 +430,6 @@ namespace ts.projectSystem { verifyDiagnostics(actual, []); } - export function getTypeRootsFromLocation(currentDirectory: string) { - currentDirectory = normalizePath(currentDirectory); - const result: string[] = []; - forEachAncestorDirectory(currentDirectory, ancestor => { - result.push(combinePaths(ancestor, "node_modules/@types")); - }); - return result; - } - describe("tsserverProjectSystem", () => { const commonFile1: FileOrFolder = { path: "/a/b/commonFile1.ts", @@ -460,7 +468,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, ["/a/b/c", ...getTypeRootsFromLocation(getDirectoryPath(appFile.path))], /*recursive*/ true); + checkWatchedDirectories(host, ["/a/b/c", combinePaths(getDirectoryPath(appFile.path), nodeModulesAtTypes)], /*recursive*/ true); }); it("can handle tsconfig file name with difference casing", () => { @@ -532,7 +540,7 @@ namespace ts.projectSystem { // watching all files except one that was open checkWatchedFiles(host, [configFile.path, file2.path, libFile.path]); const configFileDirectory = getDirectoryPath(configFile.path); - checkWatchedDirectories(host, getTypeRootsFromLocation(configFileDirectory).concat(configFileDirectory), /*recursive*/ true); + checkWatchedDirectories(host, [configFileDirectory, combinePaths(configFileDirectory, nodeModulesAtTypes)], /*recursive*/ true); }); it("create configured project with the file list", () => { @@ -621,7 +629,7 @@ namespace ts.projectSystem { const projectService = createProjectService(host); projectService.openClientFile(commonFile1.path); const configFileDir = getDirectoryPath(configFile.path); - checkWatchedDirectories(host, getTypeRootsFromLocation(configFileDir).concat(configFileDir), /*recursive*/ true); + checkWatchedDirectories(host, [configFileDir, combinePaths(configFileDir, nodeModulesAtTypes)], /*recursive*/ true); checkNumberOfConfiguredProjects(projectService, 1); const project = configuredProjectAt(projectService, 0); @@ -2433,7 +2441,7 @@ namespace ts.projectSystem { 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"); + const watchedRecursiveDirectories = ["/a/b/node_modules/@types"]; watchedRecursiveDirectories.push("/a/b"); checkWatchedDirectories(host, watchedRecursiveDirectories, /*recursive*/ true); @@ -2459,7 +2467,8 @@ namespace ts.projectSystem { }); - it("Failed lookup locations are uses parent most node_modules directory", () => { + it("Failed lookup locations uses parent most node_modules directory", () => { + const root = "/user/username/rootfolder"; const file1: FileOrFolder = { path: "/a/b/src/file1.ts", content: 'import { classc } from "module1"' @@ -2479,9 +2488,11 @@ namespace ts.projectSystem { }; const configFile: FileOrFolder = { path: "/a/b/src/tsconfig.json", - content: JSON.stringify({ files: [file1.path] }) + content: JSON.stringify({ files: ["file1.ts"] }) }; - const files = [file1, module1, module2, module3, configFile, libFile]; + const nonLibFiles = [file1, module1, module2, module3, configFile]; + nonLibFiles.forEach(f => f.path = root + f.path); + const files = nonLibFiles.concat(libFile); const host = createServerHost(files); const projectService = createProjectService(host); projectService.openClientFile(file1.path); @@ -2491,8 +2502,8 @@ namespace ts.projectSystem { checkProjectActualFiles(project, [file1.path, libFile.path, module1.path, module2.path, configFile.path]); checkWatchedFiles(host, [libFile.path, module1.path, module2.path, configFile.path]); checkWatchedDirectories(host, [], /*recursive*/ false); - const watchedRecursiveDirectories = getTypeRootsFromLocation("/a/b/src"); - watchedRecursiveDirectories.push("/a/b/src", "/a/b/node_modules"); + const watchedRecursiveDirectories = getTypeRootsFromLocation(root + "/a/b/src"); + watchedRecursiveDirectories.push(`${root}/a/b/src`, `${root}/a/b/node_modules`); checkWatchedDirectories(host, watchedRecursiveDirectories, /*recursive*/ true); }); }); @@ -4682,7 +4693,6 @@ namespace ts.projectSystem { } const f2Lookups = getLocationsForModuleLookup("f2"); callsTrackingHost.verifyCalledOnEachEntryNTimes(CalledMapsWithSingleArg.fileExists, f2Lookups, 1); - const typeRootLocations = getTypeRootsFromLocation(getDirectoryPath(root.path)); const f2DirLookups = getLocationsForDirectoryLookup(); callsTrackingHost.verifyCalledOnEachEntry(CalledMapsWithSingleArg.directoryExists, f2DirLookups); callsTrackingHost.verifyNoCall(CalledMapsWithSingleArg.getDirectories); @@ -4693,7 +4703,7 @@ namespace ts.projectSystem { verifyImportedDiagnostics(); const f1Lookups = f2Lookups.map(s => s.replace("f2", "f1")); f1Lookups.length = f1Lookups.indexOf(imported.path) + 1; - const f1DirLookups = ["/c/d", "/c", ...typeRootLocations]; + const f1DirLookups = ["/c/d", "/c", ...mapCombinedPathsInAncestor(getDirectoryPath(root.path), nodeModulesAtTypes, returnTrue)]; vertifyF1Lookups(); // setting compiler options discards module resolution cache @@ -4744,13 +4754,12 @@ namespace ts.projectSystem { function getLocationsForDirectoryLookup() { const result = createMap(); - // Type root - typeRootLocations.forEach(location => result.set(location, 1)); forEachAncestorDirectory(getDirectoryPath(root.path), ancestor => { // To resolve modules result.set(ancestor, 2); // for type roots - result.set(combinePaths(ancestor, `node_modules`), 1); + result.set(combinePaths(ancestor, nodeModules), 1); + result.set(combinePaths(ancestor, nodeModulesAtTypes), 1); }); return result; } @@ -4990,15 +4999,20 @@ namespace ts.projectSystem { describe("Verify npm install in directory with tsconfig file works when", () => { function verifyNpmInstall(timeoutDuringPartialInstallation: boolean) { - const app: FileOrFolder = { + const root = "/user/username/rootfolder/otherfolder"; + const getRootedFileOrFolder = (fileOrFolder: FileOrFolder) => { + fileOrFolder.path = root + fileOrFolder.path; + return fileOrFolder; + }; + const app: FileOrFolder = getRootedFileOrFolder({ path: "/a/b/app.ts", content: "import _ from 'lodash';" - }; - const tsconfigJson: FileOrFolder = { + }); + const tsconfigJson: FileOrFolder = getRootedFileOrFolder({ path: "/a/b/tsconfig.json", content: '{ "compilerOptions": { } }' - }; - const packageJson: FileOrFolder = { + }); + const packageJson: FileOrFolder = getRootedFileOrFolder({ path: "/a/b/package.json", content: ` { @@ -5022,7 +5036,7 @@ namespace ts.projectSystem { "license": "ISC" } ` - }; + }); const appFolder = getDirectoryPath(app.path); const projectFiles = [app, libFile, tsconfigJson]; const typeRootDirectories = getTypeRootsFromLocation(getDirectoryPath(tsconfigJson.path)); @@ -5053,16 +5067,16 @@ namespace ts.projectSystem { { "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;" }, - ]; + ].map(getRootedFileOrFolder); verifyAfterPartialOrCompleteNpmInstall(2); - filesAndFoldersToAdd.push( + 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" } - ); + ].map(getRootedFileOrFolder)); // Since we didnt add any supported extension file, there wont be any timeout scheduled verifyAfterPartialOrCompleteNpmInstall(0); @@ -5070,27 +5084,27 @@ namespace ts.projectSystem { filesAndFoldersToAdd.length--; verifyAfterPartialOrCompleteNpmInstall(0); - filesAndFoldersToAdd.push( + 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": "\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}" } - ); + ].map(getRootedFileOrFolder)); verifyAfterPartialOrCompleteNpmInstall(2); - filesAndFoldersToAdd.push( + 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" } - ); + ].map(getRootedFileOrFolder)); verifyAfterPartialOrCompleteNpmInstall(0); // remove /a/b/node_modules/.staging/rxjs-22375c61/package.json.2252192041 filesAndFoldersToAdd.length--; // and add few more folders/files - filesAndFoldersToAdd.push( + filesAndFoldersToAdd.push(...[ { "path": "/a/b/node_modules/symbol-observable" }, { "path": "/a/b/node_modules/@types" }, { "path": "/a/b/node_modules/@types/lodash" }, @@ -5098,7 +5112,7 @@ namespace ts.projectSystem { { "path": "/a/b/node_modules/rxjs" }, { "path": "/a/b/node_modules/typescript" }, { "path": "/a/b/node_modules/.bin" } - ); + ].map(getRootedFileOrFolder)); // From the type root update verifyAfterPartialOrCompleteNpmInstall(2); @@ -5108,7 +5122,7 @@ namespace ts.projectSystem { .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"; + const lodashIndexPath = root + "/a/b/node_modules/@types/lodash/index.d.ts"; projectFiles.push(find(filesAndFoldersToAdd, f => f.path === lodashIndexPath)); // we would now not have failed lookup in the parent of appFolder since lodash is available recursiveWatchedDirectories.length = 1; @@ -5570,27 +5584,32 @@ namespace ts.projectSystem { }); describe("resolution when resolution cache size", () => { - function verifyWithMaxCacheLimit(limitHit: boolean) { + function verifyWithMaxCacheLimit(limitHit: boolean, useSlashRootAsSomeNotRootFolderInUserDirectory: boolean) { + const rootFolder = useSlashRootAsSomeNotRootFolderInUserDirectory ? "/user/username/rootfolder/otherfolder/" : "/"; const file1: FileOrFolder = { - path: "/a/b/project/file1.ts", + path: rootFolder + "a/b/project/file1.ts", content: 'import a from "file2"' }; const file2: FileOrFolder = { - path: "/a/b/node_modules/file2.d.ts", + path: rootFolder + "a/b/node_modules/file2.d.ts", content: "export class a { }" }; const file3: FileOrFolder = { - path: "/a/b/project/file3.ts", + path: rootFolder + "a/b/project/file3.ts", content: "export class c { }" }; const configFile: FileOrFolder = { - path: "/a/b/project/tsconfig.json", + path: rootFolder + "a/b/project/tsconfig.json", content: JSON.stringify({ compilerOptions: { typeRoots: [] } }) }; 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 watchedRecursiveDirectories = useSlashRootAsSomeNotRootFolderInUserDirectory ? + // Folders of node_modules lookup not in changedRoot + ["a/b/project", "a/b/node_modules", "a/node_modules", "node_modules"].map(v => rootFolder + v) : + // Folder of tsconfig + ["/a/b/project"]; const host = createServerHost(projectFiles); const { session, verifyInitialOpen, verifyProjectsUpdatedInBackgroundEventHandler } = createSession(host); const projectService = session.getProjectService(); @@ -5618,15 +5637,22 @@ namespace ts.projectSystem { projectFiles.push(file2); host.reloadFS(projectFiles); host.runQueuedTimeoutCallbacks(); - watchedRecursiveDirectories.length = 2; + if (useSlashRootAsSomeNotRootFolderInUserDirectory) { + watchedRecursiveDirectories.length = 2; + } + else { + // file2 addition wont be detected + projectFiles.pop(); + assert.isTrue(host.fileExists(file2.path)); + } verifyProject(); - verifyProjectsUpdatedInBackgroundEventHandler([{ + verifyProjectsUpdatedInBackgroundEventHandler(useSlashRootAsSomeNotRootFolderInUserDirectory ? [{ eventName: server.ProjectsUpdatedInBackgroundEvent, data: { openFiles } - }]); + }] : []); function verifyProject() { checkProjectActualFiles(project, map(projectFiles, file => file.path)); @@ -5635,12 +5661,20 @@ namespace ts.projectSystem { } } - it("limit not hit", () => { - verifyWithMaxCacheLimit(/*limitHit*/ false); + it("limit not hit and project is not at root level", () => { + verifyWithMaxCacheLimit(/*limitHit*/ false, /*useSlashRootAsSomeNotRootFolderInUserDirectory*/ true); + }); + + it("limit hit and project is not at root level", () => { + verifyWithMaxCacheLimit(/*limitHit*/ true, /*useSlashRootAsSomeNotRootFolderInUserDirectory*/ true); + }); + + it("limit not hit and project is at root level", () => { + verifyWithMaxCacheLimit(/*limitHit*/ false, /*useSlashRootAsSomeNotRootFolderInUserDirectory*/ false); }); - it("limit hit", () => { - verifyWithMaxCacheLimit(/*limitHit*/ true); + it("limit hit and project is at root level", () => { + verifyWithMaxCacheLimit(/*limitHit*/ true, /*useSlashRootAsSomeNotRootFolderInUserDirectory*/ false); }); }); } From 92d191990adee9cef63f6407ff2e2bd171a3a7a0 Mon Sep 17 00:00:00 2001 From: csigs Date: Tue, 17 Oct 2017 04:10:06 +0000 Subject: [PATCH 04/51] LEGO: check in for master to temporary branch. --- .../diagnosticMessages.generated.json.lcl | 17048 ++++++++-------- 1 file changed, 8563 insertions(+), 8485 deletions(-) diff --git a/src/loc/lcl/deu/diagnosticMessages/diagnosticMessages.generated.json.lcl b/src/loc/lcl/deu/diagnosticMessages/diagnosticMessages.generated.json.lcl index 79859d9bae27a..38946acf4725d 100644 --- a/src/loc/lcl/deu/diagnosticMessages/diagnosticMessages.generated.json.lcl +++ b/src/loc/lcl/deu/diagnosticMessages/diagnosticMessages.generated.json.lcl @@ -1,8486 +1,8564 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - or -. For example '{0}' or '{1}'.]]> - - oder - erforderlich, z. B. "{0}" oder "{1}".]]> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - type.]]> - - " sein.]]> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - ()' instead.]]> - - ()".]]> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + or -. For example '{0}' or '{1}'.]]> + + oder - erforderlich, z. B. "{0}" oder "{1}".]]> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + type.]]> + + " sein.]]> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ()' instead.]]> + + ()".]]> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file From 1af25ae9f1d3ee9076af6439444c93ab21b57f69 Mon Sep 17 00:00:00 2001 From: csigs Date: Tue, 17 Oct 2017 16:10:05 +0000 Subject: [PATCH 05/51] LEGO: check in for master to temporary branch. --- .../diagnosticMessages.generated.json.lcl | 74 ++++++++++++++---- .../diagnosticMessages.generated.json.lcl | 74 ++++++++++++++---- .../diagnosticMessages.generated.json.lcl | 74 ++++++++++++++---- .../diagnosticMessages.generated.json.lcl | 75 ++++++++++++++++--- .../diagnosticMessages.generated.json.lcl | 74 ++++++++++++++---- 5 files changed, 307 insertions(+), 64 deletions(-) diff --git a/src/loc/lcl/csy/diagnosticMessages/diagnosticMessages.generated.json.lcl b/src/loc/lcl/csy/diagnosticMessages/diagnosticMessages.generated.json.lcl index 075923ea43857..d27b3a500b880 100644 --- a/src/loc/lcl/csy/diagnosticMessages/diagnosticMessages.generated.json.lcl +++ b/src/loc/lcl/csy/diagnosticMessages/diagnosticMessages.generated.json.lcl @@ -801,6 +801,12 @@ + + + + + + @@ -1338,6 +1344,18 @@ + + + + + + + + + + + + @@ -1485,6 +1503,12 @@ + + + + + + @@ -1892,12 +1916,12 @@ - - + + - + @@ -2013,6 +2037,12 @@ + + + + + + @@ -3795,6 +3825,18 @@ + + + + + + + + + + + + @@ -3987,21 +4029,15 @@ - + - - - - + - + - - - - + @@ -7263,6 +7299,12 @@ + + + + + + @@ -7743,6 +7785,12 @@ + + + + + + diff --git a/src/loc/lcl/fra/diagnosticMessages/diagnosticMessages.generated.json.lcl b/src/loc/lcl/fra/diagnosticMessages/diagnosticMessages.generated.json.lcl index 026f10b37be90..43073f7cab335 100644 --- a/src/loc/lcl/fra/diagnosticMessages/diagnosticMessages.generated.json.lcl +++ b/src/loc/lcl/fra/diagnosticMessages/diagnosticMessages.generated.json.lcl @@ -801,6 +801,12 @@ + + + + + + @@ -1338,6 +1344,18 @@ + + + + + + + + + + + + @@ -1485,6 +1503,12 @@ + + + + + + @@ -1892,12 +1916,12 @@ - - + + - + @@ -2013,6 +2037,12 @@ + + + + + + @@ -3795,6 +3825,18 @@ + + + + + + + + + + + + @@ -3987,21 +4029,15 @@ - + - - - - + - + - - - - + @@ -7263,6 +7299,12 @@ + + + + + + @@ -7743,6 +7785,12 @@ + + + + + + diff --git a/src/loc/lcl/ita/diagnosticMessages/diagnosticMessages.generated.json.lcl b/src/loc/lcl/ita/diagnosticMessages/diagnosticMessages.generated.json.lcl index b978802c56409..727bf964abf21 100644 --- a/src/loc/lcl/ita/diagnosticMessages/diagnosticMessages.generated.json.lcl +++ b/src/loc/lcl/ita/diagnosticMessages/diagnosticMessages.generated.json.lcl @@ -792,6 +792,12 @@ + + + + + + @@ -1329,6 +1335,18 @@ + + + + + + + + + + + + @@ -1476,6 +1494,12 @@ + + + + + + @@ -1883,12 +1907,12 @@ - - + + - + @@ -2004,6 +2028,12 @@ + + + + + + @@ -3786,6 +3816,18 @@ + + + + + + + + + + + + @@ -3978,21 +4020,15 @@ - + - - - - + - + - - - - + @@ -7254,6 +7290,12 @@ + + + + + + @@ -7734,6 +7776,12 @@ + + + + + + diff --git a/src/loc/lcl/plk/diagnosticMessages/diagnosticMessages.generated.json.lcl b/src/loc/lcl/plk/diagnosticMessages/diagnosticMessages.generated.json.lcl index 3a8512e54b238..4ae9e7d5e5697 100644 --- a/src/loc/lcl/plk/diagnosticMessages/diagnosticMessages.generated.json.lcl +++ b/src/loc/lcl/plk/diagnosticMessages/diagnosticMessages.generated.json.lcl @@ -782,6 +782,12 @@ + + + + + + @@ -1313,6 +1319,18 @@ + + + + + + + + + + + + @@ -1460,6 +1478,12 @@ + + + + + + @@ -1867,10 +1891,13 @@ - - + + + + + @@ -1982,6 +2009,12 @@ + + + + + + @@ -3758,6 +3791,18 @@ + + + + + + + + + + + + @@ -3950,21 +3995,15 @@ - + - - - - + - + - - - - + @@ -7217,6 +7256,12 @@ + + + + + + @@ -7697,6 +7742,12 @@ + + + + + + diff --git a/src/loc/lcl/rus/diagnosticMessages/diagnosticMessages.generated.json.lcl b/src/loc/lcl/rus/diagnosticMessages/diagnosticMessages.generated.json.lcl index aac4d18cd6799..b0e6c4b3495f0 100644 --- a/src/loc/lcl/rus/diagnosticMessages/diagnosticMessages.generated.json.lcl +++ b/src/loc/lcl/rus/diagnosticMessages/diagnosticMessages.generated.json.lcl @@ -791,6 +791,12 @@ + + + + + + @@ -1328,6 +1334,18 @@ + + + + + + + + + + + + @@ -1475,6 +1493,12 @@ + + + + + + @@ -1882,12 +1906,12 @@ - - + + - + @@ -2003,6 +2027,12 @@ + + + + + + @@ -3785,6 +3815,18 @@ + + + + + + + + + + + + @@ -3977,21 +4019,15 @@ - + - - - - + - + - - - - + @@ -7253,6 +7289,12 @@ + + + + + + @@ -7733,6 +7775,12 @@ + + + + + + From 08d7e182cd40e152a3ab8291a10fcb40913f0caa Mon Sep 17 00:00:00 2001 From: Nathan Shively-Sanders Date: Tue, 17 Oct 2017 09:56:04 -0700 Subject: [PATCH 06/51] Mark fresh spread objects w/ContainsObjectLiteral --- src/compiler/checker.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts index 69a46ed6cb236..f30d956e311df 100644 --- a/src/compiler/checker.ts +++ b/src/compiler/checker.ts @@ -7971,7 +7971,7 @@ namespace ts { const spread = createAnonymousType(undefined, members, emptyArray, emptyArray, stringIndexInfo, numberIndexInfo); spread.flags |= propagatedFlags; - spread.flags |= TypeFlags.FreshLiteral; + spread.flags |= TypeFlags.FreshLiteral | TypeFlags.ContainsObjectLiteral; (spread as ObjectType).objectFlags |= ObjectFlags.ObjectLiteral; spread.symbol = symbol; return spread; From e58aa100687e3c1e92b04d927cd78d64f233ea3e Mon Sep 17 00:00:00 2001 From: Nathan Shively-Sanders Date: Tue, 17 Oct 2017 09:56:28 -0700 Subject: [PATCH 07/51] Test excess property checks of spreads of unions. --- .../excessPropertyCheckWithUnions.errors.txt | 6 +++++ .../excessPropertyCheckWithUnions.js | 17 ++++++++++++ .../excessPropertyCheckWithUnions.symbols | 25 +++++++++++++++++ .../excessPropertyCheckWithUnions.types | 27 +++++++++++++++++++ .../reference/spreadInvalidArgumentType.types | 2 +- .../compiler/excessPropertyCheckWithUnions.ts | 7 +++++ 6 files changed, 83 insertions(+), 1 deletion(-) diff --git a/tests/baselines/reference/excessPropertyCheckWithUnions.errors.txt b/tests/baselines/reference/excessPropertyCheckWithUnions.errors.txt index 3b7e5a787d2aa..9fbbb00160540 100644 --- a/tests/baselines/reference/excessPropertyCheckWithUnions.errors.txt +++ b/tests/baselines/reference/excessPropertyCheckWithUnions.errors.txt @@ -96,4 +96,10 @@ tests/cases/compiler/excessPropertyCheckWithUnions.ts(40,1): error TS2322: Type // these two are not reported because there are two discriminant properties over = { a: 1, b: 1, first: "ok", second: "error" } over = { a: 1, b: 1, first: "ok", third: "error" } + + // Freshness disappears after spreading a union + declare let t0: { a: any, b: any } | { d: any, e: any } + declare let t1: { a: any, b: any, c: any } | { c: any, d: any, e: any } + let t2 = { ...t1 } + t0 = t2 \ No newline at end of file diff --git a/tests/baselines/reference/excessPropertyCheckWithUnions.js b/tests/baselines/reference/excessPropertyCheckWithUnions.js index c6da660b52dde..c6b45123cce76 100644 --- a/tests/baselines/reference/excessPropertyCheckWithUnions.js +++ b/tests/baselines/reference/excessPropertyCheckWithUnions.js @@ -49,9 +49,24 @@ let over: Overlapping // these two are not reported because there are two discriminant properties over = { a: 1, b: 1, first: "ok", second: "error" } over = { a: 1, b: 1, first: "ok", third: "error" } + +// Freshness disappears after spreading a union +declare let t0: { a: any, b: any } | { d: any, e: any } +declare let t1: { a: any, b: any, c: any } | { c: any, d: any, e: any } +let t2 = { ...t1 } +t0 = t2 //// [excessPropertyCheckWithUnions.js] +"use strict"; +var __assign = (this && this.__assign) || Object.assign || function(t) { + for (var s, i = 1, n = arguments.length; i < n; i++) { + s = arguments[i]; + for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) + t[p] = s[p]; + } + return t; +}; var wrong = { tag: "T", a1: "extra" }; wrong = { tag: "A", d20: 12 }; wrong = { tag: "D" }; @@ -72,3 +87,5 @@ var over; // these two are not reported because there are two discriminant properties over = { a: 1, b: 1, first: "ok", second: "error" }; over = { a: 1, b: 1, first: "ok", third: "error" }; +var t2 = __assign({}, t1); +t0 = t2; diff --git a/tests/baselines/reference/excessPropertyCheckWithUnions.symbols b/tests/baselines/reference/excessPropertyCheckWithUnions.symbols index 332166e396c8f..7778c6bf21699 100644 --- a/tests/baselines/reference/excessPropertyCheckWithUnions.symbols +++ b/tests/baselines/reference/excessPropertyCheckWithUnions.symbols @@ -142,3 +142,28 @@ over = { a: 1, b: 1, first: "ok", third: "error" } >first : Symbol(first, Decl(excessPropertyCheckWithUnions.ts, 49, 20)) >third : Symbol(third, Decl(excessPropertyCheckWithUnions.ts, 49, 33)) +// Freshness disappears after spreading a union +declare let t0: { a: any, b: any } | { d: any, e: any } +>t0 : Symbol(t0, Decl(excessPropertyCheckWithUnions.ts, 52, 11)) +>a : Symbol(a, Decl(excessPropertyCheckWithUnions.ts, 52, 17)) +>b : Symbol(b, Decl(excessPropertyCheckWithUnions.ts, 52, 25)) +>d : Symbol(d, Decl(excessPropertyCheckWithUnions.ts, 52, 38)) +>e : Symbol(e, Decl(excessPropertyCheckWithUnions.ts, 52, 46)) + +declare let t1: { a: any, b: any, c: any } | { c: any, d: any, e: any } +>t1 : Symbol(t1, Decl(excessPropertyCheckWithUnions.ts, 53, 11)) +>a : Symbol(a, Decl(excessPropertyCheckWithUnions.ts, 53, 17)) +>b : Symbol(b, Decl(excessPropertyCheckWithUnions.ts, 53, 25)) +>c : Symbol(c, Decl(excessPropertyCheckWithUnions.ts, 53, 33)) +>c : Symbol(c, Decl(excessPropertyCheckWithUnions.ts, 53, 46)) +>d : Symbol(d, Decl(excessPropertyCheckWithUnions.ts, 53, 54)) +>e : Symbol(e, Decl(excessPropertyCheckWithUnions.ts, 53, 62)) + +let t2 = { ...t1 } +>t2 : Symbol(t2, Decl(excessPropertyCheckWithUnions.ts, 54, 3)) +>t1 : Symbol(t1, Decl(excessPropertyCheckWithUnions.ts, 53, 11)) + +t0 = t2 +>t0 : Symbol(t0, Decl(excessPropertyCheckWithUnions.ts, 52, 11)) +>t2 : Symbol(t2, Decl(excessPropertyCheckWithUnions.ts, 54, 3)) + diff --git a/tests/baselines/reference/excessPropertyCheckWithUnions.types b/tests/baselines/reference/excessPropertyCheckWithUnions.types index 1d6bdd32eb269..78f5c025b380d 100644 --- a/tests/baselines/reference/excessPropertyCheckWithUnions.types +++ b/tests/baselines/reference/excessPropertyCheckWithUnions.types @@ -194,3 +194,30 @@ over = { a: 1, b: 1, first: "ok", third: "error" } >third : string >"error" : "error" +// Freshness disappears after spreading a union +declare let t0: { a: any, b: any } | { d: any, e: any } +>t0 : { a: any; b: any; } | { d: any; e: any; } +>a : any +>b : any +>d : any +>e : any + +declare let t1: { a: any, b: any, c: any } | { c: any, d: any, e: any } +>t1 : { a: any; b: any; c: any; } | { c: any; d: any; e: any; } +>a : any +>b : any +>c : any +>c : any +>d : any +>e : any + +let t2 = { ...t1 } +>t2 : { a: any; b: any; c: any; } | { c: any; d: any; e: any; } +>{ ...t1 } : { a: any; b: any; c: any; } | { c: any; d: any; e: any; } +>t1 : { a: any; b: any; c: any; } | { c: any; d: any; e: any; } + +t0 = t2 +>t0 = t2 : { a: any; b: any; c: any; } | { c: any; d: any; e: any; } +>t0 : { a: any; b: any; } | { d: any; e: any; } +>t2 : { a: any; b: any; c: any; } | { c: any; d: any; e: any; } + diff --git a/tests/baselines/reference/spreadInvalidArgumentType.types b/tests/baselines/reference/spreadInvalidArgumentType.types index 244d851589331..ae85189962215 100644 --- a/tests/baselines/reference/spreadInvalidArgumentType.types +++ b/tests/baselines/reference/spreadInvalidArgumentType.types @@ -89,7 +89,7 @@ function f(p1: T, p2: T[]) { >p1 : T var o2 = { ...p2 }; // OK ->o2 : { [n: number]: T; length: number; toString(): string; toLocaleString(): string; push(...items: T[]): number; pop(): T; concat(...items: ReadonlyArray[]): T[]; concat(...items: (T | ReadonlyArray)[]): T[]; join(separator?: string): string; reverse(): T[]; shift(): T; slice(start?: number, end?: number): T[]; sort(compareFn?: (a: T, b: T) => number): T[]; splice(start: number, deleteCount?: number): T[]; splice(start: number, deleteCount: number, ...items: T[]): T[]; unshift(...items: T[]): number; indexOf(searchElement: T, fromIndex?: number): number; lastIndexOf(searchElement: T, fromIndex?: number): number; every(callbackfn: (value: T, index: number, array: T[]) => boolean, thisArg?: any): boolean; some(callbackfn: (value: T, index: number, array: T[]) => boolean, thisArg?: any): boolean; forEach(callbackfn: (value: T, index: number, array: T[]) => void, thisArg?: any): void; map(callbackfn: (value: T, index: number, array: T[]) => U, thisArg?: any): U[]; filter(callbackfn: (value: T, index: number, array: T[]) => value is S, thisArg?: any): S[]; filter(callbackfn: (value: T, index: number, array: T[]) => any, thisArg?: any): T[]; reduce(callbackfn: (previousValue: T, currentValue: T, currentIndex: number, array: T[]) => T): T; reduce(callbackfn: (previousValue: T, currentValue: T, currentIndex: number, array: T[]) => T, initialValue: T): T; reduce(callbackfn: (previousValue: U, currentValue: T, currentIndex: number, array: T[]) => U, initialValue: U): U; reduceRight(callbackfn: (previousValue: T, currentValue: T, currentIndex: number, array: T[]) => T): T; reduceRight(callbackfn: (previousValue: T, currentValue: T, currentIndex: number, array: T[]) => T, initialValue: T): T; reduceRight(callbackfn: (previousValue: U, currentValue: T, currentIndex: number, array: T[]) => U, initialValue: U): U; } +>o2 : { [x: number]: T; length: number; toString(): string; toLocaleString(): string; push(...items: T[]): number; pop(): T; concat(...items: ReadonlyArray[]): T[]; concat(...items: (T | ReadonlyArray)[]): T[]; join(separator?: string): string; reverse(): T[]; shift(): T; slice(start?: number, end?: number): T[]; sort(compareFn?: (a: T, b: T) => number): T[]; splice(start: number, deleteCount?: number): T[]; splice(start: number, deleteCount: number, ...items: T[]): T[]; unshift(...items: T[]): number; indexOf(searchElement: T, fromIndex?: number): number; lastIndexOf(searchElement: T, fromIndex?: number): number; every(callbackfn: (value: T, index: number, array: T[]) => boolean, thisArg?: any): boolean; some(callbackfn: (value: T, index: number, array: T[]) => boolean, thisArg?: any): boolean; forEach(callbackfn: (value: T, index: number, array: T[]) => void, thisArg?: any): void; map(callbackfn: (value: T, index: number, array: T[]) => U, thisArg?: any): U[]; filter(callbackfn: (value: T, index: number, array: T[]) => value is S, thisArg?: any): S[]; filter(callbackfn: (value: T, index: number, array: T[]) => any, thisArg?: any): T[]; reduce(callbackfn: (previousValue: T, currentValue: T, currentIndex: number, array: T[]) => T): T; reduce(callbackfn: (previousValue: T, currentValue: T, currentIndex: number, array: T[]) => T, initialValue: T): T; reduce(callbackfn: (previousValue: U, currentValue: T, currentIndex: number, array: T[]) => U, initialValue: U): U; reduceRight(callbackfn: (previousValue: T, currentValue: T, currentIndex: number, array: T[]) => T): T; reduceRight(callbackfn: (previousValue: T, currentValue: T, currentIndex: number, array: T[]) => T, initialValue: T): T; reduceRight(callbackfn: (previousValue: U, currentValue: T, currentIndex: number, array: T[]) => U, initialValue: U): U; } >{ ...p2 } : { [n: number]: T; length: number; toString(): string; toLocaleString(): string; push(...items: T[]): number; pop(): T; concat(...items: ReadonlyArray[]): T[]; concat(...items: (T | ReadonlyArray)[]): T[]; join(separator?: string): string; reverse(): T[]; shift(): T; slice(start?: number, end?: number): T[]; sort(compareFn?: (a: T, b: T) => number): T[]; splice(start: number, deleteCount?: number): T[]; splice(start: number, deleteCount: number, ...items: T[]): T[]; unshift(...items: T[]): number; indexOf(searchElement: T, fromIndex?: number): number; lastIndexOf(searchElement: T, fromIndex?: number): number; every(callbackfn: (value: T, index: number, array: T[]) => boolean, thisArg?: any): boolean; some(callbackfn: (value: T, index: number, array: T[]) => boolean, thisArg?: any): boolean; forEach(callbackfn: (value: T, index: number, array: T[]) => void, thisArg?: any): void; map(callbackfn: (value: T, index: number, array: T[]) => U, thisArg?: any): U[]; filter(callbackfn: (value: T, index: number, array: T[]) => value is S, thisArg?: any): S[]; filter(callbackfn: (value: T, index: number, array: T[]) => any, thisArg?: any): T[]; reduce(callbackfn: (previousValue: T, currentValue: T, currentIndex: number, array: T[]) => T): T; reduce(callbackfn: (previousValue: T, currentValue: T, currentIndex: number, array: T[]) => T, initialValue: T): T; reduce(callbackfn: (previousValue: U, currentValue: T, currentIndex: number, array: T[]) => U, initialValue: U): U; reduceRight(callbackfn: (previousValue: T, currentValue: T, currentIndex: number, array: T[]) => T): T; reduceRight(callbackfn: (previousValue: T, currentValue: T, currentIndex: number, array: T[]) => T, initialValue: T): T; reduceRight(callbackfn: (previousValue: U, currentValue: T, currentIndex: number, array: T[]) => U, initialValue: U): U; } >p2 : T[] diff --git a/tests/cases/compiler/excessPropertyCheckWithUnions.ts b/tests/cases/compiler/excessPropertyCheckWithUnions.ts index 9a7968fe5114e..d5a2327380e76 100644 --- a/tests/cases/compiler/excessPropertyCheckWithUnions.ts +++ b/tests/cases/compiler/excessPropertyCheckWithUnions.ts @@ -1,3 +1,4 @@ +// @strict: true type ADT = { tag: "A", a1: string @@ -48,3 +49,9 @@ let over: Overlapping // these two are not reported because there are two discriminant properties over = { a: 1, b: 1, first: "ok", second: "error" } over = { a: 1, b: 1, first: "ok", third: "error" } + +// Freshness disappears after spreading a union +declare let t0: { a: any, b: any } | { d: any, e: any } +declare let t1: { a: any, b: any, c: any } | { c: any, d: any, e: any } +let t2 = { ...t1 } +t0 = t2 From 2b566b9a536e082fc05896a0ddbae2eab165f86a Mon Sep 17 00:00:00 2001 From: Andy Date: Tue, 17 Oct 2017 10:20:11 -0700 Subject: [PATCH 08/51] Add exported members of all project files in the global completion list (#19069) * checker.ts: Remove null check on symbols * tsserverProjectSystem.ts: add two tests * client.ts, completions.ts, types.ts: Add codeActions member to CompletionEntryDetails * protocol.ts, session.ts: Add codeActions member to CompletionEntryDetails protocol * protocol.ts, session.ts, types.ts: add hasAction to CompletionEntry * session.ts, services.ts, types.ts: Add formattingOptions parameter to getCompletionEntryDetails * completions.ts: define SymbolOriginInfo type * completions.ts, services.ts: Add allSourceFiles parameter to getCompletionsAtPosition * completions.ts, services.ts: Plumb allSourceFiles into new function getSymbolsFromOtherSourceFileExports inside getCompletionData * completions.ts: add symbolToOriginInfoMap parameter to getCompletionEntriesFromSymbols and to return value of getCompletionData * utilities.ts: Add getOtherModuleSymbols, getUniqueSymbolIdAsString, getUniqueSymbolId * completions.ts: Set CompletionEntry.hasAction when symbol is found in symbolToOriginInfoMap (meaning there's an import action) * completions.ts: Populate list with possible exports (implement getSymbolsFromOtherSourceFileExports) * completions.ts, services.ts: Plumb host and rulesProvider into getCompletionEntryDetails * completions.ts: Add TODO comment * importFixes.ts: Add types ImportDeclarationMap and ImportCodeFixContext * Move getImportDeclarations into getCodeActionForImport, immediately after the implementation * importFixes.ts: Move createChangeTracker into getCodeActionForImport, immediately after getImportDeclarations * importFixes.ts: Add convertToImportCodeFixContext function and reference it from the getCodeActions lambda * importFixes.ts: Add context: ImportCodeFixContext parameter to getCodeActionForImport, update call sites, destructure it, use compilerOptions in getModuleSpecifierForNewImport * importFixes.ts: Remove moduleSymbol parameter from getImportDeclarations and use the ambient one * importFixes.ts: Use cachedImportDeclarations from context in getCodeActionForImport * importFixes.ts: Move createCodeAction out, immediately above convertToImportCodeFixContext * Move the declaration for lastImportDeclaration out of the getCodeActions lambda into getCodeActionForImport * importFixes.ts: Use symbolToken in getCodeActionForImport * importFixes.ts: Remove useCaseSensitiveFileNames altogether from getCodeActions lambda * importFixes.ts: Remove local getUniqueSymbolId function and add checker parameter to calls to it * importFixes.ts: Move getCodeActionForImport out into an export, immediately below convertToImportCodeFixContext * completions.ts: In getCompletionEntryDetails, if there's symbolOriginInfo, call getCodeActionForImport * importFixes.ts: Create and use importFixContext within getCodeActions lambda * importFixes.ts: Use local newLineCharacter instead of context.newLineCharacter in getCodeActionForImport * importFixes.ts: Use local host instead of context.host in getCodeActionForImport * importFixes.ts: Remove dummy getCanonicalFileName line * Filter symbols after gathering exports instead of before * Lint * Test, fix bugs, refactor * Suggestions from code review * Update api baseline * Fix bug if previousToken is not an Identifier * Replace `startsWith` with `stringContainsCharactersInOrder` --- src/compiler/checker.ts | 18 +- src/compiler/commandLineParser.ts | 4 +- src/compiler/core.ts | 30 + src/compiler/diagnosticMessages.json | 4 +- src/compiler/moduleNameResolver.ts | 6 +- src/compiler/types.ts | 1 + src/compiler/utilities.ts | 15 + src/harness/fourslash.ts | 67 +- src/harness/harnessLanguageService.ts | 4 +- src/server/client.ts | 4 +- src/server/protocol.ts | 10 + src/server/session.ts | 23 +- src/services/codeFixProvider.ts | 4 +- src/services/codefixes/importFixes.ts | 993 +++++++++--------- src/services/completions.ts | 206 +++- src/services/pathCompletions.ts | 2 +- src/services/refactorProvider.ts | 4 +- src/services/services.ts | 10 +- src/services/shims.ts | 9 +- src/services/textChanges.ts | 13 +- src/services/types.ts | 5 +- src/services/utilities.ts | 8 +- .../reference/api/tsserverlibrary.d.ts | 22 +- tests/baselines/reference/api/typescript.d.ts | 13 +- ...letionsImport_default_addToNamedImports.ts | 19 + ...ionsImport_default_addToNamespaceImport.ts | 18 + ...Import_default_alreadyExistedWithRename.ts | 20 + ...letionsImport_default_didNotExistBefore.ts | 19 + .../completionsImport_fromAmbientModule.ts | 18 + .../fourslash/completionsImport_matching.ts | 22 + ...mpletionsImport_named_addToNamedImports.ts | 19 + ...mpletionsImport_named_didNotExistBefore.ts | 20 + ...tionsImport_named_namespaceImportExists.ts | 20 + ...pletionsImport_previousTokenIsSemicolon.ts | 11 + tests/cases/fourslash/fourslash.ts | 23 +- .../importNameCodeFixOptionalImport0.ts | 2 +- 36 files changed, 1104 insertions(+), 582 deletions(-) create mode 100644 tests/cases/fourslash/completionsImport_default_addToNamedImports.ts create mode 100644 tests/cases/fourslash/completionsImport_default_addToNamespaceImport.ts create mode 100644 tests/cases/fourslash/completionsImport_default_alreadyExistedWithRename.ts create mode 100644 tests/cases/fourslash/completionsImport_default_didNotExistBefore.ts create mode 100644 tests/cases/fourslash/completionsImport_fromAmbientModule.ts create mode 100644 tests/cases/fourslash/completionsImport_matching.ts create mode 100644 tests/cases/fourslash/completionsImport_named_addToNamedImports.ts create mode 100644 tests/cases/fourslash/completionsImport_named_didNotExistBefore.ts create mode 100644 tests/cases/fourslash/completionsImport_named_namespaceImportExists.ts create mode 100644 tests/cases/fourslash/completionsImport_previousTokenIsSemicolon.ts diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts index 69a46ed6cb236..ee6930e1b4ab1 100644 --- a/src/compiler/checker.ts +++ b/src/compiler/checker.ts @@ -314,6 +314,7 @@ namespace ts { const jsObjectLiteralIndexInfo = createIndexInfo(anyType, /*isReadonly*/ false); const globals = createSymbolTable(); + let ambientModulesCache: Symbol[] | undefined; /** * List of every ambient module with a "*" wildcard. * Unlike other ambient modules, these can't be stored in `globals` because symbol tables only deal with exact matches. @@ -25586,13 +25587,16 @@ namespace ts { } function getAmbientModules(): Symbol[] { - const result: Symbol[] = []; - globals.forEach((global, sym) => { - if (ambientModuleSymbolRegex.test(unescapeLeadingUnderscores(sym))) { - result.push(global); - } - }); - return result; + if (!ambientModulesCache) { + ambientModulesCache = []; + globals.forEach((global, sym) => { + // No need to `unescapeLeadingUnderscores`, an escaped symbol is never an ambient module. + if (ambientModuleSymbolRegex.test(sym as string)) { + ambientModulesCache.push(global); + } + }); + } + return ambientModulesCache; } function checkGrammarImportCallExpression(node: ImportCall): boolean { diff --git a/src/compiler/commandLineParser.ts b/src/compiler/commandLineParser.ts index af0697a12d914..2eca9dca05c4a 100644 --- a/src/compiler/commandLineParser.ts +++ b/src/compiler/commandLineParser.ts @@ -1183,8 +1183,8 @@ namespace ts { } } - function isDoubleQuotedString(node: Node) { - return node.kind === SyntaxKind.StringLiteral && getSourceTextOfNodeFromSourceFile(sourceFile, node).charCodeAt(0) === CharacterCodes.doubleQuote; + function isDoubleQuotedString(node: Node): boolean { + return isStringLiteral(node) && isStringDoubleQuoted(node, sourceFile); } } diff --git a/src/compiler/core.ts b/src/compiler/core.ts index f838d6abfef22..542435b84f901 100644 --- a/src/compiler/core.ts +++ b/src/compiler/core.ts @@ -191,6 +191,18 @@ namespace ts { } return undefined; } + + /** Like `forEach`, but suitable for use with numbers and strings (which may be falsy). */ + export function firstDefined(array: ReadonlyArray | undefined, callback: (element: T, index: number) => U | undefined): U | undefined { + for (let i = 0; i < array.length; i++) { + const result = callback(array[i], i); + if (result !== undefined) { + return result; + } + } + return undefined; + } + /** * Iterates through the parent chain of a node and performs the callback on each parent until the callback * returns a truthy value, then returns that value. @@ -261,6 +273,16 @@ namespace ts { return undefined; } + export function findLast(array: ReadonlyArray, predicate: (element: T, index: number) => boolean): T | undefined { + for (let i = array.length - 1; i >= 0; i--) { + const value = array[i]; + if (predicate(value, i)) { + return value; + } + } + return undefined; + } + /** Works like Array.prototype.findIndex, returning `-1` if no element satisfying the predicate is found. */ export function findIndex(array: ReadonlyArray, predicate: (element: T, index: number) => boolean): number { for (let i = 0; i < array.length; i++) { @@ -1147,6 +1169,14 @@ namespace ts { return result; } + export function arrayToNumericMap(array: ReadonlyArray, makeKey: (value: T) => number): T[] { + const result: T[] = []; + for (const value of array) { + result[makeKey(value)] = value; + } + return result; + } + /** * Creates a set from the elements of an array. * diff --git a/src/compiler/diagnosticMessages.json b/src/compiler/diagnosticMessages.json index 8cd5088049c65..f33cc4449f936 100644 --- a/src/compiler/diagnosticMessages.json +++ b/src/compiler/diagnosticMessages.json @@ -3661,7 +3661,7 @@ "category": "Error", "code": 90010 }, - "Import {0} from {1}.": { + "Import '{0}' from \"{1}\".": { "category": "Message", "code": 90013 }, @@ -3669,7 +3669,7 @@ "category": "Message", "code": 90014 }, - "Add {0} to existing import declaration from {1}.": { + "Add '{0}' to existing import declaration from \"{1}\".": { "category": "Message", "code": 90015 }, diff --git a/src/compiler/moduleNameResolver.ts b/src/compiler/moduleNameResolver.ts index 08178d3d16aad..a0e045cf0f259 100644 --- a/src/compiler/moduleNameResolver.ts +++ b/src/compiler/moduleNameResolver.ts @@ -128,7 +128,11 @@ namespace ts { } } - export function getEffectiveTypeRoots(options: CompilerOptions, host: { directoryExists?: (directoryName: string) => boolean, getCurrentDirectory?: () => string }): string[] | undefined { + export interface GetEffectiveTypeRootsHost { + directoryExists?(directoryName: string): boolean; + getCurrentDirectory?(): string; + } + export function getEffectiveTypeRoots(options: CompilerOptions, host: GetEffectiveTypeRootsHost): string[] | undefined { if (options.typeRoots) { return options.typeRoots; } diff --git a/src/compiler/types.ts b/src/compiler/types.ts index 42e8292b13bd8..6226e4f98c723 100644 --- a/src/compiler/types.ts +++ b/src/compiler/types.ts @@ -1055,6 +1055,7 @@ namespace ts { export interface StringLiteral extends LiteralExpression { kind: SyntaxKind.StringLiteral; /* @internal */ textSourceNode?: Identifier | StringLiteral | NumericLiteral; // Allows a StringLiteral to get its text from another node (used by transforms). + /** Note: this is only set when synthesizing a node, not during parsing. */ /* @internal */ singleQuote?: boolean; } diff --git a/src/compiler/utilities.ts b/src/compiler/utilities.ts index c4d3de453ba5c..f907cce85b1f3 100644 --- a/src/compiler/utilities.ts +++ b/src/compiler/utilities.ts @@ -520,6 +520,17 @@ namespace ts { } } + /* @internal */ + export function isAnyImportSyntax(node: Node): node is AnyImportSyntax { + switch (node.kind) { + case SyntaxKind.ImportDeclaration: + case SyntaxKind.ImportEqualsDeclaration: + return true; + default: + return false; + } + } + // Gets the nearest enclosing block scope container that has the provided node // as a descendant, that is not the provided node. export function getEnclosingBlockScopeContainer(node: Node): Node { @@ -1375,6 +1386,10 @@ namespace ts { return charCode === CharacterCodes.singleQuote || charCode === CharacterCodes.doubleQuote; } + export function isStringDoubleQuoted(string: StringLiteral, sourceFile: SourceFile): boolean { + return getSourceTextOfNodeFromSourceFile(sourceFile, string).charCodeAt(0) === CharacterCodes.doubleQuote; + } + /** * Returns true if the node is a variable declaration whose initializer is a function expression. * This function does not test if the node is in a JavaScript file or not. diff --git a/src/harness/fourslash.ts b/src/harness/fourslash.ts index de6d92eda05ed..493b0187b57ad 100644 --- a/src/harness/fourslash.ts +++ b/src/harness/fourslash.ts @@ -783,10 +783,10 @@ namespace FourSlash { }); } - public verifyCompletionListContains(symbol: string, text?: string, documentation?: string, kind?: string, spanIndex?: number) { + public verifyCompletionListContains(symbol: string, text?: string, documentation?: string, kind?: string, spanIndex?: number, hasAction?: boolean) { const completions = this.getCompletionListAtCaret(); if (completions) { - this.assertItemInCompletionList(completions.entries, symbol, text, documentation, kind, spanIndex); + this.assertItemInCompletionList(completions.entries, symbol, text, documentation, kind, spanIndex, hasAction); } else { this.raiseError(`No completions at position '${this.currentCaretPosition}' when looking for '${symbol}'.`); @@ -1127,7 +1127,7 @@ Actual: ${stringify(fullActual)}`); } private getCompletionEntryDetails(entryName: string) { - return this.languageService.getCompletionEntryDetails(this.activeFile.fileName, this.currentCaretPosition, entryName); + return this.languageService.getCompletionEntryDetails(this.activeFile.fileName, this.currentCaretPosition, entryName, this.formatCodeSettings); } private getReferencesAtCaret() { @@ -2289,6 +2289,29 @@ Actual: ${stringify(fullActual)}`); this.applyCodeActions(this.getCodeFixActions(fileName, errorCode), index); } + public applyCodeActionFromCompletion(markerName: string, options: FourSlashInterface.VerifyCompletionActionOptions) { + this.goToMarker(markerName); + + const actualCompletion = this.getCompletionListAtCaret().entries.find(e => e.name === options.name); + + if (!actualCompletion.hasAction) { + this.raiseError(`Completion for ${options.name} does not have an associated action.`); + } + + const details = this.getCompletionEntryDetails(options.name); + if (details.codeActions.length !== 1) { + this.raiseError(`Expected one code action, got ${details.codeActions.length}`); + } + + if (details.codeActions[0].description !== options.description) { + this.raiseError(`Expected description to be:\n${options.description}\ngot:\n${details.codeActions[0].description}`); + } + + this.applyCodeActions(details.codeActions); + + this.verifyNewContent(options); + } + public verifyRangeIs(expectedText: string, includeWhiteSpace?: boolean) { const ranges = this.getRanges(); if (ranges.length !== 1) { @@ -2360,6 +2383,10 @@ Actual: ${stringify(fullActual)}`); this.applyEdits(change.fileName, change.textChanges, /*isFormattingEdit*/ false); } + this.verifyNewContent(options); + } + + private verifyNewContent(options: FourSlashInterface.NewContentOptions) { if (options.newFileContent) { assert(!options.newRangeContent); this.verifyCurrentFileContent(options.newFileContent); @@ -2933,7 +2960,15 @@ Actual: ${stringify(fullActual)}`); return text.substring(startPos, endPos); } - private assertItemInCompletionList(items: ts.CompletionEntry[], name: string, text?: string, documentation?: string, kind?: string, spanIndex?: number) { + private assertItemInCompletionList( + items: ts.CompletionEntry[], + name: string, + text: string | undefined, + documentation: string | undefined, + kind: string | undefined, + spanIndex: number | undefined, + hasAction: boolean | undefined, + ) { for (const item of items) { if (item.name === name) { if (documentation !== undefined || text !== undefined) { @@ -2956,6 +2991,8 @@ Actual: ${stringify(fullActual)}`); assert.isTrue(TestState.textSpansEqual(span, item.replacementSpan), this.assertionMessageAtLastKnownMarker(stringify(span) + " does not equal " + stringify(item.replacementSpan) + " replacement span for " + name)); } + assert.equal(item.hasAction, hasAction); + return; } } @@ -3669,12 +3706,12 @@ namespace FourSlashInterface { // Verifies the completion list contains the specified symbol. The // completion list is brought up if necessary - public completionListContains(symbol: string, text?: string, documentation?: string, kind?: string, spanIndex?: number) { + public completionListContains(symbol: string, text?: string, documentation?: string, kind?: string, spanIndex?: number, hasAction?: boolean) { if (this.negative) { this.state.verifyCompletionListDoesNotContain(symbol, text, documentation, kind, spanIndex); } else { - this.state.verifyCompletionListContains(symbol, text, documentation, kind, spanIndex); + this.state.verifyCompletionListContains(symbol, text, documentation, kind, spanIndex, hasAction); } } @@ -3999,6 +4036,10 @@ namespace FourSlashInterface { this.state.getAndApplyCodeActions(errorCode, index); } + public applyCodeActionFromCompletion(markerName: string, options: VerifyCompletionActionOptions): void { + this.state.applyCodeActionFromCompletion(markerName, options); + } + public importFixAtPosition(expectedTextArray: string[], errorCode?: number): void { this.state.verifyImportFixAtPosition(expectedTextArray, errorCode); } @@ -4396,12 +4437,20 @@ namespace FourSlashInterface { isNewIdentifierLocation?: boolean; } - export interface VerifyCodeFixOptions { - description: string; - // One of these should be defined. + export interface NewContentOptions { + // Exactly one of these should be defined. newFileContent?: string; newRangeContent?: string; + } + + export interface VerifyCodeFixOptions extends NewContentOptions { + description: string; errorCode?: number; index?: number; } + + export interface VerifyCompletionActionOptions extends NewContentOptions { + name: string; + description: string; + } } diff --git a/src/harness/harnessLanguageService.ts b/src/harness/harnessLanguageService.ts index ad79c96d833f8..527824ee145aa 100644 --- a/src/harness/harnessLanguageService.ts +++ b/src/harness/harnessLanguageService.ts @@ -405,8 +405,8 @@ namespace Harness.LanguageService { getCompletionsAtPosition(fileName: string, position: number): ts.CompletionInfo { return unwrapJSONCallResult(this.shim.getCompletionsAtPosition(fileName, position)); } - getCompletionEntryDetails(fileName: string, position: number, entryName: string): ts.CompletionEntryDetails { - return unwrapJSONCallResult(this.shim.getCompletionEntryDetails(fileName, position, entryName)); + getCompletionEntryDetails(fileName: string, position: number, entryName: string, options: ts.FormatCodeOptions): ts.CompletionEntryDetails { + return unwrapJSONCallResult(this.shim.getCompletionEntryDetails(fileName, position, entryName, JSON.stringify(options))); } getCompletionEntrySymbol(): ts.Symbol { throw new Error("getCompletionEntrySymbol not implemented across the shim layer."); diff --git a/src/server/client.ts b/src/server/client.ts index d08d1e13d2e66..39e30848e3e52 100644 --- a/src/server/client.ts +++ b/src/server/client.ts @@ -198,7 +198,9 @@ namespace ts.server { const request = this.processRequest(CommandNames.CompletionDetails, args); const response = this.processResponse(request); Debug.assert(response.body.length === 1, "Unexpected length of completion details response body."); - return response.body[0]; + + const convertedCodeActions = map(response.body[0].codeActions, codeAction => this.convertCodeActions(codeAction, fileName)); + return { ...response.body[0], codeActions: convertedCodeActions }; } getCompletionEntrySymbol(_fileName: string, _position: number, _entryName: string): Symbol { diff --git a/src/server/protocol.ts b/src/server/protocol.ts index 3d07392bbe69f..7b9e9fe80969a 100644 --- a/src/server/protocol.ts +++ b/src/server/protocol.ts @@ -1658,6 +1658,11 @@ namespace ts.server.protocol { * this span should be used instead of the default one. */ replacementSpan?: TextSpan; + /** + * Indicates whether commiting this completion entry will require additional code actions to be + * made to avoid errors. The CompletionEntryDetails will have these actions. + */ + hasAction?: true; } /** @@ -1690,6 +1695,11 @@ namespace ts.server.protocol { * JSDoc tags for the symbol. */ tags: JSDocTagInfo[]; + + /** + * The associated code actions for this entry + */ + codeActions?: CodeAction[]; } export interface CompletionsResponse extends Response { diff --git a/src/server/session.ts b/src/server/session.ts index 800d09ff6c283..d10daecc92d1e 100644 --- a/src/server/session.ts +++ b/src/server/session.ts @@ -1178,11 +1178,12 @@ namespace ts.server { const completions = project.getLanguageService().getCompletionsAtPosition(file, position); if (simplifiedResult) { - return mapDefined(completions && completions.entries, entry => { + return mapDefined(completions && completions.entries, entry => { if (completions.isMemberCompletion || (entry.name.toLowerCase().indexOf(prefix.toLowerCase()) === 0)) { - const { name, kind, kindModifiers, sortText, replacementSpan } = entry; + const { name, kind, kindModifiers, sortText, replacementSpan, hasAction } = entry; const convertedSpan = replacementSpan ? this.decorateSpan(replacementSpan, scriptInfo) : undefined; - return { name, kind, kindModifiers, sortText, replacementSpan: convertedSpan }; + // Use `hasAction || undefined` to avoid serializing `false`. + return { name, kind, kindModifiers, sortText, replacementSpan: convertedSpan, hasAction: hasAction || undefined }; } }).sort((a, b) => compareStrings(a.name, b.name)); } @@ -1193,10 +1194,20 @@ namespace ts.server { private getCompletionEntryDetails(args: protocol.CompletionDetailsRequestArgs): ReadonlyArray { const { file, project } = this.getFileAndProject(args); - const position = this.getPositionInFile(args, file); + const scriptInfo = this.projectService.getScriptInfoForNormalizedPath(file); + const position = this.getPosition(args, scriptInfo); + const formattingOptions = project.projectService.getFormatCodeOptions(file); - return mapDefined(args.entryNames, entryName => - project.getLanguageService().getCompletionEntryDetails(file, position, entryName)); + return mapDefined(args.entryNames, entryName => { + const details = project.getLanguageService().getCompletionEntryDetails(file, position, entryName, formattingOptions); + if (details) { + const mappedCodeActions = map(details.codeActions, action => this.mapCodeAction(action, scriptInfo)); + return { ...details, codeActions: mappedCodeActions }; + } + else { + return undefined; + } + }); } private getCompileOnSaveAffectedFileList(args: protocol.FileRequestArgs): ReadonlyArray { diff --git a/src/services/codeFixProvider.ts b/src/services/codeFixProvider.ts index 13e11ed4674f3..ad9ab520dabcf 100644 --- a/src/services/codeFixProvider.ts +++ b/src/services/codeFixProvider.ts @@ -5,15 +5,13 @@ namespace ts { getCodeActions(context: CodeFixContext): CodeAction[] | undefined; } - export interface CodeFixContext { + export interface CodeFixContext extends textChanges.TextChangesContext { errorCode: number; sourceFile: SourceFile; span: TextSpan; program: Program; - newLineCharacter: string; host: LanguageServiceHost; cancellationToken: CancellationToken; - rulesProvider: formatting.RulesProvider; } export namespace codefix { diff --git a/src/services/codefixes/importFixes.ts b/src/services/codefixes/importFixes.ts index 9b54543231dd4..3e4e0f9e82246 100644 --- a/src/services/codefixes/importFixes.ts +++ b/src/services/codefixes/importFixes.ts @@ -1,5 +1,7 @@ /* @internal */ namespace ts.codefix { + import ChangeTracker = textChanges.ChangeTracker; + registerCodeFix({ errorCodes: [ Diagnostics.Cannot_find_name_0.code, @@ -11,11 +13,35 @@ namespace ts.codefix { }); type ImportCodeActionKind = "CodeChange" | "InsertingIntoExistingImport" | "NewImport"; + // Map from module Id to an array of import declarations in that module. + type ImportDeclarationMap = AnyImportSyntax[][]; + interface ImportCodeAction extends CodeAction { kind: ImportCodeActionKind; moduleSpecifier?: string; } + interface SymbolContext extends textChanges.TextChangesContext { + sourceFile: SourceFile; + symbolName: string; + } + + interface SymbolAndTokenContext extends SymbolContext { + symbolToken: Node | undefined; + } + + interface ImportCodeFixContext extends SymbolAndTokenContext { + host: LanguageServiceHost; + checker: TypeChecker; + compilerOptions: CompilerOptions; + getCanonicalFileName(fileName: string): string; + cachedImportDeclarations?: ImportDeclarationMap; + } + + export interface ImportCodeFixOptions extends ImportCodeFixContext { + kind: ImportKind; + } + const enum ModuleSpecifierComparison { Better, Equal, @@ -118,561 +144,550 @@ namespace ts.codefix { } } - function getImportCodeActions(context: CodeFixContext): ImportCodeAction[] { - const sourceFile = context.sourceFile; - const checker = context.program.getTypeChecker(); - const allSourceFiles = context.program.getSourceFiles(); + function createCodeAction( + description: DiagnosticMessage, + diagnosticArgs: string[], + changes: FileTextChanges[], + kind: ImportCodeActionKind, + moduleSpecifier: string | undefined, + ): ImportCodeAction { + return { + description: formatMessage.apply(undefined, [undefined, description].concat(diagnosticArgs)), + changes, + kind, + moduleSpecifier + }; + } + + function convertToImportCodeFixContext(context: CodeFixContext): ImportCodeFixContext { const useCaseSensitiveFileNames = context.host.useCaseSensitiveFileNames ? context.host.useCaseSensitiveFileNames() : false; + const checker = context.program.getTypeChecker(); + const symbolToken = getTokenAtPosition(context.sourceFile, context.span.start, /*includeJsDocComment*/ false); + return { + host: context.host, + newLineCharacter: context.newLineCharacter, + rulesProvider: context.rulesProvider, + sourceFile: context.sourceFile, + checker, + compilerOptions: context.program.getCompilerOptions(), + cachedImportDeclarations: [], + getCanonicalFileName: createGetCanonicalFileName(useCaseSensitiveFileNames), + symbolName: symbolToken.getText(), + symbolToken, + }; + } - const token = getTokenAtPosition(sourceFile, context.span.start, /*includeJsDocComment*/ false); - const name = token.getText(); - const symbolIdActionMap = new ImportCodeActionMap(); + export const enum ImportKind { + Named, + Default, + Namespace, + } - // this is a module id -> module import declaration map - const cachedImportDeclarations: AnyImportSyntax[][] = []; - let lastImportDeclaration: Node; - - const currentTokenMeaning = getMeaningFromLocation(token); - if (context.errorCode === Diagnostics._0_refers_to_a_UMD_global_but_the_current_file_is_a_module_Consider_adding_an_import_instead.code) { - const umdSymbol = checker.getSymbolAtLocation(token); - let symbol: ts.Symbol; - let symbolName: string; - if (umdSymbol.flags & ts.SymbolFlags.Alias) { - symbol = checker.getAliasedSymbol(umdSymbol); - symbolName = name; - } - else if (isJsxOpeningLikeElement(token.parent) && token.parent.tagName === token) { - // The error wasn't for the symbolAtLocation, it was for the JSX tag itself, which needs access to e.g. `React`. - symbol = checker.getAliasedSymbol(checker.resolveName(checker.getJsxNamespace(), token.parent.tagName, SymbolFlags.Value)); - symbolName = symbol.name; - } - else { - Debug.fail("Either the symbol or the JSX namespace should be a UMD global if we got here"); + export function getCodeActionForImport(moduleSymbol: Symbol, context: ImportCodeFixOptions): ImportCodeAction[] { + const declarations = getImportDeclarations(moduleSymbol, context.checker, context.sourceFile, context.cachedImportDeclarations); + const actions: ImportCodeAction[] = []; + if (context.symbolToken) { + // It is possible that multiple import statements with the same specifier exist in the file. + // e.g. + // + // import * as ns from "foo"; + // import { member1, member2 } from "foo"; + // + // member3/**/ <-- cusor here + // + // in this case we should provie 2 actions: + // 1. change "member3" to "ns.member3" + // 2. add "member3" to the second import statement's import list + // and it is up to the user to decide which one fits best. + for (const declaration of declarations) { + const namespace = getNamespaceImportName(declaration); + if (namespace) { + actions.push(getCodeActionForUseExistingNamespaceImport(namespace.text, context, context.symbolToken)); + } } + } + actions.push(getCodeActionForAddImport(moduleSymbol, context, declarations)); + return actions; + } - return getCodeActionForImport(symbol, symbolName, /*isDefault*/ false, /*isNamespaceImport*/ true); + function getNamespaceImportName(declaration: AnyImportSyntax): Identifier { + if (declaration.kind === SyntaxKind.ImportDeclaration) { + const namedBindings = declaration.importClause && declaration.importClause.namedBindings; + return namedBindings && namedBindings.kind === SyntaxKind.NamespaceImport ? namedBindings.name : undefined; + } + else { + return declaration.name; } + } - const candidateModules = checker.getAmbientModules(); - for (const otherSourceFile of allSourceFiles) { - if (otherSourceFile !== sourceFile && isExternalOrCommonJsModule(otherSourceFile)) { - candidateModules.push(otherSourceFile.symbol); - } + // TODO(anhans): This doesn't seem important to cache... just use an iterator instead of creating a new array? + function getImportDeclarations(moduleSymbol: Symbol, checker: TypeChecker, { imports }: SourceFile, cachedImportDeclarations: ImportDeclarationMap = []): ReadonlyArray { + const moduleSymbolId = getUniqueSymbolId(moduleSymbol, checker); + let cached = cachedImportDeclarations[moduleSymbolId]; + if (!cached) { + cached = cachedImportDeclarations[moduleSymbolId] = mapDefined(imports, importModuleSpecifier => + checker.getSymbolAtLocation(importModuleSpecifier) === moduleSymbol ? getImportDeclaration(importModuleSpecifier) : undefined); } + return cached; + } - for (const moduleSymbol of candidateModules) { - context.cancellationToken.throwIfCancellationRequested(); + function getImportDeclaration({ parent }: LiteralExpression): AnyImportSyntax | undefined { + switch (parent.kind) { + case SyntaxKind.ImportDeclaration: + return parent as ImportDeclaration; + case SyntaxKind.ExternalModuleReference: + return (parent as ExternalModuleReference).parent; + default: + Debug.assert(parent.kind === SyntaxKind.ExportDeclaration); + // Ignore these, can't add imports to them. + return undefined; + } + } - // check the default export - const defaultExport = checker.tryGetMemberInModuleExports("default", moduleSymbol); - if (defaultExport) { - const localSymbol = getLocalSymbolForExportDefault(defaultExport); - if (localSymbol && localSymbol.escapedName === name && checkSymbolHasMeaning(localSymbol, currentTokenMeaning)) { - // check if this symbol is already used - const symbolId = getUniqueSymbolId(localSymbol); - symbolIdActionMap.addActions(symbolId, getCodeActionForImport(moduleSymbol, name, /*isNamespaceImport*/ true)); - } + function getCodeActionForNewImport(context: SymbolContext & { kind: ImportKind }, moduleSpecifier: string): ImportCodeAction { + const { kind, sourceFile, newLineCharacter, symbolName } = context; + const lastImportDeclaration = findLast(sourceFile.statements, isAnyImportSyntax); + + const moduleSpecifierWithoutQuotes = stripQuotes(moduleSpecifier); + const importDecl = createImportDeclaration(/*decorators*/ undefined, /*modifiers*/ undefined, createImportClauseOfKind(kind, symbolName), createStringLiteralWithQuoteStyle(sourceFile, moduleSpecifierWithoutQuotes)); + const changes = ChangeTracker.with(context, changeTracker => { + if (lastImportDeclaration) { + changeTracker.insertNodeAfter(sourceFile, lastImportDeclaration, importDecl, { suffix: newLineCharacter }); } + else { + changeTracker.insertNodeAt(sourceFile, getSourceFileImportLocation(sourceFile), importDecl, { suffix: `${newLineCharacter}${newLineCharacter}` }); + } + }); + + // if this file doesn't have any import statements, insert an import statement and then insert a new line + // between the only import statement and user code. Otherwise just insert the statement because chances + // are there are already a new line seperating code and import statements. + return createCodeAction( + Diagnostics.Import_0_from_1, + [symbolName, moduleSpecifierWithoutQuotes], + changes, + "NewImport", + moduleSpecifierWithoutQuotes, + ); + } - // "default" is a keyword and not a legal identifier for the import, so we don't expect it here - Debug.assert(name !== "default"); + function createStringLiteralWithQuoteStyle(sourceFile: SourceFile, text: string): StringLiteral { + const literal = createLiteral(text); + const firstModuleSpecifier = firstOrUndefined(sourceFile.imports); + literal.singleQuote = !!firstModuleSpecifier && !isStringDoubleQuoted(firstModuleSpecifier, sourceFile); + return literal; + } - // check exports with the same name - const exportSymbolWithIdenticalName = checker.tryGetMemberInModuleExportsAndProperties(name, moduleSymbol); - if (exportSymbolWithIdenticalName && checkSymbolHasMeaning(exportSymbolWithIdenticalName, currentTokenMeaning)) { - const symbolId = getUniqueSymbolId(exportSymbolWithIdenticalName); - symbolIdActionMap.addActions(symbolId, getCodeActionForImport(moduleSymbol, name)); - } + function createImportClauseOfKind(kind: ImportKind, symbolName: string) { + switch (kind) { + case ImportKind.Default: + return createImportClause(createIdentifier(symbolName), /*namedBindings*/ undefined); + case ImportKind.Namespace: + return createImportClause(/*name*/ undefined, createNamespaceImport(createIdentifier(symbolName))); + case ImportKind.Named: + return createImportClause(/*name*/ undefined, createNamedImports([createImportSpecifier(/*propertyName*/ undefined, createIdentifier(symbolName))])); + default: + Debug.assertNever(kind); } + } - return symbolIdActionMap.getAllActions(); - - function getImportDeclarations(moduleSymbol: Symbol) { - const moduleSymbolId = getUniqueSymbolId(moduleSymbol); + function getModuleSpecifierForNewImport(sourceFile: SourceFile, moduleSymbol: Symbol, options: CompilerOptions, getCanonicalFileName: (file: string) => string, host: LanguageServiceHost): string | undefined { + const moduleFileName = moduleSymbol.valueDeclaration.getSourceFile().fileName; + const sourceDirectory = getDirectoryPath(sourceFile.fileName); - const cached = cachedImportDeclarations[moduleSymbolId]; - if (cached) { - return cached; - } + return tryGetModuleNameFromAmbientModule(moduleSymbol) || + tryGetModuleNameFromTypeRoots(options, host, getCanonicalFileName, moduleFileName) || + tryGetModuleNameAsNodeModule(options, moduleFileName, host, getCanonicalFileName, sourceDirectory) || + tryGetModuleNameFromBaseUrl(options, moduleFileName, getCanonicalFileName) || + options.rootDirs && tryGetModuleNameFromRootDirs(options.rootDirs, moduleFileName, sourceDirectory, getCanonicalFileName) || + removeFileExtension(getRelativePath(moduleFileName, sourceDirectory, getCanonicalFileName)); + } - const existingDeclarations = mapDefined(sourceFile.imports, importModuleSpecifier => - checker.getSymbolAtLocation(importModuleSpecifier) === moduleSymbol ? getImportDeclaration(importModuleSpecifier) : undefined); - cachedImportDeclarations[moduleSymbolId] = existingDeclarations; - return existingDeclarations; - - function getImportDeclaration({ parent }: LiteralExpression): AnyImportSyntax { - switch (parent.kind) { - case SyntaxKind.ImportDeclaration: - return parent as ImportDeclaration; - case SyntaxKind.ExternalModuleReference: - return (parent as ExternalModuleReference).parent; - default: - return undefined; - } - } + function tryGetModuleNameFromAmbientModule(moduleSymbol: Symbol): string | undefined { + const decl = moduleSymbol.valueDeclaration; + if (isModuleDeclaration(decl) && isStringLiteral(decl.name)) { + return decl.name.text; } + } - function getUniqueSymbolId(symbol: Symbol) { - return getSymbolId(skipAlias(symbol, checker)); + function tryGetModuleNameFromBaseUrl(options: CompilerOptions, moduleFileName: string, getCanonicalFileName: (file: string) => string): string | undefined { + if (!options.baseUrl) { + return undefined; } - function checkSymbolHasMeaning(symbol: Symbol, meaning: SemanticMeaning) { - const declarations = symbol.getDeclarations(); - return declarations ? some(symbol.declarations, decl => !!(getMeaningFromDeclaration(decl) & meaning)) : false; + let relativeName = getRelativePathIfInDirectory(moduleFileName, options.baseUrl, getCanonicalFileName); + if (!relativeName) { + return undefined; } - function getCodeActionForImport(moduleSymbol: Symbol, symbolName: string, isDefault?: boolean, isNamespaceImport?: boolean): ImportCodeAction[] { - const existingDeclarations = getImportDeclarations(moduleSymbol); - if (existingDeclarations.length > 0) { - // With an existing import statement, there are more than one actions the user can do. - return getCodeActionsForExistingImport(existingDeclarations); - } - else { - return [getCodeActionForNewImport()]; - } + const relativeNameWithIndex = removeFileExtension(relativeName); + relativeName = removeExtensionAndIndexPostFix(relativeName); - function getCodeActionsForExistingImport(declarations: (ImportDeclaration | ImportEqualsDeclaration)[]): ImportCodeAction[] { - const actions: ImportCodeAction[] = []; - - // It is possible that multiple import statements with the same specifier exist in the file. - // e.g. - // - // import * as ns from "foo"; - // import { member1, member2 } from "foo"; - // - // member3/**/ <-- cusor here - // - // in this case we should provie 2 actions: - // 1. change "member3" to "ns.member3" - // 2. add "member3" to the second import statement's import list - // and it is up to the user to decide which one fits best. - let namespaceImportDeclaration: ImportDeclaration | ImportEqualsDeclaration; - let namedImportDeclaration: ImportDeclaration; - let existingModuleSpecifier: string; - for (const declaration of declarations) { - if (declaration.kind === SyntaxKind.ImportDeclaration) { - const namedBindings = declaration.importClause && declaration.importClause.namedBindings; - if (namedBindings && namedBindings.kind === SyntaxKind.NamespaceImport) { - // case: - // import * as ns from "foo" - namespaceImportDeclaration = declaration; - } - else { - // cases: - // import default from "foo" - // import { bar } from "foo" or combination with the first one - // import "foo" - namedImportDeclaration = declaration; + if (options.paths) { + for (const key in options.paths) { + for (const pattern of options.paths[key]) { + const indexOfStar = pattern.indexOf("*"); + if (indexOfStar === 0 && pattern.length === 1) { + continue; + } + else if (indexOfStar !== -1) { + const prefix = pattern.substr(0, indexOfStar); + const suffix = pattern.substr(indexOfStar + 1); + if (relativeName.length >= prefix.length + suffix.length && + startsWith(relativeName, prefix) && + endsWith(relativeName, suffix)) { + const matchedStar = relativeName.substr(prefix.length, relativeName.length - suffix.length); + return key.replace("\*", matchedStar); } - existingModuleSpecifier = declaration.moduleSpecifier.getText(); } - else { - // case: - // import foo = require("foo") - namespaceImportDeclaration = declaration; - existingModuleSpecifier = getModuleSpecifierFromImportEqualsDeclaration(declaration); + else if (pattern === relativeName || pattern === relativeNameWithIndex) { + return key; } } + } + } - if (namespaceImportDeclaration) { - actions.push(getCodeActionForNamespaceImport(namespaceImportDeclaration)); - } - - if (!isNamespaceImport && namedImportDeclaration && namedImportDeclaration.importClause && - (namedImportDeclaration.importClause.name || namedImportDeclaration.importClause.namedBindings)) { - /** - * If the existing import declaration already has a named import list, just - * insert the identifier into that list. - */ - const fileTextChanges = getTextChangeForImportClause(namedImportDeclaration.importClause); - const moduleSpecifierWithoutQuotes = stripQuotes(namedImportDeclaration.moduleSpecifier.getText()); - actions.push(createCodeAction( - Diagnostics.Add_0_to_existing_import_declaration_from_1, - [name, moduleSpecifierWithoutQuotes], - fileTextChanges, - "InsertingIntoExistingImport", - moduleSpecifierWithoutQuotes - )); - } - else { - // we need to create a new import statement, but the existing module specifier can be reused. - actions.push(getCodeActionForNewImport(existingModuleSpecifier)); - } - return actions; - - function getModuleSpecifierFromImportEqualsDeclaration(declaration: ImportEqualsDeclaration) { - if (declaration.moduleReference && declaration.moduleReference.kind === SyntaxKind.ExternalModuleReference) { - return declaration.moduleReference.expression.getText(); - } - return declaration.moduleReference.getText(); - } + return relativeName; + } - function getTextChangeForImportClause(importClause: ImportClause): FileTextChanges[] { - const importList = importClause.namedBindings; - const newImportSpecifier = createImportSpecifier(/*propertyName*/ undefined, createIdentifier(name)); - // case 1: - // original text: import default from "module" - // change to: import default, { name } from "module" - // case 2: - // original text: import {} from "module" - // change to: import { name } from "module" - if (!importList || importList.elements.length === 0) { - const newImportClause = createImportClause(importClause.name, createNamedImports([newImportSpecifier])); - return createChangeTracker().replaceNode(sourceFile, importClause, newImportClause).getChanges(); - } + function tryGetModuleNameFromRootDirs(rootDirs: ReadonlyArray, moduleFileName: string, sourceDirectory: string, getCanonicalFileName: (file: string) => string): string | undefined { + const normalizedTargetPath = getPathRelativeToRootDirs(moduleFileName, rootDirs, getCanonicalFileName); + if (normalizedTargetPath === undefined) { + return undefined; + } - /** - * If the import list has one import per line, preserve that. Otherwise, insert on same line as last element - * import { - * foo - * } from "./module"; - */ - return createChangeTracker().insertNodeInListAfter( - sourceFile, - importList.elements[importList.elements.length - 1], - newImportSpecifier).getChanges(); - } + const normalizedSourcePath = getPathRelativeToRootDirs(sourceDirectory, rootDirs, getCanonicalFileName); + const relativePath = normalizedSourcePath !== undefined ? getRelativePath(normalizedTargetPath, normalizedSourcePath, getCanonicalFileName) : normalizedTargetPath; + return removeFileExtension(relativePath); + } - function getCodeActionForNamespaceImport(declaration: ImportDeclaration | ImportEqualsDeclaration): ImportCodeAction { - let namespacePrefix: string; - if (declaration.kind === SyntaxKind.ImportDeclaration) { - namespacePrefix = (declaration.importClause.namedBindings).name.getText(); - } - else { - namespacePrefix = declaration.name.getText(); - } - namespacePrefix = stripQuotes(namespacePrefix); - - /** - * Cases: - * import * as ns from "mod" - * import default, * as ns from "mod" - * import ns = require("mod") - * - * Because there is no import list, we alter the reference to include the - * namespace instead of altering the import declaration. For example, "foo" would - * become "ns.foo" - */ - return createCodeAction( - Diagnostics.Change_0_to_1, - [name, `${namespacePrefix}.${name}`], - createChangeTracker().replaceNode(sourceFile, token, createPropertyAccess(createIdentifier(namespacePrefix), name)).getChanges(), - "CodeChange" - ); - } + function tryGetModuleNameFromTypeRoots( + options: CompilerOptions, + host: GetEffectiveTypeRootsHost, + getCanonicalFileName: (file: string) => string, + moduleFileName: string, + ): string | undefined { + return firstDefined(getEffectiveTypeRoots(options, host), unNormalizedTypeRoot => { + const typeRoot = toPath(unNormalizedTypeRoot, /*basePath*/ undefined, getCanonicalFileName); + if (startsWith(moduleFileName, typeRoot)) { + return removeExtensionAndIndexPostFix(moduleFileName.substring(typeRoot.length + 1)); } + }); + } - function getCodeActionForNewImport(moduleSpecifier?: string): ImportCodeAction { - if (!lastImportDeclaration) { - // insert after any existing imports - for (let i = sourceFile.statements.length - 1; i >= 0; i--) { - const statement = sourceFile.statements[i]; - if (statement.kind === SyntaxKind.ImportEqualsDeclaration || statement.kind === SyntaxKind.ImportDeclaration) { - lastImportDeclaration = statement; - break; - } - } - } + function tryGetModuleNameAsNodeModule( + options: CompilerOptions, + moduleFileName: string, + host: LanguageServiceHost, + getCanonicalFileName: (file: string) => string, + sourceDirectory: string, + ): string | undefined { + if (getEmitModuleResolutionKind(options) !== ModuleResolutionKind.NodeJs) { + // nothing to do here + return undefined; + } - const getCanonicalFileName = createGetCanonicalFileName(useCaseSensitiveFileNames); - const moduleSpecifierWithoutQuotes = stripQuotes(moduleSpecifier || getModuleSpecifierForNewImport()); - const changeTracker = createChangeTracker(); - const importClause = isDefault - ? createImportClause(createIdentifier(symbolName), /*namedBindings*/ undefined) - : isNamespaceImport - ? createImportClause(/*name*/ undefined, createNamespaceImport(createIdentifier(symbolName))) - : createImportClause(/*name*/ undefined, createNamedImports([createImportSpecifier(/*propertyName*/ undefined, createIdentifier(symbolName))])); - const moduleSpecifierLiteral = createLiteral(moduleSpecifierWithoutQuotes); - moduleSpecifierLiteral.singleQuote = getSingleQuoteStyleFromExistingImports(); - const importDecl = createImportDeclaration(/*decorators*/ undefined, /*modifiers*/ undefined, importClause, moduleSpecifierLiteral); - if (!lastImportDeclaration) { - changeTracker.insertNodeAt(sourceFile, getSourceFileImportLocation(sourceFile), importDecl, { suffix: `${context.newLineCharacter}${context.newLineCharacter}` }); - } - else { - changeTracker.insertNodeAfter(sourceFile, lastImportDeclaration, importDecl, { suffix: context.newLineCharacter }); - } + const parts = getNodeModulePathParts(moduleFileName); - // if this file doesn't have any import statements, insert an import statement and then insert a new line - // between the only import statement and user code. Otherwise just insert the statement because chances - // are there are already a new line seperating code and import statements. - return createCodeAction( - Diagnostics.Import_0_from_1, - [symbolName, `"${moduleSpecifierWithoutQuotes}"`], - changeTracker.getChanges(), - "NewImport", - moduleSpecifierWithoutQuotes - ); - - function getSingleQuoteStyleFromExistingImports() { - const firstModuleSpecifier = forEach(sourceFile.statements, node => { - if (isImportDeclaration(node) || isExportDeclaration(node)) { - if (node.moduleSpecifier && isStringLiteral(node.moduleSpecifier)) { - return node.moduleSpecifier; - } - } - else if (isImportEqualsDeclaration(node)) { - if (isExternalModuleReference(node.moduleReference) && isStringLiteral(node.moduleReference.expression)) { - return node.moduleReference.expression; - } + if (!parts) { + return undefined; + } + + // Simplify the full file path to something that can be resolved by Node. + + // If the module could be imported by a directory name, use that directory's name + let moduleSpecifier = getDirectoryOrExtensionlessFileName(moduleFileName); + // Get a path that's relative to node_modules or the importing file's path + moduleSpecifier = getNodeResolvablePath(moduleSpecifier); + // If the module was found in @types, get the actual Node package name + return getPackageNameFromAtTypesDirectory(moduleSpecifier); + + function getDirectoryOrExtensionlessFileName(path: string): string { + // If the file is the main module, it can be imported by the package name + const packageRootPath = path.substring(0, parts.packageRootIndex); + const packageJsonPath = combinePaths(packageRootPath, "package.json"); + if (host.fileExists(packageJsonPath)) { + const packageJsonContent = JSON.parse(host.readFile(packageJsonPath)); + if (packageJsonContent) { + const mainFileRelative = packageJsonContent.typings || packageJsonContent.types || packageJsonContent.main; + if (mainFileRelative) { + const mainExportFile = toPath(mainFileRelative, packageRootPath, getCanonicalFileName); + if (mainExportFile === getCanonicalFileName(path)) { + return packageRootPath; } - }); - if (firstModuleSpecifier) { - return sourceFile.text.charCodeAt(firstModuleSpecifier.getStart()) === CharacterCodes.singleQuote; } } + } - function getModuleSpecifierForNewImport() { - const fileName = sourceFile.fileName; - const moduleFileName = moduleSymbol.valueDeclaration.getSourceFile().fileName; - const sourceDirectory = getDirectoryPath(fileName); - const options = context.program.getCompilerOptions(); - - return tryGetModuleNameFromAmbientModule() || - tryGetModuleNameFromTypeRoots() || - tryGetModuleNameAsNodeModule() || - tryGetModuleNameFromBaseUrl() || - tryGetModuleNameFromRootDirs() || - removeFileExtension(getRelativePath(moduleFileName, sourceDirectory)); - - function tryGetModuleNameFromAmbientModule(): string { - const decl = moduleSymbol.valueDeclaration; - if (isModuleDeclaration(decl) && isStringLiteral(decl.name)) { - return decl.name.text; - } - } + // We still have a file name - remove the extension + const fullModulePathWithoutExtension = removeFileExtension(path); - function tryGetModuleNameFromBaseUrl() { - if (!options.baseUrl) { - return undefined; - } + // If the file is /index, it can be imported by its directory name + if (getCanonicalFileName(fullModulePathWithoutExtension.substring(parts.fileNameIndex)) === "/index") { + return fullModulePathWithoutExtension.substring(0, parts.fileNameIndex); + } - let relativeName = getRelativePathIfInDirectory(moduleFileName, options.baseUrl); - if (!relativeName) { - return undefined; - } + return fullModulePathWithoutExtension; + } - const relativeNameWithIndex = removeFileExtension(relativeName); - relativeName = removeExtensionAndIndexPostFix(relativeName); - - if (options.paths) { - for (const key in options.paths) { - for (const pattern of options.paths[key]) { - const indexOfStar = pattern.indexOf("*"); - if (indexOfStar === 0 && pattern.length === 1) { - continue; - } - else if (indexOfStar !== -1) { - const prefix = pattern.substr(0, indexOfStar); - const suffix = pattern.substr(indexOfStar + 1); - if (relativeName.length >= prefix.length + suffix.length && - startsWith(relativeName, prefix) && - endsWith(relativeName, suffix)) { - const matchedStar = relativeName.substr(prefix.length, relativeName.length - suffix.length); - return key.replace("\*", matchedStar); - } - } - else if (pattern === relativeName || pattern === relativeNameWithIndex) { - return key; - } - } - } - } + function getNodeResolvablePath(path: string): string { + const basePath = path.substring(0, parts.topLevelNodeModulesIndex); + if (sourceDirectory.indexOf(basePath) === 0) { + // if node_modules folder is in this folder or any of its parent folders, no need to keep it. + return path.substring(parts.topLevelPackageNameIndex + 1); + } + else { + return getRelativePath(path, sourceDirectory, getCanonicalFileName); + } + } + } - return relativeName; - } + function getNodeModulePathParts(fullPath: string) { + // If fullPath can't be valid module file within node_modules, returns undefined. + // Example of expected pattern: /base/path/node_modules/[@scope/otherpackage/@otherscope/node_modules/]package/[subdirectory/]file.js + // Returns indices: ^ ^ ^ ^ + + let topLevelNodeModulesIndex = 0; + let topLevelPackageNameIndex = 0; + let packageRootIndex = 0; + let fileNameIndex = 0; + + const enum States { + BeforeNodeModules, + NodeModules, + Scope, + PackageContent + } - function tryGetModuleNameFromRootDirs() { - if (options.rootDirs) { - const normalizedTargetPath = getPathRelativeToRootDirs(moduleFileName, options.rootDirs); - const normalizedSourcePath = getPathRelativeToRootDirs(sourceDirectory, options.rootDirs); - if (normalizedTargetPath !== undefined) { - const relativePath = normalizedSourcePath !== undefined ? getRelativePath(normalizedTargetPath, normalizedSourcePath) : normalizedTargetPath; - return removeFileExtension(relativePath); - } - } - return undefined; + let partStart = 0; + let partEnd = 0; + let state = States.BeforeNodeModules; + + while (partEnd >= 0) { + partStart = partEnd; + partEnd = fullPath.indexOf("/", partStart + 1); + switch (state) { + case States.BeforeNodeModules: + if (fullPath.indexOf("/node_modules/", partStart) === partStart) { + topLevelNodeModulesIndex = partStart; + topLevelPackageNameIndex = partEnd; + state = States.NodeModules; } - - function tryGetModuleNameFromTypeRoots() { - const typeRoots = getEffectiveTypeRoots(options, context.host); - if (typeRoots) { - const normalizedTypeRoots = map(typeRoots, typeRoot => toPath(typeRoot, /*basePath*/ undefined, getCanonicalFileName)); - for (const typeRoot of normalizedTypeRoots) { - if (startsWith(moduleFileName, typeRoot)) { - const relativeFileName = moduleFileName.substring(typeRoot.length + 1); - return removeExtensionAndIndexPostFix(relativeFileName); - } - } - } + break; + case States.NodeModules: + case States.Scope: + if (state === States.NodeModules && fullPath.charAt(partStart + 1) === "@") { + state = States.Scope; } + else { + packageRootIndex = partEnd; + state = States.PackageContent; + } + break; + case States.PackageContent: + if (fullPath.indexOf("/node_modules/", partStart) === partStart) { + state = States.NodeModules; + } + else { + state = States.PackageContent; + } + break; + } + } - function tryGetModuleNameAsNodeModule() { - if (getEmitModuleResolutionKind(options) !== ModuleResolutionKind.NodeJs) { - // nothing to do here - return undefined; - } - - const parts = getNodeModulePathParts(moduleFileName); - - if (!parts) { - return undefined; - } + fileNameIndex = partStart; - // Simplify the full file path to something that can be resolved by Node. - - // If the module could be imported by a directory name, use that directory's name - let moduleSpecifier = getDirectoryOrExtensionlessFileName(moduleFileName); - // Get a path that's relative to node_modules or the importing file's path - moduleSpecifier = getNodeResolvablePath(moduleSpecifier); - // If the module was found in @types, get the actual Node package name - return getPackageNameFromAtTypesDirectory(moduleSpecifier); - - function getDirectoryOrExtensionlessFileName(path: string): string { - // If the file is the main module, it can be imported by the package name - const packageRootPath = path.substring(0, parts.packageRootIndex); - const packageJsonPath = combinePaths(packageRootPath, "package.json"); - if (context.host.fileExists(packageJsonPath)) { - const packageJsonContent = JSON.parse(context.host.readFile(packageJsonPath)); - if (packageJsonContent) { - const mainFileRelative = packageJsonContent.typings || packageJsonContent.types || packageJsonContent.main; - if (mainFileRelative) { - const mainExportFile = toPath(mainFileRelative, packageRootPath, getCanonicalFileName); - if (mainExportFile === getCanonicalFileName(path)) { - return packageRootPath; - } - } - } - } - - // We still have a file name - remove the extension - const fullModulePathWithoutExtension = removeFileExtension(path); - - // If the file is /index, it can be imported by its directory name - if (getCanonicalFileName(fullModulePathWithoutExtension.substring(parts.fileNameIndex)) === "/index") { - return fullModulePathWithoutExtension.substring(0, parts.fileNameIndex); - } - - return fullModulePathWithoutExtension; - } + return state > States.NodeModules ? { topLevelNodeModulesIndex, topLevelPackageNameIndex, packageRootIndex, fileNameIndex } : undefined; + } - function getNodeResolvablePath(path: string): string { - const basePath = path.substring(0, parts.topLevelNodeModulesIndex); - if (sourceDirectory.indexOf(basePath) === 0) { - // if node_modules folder is in this folder or any of its parent folders, no need to keep it. - return path.substring(parts.topLevelPackageNameIndex + 1); - } - else { - return getRelativePath(path, sourceDirectory); - } - } - } - } + function getPathRelativeToRootDirs(path: string, rootDirs: ReadonlyArray, getCanonicalFileName: (fileName: string) => string): string | undefined { + return firstDefined(rootDirs, rootDir => getRelativePathIfInDirectory(path, rootDir, getCanonicalFileName)); + } - function getNodeModulePathParts(fullPath: string) { - // If fullPath can't be valid module file within node_modules, returns undefined. - // Example of expected pattern: /base/path/node_modules/[@scope/otherpackage/@otherscope/node_modules/]package/[subdirectory/]file.js - // Returns indices: ^ ^ ^ ^ - - let topLevelNodeModulesIndex = 0; - let topLevelPackageNameIndex = 0; - let packageRootIndex = 0; - let fileNameIndex = 0; - - const enum States { - BeforeNodeModules, - NodeModules, - Scope, - PackageContent - } + function removeExtensionAndIndexPostFix(fileName: string) { + fileName = removeFileExtension(fileName); + if (endsWith(fileName, "/index")) { + fileName = fileName.substr(0, fileName.length - 6/* "/index".length */); + } + return fileName; + } - let partStart = 0; - let partEnd = 0; - let state = States.BeforeNodeModules; - - while (partEnd >= 0) { - partStart = partEnd; - partEnd = fullPath.indexOf("/", partStart + 1); - switch (state) { - case States.BeforeNodeModules: - if (fullPath.indexOf("/node_modules/", partStart) === partStart) { - topLevelNodeModulesIndex = partStart; - topLevelPackageNameIndex = partEnd; - state = States.NodeModules; - } - break; - case States.NodeModules: - case States.Scope: - if (state === States.NodeModules && fullPath.charAt(partStart + 1) === "@") { - state = States.Scope; - } - else { - packageRootIndex = partEnd; - state = States.PackageContent; - } - break; - case States.PackageContent: - if (fullPath.indexOf("/node_modules/", partStart) === partStart) { - state = States.NodeModules; - } - else { - state = States.PackageContent; - } - break; - } - } + function getRelativePathIfInDirectory(path: string, directoryPath: string, getCanonicalFileName: (fileName: string) => string): string | undefined { + const relativePath = getRelativePathToDirectoryOrUrl(directoryPath, path, directoryPath, getCanonicalFileName, /*isAbsolutePathAnUrl*/ false); + return isRootedDiskPath(relativePath) || startsWith(relativePath, "..") ? undefined : relativePath; + } - fileNameIndex = partStart; + function getRelativePath(path: string, directoryPath: string, getCanonicalFileName: (fileName: string) => string) { + const relativePath = getRelativePathToDirectoryOrUrl(directoryPath, path, directoryPath, getCanonicalFileName, /*isAbsolutePathAnUrl*/ false); + return !pathIsRelative(relativePath) ? "./" + relativePath : relativePath; + } - return state > States.NodeModules ? { topLevelNodeModulesIndex, topLevelPackageNameIndex, packageRootIndex, fileNameIndex } : undefined; + function getCodeActionForAddImport( + moduleSymbol: Symbol, + ctx: ImportCodeFixOptions, + declarations: ReadonlyArray): ImportCodeAction { + const fromExistingImport = firstDefined(declarations, declaration => { + if (declaration.kind === SyntaxKind.ImportDeclaration && declaration.importClause) { + const changes = tryUpdateExistingImport(ctx, ctx.kind, declaration.importClause); + if (changes) { + const moduleSpecifierWithoutQuotes = stripQuotes(declaration.moduleSpecifier.getText()); + return createCodeAction( + Diagnostics.Add_0_to_existing_import_declaration_from_1, + [ctx.symbolName, moduleSpecifierWithoutQuotes], + changes, + "InsertingIntoExistingImport", + moduleSpecifierWithoutQuotes); } + } + }); + if (fromExistingImport) { + return fromExistingImport; + } - function getPathRelativeToRootDirs(path: string, rootDirs: string[]) { - for (const rootDir of rootDirs) { - const relativeName = getRelativePathIfInDirectory(path, rootDir); - if (relativeName !== undefined) { - return relativeName; - } - } - return undefined; - } + const moduleSpecifier = firstDefined(declarations, moduleSpecifierFromAnyImport) + || getModuleSpecifierForNewImport(ctx.sourceFile, moduleSymbol, ctx.compilerOptions, ctx.getCanonicalFileName, ctx.host); + return getCodeActionForNewImport(ctx, moduleSpecifier); + } - function removeExtensionAndIndexPostFix(fileName: string) { - fileName = removeFileExtension(fileName); - if (endsWith(fileName, "/index")) { - fileName = fileName.substr(0, fileName.length - 6/* "/index".length */); - } - return fileName; - } + function moduleSpecifierFromAnyImport(node: AnyImportSyntax): string | undefined { + const expression = node.kind === SyntaxKind.ImportDeclaration + ? node.moduleSpecifier + : node.moduleReference.kind === SyntaxKind.ExternalModuleReference + ? node.moduleReference.expression + : undefined; + return expression && isStringLiteral(expression) ? expression.text : undefined; + } - function getRelativePathIfInDirectory(path: string, directoryPath: string) { - const relativePath = getRelativePathToDirectoryOrUrl(directoryPath, path, directoryPath, getCanonicalFileName, /*isAbsolutePathAnUrl*/ false); - return isRootedDiskPath(relativePath) || startsWith(relativePath, "..") ? undefined : relativePath; + function tryUpdateExistingImport(context: SymbolContext, kind: ImportKind, importClause: ImportClause): FileTextChanges[] | undefined { + const { symbolName, sourceFile } = context; + const { name, namedBindings } = importClause; + switch (kind) { + case ImportKind.Default: + return name ? undefined : ChangeTracker.with(context, t => + t.replaceNode(sourceFile, importClause, createImportClause(createIdentifier(symbolName), namedBindings))); + + case ImportKind.Named: { + const newImportSpecifier = createImportSpecifier(/*propertyName*/ undefined, createIdentifier(symbolName)); + if (namedBindings && namedBindings.kind === SyntaxKind.NamedImports && namedBindings.elements.length !== 0) { + // There are already named imports; add another. + return ChangeTracker.with(context, t => t.insertNodeInListAfter( + sourceFile, + namedBindings.elements[namedBindings.elements.length - 1], + newImportSpecifier)); } - - function getRelativePath(path: string, directoryPath: string) { - const relativePath = getRelativePathToDirectoryOrUrl(directoryPath, path, directoryPath, getCanonicalFileName, /*isAbsolutePathAnUrl*/ false); - return !pathIsRelative(relativePath) ? "./" + relativePath : relativePath; + if (!namedBindings || namedBindings.kind === SyntaxKind.NamedImports && namedBindings.elements.length === 0) { + return ChangeTracker.with(context, t => + t.replaceNode(sourceFile, importClause, createImportClause(name, createNamedImports([newImportSpecifier])))); } + return undefined; } + case ImportKind.Namespace: + return namedBindings ? undefined : ChangeTracker.with(context, t => + t.replaceNode(sourceFile, importClause, createImportClause(name, createNamespaceImport(createIdentifier(symbolName))))); + + default: + Debug.assertNever(kind); } + } + + function getCodeActionForUseExistingNamespaceImport(namespacePrefix: string, context: SymbolContext, symbolToken: Node): ImportCodeAction { + const { symbolName, sourceFile } = context; + + /** + * Cases: + * import * as ns from "mod" + * import default, * as ns from "mod" + * import ns = require("mod") + * + * Because there is no import list, we alter the reference to include the + * namespace instead of altering the import declaration. For example, "foo" would + * become "ns.foo" + */ + return createCodeAction( + Diagnostics.Change_0_to_1, + [symbolName, `${namespacePrefix}.${symbolName}`], + ChangeTracker.with(context, tracker => + tracker.replaceNode(sourceFile, symbolToken, createPropertyAccess(createIdentifier(namespacePrefix), symbolName))), + "CodeChange", + /*moduleSpecifier*/ undefined); + } + + function getImportCodeActions(context: CodeFixContext): ImportCodeAction[] { + const importFixContext = convertToImportCodeFixContext(context); + return context.errorCode === Diagnostics._0_refers_to_a_UMD_global_but_the_current_file_is_a_module_Consider_adding_an_import_instead.code + ? getActionsForUMDImport(importFixContext) + : getActionsForNonUMDImport(importFixContext, context.program.getSourceFiles(), context.cancellationToken); + } - function createChangeTracker() { - return textChanges.ChangeTracker.fromContext(context); + function getActionsForUMDImport(context: ImportCodeFixContext): ImportCodeAction[] { + const { checker, symbolToken } = context; + const umdSymbol = checker.getSymbolAtLocation(symbolToken); + let symbol: ts.Symbol; + let symbolName: string; + if (umdSymbol.flags & ts.SymbolFlags.Alias) { + symbol = checker.getAliasedSymbol(umdSymbol); + symbolName = context.symbolName; } + else if (isJsxOpeningLikeElement(symbolToken.parent) && symbolToken.parent.tagName === symbolToken) { + // The error wasn't for the symbolAtLocation, it was for the JSX tag itself, which needs access to e.g. `React`. + symbol = checker.getAliasedSymbol(checker.resolveName(checker.getJsxNamespace(), symbolToken.parent.tagName, SymbolFlags.Value)); + symbolName = symbol.name; + } + else { + Debug.fail("Either the symbol or the JSX namespace should be a UMD global if we got here"); + } + + return getCodeActionForImport(symbol, { ...context, symbolName, kind: ImportKind.Namespace }); + } + + function getActionsForNonUMDImport(context: ImportCodeFixContext, allSourceFiles: ReadonlyArray, cancellationToken: CancellationToken): ImportCodeAction[] { + const { sourceFile, checker, symbolName, symbolToken } = context; + // "default" is a keyword and not a legal identifier for the import, so we don't expect it here + Debug.assert(symbolName !== "default"); + const symbolIdActionMap = new ImportCodeActionMap(); + const currentTokenMeaning = getMeaningFromLocation(symbolToken); + + forEachExternalModule(checker, allSourceFiles, moduleSymbol => { + if (moduleSymbol === sourceFile.symbol) { + return; + } + + cancellationToken.throwIfCancellationRequested(); + // check the default export + const defaultExport = checker.tryGetMemberInModuleExports("default", moduleSymbol); + if (defaultExport) { + const localSymbol = getLocalSymbolForExportDefault(defaultExport); + if (localSymbol && localSymbol.escapedName === symbolName && checkSymbolHasMeaning(localSymbol, currentTokenMeaning)) { + // check if this symbol is already used + const symbolId = getUniqueSymbolId(localSymbol, checker); + symbolIdActionMap.addActions(symbolId, getCodeActionForImport(moduleSymbol, { ...context, kind: ImportKind.Default })); + } + } - function createCodeAction( - description: DiagnosticMessage, - diagnosticArgs: string[], - changes: FileTextChanges[], - kind: ImportCodeActionKind, - moduleSpecifier?: string): ImportCodeAction { - return { - description: formatMessage.apply(undefined, [undefined, description].concat(diagnosticArgs)), - changes, - kind, - moduleSpecifier - }; + // check exports with the same name + const exportSymbolWithIdenticalName = checker.tryGetMemberInModuleExportsAndProperties(symbolName, moduleSymbol); + if (exportSymbolWithIdenticalName && checkSymbolHasMeaning(exportSymbolWithIdenticalName, currentTokenMeaning)) { + const symbolId = getUniqueSymbolId(exportSymbolWithIdenticalName, checker); + symbolIdActionMap.addActions(symbolId, getCodeActionForImport(moduleSymbol, { ...context, kind: ImportKind.Named })); + } + }); + + return symbolIdActionMap.getAllActions(); + } + + function checkSymbolHasMeaning({ declarations }: Symbol, meaning: SemanticMeaning): boolean { + return some(declarations, decl => !!(getMeaningFromDeclaration(decl) & meaning)); + } + + export function forEachExternalModule(checker: TypeChecker, allSourceFiles: ReadonlyArray, cb: (module: Symbol) => void) { + for (const ambient of checker.getAmbientModules()) { + cb(ambient); + } + for (const sourceFile of allSourceFiles) { + if (isExternalOrCommonJsModule(sourceFile)) { + cb(sourceFile.symbol); + } } } } diff --git a/src/services/completions.ts b/src/services/completions.ts index c222425693709..7afa85ce26df4 100644 --- a/src/services/completions.ts +++ b/src/services/completions.ts @@ -4,13 +4,31 @@ namespace ts.Completions { export type Log = (message: string) => void; + interface SymbolOriginInfo { + moduleSymbol: Symbol; + isDefaultExport: boolean; + } + /** + * Map from symbol id -> SymbolOriginInfo. + * Only populated for symbols that come from other modules. + */ + type SymbolOriginInfoMap = SymbolOriginInfo[]; + const enum KeywordCompletionFilters { None, ClassElementKeywords, // Keywords at class keyword ConstructorParameterKeywords, // Keywords at constructor parameter } - export function getCompletionsAtPosition(host: LanguageServiceHost, typeChecker: TypeChecker, log: Log, compilerOptions: CompilerOptions, sourceFile: SourceFile, position: number): CompletionInfo | undefined { + export function getCompletionsAtPosition( + host: LanguageServiceHost, + typeChecker: TypeChecker, + log: Log, + compilerOptions: CompilerOptions, + sourceFile: SourceFile, + position: number, + allSourceFiles: ReadonlyArray, + ): CompletionInfo | undefined { if (isInReferenceComment(sourceFile, position)) { return PathCompletions.getTripleSlashReferenceCompletion(sourceFile, position, compilerOptions, host); } @@ -19,12 +37,12 @@ namespace ts.Completions { return getStringLiteralCompletionEntries(sourceFile, position, typeChecker, compilerOptions, host, log); } - const completionData = getCompletionData(typeChecker, log, sourceFile, position); + const completionData = getCompletionData(typeChecker, log, sourceFile, position, allSourceFiles); if (!completionData) { return undefined; } - const { symbols, isGlobalCompletion, isMemberCompletion, allowStringLiteral, isNewIdentifierLocation, location, request, keywordFilters } = completionData; + const { symbols, isGlobalCompletion, isMemberCompletion, allowStringLiteral, isNewIdentifierLocation, location, request, keywordFilters, symbolToOriginInfoMap } = completionData; if (sourceFile.languageVariant === LanguageVariant.JSX && location && location.parent && location.parent.kind === SyntaxKind.JsxClosingElement) { @@ -56,7 +74,7 @@ namespace ts.Completions { const entries: CompletionEntry[] = []; if (isSourceFileJavaScript(sourceFile)) { - const uniqueNames = getCompletionEntriesFromSymbols(symbols, entries, location, /*performCharacterChecks*/ true, typeChecker, compilerOptions.target, log, allowStringLiteral); + const uniqueNames = getCompletionEntriesFromSymbols(symbols, entries, location, /*performCharacterChecks*/ true, typeChecker, compilerOptions.target, log, allowStringLiteral, symbolToOriginInfoMap); getJavaScriptCompletionEntries(sourceFile, location.pos, uniqueNames, compilerOptions.target, entries); } else { @@ -64,7 +82,7 @@ namespace ts.Completions { return undefined; } - getCompletionEntriesFromSymbols(symbols, entries, location, /*performCharacterChecks*/ true, typeChecker, compilerOptions.target, log, allowStringLiteral); + getCompletionEntriesFromSymbols(symbols, entries, location, /*performCharacterChecks*/ true, typeChecker, compilerOptions.target, log, allowStringLiteral, symbolToOriginInfoMap); } // TODO add filter for keyword based on type/value/namespace and also location @@ -134,7 +152,17 @@ namespace ts.Completions { }; } - function getCompletionEntriesFromSymbols(symbols: Symbol[], entries: Push, location: Node, performCharacterChecks: boolean, typeChecker: TypeChecker, target: ScriptTarget, log: Log, allowStringLiteral: boolean): Map { + function getCompletionEntriesFromSymbols( + symbols: ReadonlyArray, + entries: Push, + location: Node, + performCharacterChecks: boolean, + typeChecker: TypeChecker, + target: ScriptTarget, + log: Log, + allowStringLiteral: boolean, + symbolToOriginInfoMap?: SymbolOriginInfoMap, + ): Map { const start = timestamp(); const uniqueNames = createMap(); if (symbols) { @@ -143,6 +171,9 @@ namespace ts.Completions { if (entry) { const id = entry.name; if (!uniqueNames.has(id)) { + if (symbolToOriginInfoMap && symbolToOriginInfoMap[getUniqueSymbolId(symbol, typeChecker)]) { + entry.hasAction = true; + } entries.push(entry); uniqueNames.set(id, true); } @@ -298,53 +329,89 @@ namespace ts.Completions { } } - export function getCompletionEntryDetails(typeChecker: TypeChecker, log: (message: string) => void, compilerOptions: CompilerOptions, sourceFile: SourceFile, position: number, entryName: string): CompletionEntryDetails { + export function getCompletionEntryDetails( + typeChecker: TypeChecker, + log: (message: string) => void, + compilerOptions: CompilerOptions, + sourceFile: SourceFile, + position: number, + name: string, + allSourceFiles: ReadonlyArray, + host: LanguageServiceHost, + rulesProvider: formatting.RulesProvider, + ): CompletionEntryDetails { + // Compute all the completion symbols again. - const completionData = getCompletionData(typeChecker, log, sourceFile, position); + const completionData = getCompletionData(typeChecker, log, sourceFile, position, allSourceFiles); if (completionData) { - const { symbols, location, allowStringLiteral } = completionData; + const { symbols, location, allowStringLiteral, symbolToOriginInfoMap } = completionData; // Find the symbol with the matching entry name. // We don't need to perform character checks here because we're only comparing the // name against 'entryName' (which is known to be good), not building a new // completion entry. - const symbol = forEach(symbols, s => getCompletionEntryDisplayNameForSymbol(s, compilerOptions.target, /*performCharacterChecks*/ false, allowStringLiteral) === entryName ? s : undefined); + const symbol = find(symbols, s => getCompletionEntryDisplayNameForSymbol(s, compilerOptions.target, /*performCharacterChecks*/ false, allowStringLiteral) === name); if (symbol) { + const codeActions = getCompletionEntryCodeActions(symbolToOriginInfoMap, symbol, typeChecker, host, compilerOptions, sourceFile, rulesProvider); + const kindModifiers = SymbolDisplay.getSymbolModifiers(symbol); const { displayParts, documentation, symbolKind, tags } = SymbolDisplay.getSymbolDisplayPartsDocumentationAndSymbolKind(typeChecker, symbol, sourceFile, location, location, SemanticMeaning.All); - return { - name: entryName, - kindModifiers: SymbolDisplay.getSymbolModifiers(symbol), - kind: symbolKind, - displayParts, - documentation, - tags - }; + return { name, kindModifiers, kind: symbolKind, displayParts, documentation, tags, codeActions }; } } // Didn't find a symbol with this name. See if we can find a keyword instead. const keywordCompletion = forEach( getKeywordCompletions(KeywordCompletionFilters.None), - c => c.name === entryName + c => c.name === name ); if (keywordCompletion) { return { - name: entryName, + name, kind: ScriptElementKind.keyword, kindModifiers: ScriptElementKindModifier.none, - displayParts: [displayPart(entryName, SymbolDisplayPartKind.keyword)], + displayParts: [displayPart(name, SymbolDisplayPartKind.keyword)], documentation: undefined, - tags: undefined + tags: undefined, + codeActions: undefined, }; } return undefined; } - export function getCompletionEntrySymbol(typeChecker: TypeChecker, log: (message: string) => void, compilerOptions: CompilerOptions, sourceFile: SourceFile, position: number, entryName: string): Symbol | undefined { + function getCompletionEntryCodeActions(symbolToOriginInfoMap: SymbolOriginInfoMap, symbol: Symbol, checker: TypeChecker, host: LanguageServiceHost, compilerOptions: CompilerOptions, sourceFile: SourceFile, rulesProvider: formatting.RulesProvider): CodeAction[] | undefined { + const symbolOriginInfo = symbolToOriginInfoMap[getUniqueSymbolId(symbol, checker)]; + if (!symbolOriginInfo) { + return undefined; + } + + const { moduleSymbol, isDefaultExport } = symbolOriginInfo; + return codefix.getCodeActionForImport(moduleSymbol, { + host, + checker, + newLineCharacter: host.getNewLine(), + compilerOptions, + sourceFile, + rulesProvider, + symbolName: symbol.name, + getCanonicalFileName: createGetCanonicalFileName(host.useCaseSensitiveFileNames ? host.useCaseSensitiveFileNames() : false), + symbolToken: undefined, + kind: isDefaultExport ? codefix.ImportKind.Default : codefix.ImportKind.Named, + }); + } + + export function getCompletionEntrySymbol( + typeChecker: TypeChecker, + log: (message: string) => void, + compilerOptions: CompilerOptions, + sourceFile: SourceFile, + position: number, + entryName: string, + allSourceFiles: ReadonlyArray, + ): Symbol | undefined { // Compute all the completion symbols again. - const completionData = getCompletionData(typeChecker, log, sourceFile, position); + const completionData = getCompletionData(typeChecker, log, sourceFile, position, allSourceFiles); if (!completionData) { return undefined; } @@ -353,7 +420,7 @@ namespace ts.Completions { // We don't need to perform character checks here because we're only comparing the // name against 'entryName' (which is known to be good), not building a new // completion entry. - return forEach(symbols, s => getCompletionEntryDisplayNameForSymbol(s, compilerOptions.target, /*performCharacterChecks*/ false, allowStringLiteral) === entryName ? s : undefined); + return find(symbols, s => getCompletionEntryDisplayNameForSymbol(s, compilerOptions.target, /*performCharacterChecks*/ false, allowStringLiteral) === entryName); } interface CompletionData { @@ -366,10 +433,17 @@ namespace ts.Completions { isRightOfDot: boolean; request?: Request; keywordFilters: KeywordCompletionFilters; + symbolToOriginInfoMap: SymbolOriginInfoMap; } type Request = { kind: "JsDocTagName" } | { kind: "JsDocTag" } | { kind: "JsDocParameterName", tag: JSDocParameterTag }; - function getCompletionData(typeChecker: TypeChecker, log: (message: string) => void, sourceFile: SourceFile, position: number): CompletionData | undefined { + function getCompletionData( + typeChecker: TypeChecker, + log: (message: string) => void, + sourceFile: SourceFile, + position: number, + allSourceFiles: ReadonlyArray, + ): CompletionData | undefined { const isJavaScriptFile = isSourceFileJavaScript(sourceFile); let request: Request | undefined; @@ -441,7 +515,18 @@ namespace ts.Completions { } if (request) { - return { symbols: undefined, isGlobalCompletion: false, isMemberCompletion: false, allowStringLiteral: false, isNewIdentifierLocation: false, location: undefined, isRightOfDot: false, request, keywordFilters: KeywordCompletionFilters.None }; + return { + symbols: undefined, + isGlobalCompletion: false, + isMemberCompletion: false, + allowStringLiteral: false, + isNewIdentifierLocation: false, + location: undefined, + isRightOfDot: false, + request, + keywordFilters: KeywordCompletionFilters.None, + symbolToOriginInfoMap: undefined, + }; } if (!insideJsDocTagTypeExpression) { @@ -543,6 +628,7 @@ namespace ts.Completions { let isNewIdentifierLocation: boolean; let keywordFilters = KeywordCompletionFilters.None; let symbols: Symbol[] = []; + const symbolToOriginInfoMap: SymbolOriginInfoMap = []; if (isRightOfDot) { getTypeScriptMemberSymbols(); @@ -579,7 +665,7 @@ namespace ts.Completions { log("getCompletionData: Semantic work: " + (timestamp() - semanticStart)); - return { symbols, isGlobalCompletion, isMemberCompletion, allowStringLiteral, isNewIdentifierLocation, location, isRightOfDot: (isRightOfDot || isRightOfOpenTag), request, keywordFilters }; + return { symbols, isGlobalCompletion, isMemberCompletion, allowStringLiteral, isNewIdentifierLocation, location, isRightOfDot: (isRightOfDot || isRightOfOpenTag), request, keywordFilters, symbolToOriginInfoMap }; type JSDocTagWithTypeExpression = JSDocParameterTag | JSDocPropertyTag | JSDocReturnTag | JSDocTypeTag | JSDocTypedefTag; @@ -752,13 +838,16 @@ namespace ts.Completions { } const symbolMeanings = SymbolFlags.Type | SymbolFlags.Value | SymbolFlags.Namespace | SymbolFlags.Alias; - symbols = filterGlobalCompletion(typeChecker.getSymbolsInScope(scopeNode, symbolMeanings)); + + symbols = typeChecker.getSymbolsInScope(scopeNode, symbolMeanings); + getSymbolsFromOtherSourceFileExports(symbols, previousToken && isIdentifier(previousToken) ? previousToken.text : ""); + filterGlobalCompletion(symbols); return true; } - function filterGlobalCompletion(symbols: Symbol[]) { - return filter(symbols, symbol => { + function filterGlobalCompletion(symbols: Symbol[]): void { + filterMutate(symbols, symbol => { if (!isSourceFile(location)) { // export = /**/ here we want to get all meanings, so any symbol is ok if (isExportAssignment(location.parent)) { @@ -832,6 +921,59 @@ namespace ts.Completions { } } + function getSymbolsFromOtherSourceFileExports(symbols: Symbol[], tokenText: string): void { + const tokenTextLowerCase = tokenText.toLowerCase(); + const symbolIdMap = arrayToNumericMap(symbols, s => getUniqueSymbolId(s, typeChecker)); + + codefix.forEachExternalModule(typeChecker, allSourceFiles, moduleSymbol => { + if (moduleSymbol === sourceFile.symbol) { + return; + } + + for (let symbol of typeChecker.getExportsOfModule(moduleSymbol)) { + let { name } = symbol; + const isDefaultExport = name === "default"; + if (isDefaultExport) { + const localSymbol = getLocalSymbolForExportDefault(symbol); + if (localSymbol) { + symbol = localSymbol; + name = localSymbol.name; + } + } + + const id = getUniqueSymbolId(symbol, typeChecker); + if (!symbolIdMap[id] && stringContainsCharactersInOrder(name.toLowerCase(), tokenTextLowerCase)) { + symbols.push(symbol); + symbolToOriginInfoMap[id] = { moduleSymbol, isDefaultExport }; + } + } + }); + } + + /** + * True if you could remove some characters in `a` to get `b`. + * E.g., true for "abcdef" and "bdf". + * But not true for "abcdef" and "dbf". + */ + function stringContainsCharactersInOrder(str: string, characters: string): boolean { + if (characters.length === 0) { + return true; + } + + let characterIndex = 0; + for (let strIndex = 0; strIndex < str.length; strIndex++) { + if (str.charCodeAt(strIndex) === characters.charCodeAt(characterIndex)) { + characterIndex++; + if (characterIndex === characters.length) { + return true; + } + } + } + + // Did not find all characters + return false; + } + /** * Finds the first node that "embraces" the position, so that one may * accurately aggregate locals from the closest containing scope. @@ -1627,7 +1769,7 @@ namespace ts.Completions { // First check of the displayName is not external module; if it is an external module, it is not valid entry if (symbol.flags & SymbolFlags.Namespace) { const firstCharCode = name.charCodeAt(0); - if (firstCharCode === CharacterCodes.singleQuote || firstCharCode === CharacterCodes.doubleQuote) { + if (isSingleOrDoubleQuote(firstCharCode)) { // If the symbol is external module, don't show it in the completion list // (i.e declare module "http" { const x; } | // <= request completion here, "http" should not be there) return undefined; diff --git a/src/services/pathCompletions.ts b/src/services/pathCompletions.ts index e3bf9deac8932..780b14db719f2 100644 --- a/src/services/pathCompletions.ts +++ b/src/services/pathCompletions.ts @@ -339,7 +339,7 @@ namespace ts.Completions.PathCompletions { } } else if (host.getDirectories) { - let typeRoots: string[]; + let typeRoots: ReadonlyArray; try { // Wrap in try catch because getEffectiveTypeRoots touches the filesystem typeRoots = getEffectiveTypeRoots(options, host); diff --git a/src/services/refactorProvider.ts b/src/services/refactorProvider.ts index e956a4121c791..b338882e6db68 100644 --- a/src/services/refactorProvider.ts +++ b/src/services/refactorProvider.ts @@ -14,13 +14,11 @@ namespace ts { getAvailableActions(context: RefactorContext): ApplicableRefactorInfo[] | undefined; } - export interface RefactorContext { + export interface RefactorContext extends textChanges.TextChangesContext { file: SourceFile; startPosition: number; endPosition?: number; program: Program; - newLineCharacter: string; - rulesProvider?: formatting.RulesProvider; cancellationToken?: CancellationToken; } diff --git a/src/services/services.ts b/src/services/services.ts index 6bdc96d8b4d65..0399ca1658e9d 100644 --- a/src/services/services.ts +++ b/src/services/services.ts @@ -1324,17 +1324,19 @@ namespace ts { function getCompletionsAtPosition(fileName: string, position: number): CompletionInfo { synchronizeHostData(); - return Completions.getCompletionsAtPosition(host, program.getTypeChecker(), log, program.getCompilerOptions(), getValidSourceFile(fileName), position); + return Completions.getCompletionsAtPosition(host, program.getTypeChecker(), log, program.getCompilerOptions(), getValidSourceFile(fileName), position, program.getSourceFiles()); } - function getCompletionEntryDetails(fileName: string, position: number, entryName: string): CompletionEntryDetails { + function getCompletionEntryDetails(fileName: string, position: number, entryName: string, formattingOptions?: FormatCodeSettings): CompletionEntryDetails { synchronizeHostData(); - return Completions.getCompletionEntryDetails(program.getTypeChecker(), log, program.getCompilerOptions(), getValidSourceFile(fileName), position, entryName); + const ruleProvider = formattingOptions ? getRuleProvider(formattingOptions) : undefined; + return Completions.getCompletionEntryDetails( + program.getTypeChecker(), log, program.getCompilerOptions(), getValidSourceFile(fileName), position, entryName, program.getSourceFiles(), host, ruleProvider); } function getCompletionEntrySymbol(fileName: string, position: number, entryName: string): Symbol { synchronizeHostData(); - return Completions.getCompletionEntrySymbol(program.getTypeChecker(), log, program.getCompilerOptions(), getValidSourceFile(fileName), position, entryName); + return Completions.getCompletionEntrySymbol(program.getTypeChecker(), log, program.getCompilerOptions(), getValidSourceFile(fileName), position, entryName, program.getSourceFiles()); } function getQuickInfoAtPosition(fileName: string, position: number): QuickInfo { diff --git a/src/services/shims.ts b/src/services/shims.ts index 9d4baccc3c4e0..c9f5c4c2fb23c 100644 --- a/src/services/shims.ts +++ b/src/services/shims.ts @@ -141,7 +141,7 @@ namespace ts { getEncodedSemanticClassifications(fileName: string, start: number, length: number): string; getCompletionsAtPosition(fileName: string, position: number): string; - getCompletionEntryDetails(fileName: string, position: number, entryName: string): string; + getCompletionEntryDetails(fileName: string, position: number, entryName: string, options: string/*Services.FormatCodeOptions*/): string; getQuickInfoAtPosition(fileName: string, position: number): string; @@ -893,10 +893,13 @@ namespace ts { } /** Get a string based representation of a completion list entry details */ - public getCompletionEntryDetails(fileName: string, position: number, entryName: string) { + public getCompletionEntryDetails(fileName: string, position: number, entryName: string, options: string/*Services.FormatCodeOptions*/) { return this.forwardJSONCall( `getCompletionEntryDetails('${fileName}', ${position}, '${entryName}')`, - () => this.languageService.getCompletionEntryDetails(fileName, position, entryName) + () => { + const localOptions: ts.FormatCodeOptions = JSON.parse(options); + return this.languageService.getCompletionEntryDetails(fileName, position, entryName, localOptions); + } ); } diff --git a/src/services/textChanges.ts b/src/services/textChanges.ts index 9e4c2ed3a6003..63cfa64ef07b9 100644 --- a/src/services/textChanges.ts +++ b/src/services/textChanges.ts @@ -186,14 +186,25 @@ namespace ts.textChanges { return s; } + export interface TextChangesContext { + newLineCharacter: string; + rulesProvider: formatting.RulesProvider; + } + export class ChangeTracker { private changes: Change[] = []; private readonly newLineCharacter: string; - public static fromContext(context: RefactorContext | CodeFixContext) { + public static fromContext(context: TextChangesContext): ChangeTracker { return new ChangeTracker(context.newLineCharacter === "\n" ? NewLineKind.LineFeed : NewLineKind.CarriageReturnLineFeed, context.rulesProvider); } + public static with(context: TextChangesContext, cb: (tracker: ChangeTracker) => void): FileTextChanges[] { + const tracker = ChangeTracker.fromContext(context); + cb(tracker); + return tracker.getChanges(); + } + constructor( private readonly newLine: NewLineKind, private readonly rulesProvider: formatting.RulesProvider, diff --git a/src/services/types.ts b/src/services/types.ts index e853eb7b96cad..7bf4a909a7a34 100644 --- a/src/services/types.ts +++ b/src/services/types.ts @@ -230,7 +230,8 @@ namespace ts { getEncodedSemanticClassifications(fileName: string, span: TextSpan): Classifications; getCompletionsAtPosition(fileName: string, position: number): CompletionInfo; - getCompletionEntryDetails(fileName: string, position: number, entryName: string): CompletionEntryDetails; + // "options" is optional only for backwards-compatibility + getCompletionEntryDetails(fileName: string, position: number, entryName: string, options?: FormatCodeOptions | FormatCodeSettings): CompletionEntryDetails; getCompletionEntrySymbol(fileName: string, position: number, entryName: string): Symbol; getQuickInfoAtPosition(fileName: string, position: number): QuickInfo; @@ -668,6 +669,7 @@ namespace ts { * be used in that case */ replacementSpan?: TextSpan; + hasAction?: true; } export interface CompletionEntryDetails { @@ -677,6 +679,7 @@ namespace ts { displayParts: SymbolDisplayPart[]; documentation: SymbolDisplayPart[]; tags: JSDocTagInfo[]; + codeActions?: CodeAction[]; } export interface OutliningSpan { diff --git a/src/services/utilities.ts b/src/services/utilities.ts index ffeb5e70eed42..716a4188db25b 100644 --- a/src/services/utilities.ts +++ b/src/services/utilities.ts @@ -1281,9 +1281,7 @@ namespace ts { */ export function stripQuotes(name: string) { const length = name.length; - if (length >= 2 && - name.charCodeAt(0) === name.charCodeAt(length - 1) && - (name.charCodeAt(0) === CharacterCodes.doubleQuote || name.charCodeAt(0) === CharacterCodes.singleQuote)) { + if (length >= 2 && name.charCodeAt(0) === name.charCodeAt(length - 1) && isSingleOrDoubleQuote(name.charCodeAt(0))) { return name.substring(1, length - 1); } return name; @@ -1300,6 +1298,10 @@ namespace ts { return ensureScriptKind(fileName, host && host.getScriptKind && host.getScriptKind(fileName)); } + export function getUniqueSymbolId(symbol: Symbol, checker: TypeChecker) { + return getSymbolId(skipAlias(symbol, checker)); + } + export function getFirstNonSpaceCharacterPosition(text: string, position: number) { while (isWhiteSpaceLike(text.charCodeAt(position))) { position += 1; diff --git a/tests/baselines/reference/api/tsserverlibrary.d.ts b/tests/baselines/reference/api/tsserverlibrary.d.ts index e983ac73b1551..56c7fa5f98c44 100644 --- a/tests/baselines/reference/api/tsserverlibrary.d.ts +++ b/tests/baselines/reference/api/tsserverlibrary.d.ts @@ -3207,10 +3207,11 @@ declare namespace ts { }; } declare namespace ts { - function getEffectiveTypeRoots(options: CompilerOptions, host: { - directoryExists?: (directoryName: string) => boolean; - getCurrentDirectory?: () => string; - }): string[] | undefined; + interface GetEffectiveTypeRootsHost { + directoryExists?(directoryName: string): boolean; + getCurrentDirectory?(): string; + } + function getEffectiveTypeRoots(options: CompilerOptions, host: GetEffectiveTypeRootsHost): string[] | undefined; /** * @param {string | undefined} containingFile - file that contains type reference directive, can be undefined if containing file is unknown. * This is possible in case if resolution is performed for directives specified via 'types' parameter. In this case initial path for secondary lookups @@ -3913,7 +3914,7 @@ declare namespace ts { getEncodedSyntacticClassifications(fileName: string, span: TextSpan): Classifications; getEncodedSemanticClassifications(fileName: string, span: TextSpan): Classifications; getCompletionsAtPosition(fileName: string, position: number): CompletionInfo; - getCompletionEntryDetails(fileName: string, position: number, entryName: string): CompletionEntryDetails; + getCompletionEntryDetails(fileName: string, position: number, entryName: string, options?: FormatCodeOptions | FormatCodeSettings): CompletionEntryDetails; getCompletionEntrySymbol(fileName: string, position: number, entryName: string): Symbol; getQuickInfoAtPosition(fileName: string, position: number): QuickInfo; getNameOrDottedNameSpan(fileName: string, startPos: number, endPos: number): TextSpan; @@ -4281,6 +4282,7 @@ declare namespace ts { * be used in that case */ replacementSpan?: TextSpan; + hasAction?: true; } interface CompletionEntryDetails { name: string; @@ -4289,6 +4291,7 @@ declare namespace ts { displayParts: SymbolDisplayPart[]; documentation: SymbolDisplayPart[]; tags: JSDocTagInfo[]; + codeActions?: CodeAction[]; } interface OutliningSpan { /** The span of the document to actually collapse. */ @@ -6038,6 +6041,11 @@ declare namespace ts.server.protocol { * this span should be used instead of the default one. */ replacementSpan?: TextSpan; + /** + * Indicates whether commiting this completion entry will require additional code actions to be + * made to avoid errors. The CompletionEntryDetails will have these actions. + */ + hasAction?: true; } /** * Additional completion entry details, available on demand @@ -6067,6 +6075,10 @@ declare namespace ts.server.protocol { * JSDoc tags for the symbol. */ tags: JSDocTagInfo[]; + /** + * The associated code actions for this entry + */ + codeActions?: CodeAction[]; } interface CompletionsResponse extends Response { body?: CompletionEntry[]; diff --git a/tests/baselines/reference/api/typescript.d.ts b/tests/baselines/reference/api/typescript.d.ts index 14fae7d0d77a0..2668a25bcfcd2 100644 --- a/tests/baselines/reference/api/typescript.d.ts +++ b/tests/baselines/reference/api/typescript.d.ts @@ -3154,10 +3154,11 @@ declare namespace ts { function updateSourceFile(sourceFile: SourceFile, newText: string, textChangeRange: TextChangeRange, aggressiveChecks?: boolean): SourceFile; } declare namespace ts { - function getEffectiveTypeRoots(options: CompilerOptions, host: { - directoryExists?: (directoryName: string) => boolean; - getCurrentDirectory?: () => string; - }): string[] | undefined; + interface GetEffectiveTypeRootsHost { + directoryExists?(directoryName: string): boolean; + getCurrentDirectory?(): string; + } + function getEffectiveTypeRoots(options: CompilerOptions, host: GetEffectiveTypeRootsHost): string[] | undefined; /** * @param {string | undefined} containingFile - file that contains type reference directive, can be undefined if containing file is unknown. * This is possible in case if resolution is performed for directives specified via 'types' parameter. In this case initial path for secondary lookups @@ -3913,7 +3914,7 @@ declare namespace ts { getEncodedSyntacticClassifications(fileName: string, span: TextSpan): Classifications; getEncodedSemanticClassifications(fileName: string, span: TextSpan): Classifications; getCompletionsAtPosition(fileName: string, position: number): CompletionInfo; - getCompletionEntryDetails(fileName: string, position: number, entryName: string): CompletionEntryDetails; + getCompletionEntryDetails(fileName: string, position: number, entryName: string, options?: FormatCodeOptions | FormatCodeSettings): CompletionEntryDetails; getCompletionEntrySymbol(fileName: string, position: number, entryName: string): Symbol; getQuickInfoAtPosition(fileName: string, position: number): QuickInfo; getNameOrDottedNameSpan(fileName: string, startPos: number, endPos: number): TextSpan; @@ -4281,6 +4282,7 @@ declare namespace ts { * be used in that case */ replacementSpan?: TextSpan; + hasAction?: true; } interface CompletionEntryDetails { name: string; @@ -4289,6 +4291,7 @@ declare namespace ts { displayParts: SymbolDisplayPart[]; documentation: SymbolDisplayPart[]; tags: JSDocTagInfo[]; + codeActions?: CodeAction[]; } interface OutliningSpan { /** The span of the document to actually collapse. */ diff --git a/tests/cases/fourslash/completionsImport_default_addToNamedImports.ts b/tests/cases/fourslash/completionsImport_default_addToNamedImports.ts new file mode 100644 index 0000000000000..5662b57fdf678 --- /dev/null +++ b/tests/cases/fourslash/completionsImport_default_addToNamedImports.ts @@ -0,0 +1,19 @@ +/// + +// @Filename: /a.ts +////export default function foo() {} +////export const x = 0; + +// @Filename: /b.ts +////import { x } from "./a"; +////f/**/; + +goTo.marker(""); +verify.completionListContains("foo", "function foo(): void", "", "function", /*spanIndex*/ undefined, /*hasAction*/ true); + +verify.applyCodeActionFromCompletion("", { + name: "foo", + description: `Add 'foo' to existing import declaration from "./a".`, + newFileContent: `import foo, { x } from "./a"; +f;`, +}); diff --git a/tests/cases/fourslash/completionsImport_default_addToNamespaceImport.ts b/tests/cases/fourslash/completionsImport_default_addToNamespaceImport.ts new file mode 100644 index 0000000000000..b442ed21549e1 --- /dev/null +++ b/tests/cases/fourslash/completionsImport_default_addToNamespaceImport.ts @@ -0,0 +1,18 @@ +/// + +// @Filename: /a.ts +////export default function foo() {} + +// @Filename: /b.ts +////import * as a from "./a"; +////f/**/; + +goTo.marker(""); +verify.completionListContains("foo", "function foo(): void", "", "function", /*spanIndex*/ undefined, /*hasAction*/ true); + +verify.applyCodeActionFromCompletion("", { + name: "foo", + description: `Add 'foo' to existing import declaration from "./a".`, + newFileContent: `import foo, * as a from "./a"; +f;`, +}); diff --git a/tests/cases/fourslash/completionsImport_default_alreadyExistedWithRename.ts b/tests/cases/fourslash/completionsImport_default_alreadyExistedWithRename.ts new file mode 100644 index 0000000000000..3ee4decb0f8b7 --- /dev/null +++ b/tests/cases/fourslash/completionsImport_default_alreadyExistedWithRename.ts @@ -0,0 +1,20 @@ +/// + +// @Filename: /a.ts +////export default function foo() {} + +// @Filename: /b.ts +////import f_o_o from "./a"; +////f/**/; + +goTo.marker(""); +verify.completionListContains("foo", "function foo(): void", "", "function", /*spanIndex*/ undefined, /*hasAction*/ true); + +verify.applyCodeActionFromCompletion("", { + name: "foo", + description: `Import 'foo' from "./a".`, + // TODO: GH#18445 + newFileContent: `import f_o_o from "./a"; +import foo from "./a";\r +f;`, +}); diff --git a/tests/cases/fourslash/completionsImport_default_didNotExistBefore.ts b/tests/cases/fourslash/completionsImport_default_didNotExistBefore.ts new file mode 100644 index 0000000000000..6dbd437d36470 --- /dev/null +++ b/tests/cases/fourslash/completionsImport_default_didNotExistBefore.ts @@ -0,0 +1,19 @@ +/// + +// @Filename: /a.ts +////export default function foo() {} + +// @Filename: /b.ts +////f/**/; + +goTo.marker(""); +verify.completionListContains("foo", "function foo(): void", "", "function", /*spanIndex*/ undefined, /*hasAction*/ true); + +verify.applyCodeActionFromCompletion("", { + name: "foo", + description: `Import 'foo' from "./a".`, + // TODO: GH#18445 + newFileContent: `import foo from "./a";\r +\r +f;`, +}); diff --git a/tests/cases/fourslash/completionsImport_fromAmbientModule.ts b/tests/cases/fourslash/completionsImport_fromAmbientModule.ts new file mode 100644 index 0000000000000..b45ad9824865a --- /dev/null +++ b/tests/cases/fourslash/completionsImport_fromAmbientModule.ts @@ -0,0 +1,18 @@ +/// + +// @Filename: /a.ts +////declare module "m" { +//// export const x: number; +////} + +// @Filename: /b.ts +/////**/ + +verify.applyCodeActionFromCompletion("", { + name: "x", + description: `Import 'x' from "m".`, + // TODO: GH#18445 + newFileContent: `import { x } from "m";\r +\r +`, +}); diff --git a/tests/cases/fourslash/completionsImport_matching.ts b/tests/cases/fourslash/completionsImport_matching.ts new file mode 100644 index 0000000000000..3470d59bff20b --- /dev/null +++ b/tests/cases/fourslash/completionsImport_matching.ts @@ -0,0 +1,22 @@ +/// + +// @Filename: /a.ts +// Not included: +////export function abcde() {} +////export function dbf() {} +// Included: +////export function bdf() {} +////export function abcdef() {} +////export function BDF() {} + +// @Filename: /b.ts +////bdf/**/ + +goTo.marker(""); + +verify.not.completionListContains("abcde"); +verify.not.completionListContains("dbf"); + +verify.completionListContains("bdf", "function bdf(): void", "", "function", /*spanIndex*/ undefined, /*hasAction*/ true); +verify.completionListContains("abcdef", "function abcdef(): void", "", "function", /*spanIndex*/ undefined, /*hasAction*/ true); +verify.completionListContains("BDF", "function BDF(): void", "", "function", /*spanIndex*/ undefined, /*hasAction*/ true); diff --git a/tests/cases/fourslash/completionsImport_named_addToNamedImports.ts b/tests/cases/fourslash/completionsImport_named_addToNamedImports.ts new file mode 100644 index 0000000000000..23119ad491f79 --- /dev/null +++ b/tests/cases/fourslash/completionsImport_named_addToNamedImports.ts @@ -0,0 +1,19 @@ +/// + +// @Filename: /a.ts +////export function foo() {} +////export const x = 0; + +// @Filename: /b.ts +////import { x } from "./a"; +////f/**/; + +goTo.marker(""); +verify.completionListContains("foo", "function foo(): void", "", "function", /*spanIndex*/ undefined, /*hasAction*/ true); + +verify.applyCodeActionFromCompletion("", { + name: "foo", + description: `Add 'foo' to existing import declaration from "./a".`, + newFileContent: `import { x, foo } from "./a"; +f;`, +}); diff --git a/tests/cases/fourslash/completionsImport_named_didNotExistBefore.ts b/tests/cases/fourslash/completionsImport_named_didNotExistBefore.ts new file mode 100644 index 0000000000000..95c68c2a05e88 --- /dev/null +++ b/tests/cases/fourslash/completionsImport_named_didNotExistBefore.ts @@ -0,0 +1,20 @@ +/// + +// @Filename: /a.ts +////export function Test1() {} +////export function Test2() {} + +// @Filename: /b.ts +////import { Test2 } from "./a"; +////t/**/ + +goTo.marker(""); +verify.completionListContains("Test1", "function Test1(): void", "", "function", /*spanIndex*/ undefined, /*hasAction*/ true); +verify.completionListContains("Test2", "import Test2", "", "alias", /*spanIndex*/ undefined, /*hasAction*/ undefined); + +verify.applyCodeActionFromCompletion("", { + name: "Test1", + description: `Add 'Test1' to existing import declaration from "./a".`, + newFileContent: `import { Test2, Test1 } from "./a"; +t`, +}); diff --git a/tests/cases/fourslash/completionsImport_named_namespaceImportExists.ts b/tests/cases/fourslash/completionsImport_named_namespaceImportExists.ts new file mode 100644 index 0000000000000..da00907e60fb7 --- /dev/null +++ b/tests/cases/fourslash/completionsImport_named_namespaceImportExists.ts @@ -0,0 +1,20 @@ +/// + +// @Filename: /a.ts +////export function foo() {} + +// @Filename: /b.ts +////import * as a from "./a"; +////f/**/; + +goTo.marker(""); +verify.completionListContains("foo", "function foo(): void", "", "function", /*spanIndex*/ undefined, /*hasAction*/ true); + +verify.applyCodeActionFromCompletion("", { + name: "foo", + description: `Import 'foo' from "./a".`, + // TODO: GH#18445 + newFileContent: `import * as a from "./a"; +import { foo } from "./a";\r +f;`, +}); diff --git a/tests/cases/fourslash/completionsImport_previousTokenIsSemicolon.ts b/tests/cases/fourslash/completionsImport_previousTokenIsSemicolon.ts new file mode 100644 index 0000000000000..904de35354ac5 --- /dev/null +++ b/tests/cases/fourslash/completionsImport_previousTokenIsSemicolon.ts @@ -0,0 +1,11 @@ +/// + +// @Filename: /a.ts +////export function foo() {} + +// @Filename: /b.ts +////import * as a from 'a'; +/////**/ + +goTo.marker(""); +verify.completionListContains("foo", "function foo(): void", "", "function", /*spanIndex*/ undefined, /*hasAction*/ true); diff --git a/tests/cases/fourslash/fourslash.ts b/tests/cases/fourslash/fourslash.ts index f4d47abc9c9c4..e1d9607de8aaf 100644 --- a/tests/cases/fourslash/fourslash.ts +++ b/tests/cases/fourslash/fourslash.ts @@ -140,7 +140,14 @@ declare namespace FourSlashInterface { allowedConstructorParameterKeywords: string[]; constructor(negative?: boolean); completionListCount(expectedCount: number): void; - completionListContains(symbol: string, text?: string, documentation?: string, kind?: string, spanIndex?: number): void; + completionListContains( + symbol: string, + text?: string, + documentation?: string, + kind?: string, + spanIndex?: number, + hasAction?: boolean, + ): void; completionListItemsCountIsGreaterThan(count: number): void; completionListIsEmpty(): void; completionListContainsClassElementKeywords(): void; @@ -173,6 +180,20 @@ declare namespace FourSlashInterface { assertHasRanges(ranges: Range[]): void; caretAtMarker(markerName?: string): void; completionsAt(markerName: string, completions: string[], options?: { isNewIdentifierLocation?: boolean }): void; + completionsAndDetailsAt( + markerName: string, + completions: { + excludes?: ReadonlyArray, + //TODO: better type + entries: ReadonlyArray<{ entry: any, details: any }>, + }, + ): void; //TODO: better type + applyCodeActionFromCompletion(markerName: string, options: { + name: string, + description: string, + newFileContent?: string, + newRangeContent?: string, + }); indentationIs(numberOfSpaces: number): void; indentationAtPositionIs(fileName: string, position: number, numberOfSpaces: number, indentStyle?: ts.IndentStyle, baseIndentSize?: number): void; textAtCaretIs(text: string): void; diff --git a/tests/cases/fourslash/importNameCodeFixOptionalImport0.ts b/tests/cases/fourslash/importNameCodeFixOptionalImport0.ts index 34cebad941544..218ec4fabb724 100644 --- a/tests/cases/fourslash/importNameCodeFixOptionalImport0.ts +++ b/tests/cases/fourslash/importNameCodeFixOptionalImport0.ts @@ -8,7 +8,7 @@ //// export function foo() {}; // @Filename: a/foo.ts -//// export { foo } from "./foo/bar"; +//// export { foo } from "./foo/bar"; verify.importFixAtPosition([ `import * as ns from "./foo"; From f09fe3d0eca877fe4282d860114dd4cf0a476958 Mon Sep 17 00:00:00 2001 From: Wesley Wigham Date: Tue, 17 Oct 2017 10:45:48 -0700 Subject: [PATCH 09/51] Dont try to run unit tests with rwc tests again (#19240) --- src/harness/parallel/host.ts | 2 +- src/harness/runner.ts | 9 +++++---- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/src/harness/parallel/host.ts b/src/harness/parallel/host.ts index d7bba70408e7f..bf218c1b3a127 100644 --- a/src/harness/parallel/host.ts +++ b/src/harness/parallel/host.ts @@ -137,7 +137,7 @@ namespace Harness.Parallel.Host { let closedWorkers = 0; for (let i = 0; i < workerCount; i++) { // TODO: Just send the config over the IPC channel or in the command line arguments - const config: TestConfig = { light: Harness.lightMode, listenForWork: true, runUnitTests: runners.length !== 1 }; + const config: TestConfig = { light: Harness.lightMode, listenForWork: true, runUnitTests }; const configPath = ts.combinePaths(taskConfigsFolder, `task-config${i}.json`); Harness.IO.writeFile(configPath, JSON.stringify(config)); const child = fork(__filename, [`--config="${configPath}"`]); diff --git a/src/harness/runner.ts b/src/harness/runner.ts index c1345f422c394..db807b976bb18 100644 --- a/src/harness/runner.ts +++ b/src/harness/runner.ts @@ -82,7 +82,7 @@ let testConfigContent = let taskConfigsFolder: string; let workerCount: number; -let runUnitTests = true; +let runUnitTests: boolean | undefined; let noColors = false; interface TestConfig { @@ -108,9 +108,7 @@ function handleTestConfig() { if (testConfig.light) { Harness.lightMode = true; } - if (testConfig.runUnitTests !== undefined) { - runUnitTests = testConfig.runUnitTests; - } + runUnitTests = testConfig.runUnitTests; if (testConfig.workerCount) { workerCount = +testConfig.workerCount; } @@ -199,6 +197,9 @@ function handleTestConfig() { runners.push(new FourSlashRunner(FourSlashTestType.Server)); // runners.push(new GeneratedFourslashRunner()); } + if (runUnitTests === undefined) { + runUnitTests = runners.length !== 1; // Don't run unit tests when running only one runner if unit tests were not explicitly asked for + } } function beginTests() { From d0c4d13fe2a836e76ffadd6dd7b6328e1cc056e9 Mon Sep 17 00:00:00 2001 From: Andy Date: Tue, 17 Oct 2017 11:34:59 -0700 Subject: [PATCH 10/51] In tsserver, indent logged JSON (#19080) --- src/server/server.ts | 8 ++++---- src/server/session.ts | 10 +++++----- src/server/typingsInstaller/nodeTypingsInstaller.ts | 2 +- src/server/utilities.ts | 11 +++++++++++ 4 files changed, 21 insertions(+), 10 deletions(-) diff --git a/src/server/server.ts b/src/server/server.ts index f24251ee6ac90..17905ece33ade 100644 --- a/src/server/server.ts +++ b/src/server/server.ts @@ -350,14 +350,14 @@ namespace ts.server { const request = createInstallTypingsRequest(project, typeAcquisition, unresolvedImports); if (this.logger.hasLevel(LogLevel.verbose)) { if (this.logger.hasLevel(LogLevel.verbose)) { - this.logger.info(`Scheduling throttled operation: ${JSON.stringify(request)}`); + this.logger.info(`Scheduling throttled operation:${stringifyIndented(request)}`); } } const operationId = project.getProjectName(); const operation = () => { if (this.logger.hasLevel(LogLevel.verbose)) { - this.logger.info(`Sending request: ${JSON.stringify(request)}`); + this.logger.info(`Sending request:${stringifyIndented(request)}`); } this.installer.send(request); }; @@ -377,7 +377,7 @@ namespace ts.server { private handleMessage(response: SetTypings | InvalidateCachedTypings | BeginInstallTypes | EndInstallTypes | InitializationFailedResponse) { if (this.logger.hasLevel(LogLevel.verbose)) { - this.logger.info(`Received response: ${JSON.stringify(response)}`); + this.logger.info(`Received response:${stringifyIndented(response)}`); } switch (response.kind) { @@ -780,7 +780,7 @@ namespace ts.server { try { const args = [combinePaths(__dirname, "watchGuard.js"), path]; if (logger.hasLevel(LogLevel.verbose)) { - logger.info(`Starting ${process.execPath} with args ${JSON.stringify(args)}`); + logger.info(`Starting ${process.execPath} with args:${stringifyIndented(args)}`); } childProcess.execFileSync(process.execPath, args, { stdio: "ignore", env: { "ELECTRON_RUN_AS_NODE": "1" } }); status = true; diff --git a/src/server/session.ts b/src/server/session.ts index d10daecc92d1e..017784530d5bf 100644 --- a/src/server/session.ts +++ b/src/server/session.ts @@ -131,7 +131,7 @@ namespace ts.server { const json = JSON.stringify(msg); if (verboseLogging) { - logger.info(msg.type + ": " + json); + logger.info(msg.type + ":\n" + indent(json)); } const len = byteLength(json, "utf8"); @@ -383,9 +383,9 @@ namespace ts.server { public logError(err: Error, cmd: string) { let msg = "Exception on executing command " + cmd; if (err.message) { - msg += ":\n" + err.message; + msg += ":\n" + indent(err.message); if ((err).stack) { - msg += "\n" + (err).stack; + msg += "\n" + indent((err).stack); } } this.logger.msg(msg, Msg.Err); @@ -1962,7 +1962,7 @@ namespace ts.server { return this.executeWithRequestId(request.seq, () => handler(request)); } else { - this.logger.msg(`Unrecognized JSON command: ${JSON.stringify(request)}`, Msg.Err); + this.logger.msg(`Unrecognized JSON command:${stringifyIndented(request)}`, Msg.Err); this.output(undefined, CommandNames.Unknown, request.seq, `Unrecognized JSON command: ${request.command}`); return { responseRequired: false }; } @@ -1974,7 +1974,7 @@ namespace ts.server { if (this.logger.hasLevel(LogLevel.requestTime)) { start = this.hrtime(); if (this.logger.hasLevel(LogLevel.verbose)) { - this.logger.info(`request: ${message}`); + this.logger.info(`request:${indent(message)}`); } } diff --git a/src/server/typingsInstaller/nodeTypingsInstaller.ts b/src/server/typingsInstaller/nodeTypingsInstaller.ts index 98478c2d5fc39..d32dd2a4b5f76 100644 --- a/src/server/typingsInstaller/nodeTypingsInstaller.ts +++ b/src/server/typingsInstaller/nodeTypingsInstaller.ts @@ -145,7 +145,7 @@ namespace ts.server.typingsInstaller { protected sendResponse(response: SetTypings | InvalidateCachedTypings | BeginInstallTypes | EndInstallTypes | InitializationFailedResponse) { if (this.log.isEnabled()) { - this.log.writeLine(`Sending response: ${JSON.stringify(response)}`); + this.log.writeLine(`Sending response:\n ${JSON.stringify(response)}`); } process.send(response); if (this.log.isEnabled()) { diff --git a/src/server/utilities.ts b/src/server/utilities.ts index 6832e7ba34714..69399b672b35d 100644 --- a/src/server/utilities.ts +++ b/src/server/utilities.ts @@ -318,4 +318,15 @@ namespace ts.server { deleted(oldItems[oldIndex++]); } } + + /* @internal */ + export function indent(string: string): string { + return "\n " + string; + } + + /** Put stringified JSON on the next line, indented. */ + /* @internal */ + export function stringifyIndented(json: {}): string { + return "\n " + JSON.stringify(json); + } } From 28509e1732f7861119b3c368a46a845143aafd03 Mon Sep 17 00:00:00 2001 From: Andy Date: Tue, 17 Oct 2017 11:57:47 -0700 Subject: [PATCH 11/51] noUnusedLocals: Warn for recursive call to private method (#18920) --- src/compiler/checker.ts | 15 +++++++-- .../noUnusedLocals_selfReference.errors.txt | 12 ++++--- .../reference/noUnusedLocals_selfReference.js | 22 ++++++------- .../noUnusedLocals_selfReference.symbols | 31 +++++++++---------- .../noUnusedLocals_selfReference.types | 21 ++++++------- .../compiler/noUnusedLocals_selfReference.ts | 7 ++--- 6 files changed, 57 insertions(+), 51 deletions(-) diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts index 2aff217fc624e..219f370fd53f5 100644 --- a/src/compiler/checker.ts +++ b/src/compiler/checker.ts @@ -15028,7 +15028,7 @@ namespace ts { checkPropertyNotUsedBeforeDeclaration(prop, node, right); - markPropertyAsReferenced(prop, node); + markPropertyAsReferenced(prop, node, left.kind === SyntaxKind.ThisKeyword); getNodeLinks(node).resolvedSymbol = prop; @@ -15218,12 +15218,21 @@ namespace ts { return bestCandidate; } - function markPropertyAsReferenced(prop: Symbol, nodeForCheckWriteOnly: Node | undefined) { + function markPropertyAsReferenced(prop: Symbol, nodeForCheckWriteOnly: Node | undefined, isThisAccess: boolean) { if (prop && noUnusedIdentifiers && (prop.flags & SymbolFlags.ClassMember) && prop.valueDeclaration && hasModifier(prop.valueDeclaration, ModifierFlags.Private) && !(nodeForCheckWriteOnly && isWriteOnlyAccess(nodeForCheckWriteOnly))) { + + if (isThisAccess) { + // Find any FunctionLikeDeclaration because those create a new 'this' binding. But this should only matter for methods (or getters/setters). + const containingMethod = findAncestor(nodeForCheckWriteOnly, isFunctionLikeDeclaration); + if (containingMethod && containingMethod.symbol === prop) { + return; + } + } + if (getCheckFlags(prop) & CheckFlags.Instantiated) { getSymbolLinks(prop).target.isReferenced = true; } @@ -20716,7 +20725,7 @@ namespace ts { const parentType = getTypeForBindingElementParent(parent); const name = node.propertyName || node.name; const property = getPropertyOfType(parentType, getTextOfPropertyName(name)); - markPropertyAsReferenced(property, /*nodeForCheckWriteOnly*/ undefined); // A destructuring is never a write-only reference. + markPropertyAsReferenced(property, /*nodeForCheckWriteOnly*/ undefined, /*isThisAccess*/ false); // A destructuring is never a write-only reference. if (parent.initializer && property) { checkPropertyAccessibility(parent, parent.initializer, parentType, property); } diff --git a/tests/baselines/reference/noUnusedLocals_selfReference.errors.txt b/tests/baselines/reference/noUnusedLocals_selfReference.errors.txt index 603bc54d4481a..e1d198ff5590b 100644 --- a/tests/baselines/reference/noUnusedLocals_selfReference.errors.txt +++ b/tests/baselines/reference/noUnusedLocals_selfReference.errors.txt @@ -2,9 +2,10 @@ tests/cases/compiler/noUnusedLocals_selfReference.ts(3,10): error TS6133: 'f' is tests/cases/compiler/noUnusedLocals_selfReference.ts(5,14): error TS6133: 'g' is declared but its value is never read. tests/cases/compiler/noUnusedLocals_selfReference.ts(9,7): error TS6133: 'C' is declared but its value is never read. tests/cases/compiler/noUnusedLocals_selfReference.ts(12,6): error TS6133: 'E' is declared but its value is never read. +tests/cases/compiler/noUnusedLocals_selfReference.ts(14,19): error TS6133: 'm' is declared but its value is never read. -==== tests/cases/compiler/noUnusedLocals_selfReference.ts (4 errors) ==== +==== tests/cases/compiler/noUnusedLocals_selfReference.ts (5 errors) ==== export {}; // Make this a module scope, so these are local variables. function f() { @@ -26,11 +27,12 @@ tests/cases/compiler/noUnusedLocals_selfReference.ts(12,6): error TS6133: 'E' is ~ !!! error TS6133: 'E' is declared but its value is never read. + class P { private m() { this.m; } } + ~ +!!! error TS6133: 'm' is declared but its value is never read. + P; + // Does not detect mutual recursion. function g() { D; } class D { m() { g; } } - - // Does not work on private methods. - class P { private m() { this.m; } } - P; \ No newline at end of file diff --git a/tests/baselines/reference/noUnusedLocals_selfReference.js b/tests/baselines/reference/noUnusedLocals_selfReference.js index a8f3d6a8aedf6..c23d829534080 100644 --- a/tests/baselines/reference/noUnusedLocals_selfReference.js +++ b/tests/baselines/reference/noUnusedLocals_selfReference.js @@ -12,13 +12,12 @@ class C { } enum E { A = 0, B = E.A } +class P { private m() { this.m; } } +P; + // Does not detect mutual recursion. function g() { D; } class D { m() { g; } } - -// Does not work on private methods. -class P { private m() { this.m; } } -P; //// [noUnusedLocals_selfReference.js] @@ -41,6 +40,13 @@ var E; E[E["A"] = 0] = "A"; E[E["B"] = 0] = "B"; })(E || (E = {})); +var P = /** @class */ (function () { + function P() { + } + P.prototype.m = function () { this.m; }; + return P; +}()); +P; // Does not detect mutual recursion. function g() { D; } var D = /** @class */ (function () { @@ -49,11 +55,3 @@ var D = /** @class */ (function () { D.prototype.m = function () { g; }; return D; }()); -// Does not work on private methods. -var P = /** @class */ (function () { - function P() { - } - P.prototype.m = function () { this.m; }; - return P; -}()); -P; diff --git a/tests/baselines/reference/noUnusedLocals_selfReference.symbols b/tests/baselines/reference/noUnusedLocals_selfReference.symbols index 015a78d87d3f5..cd4195094fed4 100644 --- a/tests/baselines/reference/noUnusedLocals_selfReference.symbols +++ b/tests/baselines/reference/noUnusedLocals_selfReference.symbols @@ -29,24 +29,23 @@ enum E { A = 0, B = E.A } >E : Symbol(E, Decl(noUnusedLocals_selfReference.ts, 10, 1)) >A : Symbol(E.A, Decl(noUnusedLocals_selfReference.ts, 11, 8)) +class P { private m() { this.m; } } +>P : Symbol(P, Decl(noUnusedLocals_selfReference.ts, 11, 25)) +>m : Symbol(P.m, Decl(noUnusedLocals_selfReference.ts, 13, 9)) +>this.m : Symbol(P.m, Decl(noUnusedLocals_selfReference.ts, 13, 9)) +>this : Symbol(P, Decl(noUnusedLocals_selfReference.ts, 11, 25)) +>m : Symbol(P.m, Decl(noUnusedLocals_selfReference.ts, 13, 9)) + +P; +>P : Symbol(P, Decl(noUnusedLocals_selfReference.ts, 11, 25)) + // Does not detect mutual recursion. function g() { D; } ->g : Symbol(g, Decl(noUnusedLocals_selfReference.ts, 11, 25)) ->D : Symbol(D, Decl(noUnusedLocals_selfReference.ts, 14, 19)) +>g : Symbol(g, Decl(noUnusedLocals_selfReference.ts, 14, 2)) +>D : Symbol(D, Decl(noUnusedLocals_selfReference.ts, 17, 19)) class D { m() { g; } } ->D : Symbol(D, Decl(noUnusedLocals_selfReference.ts, 14, 19)) ->m : Symbol(D.m, Decl(noUnusedLocals_selfReference.ts, 15, 9)) ->g : Symbol(g, Decl(noUnusedLocals_selfReference.ts, 11, 25)) - -// Does not work on private methods. -class P { private m() { this.m; } } ->P : Symbol(P, Decl(noUnusedLocals_selfReference.ts, 15, 22)) ->m : Symbol(P.m, Decl(noUnusedLocals_selfReference.ts, 18, 9)) ->this.m : Symbol(P.m, Decl(noUnusedLocals_selfReference.ts, 18, 9)) ->this : Symbol(P, Decl(noUnusedLocals_selfReference.ts, 15, 22)) ->m : Symbol(P.m, Decl(noUnusedLocals_selfReference.ts, 18, 9)) - -P; ->P : Symbol(P, Decl(noUnusedLocals_selfReference.ts, 15, 22)) +>D : Symbol(D, Decl(noUnusedLocals_selfReference.ts, 17, 19)) +>m : Symbol(D.m, Decl(noUnusedLocals_selfReference.ts, 18, 9)) +>g : Symbol(g, Decl(noUnusedLocals_selfReference.ts, 14, 2)) diff --git a/tests/baselines/reference/noUnusedLocals_selfReference.types b/tests/baselines/reference/noUnusedLocals_selfReference.types index 7e75062db34d5..cbaa413c65152 100644 --- a/tests/baselines/reference/noUnusedLocals_selfReference.types +++ b/tests/baselines/reference/noUnusedLocals_selfReference.types @@ -30,17 +30,6 @@ enum E { A = 0, B = E.A } >E : typeof E >A : E -// Does not detect mutual recursion. -function g() { D; } ->g : () => void ->D : typeof D - -class D { m() { g; } } ->D : D ->m : () => void ->g : () => void - -// Does not work on private methods. class P { private m() { this.m; } } >P : P >m : () => void @@ -51,3 +40,13 @@ class P { private m() { this.m; } } P; >P : typeof P +// Does not detect mutual recursion. +function g() { D; } +>g : () => void +>D : typeof D + +class D { m() { g; } } +>D : D +>m : () => void +>g : () => void + diff --git a/tests/cases/compiler/noUnusedLocals_selfReference.ts b/tests/cases/compiler/noUnusedLocals_selfReference.ts index fc6b02b600634..10ec9ebf782a9 100644 --- a/tests/cases/compiler/noUnusedLocals_selfReference.ts +++ b/tests/cases/compiler/noUnusedLocals_selfReference.ts @@ -13,10 +13,9 @@ class C { } enum E { A = 0, B = E.A } +class P { private m() { this.m; } } +P; + // Does not detect mutual recursion. function g() { D; } class D { m() { g; } } - -// Does not work on private methods. -class P { private m() { this.m; } } -P; From 49cfbb438f43cc93d49e603fac2da08664623ef0 Mon Sep 17 00:00:00 2001 From: Sheetal Nandi Date: Tue, 17 Oct 2017 12:18:17 -0700 Subject: [PATCH 12/51] Added test for windows style paths watched directories --- .../unittests/tsserverProjectSystem.ts | 40 +++++++++++++++++++ src/harness/virtualFileSystemWithWatch.ts | 31 +++++++++++--- 2 files changed, 65 insertions(+), 6 deletions(-) diff --git a/src/harness/unittests/tsserverProjectSystem.ts b/src/harness/unittests/tsserverProjectSystem.ts index 175b579103f57..bc88a4b3837d9 100644 --- a/src/harness/unittests/tsserverProjectSystem.ts +++ b/src/harness/unittests/tsserverProjectSystem.ts @@ -5760,4 +5760,44 @@ namespace ts.projectSystem { } }); }); + + describe("Watched recursive directories with windows style file system", () => { + function verifyWatchedDirectories(useProjectAtRoot: boolean) { + const root = useProjectAtRoot ? "c:/" : "c:/myfolder/allproject/"; + const configFile: FileOrFolder = { + path: root + "project/tsconfig.json", + content: "{}" + }; + const file1: FileOrFolder = { + path: root + "project/file1.ts", + content: "let x = 10;" + }; + const file2: FileOrFolder = { + path: root + "project/file2.ts", + content: "let y = 10;" + }; + const files = [configFile, file1, file2, libFile]; + const host = createServerHost(files, { useWindowsStylePaths: true }); + const projectService = createProjectService(host); + projectService.openClientFile(file1.path); + const project = projectService.configuredProjects.get(configFile.path); + assert.isDefined(project); + const winsowsStyleLibFilePath = "c:/" + libFile.path.substring(1); + checkProjectActualFiles(project, files.map(f => f === libFile ? winsowsStyleLibFilePath : f.path)); + checkWatchedFiles(host, mapDefined(files, f => f === libFile ? winsowsStyleLibFilePath : f === file1 ? undefined : f.path)); + checkWatchedDirectories(host, [], /*recursive*/ false); + checkWatchedDirectories(host, [ + root + "project", + root + "project/node_modules/@types" + ].concat(useProjectAtRoot ? [] : [root + nodeModulesAtTypes]), /*recursive*/ true); + } + + it("When project is in rootFolder", () => { + verifyWatchedDirectories(/*useProjectAtRoot*/ true); + }); + + it("When files at some folder other than root", () => { + verifyWatchedDirectories(/*useProjectAtRoot*/ false); + }); + }); } diff --git a/src/harness/virtualFileSystemWithWatch.ts b/src/harness/virtualFileSystemWithWatch.ts index 67b3e264db4ad..4463bbac7a556 100644 --- a/src/harness/virtualFileSystemWithWatch.ts +++ b/src/harness/virtualFileSystemWithWatch.ts @@ -28,6 +28,7 @@ namespace ts.TestFSWithWatch { executingFilePath?: string; currentDirectory?: string; newLine?: string; + useWindowsStylePaths?: boolean; } export function createWatchedSystem(fileOrFolderList: ReadonlyArray, params?: TestServerHostCreationParameters): TestServerHost { @@ -39,7 +40,8 @@ namespace ts.TestFSWithWatch { params.executingFilePath || getExecutingFilePathFromLibFile(), params.currentDirectory || "/", fileOrFolderList, - params.newLine); + params.newLine, + params.useWindowsStylePaths); return host; } @@ -52,7 +54,8 @@ namespace ts.TestFSWithWatch { params.executingFilePath || getExecutingFilePathFromLibFile(), params.currentDirectory || "/", fileOrFolderList, - params.newLine); + params.newLine, + params.useWindowsStylePaths); return host; } @@ -230,11 +233,14 @@ namespace ts.TestFSWithWatch { readonly watchedDirectories = createMultiMap(); readonly watchedDirectoriesRecursive = createMultiMap(); readonly watchedFiles = createMultiMap(); + private readonly executingFilePath: string; + private readonly currentDirectory: string; - constructor(public withSafeList: boolean, public useCaseSensitiveFileNames: boolean, private executingFilePath: string, private currentDirectory: string, fileOrFolderList: ReadonlyArray, public readonly newLine = "\n") { + constructor(public withSafeList: boolean, public useCaseSensitiveFileNames: boolean, executingFilePath: string, currentDirectory: string, fileOrFolderList: ReadonlyArray, public readonly newLine = "\n", public readonly useWindowsStylePath?: boolean) { this.getCanonicalFileName = createGetCanonicalFileName(useCaseSensitiveFileNames); this.toPath = s => toPath(s, currentDirectory, this.getCanonicalFileName); - + this.executingFilePath = this.getHostSpecificPath(executingFilePath); + this.currentDirectory = this.getHostSpecificPath(currentDirectory); this.reloadFS(fileOrFolderList); } @@ -250,11 +256,24 @@ namespace ts.TestFSWithWatch { return this.toPath(this.toNormalizedAbsolutePath(s)); } + getHostSpecificPath(s: string) { + if (this.useWindowsStylePath && s.startsWith(directorySeparator)) { + return "c:/" + s.substring(1); + } + return s; + } + reloadFS(fileOrFolderList: ReadonlyArray) { const mapNewLeaves = createMap(); const isNewFs = this.fs.size === 0; - // always inject safelist file in the list of files - for (const fileOrDirectory of fileOrFolderList.concat(this.withSafeList ? safeList : [])) { + fileOrFolderList = fileOrFolderList.concat(this.withSafeList ? safeList : []); + const filesOrFoldersToLoad: ReadonlyArray = !this.useWindowsStylePath ? fileOrFolderList : + fileOrFolderList.map(f => { + const result = clone(f); + result.path = this.getHostSpecificPath(f.path); + return result; + }); + for (const fileOrDirectory of filesOrFoldersToLoad) { const path = this.toFullPath(fileOrDirectory.path); mapNewLeaves.set(path, true); // If its a change From a5861af00e06ee8201f90446cb0ae37173ddde0b Mon Sep 17 00:00:00 2001 From: Sheetal Nandi Date: Tue, 17 Oct 2017 14:13:12 -0700 Subject: [PATCH 13/51] Handle when directory watcher is invoked on file change Fixes #19206 --- src/compiler/core.ts | 17 ++++++++-- src/compiler/watch.ts | 13 ++++++-- src/harness/unittests/tscWatchMode.ts | 40 +++++++++++++++++++++++ src/harness/virtualFileSystemWithWatch.ts | 9 +++-- 4 files changed, 72 insertions(+), 7 deletions(-) diff --git a/src/compiler/core.ts b/src/compiler/core.ts index 542435b84f901..8f3eacae667bb 100644 --- a/src/compiler/core.ts +++ b/src/compiler/core.ts @@ -2701,8 +2701,14 @@ namespace ts { export function assertTypeIsNever(_: never): void { } + export interface FileAndDirectoryExistence { + fileExists: boolean; + directoryExists: boolean; + } + export interface CachedDirectoryStructureHost extends DirectoryStructureHost { - addOrDeleteFileOrDirectory(fileOrDirectory: string, fileOrDirectoryPath: Path): void; + /** Returns the queried result for the file exists and directory exists if at all it was done */ + addOrDeleteFileOrDirectory(fileOrDirectory: string, fileOrDirectoryPath: Path): FileAndDirectoryExistence | undefined; addOrDeleteFile(fileName: string, filePath: Path, eventKind: FileWatcherEventKind): void; clearCache(): void; } @@ -2872,8 +2878,13 @@ namespace ts { if (parentResult) { const baseName = getBaseNameOfFileName(fileOrDirectory); if (parentResult) { - updateFilesOfFileSystemEntry(parentResult, baseName, host.fileExists(fileOrDirectoryPath)); - updateFileSystemEntry(parentResult.directories, baseName, host.directoryExists(fileOrDirectoryPath)); + const fsQueryResult: FileAndDirectoryExistence = { + fileExists: host.fileExists(fileOrDirectoryPath), + directoryExists: host.directoryExists(fileOrDirectoryPath) + }; + updateFilesOfFileSystemEntry(parentResult, baseName, fsQueryResult.fileExists); + updateFileSystemEntry(parentResult.directories, baseName, fsQueryResult.directoryExists); + return fsQueryResult; } } } diff --git a/src/compiler/watch.ts b/src/compiler/watch.ts index 4fc67c1cc8fc2..6f61469197b21 100644 --- a/src/compiler/watch.ts +++ b/src/compiler/watch.ts @@ -605,8 +605,17 @@ namespace ts { const fileOrDirectoryPath = toPath(fileOrDirectory); // Since the file existance changed, update the sourceFiles cache - (directoryStructureHost as CachedDirectoryStructureHost).addOrDeleteFileOrDirectory(fileOrDirectory, fileOrDirectoryPath); - removeSourceFile(fileOrDirectoryPath); + const result = (directoryStructureHost as CachedDirectoryStructureHost).addOrDeleteFileOrDirectory(fileOrDirectory, fileOrDirectoryPath); + + // Instead of deleting the file, mark it as changed instead + // Many times node calls add/remove/file when watching directories recursively + const hostSourceFile = sourceFilesCache.get(fileOrDirectoryPath); + if (hostSourceFile && !isString(hostSourceFile) && (result ? result.fileExists : directoryStructureHost.fileExists(fileOrDirectory))) { + hostSourceFile.version++; + } + else { + removeSourceFile(fileOrDirectoryPath); + } // 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 diff --git a/src/harness/unittests/tscWatchMode.ts b/src/harness/unittests/tscWatchMode.ts index b25a7b1eb53c2..3c2856f3caffc 100644 --- a/src/harness/unittests/tscWatchMode.ts +++ b/src/harness/unittests/tscWatchMode.ts @@ -1819,4 +1819,44 @@ declare module "fs" { checkOutputErrors(host); }); }); + + describe("tsc-watch with when module emit is specified as node", () => { + it("when instead of filechanged recursive directory watcher is invoked", () => { + const configFile: FileOrFolder = { + path: "/a/rootFolder/project/tsconfig.json", + content: JSON.stringify({ + "compilerOptions": { + "module": "none", + "allowJs": true, + "outDir": "Static/scripts/" + }, + "include": [ + "Scripts/**/*" + ], + }) + }; + const outputFolder = "/a/rootFolder/project/Static/scripts/"; + const file1: FileOrFolder = { + path: "/a/rootFolder/project/Scripts/TypeScript.ts", + content: "var z = 10;" + }; + const file2: FileOrFolder = { + path: "/a/rootFolder/project/Scripts/Javascript.js", + content: "var zz = 10;" + }; + const files = [configFile, file1, file2, libFile]; + const host = createWatchedSystem(files); + const watch = createWatchModeWithConfigFile(configFile.path, host); + + checkProgramActualFiles(watch(), mapDefined(files, f => f === configFile ? undefined : f.path)); + file1.content = "var zz30 = 100;"; + host.reloadFS(files, /*invokeDirectoryWatcherInsteadOfFileChanged*/ true); + host.runQueuedTimeoutCallbacks(); + + checkProgramActualFiles(watch(), mapDefined(files, f => f === configFile ? undefined : f.path)); + const outputFile1 = changeExtension((outputFolder + getBaseFileName(file1.path)), ".js"); + assert.isTrue(host.fileExists(outputFile1)); + assert.equal(host.readFile(outputFile1), file1.content + host.newLine); + }); + }); } diff --git a/src/harness/virtualFileSystemWithWatch.ts b/src/harness/virtualFileSystemWithWatch.ts index 67b3e264db4ad..c74be545f8fc4 100644 --- a/src/harness/virtualFileSystemWithWatch.ts +++ b/src/harness/virtualFileSystemWithWatch.ts @@ -250,7 +250,7 @@ namespace ts.TestFSWithWatch { return this.toPath(this.toNormalizedAbsolutePath(s)); } - reloadFS(fileOrFolderList: ReadonlyArray) { + reloadFS(fileOrFolderList: ReadonlyArray, invokeDirectoryWatcherInsteadOfFileChanged?: boolean) { const mapNewLeaves = createMap(); const isNewFs = this.fs.size === 0; // always inject safelist file in the list of files @@ -265,7 +265,12 @@ namespace ts.TestFSWithWatch { // Update file if (currentEntry.content !== fileOrDirectory.content) { currentEntry.content = fileOrDirectory.content; - this.invokeFileWatcher(currentEntry.fullPath, FileWatcherEventKind.Changed); + if (invokeDirectoryWatcherInsteadOfFileChanged) { + this.invokeDirectoryWatcher(getDirectoryPath(currentEntry.fullPath), currentEntry.fullPath); + } + else { + this.invokeFileWatcher(currentEntry.fullPath, FileWatcherEventKind.Changed); + } } } else { From d05443bb1d3c346fbef4c2468ff06b00041b688d Mon Sep 17 00:00:00 2001 From: Andy Date: Tue, 17 Oct 2017 15:04:09 -0700 Subject: [PATCH 14/51] Add quickfix and refactoring to install @types packages (#19130) * Add quickfix and refactoring to install @types packages * Move `validatePackageName` to `jsTyping.ts` * Remove combinePaths overloads * Respond to code review * Update api baselines * Use native PromiseConstructor * Return false instead of undefined * Remove getProjectRootPath * Update api --- scripts/buildProtocol.ts | 33 ++++--- src/compiler/core.ts | 2 +- src/compiler/moduleNameResolver.ts | 32 ++++--- src/harness/fourslash.ts | 55 +++++++++-- src/harness/harnessLanguageService.ts | 7 ++ src/harness/unittests/compileOnSave.ts | 2 +- src/harness/unittests/extractTestHelpers.ts | 10 ++ src/harness/unittests/projectErrors.ts | 10 +- src/harness/unittests/session.ts | 10 +- .../unittests/tsserverProjectSystem.ts | 41 +++++---- src/harness/unittests/typingsInstaller.ts | 20 ++-- src/server/client.ts | 4 +- src/server/project.ts | 10 ++ src/server/protocol.ts | 24 ++++- src/server/server.ts | 54 ++++++++++- src/server/session.ts | 75 ++++++++++----- src/server/shared.ts | 2 + src/server/types.ts | 40 +++++++- src/server/typingsCache.ts | 17 ++++ .../typingsInstaller/nodeTypingsInstaller.ts | 50 ++++++++-- .../typingsInstaller/typingsInstaller.ts | 74 ++------------- src/services/codefixes/fixCannotFindModule.ts | 35 +++++++ src/services/codefixes/fixes.ts | 1 + src/services/jsTyping.ts | 61 +++++++++++++ src/services/refactorProvider.ts | 1 + .../refactors/installTypesForPackage.ts | 63 +++++++++++++ src/services/refactors/refactors.ts | 1 + src/services/services.ts | 17 +++- src/services/types.ts | 32 ++++++- src/services/utilities.ts | 9 ++ .../reference/api/tsserverlibrary.d.ts | 91 +++++++++++++++---- tests/baselines/reference/api/typescript.d.ts | 23 ++++- .../fourslash/codeFixCannotFindModule.ts | 15 +++ tests/cases/fourslash/fourslash.ts | 11 ++- .../refactorInstallTypesForPackage.ts | 25 +++++ ...ctorInstallTypesForPackage_importEquals.ts | 25 +++++ 36 files changed, 773 insertions(+), 209 deletions(-) create mode 100644 src/services/codefixes/fixCannotFindModule.ts create mode 100644 src/services/refactors/installTypesForPackage.ts create mode 100644 tests/cases/fourslash/codeFixCannotFindModule.ts create mode 100644 tests/cases/fourslash/refactorInstallTypesForPackage.ts create mode 100644 tests/cases/fourslash/refactorInstallTypesForPackage_importEquals.ts diff --git a/scripts/buildProtocol.ts b/scripts/buildProtocol.ts index c2ac33c83fc19..899ab700bf372 100644 --- a/scripts/buildProtocol.ts +++ b/scripts/buildProtocol.ts @@ -51,22 +51,25 @@ class DeclarationsWalker { return this.processType((type).typeArguments[0]); } else { - for (const decl of s.getDeclarations()) { - const sourceFile = decl.getSourceFile(); - if (sourceFile === this.protocolFile || path.basename(sourceFile.fileName) === "lib.d.ts") { - return; - } - if (decl.kind === ts.SyntaxKind.EnumDeclaration && !isStringEnum(decl as ts.EnumDeclaration)) { - this.removedTypes.push(type); - return; - } - else { - // splice declaration in final d.ts file - let text = decl.getFullText(); - this.text += `${text}\n`; - // recursively pull all dependencies into result dts file + const declarations = s.getDeclarations(); + if (declarations) { + for (const decl of declarations) { + const sourceFile = decl.getSourceFile(); + if (sourceFile === this.protocolFile || path.basename(sourceFile.fileName) === "lib.d.ts") { + return; + } + if (decl.kind === ts.SyntaxKind.EnumDeclaration && !isStringEnum(decl as ts.EnumDeclaration)) { + this.removedTypes.push(type); + return; + } + else { + // splice declaration in final d.ts file + let text = decl.getFullText(); + this.text += `${text}\n`; + // recursively pull all dependencies into result dts file - this.visitTypeNodes(decl); + this.visitTypeNodes(decl); + } } } } diff --git a/src/compiler/core.ts b/src/compiler/core.ts index 542435b84f901..bc28965b7c5da 100644 --- a/src/compiler/core.ts +++ b/src/compiler/core.ts @@ -1861,7 +1861,7 @@ namespace ts { return i < 0 ? path : path.substring(i + 1); } - export function combinePaths(path1: string, path2: string) { + export function combinePaths(path1: string, path2: string): string { if (!(path1 && path1.length)) return path2; if (!(path2 && path2.length)) return path1; if (getRootLength(path2) !== 0) return path2; diff --git a/src/compiler/moduleNameResolver.ts b/src/compiler/moduleNameResolver.ts index a0e045cf0f259..b0633ef4d5a26 100644 --- a/src/compiler/moduleNameResolver.ts +++ b/src/compiler/moduleNameResolver.ts @@ -1005,7 +1005,8 @@ namespace ts { return withPackageId(packageId, pathAndExtension); } - function getPackageName(moduleName: string): { packageName: string, rest: string } { + /* @internal */ + export function getPackageName(moduleName: string): { packageName: string, rest: string } { let idx = moduleName.indexOf(directorySeparator); if (moduleName[0] === "@") { idx = moduleName.indexOf(directorySeparator, idx + 1); @@ -1063,18 +1064,27 @@ namespace ts { const mangledScopedPackageSeparator = "__"; /** For a scoped package, we must look in `@types/foo__bar` instead of `@types/@foo/bar`. */ - function mangleScopedPackage(moduleName: string, state: ModuleResolutionState): string { - if (startsWith(moduleName, "@")) { - const replaceSlash = moduleName.replace(ts.directorySeparator, mangledScopedPackageSeparator); - if (replaceSlash !== moduleName) { - const mangled = replaceSlash.slice(1); // Take off the "@" - if (state.traceEnabled) { - trace(state.host, Diagnostics.Scoped_package_detected_looking_in_0, mangled); - } - return mangled; + function mangleScopedPackage(packageName: string, state: ModuleResolutionState): string { + const mangled = getMangledNameForScopedPackage(packageName); + if (state.traceEnabled && mangled !== packageName) { + trace(state.host, Diagnostics.Scoped_package_detected_looking_in_0, mangled); + } + return mangled; + } + + /* @internal */ + export function getTypesPackageName(packageName: string): string { + return `@types/${getMangledNameForScopedPackage(packageName)}`; + } + + function getMangledNameForScopedPackage(packageName: string): string { + if (startsWith(packageName, "@")) { + const replaceSlash = packageName.replace(ts.directorySeparator, mangledScopedPackageSeparator); + if (replaceSlash !== packageName) { + return replaceSlash.slice(1); // Take off the "@" } } - return moduleName; + return packageName; } /* @internal */ diff --git a/src/harness/fourslash.ts b/src/harness/fourslash.ts index 493b0187b57ad..9ac2aac764529 100644 --- a/src/harness/fourslash.ts +++ b/src/harness/fourslash.ts @@ -953,6 +953,10 @@ namespace FourSlash { return this.getChecker().getSymbolsInScope(node, ts.SymbolFlags.Value | ts.SymbolFlags.Type | ts.SymbolFlags.Namespace); } + public setTypesRegistry(map: ts.MapLike): void { + this.languageServiceAdapterHost.typesRegistry = ts.createMapFromTemplate(map); + } + public verifyTypeOfSymbolAtLocation(range: Range, symbol: ts.Symbol, expected: string): void { const node = this.goToAndGetNode(range); const checker = this.getChecker(); @@ -2777,16 +2781,26 @@ Actual: ${stringify(fullActual)}`); } } - public verifyCodeFixAvailable(negative: boolean) { - const codeFix = this.getCodeFixActions(this.activeFile.fileName); + public verifyCodeFixAvailable(negative: boolean, info: FourSlashInterface.VerifyCodeFixAvailableOptions[] | undefined) { + const codeFixes = this.getCodeFixActions(this.activeFile.fileName); - if (negative && codeFix.length) { - this.raiseError(`verifyCodeFixAvailable failed - expected no fixes but found one.`); + if (negative) { + if (codeFixes.length) { + this.raiseError(`verifyCodeFixAvailable failed - expected no fixes but found one.`); + } + return; } - if (!(negative || codeFix.length)) { + if (!codeFixes.length) { this.raiseError(`verifyCodeFixAvailable failed - expected code fixes but none found.`); } + if (info) { + assert.equal(info.length, codeFixes.length); + ts.zipWith(codeFixes, info, (fix, info) => { + assert.equal(fix.description, info.description); + this.assertObjectsEqual(fix.commands, info.commands); + }); + } } public verifyApplicableRefactorAvailableAtMarker(negative: boolean, markerName: string) { @@ -2830,6 +2844,14 @@ Actual: ${stringify(fullActual)}`); } } + public verifyRefactor({ name, actionName, refactors }: FourSlashInterface.VerifyRefactorOptions) { + const selection = this.getSelection(); + + const actualRefactors = (this.languageService.getApplicableRefactors(this.activeFile.fileName, selection) || ts.emptyArray) + .filter(r => r.name === name && r.actions.some(a => a.name === actionName)); + this.assertObjectsEqual(actualRefactors, refactors); + } + public verifyApplicableRefactorAvailableForRange(negative: boolean) { const ranges = this.getRanges(); if (!(ranges && ranges.length === 1)) { @@ -3614,6 +3636,10 @@ namespace FourSlashInterface { public symbolsInScope(range: FourSlash.Range): ts.Symbol[] { return this.state.symbolsInScope(range); } + + public setTypesRegistry(map: ts.MapLike): void { + this.state.setTypesRegistry(map); + } } export class GoTo { @@ -3789,8 +3815,8 @@ namespace FourSlashInterface { this.state.verifyCodeFix(options); } - public codeFixAvailable() { - this.state.verifyCodeFixAvailable(this.negative); + public codeFixAvailable(options?: VerifyCodeFixAvailableOptions[]) { + this.state.verifyCodeFixAvailable(this.negative, options); } public applicableRefactorAvailableAtMarker(markerName: string) { @@ -3801,6 +3827,10 @@ namespace FourSlashInterface { this.state.verifyApplicableRefactorAvailableForRange(this.negative); } + public refactor(options: VerifyRefactorOptions) { + this.state.verifyRefactor(options); + } + public refactorAvailable(name: string, actionName?: string) { this.state.verifyRefactorAvailable(this.negative, name, actionName); } @@ -4449,6 +4479,17 @@ namespace FourSlashInterface { index?: number; } + export interface VerifyCodeFixAvailableOptions { + description: string; + commands?: ts.CodeActionCommand[]; + } + + export interface VerifyRefactorOptions { + name: string; + actionName: string; + refactors: ts.ApplicableRefactorInfo[]; + } + export interface VerifyCompletionActionOptions extends NewContentOptions { name: string; description: string; diff --git a/src/harness/harnessLanguageService.ts b/src/harness/harnessLanguageService.ts index 527824ee145aa..a12554addfe6f 100644 --- a/src/harness/harnessLanguageService.ts +++ b/src/harness/harnessLanguageService.ts @@ -123,6 +123,7 @@ namespace Harness.LanguageService { } export class LanguageServiceAdapterHost { + public typesRegistry: ts.Map | undefined; protected virtualFileSystem: Utils.VirtualFileSystem = new Utils.VirtualFileSystem(virtualFileSystemRoot, /*useCaseSensitiveFilenames*/false); constructor(protected cancellationToken = DefaultHostCancellationToken.Instance, @@ -182,6 +183,11 @@ namespace Harness.LanguageService { /// Native adapter class NativeLanguageServiceHost extends LanguageServiceAdapterHost implements ts.LanguageServiceHost { + isKnownTypesPackageName(name: string): boolean { + return this.typesRegistry && this.typesRegistry.has(name); + } + installPackage = ts.notImplemented; + getCompilationSettings() { return this.settings; } getCancellationToken() { return this.cancellationToken; } getDirectories(path: string): string[] { @@ -493,6 +499,7 @@ namespace Harness.LanguageService { getCodeFixesAtPosition(): ts.CodeAction[] { throw new Error("Not supported on the shim."); } + applyCodeActionCommand = ts.notImplemented; getCodeFixDiagnostics(): ts.Diagnostic[] { throw new Error("Not supported on the shim."); } diff --git a/src/harness/unittests/compileOnSave.ts b/src/harness/unittests/compileOnSave.ts index fdec5b192ee83..7be6ab5b323ec 100644 --- a/src/harness/unittests/compileOnSave.ts +++ b/src/harness/unittests/compileOnSave.ts @@ -12,7 +12,7 @@ namespace ts.projectSystem { describe("CompileOnSave affected list", () => { function sendAffectedFileRequestAndCheckResult(session: server.Session, request: server.protocol.Request, expectedFileList: { projectFileName: string, files: FileOrFolder[] }[]) { - const response: server.protocol.CompileOnSaveAffectedFileListSingleProject[] = session.executeCommand(request).response; + const response = session.executeCommand(request).response as server.protocol.CompileOnSaveAffectedFileListSingleProject[]; const actualResult = response.sort((list1, list2) => compareStrings(list1.projectFileName, list2.projectFileName)); expectedFileList = expectedFileList.sort((list1, list2) => compareStrings(list1.projectFileName, list2.projectFileName)); diff --git a/src/harness/unittests/extractTestHelpers.ts b/src/harness/unittests/extractTestHelpers.ts index 1b51cdadc2785..49c2c1d327704 100644 --- a/src/harness/unittests/extractTestHelpers.ts +++ b/src/harness/unittests/extractTestHelpers.ts @@ -97,6 +97,14 @@ namespace ts { return rulesProvider; } + const notImplementedHost: LanguageServiceHost = { + getCompilationSettings: notImplemented, + getScriptFileNames: notImplemented, + getScriptVersion: notImplemented, + getScriptSnapshot: notImplemented, + getDefaultLibFileName: notImplemented, + }; + export function testExtractSymbol(caption: string, text: string, baselineFolder: string, description: DiagnosticMessage) { const t = extractTest(text); const selectionRange = t.ranges.get("selection"); @@ -125,6 +133,7 @@ namespace ts { file: sourceFile, startPosition: selectionRange.start, endPosition: selectionRange.end, + host: notImplementedHost, rulesProvider: getRuleProvider() }; const rangeToExtract = refactor.extractSymbol.getRangeToExtract(sourceFile, createTextSpanFromBounds(selectionRange.start, selectionRange.end)); @@ -188,6 +197,7 @@ namespace ts { file: sourceFile, startPosition: selectionRange.start, endPosition: selectionRange.end, + host: notImplementedHost, rulesProvider: getRuleProvider() }; const rangeToExtract = refactor.extractSymbol.getRangeToExtract(sourceFile, createTextSpanFromBounds(selectionRange.start, selectionRange.end)); diff --git a/src/harness/unittests/projectErrors.ts b/src/harness/unittests/projectErrors.ts index d72168383c170..dae465a3ef333 100644 --- a/src/harness/unittests/projectErrors.ts +++ b/src/harness/unittests/projectErrors.ts @@ -57,7 +57,7 @@ namespace ts.projectSystem { }); checkNumberOfProjects(projectService, { externalProjects: 1 }); - const diags = session.executeCommand(compilerOptionsRequest).response; + const diags = session.executeCommand(compilerOptionsRequest).response as server.protocol.DiagnosticWithLinePosition[]; // only file1 exists - expect error checkDiagnosticsWithLinePos(diags, ["File '/a/b/applib.ts' not found."]); } @@ -65,7 +65,7 @@ namespace ts.projectSystem { { // only file2 exists - expect error checkNumberOfProjects(projectService, { externalProjects: 1 }); - const diags = session.executeCommand(compilerOptionsRequest).response; + const diags = session.executeCommand(compilerOptionsRequest).response as server.protocol.DiagnosticWithLinePosition[]; checkDiagnosticsWithLinePos(diags, ["File '/a/b/app.ts' not found."]); } @@ -73,7 +73,7 @@ namespace ts.projectSystem { { // both files exist - expect no errors checkNumberOfProjects(projectService, { externalProjects: 1 }); - const diags = session.executeCommand(compilerOptionsRequest).response; + const diags = session.executeCommand(compilerOptionsRequest).response as server.protocol.DiagnosticWithLinePosition[]; checkDiagnosticsWithLinePos(diags, []); } }); @@ -103,13 +103,13 @@ namespace ts.projectSystem { seq: 2, arguments: { projectFileName: project.getProjectName() } }; - let diags = session.executeCommand(compilerOptionsRequest).response; + let diags = session.executeCommand(compilerOptionsRequest).response as server.protocol.DiagnosticWithLinePosition[]; checkDiagnosticsWithLinePos(diags, ["File '/a/b/applib.ts' not found."]); host.reloadFS([file1, file2, config, libFile]); checkNumberOfProjects(projectService, { configuredProjects: 1 }); - diags = session.executeCommand(compilerOptionsRequest).response; + diags = session.executeCommand(compilerOptionsRequest).response as server.protocol.DiagnosticWithLinePosition[]; checkDiagnosticsWithLinePos(diags, []); }); diff --git a/src/harness/unittests/session.ts b/src/harness/unittests/session.ts index 3b5efc2d6defb..fbef9cb455841 100644 --- a/src/harness/unittests/session.ts +++ b/src/harness/unittests/session.ts @@ -315,7 +315,7 @@ namespace ts.server { item: false }; const command = "newhandle"; - const result = { + const result: ts.server.HandlerResponse = { response: respBody, responseRequired: true }; @@ -332,7 +332,7 @@ namespace ts.server { const respBody = { item: false }; - const resp = { + const resp: ts.server.HandlerResponse = { response: respBody, responseRequired: true }; @@ -372,7 +372,7 @@ namespace ts.server { }; const command = "test"; - session.output(body, command); + session.output(body, command, /*reqSeq*/ 0); expect(lastSent).to.deep.equal({ seq: 0, @@ -475,7 +475,7 @@ namespace ts.server { }; const command = "test"; - session.output(body, command); + session.output(body, command, /*reqSeq*/ 0); expect(session.lastSent).to.deep.equal({ seq: 0, @@ -542,7 +542,7 @@ namespace ts.server { handleRequest(msg: protocol.Request) { let response: protocol.Response; try { - ({ response } = this.executeCommand(msg)); + response = this.executeCommand(msg).response as protocol.Response; } catch (e) { this.output(undefined, msg.command, msg.seq, e.toString()); diff --git a/src/harness/unittests/tsserverProjectSystem.ts b/src/harness/unittests/tsserverProjectSystem.ts index bc88a4b3837d9..943fae87af086 100644 --- a/src/harness/unittests/tsserverProjectSystem.ts +++ b/src/harness/unittests/tsserverProjectSystem.ts @@ -70,6 +70,9 @@ namespace ts.projectSystem { protected postExecActions: PostExecAction[] = []; + isKnownTypesPackageName = notImplemented; + installPackage = notImplemented; + executePendingCommands() { const actionsToRun = this.postExecActions; this.postExecActions = []; @@ -769,7 +772,7 @@ namespace ts.projectSystem { ); // Two errors: CommonFile2 not found and cannot find name y - let diags: server.protocol.Diagnostic[] = session.executeCommand(getErrRequest).response; + let diags = session.executeCommand(getErrRequest).response as server.protocol.Diagnostic[]; verifyDiagnostics(diags, [ { diagnosticMessage: Diagnostics.Cannot_find_name_0, errorTextArguments: ["y"] }, { diagnosticMessage: Diagnostics.File_0_not_found, errorTextArguments: [commonFile2.path] } @@ -781,7 +784,7 @@ 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; + diags = session.executeCommand(getErrRequest).response as server.protocol.Diagnostic[]; verifyNoDiagnostics(diags); }); @@ -2614,11 +2617,11 @@ namespace ts.projectSystem { // Try to find some interface type defined in lib.d.ts const libTypeNavToRequest = makeSessionRequest(CommandNames.Navto, { searchValue: "Document", file: file1.path, projectFileName: configFile.path }); - const items: protocol.NavtoItem[] = session.executeCommand(libTypeNavToRequest).response; + const items = session.executeCommand(libTypeNavToRequest).response as protocol.NavtoItem[]; assert.isFalse(containsNavToItem(items, "Document", "interface"), `Found lib.d.ts symbol in JavaScript project nav to request result.`); const localFunctionNavToRequst = makeSessionRequest(CommandNames.Navto, { searchValue: "foo", file: file1.path, projectFileName: configFile.path }); - const items2: protocol.NavtoItem[] = session.executeCommand(localFunctionNavToRequst).response; + const items2 = session.executeCommand(localFunctionNavToRequst).response as protocol.NavtoItem[]; assert.isTrue(containsNavToItem(items2, "foo", "function"), `Cannot find function symbol "foo".`); }); }); @@ -3073,7 +3076,7 @@ namespace ts.projectSystem { server.CommandNames.SemanticDiagnosticsSync, { file: file1.path } ); - let diags: server.protocol.Diagnostic[] = session.executeCommand(getErrRequest).response; + let diags = session.executeCommand(getErrRequest).response as server.protocol.Diagnostic[]; verifyNoDiagnostics(diags); const moduleFileOldPath = moduleFile.path; @@ -3081,7 +3084,7 @@ namespace ts.projectSystem { moduleFile.path = moduleFileNewPath; host.reloadFS([moduleFile, file1]); host.runQueuedTimeoutCallbacks(); - diags = session.executeCommand(getErrRequest).response; + diags = session.executeCommand(getErrRequest).response as server.protocol.Diagnostic[]; verifyDiagnostics(diags, [ { diagnosticMessage: Diagnostics.Cannot_find_module_0, errorTextArguments: ["./moduleFile"] } ]); @@ -3099,7 +3102,7 @@ namespace ts.projectSystem { session.executeCommand(changeRequest); host.runQueuedTimeoutCallbacks(); - diags = session.executeCommand(getErrRequest).response; + diags = session.executeCommand(getErrRequest).response as server.protocol.Diagnostic[]; verifyNoDiagnostics(diags); }); @@ -3124,7 +3127,7 @@ namespace ts.projectSystem { server.CommandNames.SemanticDiagnosticsSync, { file: file1.path } ); - let diags: server.protocol.Diagnostic[] = session.executeCommand(getErrRequest).response; + let diags = session.executeCommand(getErrRequest).response as server.protocol.Diagnostic[]; verifyNoDiagnostics(diags); const moduleFileOldPath = moduleFile.path; @@ -3132,7 +3135,7 @@ namespace ts.projectSystem { moduleFile.path = moduleFileNewPath; host.reloadFS([moduleFile, file1, configFile]); host.runQueuedTimeoutCallbacks(); - diags = session.executeCommand(getErrRequest).response; + diags = session.executeCommand(getErrRequest).response as server.protocol.Diagnostic[]; verifyDiagnostics(diags, [ { diagnosticMessage: Diagnostics.Cannot_find_module_0, errorTextArguments: ["./moduleFile"] } ]); @@ -3140,7 +3143,7 @@ namespace ts.projectSystem { moduleFile.path = moduleFileOldPath; host.reloadFS([moduleFile, file1, configFile]); host.runQueuedTimeoutCallbacks(); - diags = session.executeCommand(getErrRequest).response; + diags = session.executeCommand(getErrRequest).response as server.protocol.Diagnostic[]; verifyNoDiagnostics(diags); }); @@ -3207,7 +3210,7 @@ namespace ts.projectSystem { server.CommandNames.SemanticDiagnosticsSync, { file: file1.path } ); - let diags: server.protocol.Diagnostic[] = session.executeCommand(getErrRequest).response; + let diags = session.executeCommand(getErrRequest).response as server.protocol.Diagnostic[]; verifyDiagnostics(diags, [ { diagnosticMessage: Diagnostics.Cannot_find_module_0, errorTextArguments: ["./moduleFile"] } ]); @@ -3223,7 +3226,7 @@ namespace ts.projectSystem { session.executeCommand(changeRequest); // Recheck - diags = session.executeCommand(getErrRequest).response; + diags = session.executeCommand(getErrRequest).response as server.protocol.Diagnostic[]; verifyNoDiagnostics(diags); }); }); @@ -3923,7 +3926,7 @@ namespace ts.projectSystem { command: server.CommandNames.CompilerOptionsDiagnosticsFull, seq: 2, arguments: { projectFileName: projectName } - }).response; + }).response as ReadonlyArray; assert.isTrue(diags.length === 0); session.executeCommand({ @@ -3937,7 +3940,7 @@ namespace ts.projectSystem { command: server.CommandNames.CompilerOptionsDiagnosticsFull, seq: 4, arguments: { projectFileName: projectName } - }).response; + }).response as ReadonlyArray; assert.isTrue(diagsAfterUpdate.length === 0); }); @@ -3964,7 +3967,7 @@ namespace ts.projectSystem { command: server.CommandNames.CompilerOptionsDiagnosticsFull, seq: 2, arguments: { projectFileName } - }).response; + }).response as ReadonlyArray; assert.isTrue(diags.length === 0); session.executeCommand({ @@ -3982,7 +3985,7 @@ namespace ts.projectSystem { command: server.CommandNames.CompilerOptionsDiagnosticsFull, seq: 4, arguments: { projectFileName } - }).response; + }).response as ReadonlyArray; assert.isTrue(diagsAfterUpdate.length === 0); }); }); @@ -4463,7 +4466,7 @@ namespace ts.projectSystem { command: server.CommandNames.SemanticDiagnosticsSync, seq: 2, arguments: { file: configFile.path, projectFileName: projectName, includeLinePosition: true } - }).response; + }).response as ReadonlyArray; assert.isTrue(diags.length === 2); configFile.content = configFileContentWithoutCommentLine; @@ -4474,7 +4477,7 @@ namespace ts.projectSystem { command: server.CommandNames.SemanticDiagnosticsSync, seq: 2, arguments: { file: configFile.path, projectFileName: projectName, includeLinePosition: true } - }).response; + }).response as ReadonlyArray; assert.isTrue(diagsAfterEdit.length === 2); verifyDiagnostic(diags[0], diagsAfterEdit[0]); @@ -4864,7 +4867,7 @@ namespace ts.projectSystem { line: undefined, offset: undefined }); - const { response } = session.executeCommand(getDefinitionRequest); + const response = session.executeCommand(getDefinitionRequest).response as server.protocol.FileSpan[]; assert.equal(response[0].file, moduleFile.path, "Should go to definition of vessel: response: " + JSON.stringify(response)); callsTrackingHost.verifyNoHostCalls(); diff --git a/src/harness/unittests/typingsInstaller.ts b/src/harness/unittests/typingsInstaller.ts index e644f8730107b..fb1a7a26a7ae0 100644 --- a/src/harness/unittests/typingsInstaller.ts +++ b/src/harness/unittests/typingsInstaller.ts @@ -4,6 +4,8 @@ namespace ts.projectSystem { import TI = server.typingsInstaller; + import validatePackageName = JsTyping.validatePackageName; + import PackageNameValidationResult = JsTyping.PackageNameValidationResult; interface InstallerParams { globalTypingsCacheLocation?: string; @@ -266,7 +268,7 @@ namespace ts.projectSystem { }; const host = createServerHost([file1]); let enqueueIsCalled = false; - const installer = new (class extends Installer { + const installer: Installer = new (class extends Installer { constructor() { super(host, { typesRegistry: createTypesRegistry("jquery") }); } @@ -983,21 +985,21 @@ namespace ts.projectSystem { for (let i = 0; i < 8; i++) { packageName += packageName; } - assert.equal(TI.validatePackageName(packageName), TI.PackageNameValidationResult.NameTooLong); + assert.equal(validatePackageName(packageName), PackageNameValidationResult.NameTooLong); }); it("name cannot start with dot", () => { - assert.equal(TI.validatePackageName(".foo"), TI.PackageNameValidationResult.NameStartsWithDot); + assert.equal(validatePackageName(".foo"), PackageNameValidationResult.NameStartsWithDot); }); it("name cannot start with underscore", () => { - assert.equal(TI.validatePackageName("_foo"), TI.PackageNameValidationResult.NameStartsWithUnderscore); + assert.equal(validatePackageName("_foo"), PackageNameValidationResult.NameStartsWithUnderscore); }); it("scoped packages not supported", () => { - assert.equal(TI.validatePackageName("@scope/bar"), TI.PackageNameValidationResult.ScopedPackagesNotSupported); + assert.equal(validatePackageName("@scope/bar"), PackageNameValidationResult.ScopedPackagesNotSupported); }); it("non URI safe characters are not supported", () => { - assert.equal(TI.validatePackageName(" scope "), TI.PackageNameValidationResult.NameContainsNonURISafeCharacters); - assert.equal(TI.validatePackageName("; say ‘Hello from TypeScript!’ #"), TI.PackageNameValidationResult.NameContainsNonURISafeCharacters); - assert.equal(TI.validatePackageName("a/b/c"), TI.PackageNameValidationResult.NameContainsNonURISafeCharacters); + assert.equal(validatePackageName(" scope "), PackageNameValidationResult.NameContainsNonURISafeCharacters); + assert.equal(validatePackageName("; say ‘Hello from TypeScript!’ #"), PackageNameValidationResult.NameContainsNonURISafeCharacters); + assert.equal(validatePackageName("a/b/c"), PackageNameValidationResult.NameContainsNonURISafeCharacters); }); }); @@ -1250,7 +1252,7 @@ namespace ts.projectSystem { const host = createServerHost([f1, packageFile]); let beginEvent: server.BeginInstallTypes; let endEvent: server.EndInstallTypes; - const installer = new (class extends Installer { + const installer: Installer = new (class extends Installer { constructor() { super(host, { globalTypingsCacheLocation: cachePath, typesRegistry: createTypesRegistry("commander") }); } diff --git a/src/server/client.ts b/src/server/client.ts index 39e30848e3e52..2781d7cf20ae5 100644 --- a/src/server/client.ts +++ b/src/server/client.ts @@ -14,7 +14,7 @@ namespace ts.server { } /* @internal */ - export function extractMessage(message: string) { + export function extractMessage(message: string): string { // Read the content length const contentLengthPrefix = "Content-Length: "; const lines = message.split(/\r?\n/); @@ -542,6 +542,8 @@ namespace ts.server { return response.body.map(entry => this.convertCodeActions(entry, file)); } + applyCodeActionCommand = notImplemented; + private createFileLocationOrRangeRequestArgs(positionOrRange: number | TextRange, fileName: string): protocol.FileLocationOrRangeRequestArgs { return typeof positionOrRange === "number" ? this.createFileLocationRequestArgs(fileName, positionOrRange) diff --git a/src/server/project.ts b/src/server/project.ts index 9c3fab63d2306..725ce725c3881 100644 --- a/src/server/project.ts +++ b/src/server/project.ts @@ -242,6 +242,16 @@ namespace ts.server { this.markAsDirty(); } + isKnownTypesPackageName(name: string): boolean { + return this.typingsCache.isKnownTypesPackageName(name); + } + installPackage(options: InstallPackageOptions): PromiseLike { + return this.typingsCache.installPackage({ ...options, projectRootPath: this.toPath(this.currentDirectory) }); + } + private get typingsCache(): TypingsCache { + return this.projectService.typingsCache; + } + // Method of LanguageServiceHost getCompilationSettings() { return this.compilerOptions; diff --git a/src/server/protocol.ts b/src/server/protocol.ts index 7b9e9fe80969a..35e9f20d05825 100644 --- a/src/server/protocol.ts +++ b/src/server/protocol.ts @@ -94,6 +94,7 @@ namespace ts.server.protocol { BreakpointStatement = "breakpointStatement", CompilerOptionsForInferredProjects = "compilerOptionsForInferredProjects", GetCodeFixes = "getCodeFixes", + ApplyCodeActionCommand = "applyCodeActionCommand", /* @internal */ GetCodeFixesFull = "getCodeFixes-full", GetSupportedCodeFixes = "getSupportedCodeFixes", @@ -125,6 +126,8 @@ namespace ts.server.protocol { * Client-initiated request message */ export interface Request extends Message { + type: "request"; + /** * The command to execute */ @@ -147,6 +150,8 @@ namespace ts.server.protocol { * Server-initiated event message */ export interface Event extends Message { + type: "event"; + /** * Name of event */ @@ -162,6 +167,8 @@ namespace ts.server.protocol { * Response by server to client request message. */ export interface Response extends Message { + type: "response"; + /** * Sequence number of the request message. */ @@ -178,7 +185,8 @@ namespace ts.server.protocol { command: string; /** - * Contains error message if success === false. + * If success === false, this should always be provided. + * Otherwise, may (or may not) contain a success message. */ message?: string; @@ -520,6 +528,14 @@ namespace ts.server.protocol { arguments: CodeFixRequestArgs; } + export interface ApplyCodeActionCommandRequest extends Request { + command: CommandTypes.ApplyCodeActionCommand; + arguments: ApplyCodeActionCommandRequestArgs; + } + + // All we need is the `success` and `message` fields of Response. + export interface ApplyCodeActionCommandResponse extends Response {} + export interface FileRangeRequestArgs extends FileRequestArgs { /** * The line number for the request (1-based). @@ -564,6 +580,10 @@ namespace ts.server.protocol { errorCodes?: number[]; } + export interface ApplyCodeActionCommandRequestArgs extends FileRequestArgs { + command: {}; + } + /** * Response for GetCodeFixes request. */ @@ -1541,6 +1561,8 @@ namespace ts.server.protocol { description: string; /** Text changes to apply to each file as part of the code action */ changes: FileCodeEdits[]; + /** A command is an opaque object that should be passed to `ApplyCodeActionCommandRequestArgs` without modification. */ + commands?: {}[]; } /** diff --git a/src/server/server.ts b/src/server/server.ts index 17905ece33ade..decf614cf8ea7 100644 --- a/src/server/server.ts +++ b/src/server/server.ts @@ -250,6 +250,9 @@ namespace ts.server { private activeRequestCount = 0; private requestQueue: QueuedOperation[] = []; private requestMap = createMap(); // Maps operation ID to newest requestQueue entry with that ID + /** We will lazily request the types registry on the first call to `isKnownTypesPackageName` and store it in `typesRegistryCache`. */ + private requestedRegistry: boolean; + private typesRegistryCache: Map | undefined; // This number is essentially arbitrary. Processing more than one typings request // at a time makes sense, but having too many in the pipe results in a hang @@ -258,7 +261,7 @@ namespace ts.server { // buffer, but we have yet to find a way to retrieve that value. private static readonly maxActiveRequestCount = 10; private static readonly requestDelayMillis = 100; - + private packageInstalledPromise: { resolve(value: ApplyCodeActionCommandResult): void, reject(reason: any): void }; constructor( private readonly telemetryEnabled: boolean, @@ -278,6 +281,31 @@ namespace ts.server { } } + isKnownTypesPackageName(name: string): boolean { + // We want to avoid looking this up in the registry as that is expensive. So first check that it's actually an NPM package. + const validationResult = JsTyping.validatePackageName(name); + if (validationResult !== JsTyping.PackageNameValidationResult.Ok) { + return false; + } + + if (this.requestedRegistry) { + return !!this.typesRegistryCache && this.typesRegistryCache.has(name); + } + + this.requestedRegistry = true; + this.send({ kind: "typesRegistry" }); + return false; + } + + installPackage(options: InstallPackageOptionsWithProjectRootPath): PromiseLike { + const rq: InstallPackageRequest = { kind: "installPackage", ...options }; + this.send(rq); + Debug.assert(this.packageInstalledPromise === undefined); + return new Promise((resolve, reject) => { + this.packageInstalledPromise = { resolve, reject }; + }); + } + private reportInstallerProcessId() { if (this.installerPidReported) { return; @@ -343,7 +371,11 @@ namespace ts.server { } onProjectClosed(p: Project): void { - this.installer.send({ projectName: p.getProjectName(), kind: "closeProject" }); + this.send({ projectName: p.getProjectName(), kind: "closeProject" }); + } + + private send(rq: TypingInstallerRequestUnion): void { + this.installer.send(rq); } enqueueInstallTypingsRequest(project: Project, typeAcquisition: TypeAcquisition, unresolvedImports: SortedReadonlyArray): void { @@ -359,7 +391,7 @@ namespace ts.server { if (this.logger.hasLevel(LogLevel.verbose)) { this.logger.info(`Sending request:${stringifyIndented(request)}`); } - this.installer.send(request); + this.send(request); }; const queuedRequest: QueuedOperation = { operationId, operation }; @@ -375,12 +407,26 @@ namespace ts.server { } } - private handleMessage(response: SetTypings | InvalidateCachedTypings | BeginInstallTypes | EndInstallTypes | InitializationFailedResponse) { + private handleMessage(response: TypesRegistryResponse | PackageInstalledResponse | SetTypings | InvalidateCachedTypings | BeginInstallTypes | EndInstallTypes | InitializationFailedResponse) { if (this.logger.hasLevel(LogLevel.verbose)) { this.logger.info(`Received response:${stringifyIndented(response)}`); } switch (response.kind) { + case EventTypesRegistry: + this.typesRegistryCache = ts.createMapFromTemplate(response.typesRegistry); + break; + case EventPackageInstalled: { + const { success, message } = response; + if (success) { + this.packageInstalledPromise.resolve({ successMessage: message }); + } + else { + this.packageInstalledPromise.reject(message); + } + this.packageInstalledPromise = undefined; + break; + } case EventInitializationFailed: { if (!this.eventSender) { diff --git a/src/server/session.ts b/src/server/session.ts index 017784530d5bf..7725a699ee273 100644 --- a/src/server/session.ts +++ b/src/server/session.ts @@ -411,19 +411,27 @@ namespace ts.server { this.send(ev); } - public output(info: any, cmdName: string, reqSeq = 0, errorMsg?: string) { + // For backwards-compatibility only. + public output(info: any, cmdName: string, reqSeq?: number, errorMsg?: string): void { + this.doOutput(info, cmdName, reqSeq, /*success*/ !errorMsg, errorMsg); + } + + private doOutput(info: {} | undefined, cmdName: string, reqSeq: number, success: boolean, message?: string): void { const res: protocol.Response = { seq: 0, type: "response", command: cmdName, request_seq: reqSeq, - success: !errorMsg, + success, }; - if (!errorMsg) { + if (success) { res.body = info; } else { - res.message = errorMsg; + Debug.assert(info === undefined); + } + if (message) { + res.message = message; } this.send(res); } @@ -1307,7 +1315,7 @@ namespace ts.server { this.changeSeq++; // make sure no changes happen before this one is finished if (project.reloadScript(file, tempFileName)) { - this.output(undefined, CommandNames.Reload, reqSeq); + this.doOutput(/*info*/ undefined, CommandNames.Reload, reqSeq, /*success*/ true); } } @@ -1545,6 +1553,15 @@ namespace ts.server { } } + private applyCodeActionCommand(commandName: string, requestSeq: number, args: protocol.ApplyCodeActionCommandRequestArgs): void { + const { file, project } = this.getFileAndProject(args); + const output = (success: boolean, message: string) => this.doOutput({}, commandName, requestSeq, success, message); + const command = args.command as CodeActionCommand; // They should be sending back the command we sent them. + project.getLanguageService().applyCodeActionCommand(file, command).then( + ({ successMessage }) => { output(/*success*/ true, successMessage); }, + error => { output(/*success*/ false, error); }); + } + private getStartAndEndPosition(args: protocol.FileRangeRequestArgs, scriptInfo: ScriptInfo) { let startPosition: number = undefined, endPosition: number = undefined; if (args.startPosition !== undefined) { @@ -1567,14 +1584,12 @@ namespace ts.server { return { startPosition, endPosition }; } - private mapCodeAction(codeAction: CodeAction, scriptInfo: ScriptInfo): protocol.CodeAction { - return { - description: codeAction.description, - changes: codeAction.changes.map(change => ({ - fileName: change.fileName, - textChanges: change.textChanges.map(textChange => this.convertTextChangeToCodeEdit(textChange, scriptInfo)) - })) - }; + private mapCodeAction({ description, changes: unmappedChanges, commands }: CodeAction, scriptInfo: ScriptInfo): protocol.CodeAction { + const changes = unmappedChanges.map(change => ({ + fileName: change.fileName, + textChanges: change.textChanges.map(textChange => this.convertTextChangeToCodeEdit(textChange, scriptInfo)) + })); + return { description, changes, commands }; } private mapTextChangesToCodeEdits(project: Project, textChanges: FileTextChanges): protocol.FileCodeEdits { @@ -1660,15 +1675,15 @@ namespace ts.server { exit() { } - private notRequired() { + private notRequired(): HandlerResponse { return { responseRequired: false }; } - private requiredResponse(response: any) { + private requiredResponse(response: {}): HandlerResponse { return { response, responseRequired: true }; } - private handlers = createMapFromTemplate<(request: protocol.Request) => { response?: any, responseRequired?: boolean }>({ + private handlers = createMapFromTemplate<(request: protocol.Request) => HandlerResponse>({ [CommandNames.OpenExternalProject]: (request: protocol.OpenExternalProjectRequest) => { this.projectService.openExternalProject(request.arguments, /*suppressRefreshOfInferredProjects*/ false); // TODO: report errors @@ -1846,7 +1861,7 @@ namespace ts.server { }, [CommandNames.Configure]: (request: protocol.ConfigureRequest) => { this.projectService.setHostConfiguration(request.arguments); - this.output(undefined, CommandNames.Configure, request.seq); + this.doOutput(/*info*/ undefined, CommandNames.Configure, request.seq, /*success*/ true); return this.notRequired(); }, [CommandNames.Reload]: (request: protocol.ReloadRequest) => { @@ -1913,6 +1928,10 @@ namespace ts.server { [CommandNames.GetCodeFixesFull]: (request: protocol.CodeFixRequest) => { return this.requiredResponse(this.getCodeFixes(request.arguments, /*simplifiedResult*/ false)); }, + [CommandNames.ApplyCodeActionCommand]: (request: protocol.ApplyCodeActionCommandRequest) => { + this.applyCodeActionCommand(request.command, request.seq, request.arguments); + return this.notRequired(); // Response will come asynchronously. + }, [CommandNames.GetSupportedCodeFixes]: () => { return this.requiredResponse(this.getSupportedCodeFixes()); }, @@ -1927,7 +1946,7 @@ namespace ts.server { } }); - public addProtocolHandler(command: string, handler: (request: protocol.Request) => { response?: any, responseRequired: boolean }) { + public addProtocolHandler(command: string, handler: (request: protocol.Request) => HandlerResponse) { if (this.handlers.has(command)) { throw new Error(`Protocol handler already exists for command "${command}"`); } @@ -1956,14 +1975,14 @@ namespace ts.server { } } - public executeCommand(request: protocol.Request): { response?: any, responseRequired?: boolean } { + public executeCommand(request: protocol.Request): HandlerResponse { const handler = this.handlers.get(request.command); if (handler) { return this.executeWithRequestId(request.seq, () => handler(request)); } else { this.logger.msg(`Unrecognized JSON command:${stringifyIndented(request)}`, Msg.Err); - this.output(undefined, CommandNames.Unknown, request.seq, `Unrecognized JSON command: ${request.command}`); + this.doOutput(/*info*/ undefined, CommandNames.Unknown, request.seq, /*success*/ false, `Unrecognized JSON command: ${request.command}`); return { responseRequired: false }; } } @@ -1994,25 +2013,31 @@ namespace ts.server { } if (response) { - this.output(response, request.command, request.seq); + this.doOutput(response, request.command, request.seq, /*success*/ true); } else if (responseRequired) { - this.output(undefined, request.command, request.seq, "No content available."); + this.doOutput(/*info*/ undefined, request.command, request.seq, /*success*/ false, "No content available."); } } catch (err) { if (err instanceof OperationCanceledException) { // Handle cancellation exceptions - this.output({ canceled: true }, request.command, request.seq); + this.doOutput({ canceled: true }, request.command, request.seq, /*success*/ true); return; } this.logError(err, message); - this.output( - undefined, + this.doOutput( + /*info*/ undefined, request ? request.command : CommandNames.Unknown, request ? request.seq : 0, + /*success*/ false, "Error processing request. " + (err).message + "\n" + (err).stack); } } } + + export interface HandlerResponse { + response?: {}; + responseRequired?: boolean; + } } diff --git a/src/server/shared.ts b/src/server/shared.ts index 66f739a5b974a..a8a122c3327ee 100644 --- a/src/server/shared.ts +++ b/src/server/shared.ts @@ -3,6 +3,8 @@ namespace ts.server { export const ActionSet: ActionSet = "action::set"; export const ActionInvalidate: ActionInvalidate = "action::invalidate"; + export const EventTypesRegistry: EventTypesRegistry = "event::typesRegistry"; + export const EventPackageInstalled: EventPackageInstalled = "event::packageInstalled"; export const EventBeginInstallTypes: EventBeginInstallTypes = "event::beginInstallTypes"; export const EventEndInstallTypes: EventEndInstallTypes = "event::endInstallTypes"; export const EventInitializationFailed: EventInitializationFailed = "event::initializationFailed"; diff --git a/src/server/types.ts b/src/server/types.ts index 4fc4356a4a97f..af5e121278eff 100644 --- a/src/server/types.ts +++ b/src/server/types.ts @@ -28,12 +28,14 @@ declare namespace ts.server { " __sortedArrayBrand": any; } - export interface TypingInstallerRequest { + export interface TypingInstallerRequestWithProjectName { readonly projectName: string; - readonly kind: "discover" | "closeProject"; } - export interface DiscoverTypings extends TypingInstallerRequest { + /* @internal */ + export type TypingInstallerRequestUnion = DiscoverTypings | CloseProject | TypesRegistryRequest | InstallPackageRequest; + + export interface DiscoverTypings extends TypingInstallerRequestWithProjectName { readonly fileNames: string[]; readonly projectRootPath: Path; readonly compilerOptions: CompilerOptions; @@ -43,18 +45,46 @@ declare namespace ts.server { readonly kind: "discover"; } - export interface CloseProject extends TypingInstallerRequest { + export interface CloseProject extends TypingInstallerRequestWithProjectName { readonly kind: "closeProject"; } + export interface TypesRegistryRequest { + readonly kind: "typesRegistry"; + } + + export interface InstallPackageRequest { + readonly kind: "installPackage"; + readonly fileName: Path; + readonly packageName: string; + readonly projectRootPath: Path; + } + export type ActionSet = "action::set"; export type ActionInvalidate = "action::invalidate"; + export type EventTypesRegistry = "event::typesRegistry"; + export type EventPackageInstalled = "event::packageInstalled"; export type EventBeginInstallTypes = "event::beginInstallTypes"; export type EventEndInstallTypes = "event::endInstallTypes"; export type EventInitializationFailed = "event::initializationFailed"; export interface TypingInstallerResponse { - readonly kind: ActionSet | ActionInvalidate | EventBeginInstallTypes | EventEndInstallTypes | EventInitializationFailed; + readonly kind: ActionSet | ActionInvalidate | EventTypesRegistry | EventPackageInstalled | EventBeginInstallTypes | EventEndInstallTypes | EventInitializationFailed; + } + /* @internal */ + export type TypingInstallerResponseUnion = SetTypings | InvalidateCachedTypings | TypesRegistryResponse | PackageInstalledResponse | InstallTypes | InitializationFailedResponse; + + /* @internal */ + export interface TypesRegistryResponse extends TypingInstallerResponse { + readonly kind: EventTypesRegistry; + readonly typesRegistry: MapLike; + } + + /* @internal */ + export interface PackageInstalledResponse extends TypingInstallerResponse { + readonly kind: EventPackageInstalled; + readonly success: boolean; + readonly message: string; } export interface InitializationFailedResponse extends TypingInstallerResponse { diff --git a/src/server/typingsCache.ts b/src/server/typingsCache.ts index 207824616a980..ddcf85063fdda 100644 --- a/src/server/typingsCache.ts +++ b/src/server/typingsCache.ts @@ -1,7 +1,13 @@ /// namespace ts.server { + export interface InstallPackageOptionsWithProjectRootPath extends InstallPackageOptions { + projectRootPath: Path; + } + export interface ITypingsInstaller { + isKnownTypesPackageName(name: string): boolean; + installPackage(options: InstallPackageOptionsWithProjectRootPath): PromiseLike; enqueueInstallTypingsRequest(p: Project, typeAcquisition: TypeAcquisition, unresolvedImports: SortedReadonlyArray): void; attach(projectService: ProjectService): void; onProjectClosed(p: Project): void; @@ -9,6 +15,9 @@ namespace ts.server { } export const nullTypingsInstaller: ITypingsInstaller = { + isKnownTypesPackageName: returnFalse, + // Should never be called because we never provide a types registry. + installPackage: notImplemented, enqueueInstallTypingsRequest: noop, attach: noop, onProjectClosed: noop, @@ -77,6 +86,14 @@ namespace ts.server { constructor(private readonly installer: ITypingsInstaller) { } + isKnownTypesPackageName(name: string): boolean { + return this.installer.isKnownTypesPackageName(name); + } + + installPackage(options: InstallPackageOptionsWithProjectRootPath): PromiseLike { + return this.installer.installPackage(options); + } + getTypingsForProject(project: Project, unresolvedImports: SortedReadonlyArray, forceRefresh: boolean): SortedReadonlyArray { const typeAcquisition = project.getTypeAcquisition(); diff --git a/src/server/typingsInstaller/nodeTypingsInstaller.ts b/src/server/typingsInstaller/nodeTypingsInstaller.ts index d32dd2a4b5f76..2a1036010a733 100644 --- a/src/server/typingsInstaller/nodeTypingsInstaller.ts +++ b/src/server/typingsInstaller/nodeTypingsInstaller.ts @@ -53,7 +53,7 @@ namespace ts.server.typingsInstaller { } try { const content = JSON.parse(host.readFile(typesRegistryFilePath)); - return createMapFromTemplate(content.entries); + return createMapFromTemplate(content.entries); } catch (e) { if (log.isEnabled()) { @@ -79,7 +79,7 @@ namespace ts.server.typingsInstaller { private readonly npmPath: string; readonly typesRegistry: Map; - private delayedInitializationError: InitializationFailedResponse; + private delayedInitializationError: InitializationFailedResponse | undefined; constructor(globalTypingsCacheLocation: string, typingSafeListLocation: string, typesMapLocation: string, npmLocation: string | undefined, throttleLimit: number, log: Log) { super( @@ -127,7 +127,7 @@ namespace ts.server.typingsInstaller { } listen() { - process.on("message", (req: DiscoverTypings | CloseProject) => { + process.on("message", (req: TypingInstallerRequestUnion) => { if (this.delayedInitializationError) { // report initializationFailed error this.sendResponse(this.delayedInitializationError); @@ -139,11 +139,39 @@ namespace ts.server.typingsInstaller { break; case "closeProject": this.closeProject(req); + break; + case "typesRegistry": { + const typesRegistry: { [key: string]: void } = {}; + this.typesRegistry.forEach((value, key) => { + typesRegistry[key] = value; + }); + const response: TypesRegistryResponse = { kind: EventTypesRegistry, typesRegistry }; + this.sendResponse(response); + break; + } + case "installPackage": { + const { fileName, packageName, projectRootPath } = req; + const cwd = getDirectoryOfPackageJson(fileName, this.installTypingHost) || projectRootPath; + if (cwd) { + this.installWorker(-1, [packageName], cwd, success => { + const message = success ? `Package ${packageName} installed.` : `There was an error installing ${packageName}.`; + const response: PackageInstalledResponse = { kind: EventPackageInstalled, success, message }; + this.sendResponse(response); + }); + } + else { + const response: PackageInstalledResponse = { kind: EventPackageInstalled, success: false, message: "Could not determine a project root path." }; + this.sendResponse(response); + } + break; + } + default: + Debug.assertNever(req); } }); } - protected sendResponse(response: SetTypings | InvalidateCachedTypings | BeginInstallTypes | EndInstallTypes | InitializationFailedResponse) { + protected sendResponse(response: TypingInstallerResponseUnion) { if (this.log.isEnabled()) { this.log.writeLine(`Sending response:\n ${JSON.stringify(response)}`); } @@ -153,11 +181,11 @@ namespace ts.server.typingsInstaller { } } - protected installWorker(requestId: number, args: string[], cwd: string, onRequestCompleted: RequestCompletedAction): void { + protected installWorker(requestId: number, packageNames: string[], cwd: string, onRequestCompleted: RequestCompletedAction): void { if (this.log.isEnabled()) { - this.log.writeLine(`#${requestId} with arguments'${JSON.stringify(args)}'.`); + this.log.writeLine(`#${requestId} with arguments'${JSON.stringify(packageNames)}'.`); } - const command = `${this.npmPath} install --ignore-scripts ${args.join(" ")} --save-dev --user-agent="typesInstaller/${version}"`; + const command = `${this.npmPath} install --ignore-scripts ${packageNames.join(" ")} --save-dev --user-agent="typesInstaller/${version}"`; const start = Date.now(); const hasError = this.execSyncAndLog(command, { cwd }); if (this.log.isEnabled()) { @@ -186,6 +214,14 @@ namespace ts.server.typingsInstaller { } } + function getDirectoryOfPackageJson(fileName: string, host: InstallTypingHost): string | undefined { + return forEachAncestorDirectory(getDirectoryPath(fileName), directory => { + if (host.fileExists(combinePaths(directory, "package.json"))) { + return directory; + } + }); + } + const logFilePath = findArgument(server.Arguments.LogFile); const globalTypingsCacheLocation = findArgument(server.Arguments.GlobalCacheLocation); const typingSafeListLocation = findArgument(server.Arguments.TypingSafeListLocation); diff --git a/src/server/typingsInstaller/typingsInstaller.ts b/src/server/typingsInstaller/typingsInstaller.ts index 3eae0755747b7..26e7781b4406e 100644 --- a/src/server/typingsInstaller/typingsInstaller.ts +++ b/src/server/typingsInstaller/typingsInstaller.ts @@ -32,50 +32,11 @@ namespace ts.server.typingsInstaller { } } - export enum PackageNameValidationResult { - Ok, - ScopedPackagesNotSupported, - EmptyName, - NameTooLong, - NameStartsWithDot, - NameStartsWithUnderscore, - NameContainsNonURISafeCharacters - } - - - export const MaxPackageNameLength = 214; - /** - * Validates package name using rules defined at https://docs.npmjs.com/files/package.json - */ - export function validatePackageName(packageName: string): PackageNameValidationResult { - if (!packageName) { - return PackageNameValidationResult.EmptyName; - } - if (packageName.length > MaxPackageNameLength) { - return PackageNameValidationResult.NameTooLong; - } - if (packageName.charCodeAt(0) === CharacterCodes.dot) { - return PackageNameValidationResult.NameStartsWithDot; - } - if (packageName.charCodeAt(0) === CharacterCodes._) { - return PackageNameValidationResult.NameStartsWithUnderscore; - } - // check if name is scope package like: starts with @ and has one '/' in the middle - // scoped packages are not currently supported - // TODO: when support will be added we'll need to split and check both scope and package name - if (/^@[^/]+\/[^/]+$/.test(packageName)) { - return PackageNameValidationResult.ScopedPackagesNotSupported; - } - if (encodeURIComponent(packageName) !== packageName) { - return PackageNameValidationResult.NameContainsNonURISafeCharacters; - } - return PackageNameValidationResult.Ok; - } export type RequestCompletedAction = (success: boolean) => void; interface PendingRequest { requestId: number; - args: string[]; + packageNames: string[]; cwd: string; onRequestCompleted: RequestCompletedAction; } @@ -255,8 +216,8 @@ namespace ts.server.typingsInstaller { if (this.missingTypingsSet.get(typing) || this.packageNameToTypingLocation.get(typing)) { continue; } - const validationResult = validatePackageName(typing); - if (validationResult === PackageNameValidationResult.Ok) { + const validationResult = JsTyping.validatePackageName(typing); + if (validationResult === JsTyping.PackageNameValidationResult.Ok) { if (this.typesRegistry.has(typing)) { result.push(typing); } @@ -270,26 +231,7 @@ namespace ts.server.typingsInstaller { // add typing name to missing set so we won't process it again this.missingTypingsSet.set(typing, true); if (this.log.isEnabled()) { - switch (validationResult) { - case PackageNameValidationResult.EmptyName: - this.log.writeLine(`Package name '${typing}' cannot be empty`); - break; - case PackageNameValidationResult.NameTooLong: - this.log.writeLine(`Package name '${typing}' should be less than ${MaxPackageNameLength} characters`); - break; - case PackageNameValidationResult.NameStartsWithDot: - this.log.writeLine(`Package name '${typing}' cannot start with '.'`); - break; - case PackageNameValidationResult.NameStartsWithUnderscore: - this.log.writeLine(`Package name '${typing}' cannot start with '_'`); - break; - case PackageNameValidationResult.ScopedPackagesNotSupported: - this.log.writeLine(`Package '${typing}' is scoped and currently is not supported`); - break; - case PackageNameValidationResult.NameContainsNonURISafeCharacters: - this.log.writeLine(`Package name '${typing}' contains non URI safe characters`); - break; - } + this.log.writeLine(JsTyping.renderPackageNameValidationFailure(validationResult, typing)); } } } @@ -430,8 +372,8 @@ namespace ts.server.typingsInstaller { }; } - private installTypingsAsync(requestId: number, args: string[], cwd: string, onRequestCompleted: RequestCompletedAction): void { - this.pendingRunRequests.unshift({ requestId, args, cwd, onRequestCompleted }); + private installTypingsAsync(requestId: number, packageNames: string[], cwd: string, onRequestCompleted: RequestCompletedAction): void { + this.pendingRunRequests.unshift({ requestId, packageNames, cwd, onRequestCompleted }); this.executeWithThrottling(); } @@ -439,7 +381,7 @@ namespace ts.server.typingsInstaller { while (this.inFlightRequestCount < this.throttleLimit && this.pendingRunRequests.length) { this.inFlightRequestCount++; const request = this.pendingRunRequests.pop(); - this.installWorker(request.requestId, request.args, request.cwd, ok => { + this.installWorker(request.requestId, request.packageNames, request.cwd, ok => { this.inFlightRequestCount--; request.onRequestCompleted(ok); this.executeWithThrottling(); @@ -447,7 +389,7 @@ namespace ts.server.typingsInstaller { } } - protected abstract installWorker(requestId: number, args: string[], cwd: string, onRequestCompleted: RequestCompletedAction): void; + protected abstract installWorker(requestId: number, packageNames: string[], cwd: string, onRequestCompleted: RequestCompletedAction): void; protected abstract sendResponse(response: SetTypings | InvalidateCachedTypings | BeginInstallTypes | EndInstallTypes): void; } diff --git a/src/services/codefixes/fixCannotFindModule.ts b/src/services/codefixes/fixCannotFindModule.ts new file mode 100644 index 0000000000000..b7fde9d917ef9 --- /dev/null +++ b/src/services/codefixes/fixCannotFindModule.ts @@ -0,0 +1,35 @@ +/* @internal */ +namespace ts.codefix { + registerCodeFix({ + errorCodes: [ + Diagnostics.Cannot_find_module_0.code, + Diagnostics.Could_not_find_a_declaration_file_for_module_0_1_implicitly_has_an_any_type.code, + ], + getCodeActions: context => { + const { sourceFile, span: { start } } = context; + const token = getTokenAtPosition(sourceFile, start, /*includeJsDocComment*/ false); + if (!isStringLiteral(token)) { + throw Debug.fail(); // These errors should only happen on the module name. + } + + const action = tryGetCodeActionForInstallPackageTypes(context.host, token.text); + return action && [action]; + }, + }); + + export function tryGetCodeActionForInstallPackageTypes(host: LanguageServiceHost, moduleName: string): CodeAction | undefined { + const { packageName } = getPackageName(moduleName); + + if (!host.isKnownTypesPackageName(packageName)) { + // If !registry, registry not available yet, can't do anything. + return undefined; + } + + const typesPackageName = getTypesPackageName(packageName); + return { + description: `Install '${typesPackageName}'`, + changes: [], + commands: [{ type: "install package", packageName: typesPackageName }], + }; + } +} diff --git a/src/services/codefixes/fixes.ts b/src/services/codefixes/fixes.ts index 7ee0aaa679973..5b1ee8ec8572c 100644 --- a/src/services/codefixes/fixes.ts +++ b/src/services/codefixes/fixes.ts @@ -3,6 +3,7 @@ /// /// /// +/// /// /// /// diff --git a/src/services/jsTyping.ts b/src/services/jsTyping.ts index f1859a90b98fa..572858dd2fd0d 100644 --- a/src/services/jsTyping.ts +++ b/src/services/jsTyping.ts @@ -246,4 +246,65 @@ namespace ts.JsTyping { } } + + export const enum PackageNameValidationResult { + Ok, + ScopedPackagesNotSupported, + EmptyName, + NameTooLong, + NameStartsWithDot, + NameStartsWithUnderscore, + NameContainsNonURISafeCharacters + } + + const MaxPackageNameLength = 214; + + /** + * Validates package name using rules defined at https://docs.npmjs.com/files/package.json + */ + export function validatePackageName(packageName: string): PackageNameValidationResult { + if (!packageName) { + return PackageNameValidationResult.EmptyName; + } + if (packageName.length > MaxPackageNameLength) { + return PackageNameValidationResult.NameTooLong; + } + if (packageName.charCodeAt(0) === CharacterCodes.dot) { + return PackageNameValidationResult.NameStartsWithDot; + } + if (packageName.charCodeAt(0) === CharacterCodes._) { + return PackageNameValidationResult.NameStartsWithUnderscore; + } + // check if name is scope package like: starts with @ and has one '/' in the middle + // scoped packages are not currently supported + // TODO: when support will be added we'll need to split and check both scope and package name + if (/^@[^/]+\/[^/]+$/.test(packageName)) { + return PackageNameValidationResult.ScopedPackagesNotSupported; + } + if (encodeURIComponent(packageName) !== packageName) { + return PackageNameValidationResult.NameContainsNonURISafeCharacters; + } + return PackageNameValidationResult.Ok; + } + + export function renderPackageNameValidationFailure(result: PackageNameValidationResult, typing: string): string { + switch (result) { + case PackageNameValidationResult.EmptyName: + return `Package name '${typing}' cannot be empty`; + case PackageNameValidationResult.NameTooLong: + return `Package name '${typing}' should be less than ${MaxPackageNameLength} characters`; + case PackageNameValidationResult.NameStartsWithDot: + return `Package name '${typing}' cannot start with '.'`; + case PackageNameValidationResult.NameStartsWithUnderscore: + return `Package name '${typing}' cannot start with '_'`; + case PackageNameValidationResult.ScopedPackagesNotSupported: + return `Package '${typing}' is scoped and currently is not supported`; + case PackageNameValidationResult.NameContainsNonURISafeCharacters: + return `Package name '${typing}' contains non URI safe characters`; + case PackageNameValidationResult.Ok: + throw Debug.fail(); // Shouldn't have called this. + default: + Debug.assertNever(result); + } + } } diff --git a/src/services/refactorProvider.ts b/src/services/refactorProvider.ts index b338882e6db68..e0a1924793976 100644 --- a/src/services/refactorProvider.ts +++ b/src/services/refactorProvider.ts @@ -19,6 +19,7 @@ namespace ts { startPosition: number; endPosition?: number; program: Program; + host: LanguageServiceHost; cancellationToken?: CancellationToken; } diff --git a/src/services/refactors/installTypesForPackage.ts b/src/services/refactors/installTypesForPackage.ts new file mode 100644 index 0000000000000..ded4a1d47afaf --- /dev/null +++ b/src/services/refactors/installTypesForPackage.ts @@ -0,0 +1,63 @@ +/* @internal */ +namespace ts.refactor.installTypesForPackage { + const actionName = "install"; + + const installTypesForPackage: Refactor = { + name: "Install missing types package", + description: "Install missing types package", + getEditsForAction, + getAvailableActions, + }; + + registerRefactor(installTypesForPackage); + + function getAvailableActions(context: RefactorContext): ApplicableRefactorInfo[] | undefined { + if (context.program.getCompilerOptions().noImplicitAny) { + // Then it will be available via `fixCannotFindModule`. + return undefined; + } + + const action = getAction(context); + return action && [ + { + name: installTypesForPackage.name, + description: installTypesForPackage.description, + actions: [ + { + description: action.description, + name: actionName, + }, + ], + }, + ]; + } + + function getEditsForAction(context: RefactorContext, _actionName: string): RefactorEditInfo | undefined { + Debug.assertEqual(actionName, _actionName); + const action = getAction(context)!; // Should be defined if we said there was an action available. + return { + edits: [], + renameFilename: undefined, + renameLocation: undefined, + commands: action.commands, + }; + } + + function getAction(context: RefactorContext): CodeAction | undefined { + const { file, startPosition } = context; + const node = getTokenAtPosition(file, startPosition, /*includeJsDocComment*/ false); + if (isStringLiteral(node) && isModuleIdentifier(node) && getResolvedModule(file, node.text) === undefined) { + return codefix.tryGetCodeActionForInstallPackageTypes(context.host, node.text); + } + } + + function isModuleIdentifier(node: StringLiteral): boolean { + switch (node.parent.kind) { + case SyntaxKind.ImportDeclaration: + case SyntaxKind.ExternalModuleReference: + return true; + default: + return false; + } + } +} \ No newline at end of file diff --git a/src/services/refactors/refactors.ts b/src/services/refactors/refactors.ts index 21aeda7ae5278..f4b56422a89bd 100644 --- a/src/services/refactors/refactors.ts +++ b/src/services/refactors/refactors.ts @@ -1,3 +1,4 @@ /// /// /// +/// diff --git a/src/services/services.ts b/src/services/services.ts index 0399ca1658e9d..a5c9c1d353a18 100644 --- a/src/services/services.ts +++ b/src/services/services.ts @@ -1763,6 +1763,19 @@ namespace ts { }); } + function applyCodeActionCommand(fileName: Path, action: CodeActionCommand): PromiseLike { + fileName = toPath(fileName, currentDirectory, getCanonicalFileName); + switch (action.type) { + case "install package": + return host.installPackage + ? host.installPackage({ fileName, packageName: action.packageName }) + : Promise.reject("Host does not implement `installPackage`"); + default: + Debug.fail(); + // TODO: Debug.assertNever(action); will only work if there is more than one type. + } + } + function getDocCommentTemplateAtPosition(fileName: string, position: number): TextInsertion { return JsDoc.getDocCommentTemplateAtPosition(getNewLineOrDefaultFromHost(host), syntaxTreeCache.getCurrentSourceFile(fileName), position); } @@ -1972,8 +1985,9 @@ namespace ts { endPosition, program: getProgram(), newLineCharacter: formatOptions ? formatOptions.newLineCharacter : host.getNewLine(), + host, rulesProvider: getRuleProvider(formatOptions), - cancellationToken + cancellationToken, }; } @@ -2035,6 +2049,7 @@ namespace ts { isValidBraceCompletionAtPosition, getSpanOfEnclosingComment, getCodeFixesAtPosition, + applyCodeActionCommand, getEmitOutput, getNonBoundSourceFile, getSourceFile, diff --git a/src/services/types.ts b/src/services/types.ts index 7bf4a909a7a34..7ec559be92283 100644 --- a/src/services/types.ts +++ b/src/services/types.ts @@ -145,10 +145,15 @@ namespace ts { isCancellationRequested(): boolean; } + export interface InstallPackageOptions { + fileName: Path; + packageName: string; + } + // // Public interface of the host of a language service instance. // - export interface LanguageServiceHost { + export interface LanguageServiceHost extends GetEffectiveTypeRootsHost { getCompilationSettings(): CompilerOptions; getNewLine?(): string; getProjectVersion?(): string; @@ -158,7 +163,6 @@ namespace ts { getScriptSnapshot(fileName: string): IScriptSnapshot | undefined; getLocalizedDiagnosticMessages?(): any; getCancellationToken?(): HostCancellationToken; - getCurrentDirectory(): string; getDefaultLibFileName(options: CompilerOptions): string; log?(s: string): void; trace?(s: string): void; @@ -187,7 +191,6 @@ namespace ts { resolveTypeReferenceDirectives?(typeDirectiveNames: string[], containingFile: string): ResolvedTypeReferenceDirective[]; /* @internal */ hasInvalidatedResolution?: HasInvalidatedResolution; /* @internal */ hasChangedAutomaticTypeDirectiveNames?: boolean; - directoryExists?(directoryName: string): boolean; /* * getDirectories is also required for full import and type reference completions. Without it defined, certain @@ -199,6 +202,9 @@ namespace ts { * Gets a set of custom transformers to use during emit. */ getCustomTransformers?(): CustomTransformers | undefined; + + isKnownTypesPackageName?(name: string): boolean; + installPackage?(options: InstallPackageOptions): PromiseLike; } // @@ -276,6 +282,7 @@ namespace ts { getSpanOfEnclosingComment(fileName: string, position: number, onlyMultiLine: boolean): TextSpan; getCodeFixesAtPosition(fileName: string, start: number, end: number, errorCodes: number[], formatOptions: FormatCodeSettings): CodeAction[]; + applyCodeActionCommand(fileName: string, action: CodeActionCommand): PromiseLike; getApplicableRefactors(fileName: string, positionOrRaneg: number | TextRange): ApplicableRefactorInfo[]; getEditsForRefactor(fileName: string, formatOptions: FormatCodeSettings, positionOrRange: number | TextRange, refactorName: string, actionName: string): RefactorEditInfo | undefined; @@ -295,6 +302,10 @@ namespace ts { dispose(): void; } + export interface ApplyCodeActionCommandResult { + successMessage: string; + } + export interface Classifications { spans: number[]; endOfLineState: EndOfLineState; @@ -367,6 +378,20 @@ namespace ts { description: string; /** Text changes to apply to each file as part of the code action */ changes: FileTextChanges[]; + /** + * If the user accepts the code fix, the editor should send the action back in a `applyAction` request. + * This allows the language service to have side effects (e.g. installing dependencies) upon a code fix. + */ + commands?: CodeActionCommand[]; + } + + // Publicly, this type is just `{}`. Internally it is a union of all the actions we use. + // See `commands?: {}[]` in protocol.ts + export type CodeActionCommand = InstallPackageAction; + + export interface InstallPackageAction { + /* @internal */ type: "install package"; + /* @internal */ packageName: string; } /** @@ -420,6 +445,7 @@ namespace ts { edits: FileTextChanges[]; renameFilename: string | undefined; renameLocation: number | undefined; + commands?: CodeActionCommand[]; } export interface TextInsertion { diff --git a/src/services/utilities.ts b/src/services/utilities.ts index 716a4188db25b..2b116eb1cd05c 100644 --- a/src/services/utilities.ts +++ b/src/services/utilities.ts @@ -1,3 +1,12 @@ +/* @internal */ // Don't expose that we use this +// Based on lib.es6.d.ts +interface PromiseConstructor { + new (executor: (resolve: (value?: T | PromiseLike) => void, reject: (reason?: any) => void) => void): Promise; + reject(reason: any): Promise; +} +/* @internal */ +declare var Promise: PromiseConstructor; + // These utilities are common to multiple language service features. /* @internal */ namespace ts { diff --git a/tests/baselines/reference/api/tsserverlibrary.d.ts b/tests/baselines/reference/api/tsserverlibrary.d.ts index 56c7fa5f98c44..15eca3da329f4 100644 --- a/tests/baselines/reference/api/tsserverlibrary.d.ts +++ b/tests/baselines/reference/api/tsserverlibrary.d.ts @@ -3869,7 +3869,11 @@ declare namespace ts { interface HostCancellationToken { isCancellationRequested(): boolean; } - interface LanguageServiceHost { + interface InstallPackageOptions { + fileName: Path; + packageName: string; + } + interface LanguageServiceHost extends GetEffectiveTypeRootsHost { getCompilationSettings(): CompilerOptions; getNewLine?(): string; getProjectVersion?(): string; @@ -3879,7 +3883,6 @@ declare namespace ts { getScriptSnapshot(fileName: string): IScriptSnapshot | undefined; getLocalizedDiagnosticMessages?(): any; getCancellationToken?(): HostCancellationToken; - getCurrentDirectory(): string; getDefaultLibFileName(options: CompilerOptions): string; log?(s: string): void; trace?(s: string): void; @@ -3891,12 +3894,13 @@ declare namespace ts { getTypeRootsVersion?(): number; resolveModuleNames?(moduleNames: string[], containingFile: string, reusedNames?: string[]): ResolvedModule[]; resolveTypeReferenceDirectives?(typeDirectiveNames: string[], containingFile: string): ResolvedTypeReferenceDirective[]; - directoryExists?(directoryName: string): boolean; getDirectories?(directoryName: string): string[]; /** * Gets a set of custom transformers to use during emit. */ getCustomTransformers?(): CustomTransformers | undefined; + isKnownTypesPackageName?(name: string): boolean; + installPackage?(options: InstallPackageOptions): PromiseLike; } interface LanguageService { cleanupSemanticCache(): void; @@ -3944,6 +3948,7 @@ declare namespace ts { isValidBraceCompletionAtPosition(fileName: string, position: number, openingBrace: number): boolean; getSpanOfEnclosingComment(fileName: string, position: number, onlyMultiLine: boolean): TextSpan; getCodeFixesAtPosition(fileName: string, start: number, end: number, errorCodes: number[], formatOptions: FormatCodeSettings): CodeAction[]; + applyCodeActionCommand(fileName: string, action: CodeActionCommand): PromiseLike; 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; @@ -3951,6 +3956,9 @@ declare namespace ts { getProgram(): Program; dispose(): void; } + interface ApplyCodeActionCommandResult { + successMessage: string; + } interface Classifications { spans: number[]; endOfLineState: EndOfLineState; @@ -4015,6 +4023,14 @@ declare namespace ts { description: string; /** Text changes to apply to each file as part of the code action */ changes: FileTextChanges[]; + /** + * If the user accepts the code fix, the editor should send the action back in a `applyAction` request. + * This allows the language service to have side effects (e.g. installing dependencies) upon a code fix. + */ + commands?: CodeActionCommand[]; + } + type CodeActionCommand = InstallPackageAction; + interface InstallPackageAction { } /** * A set of one or more available refactoring actions, grouped under a parent refactoring. @@ -4063,6 +4079,7 @@ declare namespace ts { edits: FileTextChanges[]; renameFilename: string | undefined; renameLocation: number | undefined; + commands?: CodeActionCommand[]; } interface TextInsertion { newText: string; @@ -4633,11 +4650,10 @@ declare namespace ts.server { interface SortedReadonlyArray extends ReadonlyArray { " __sortedArrayBrand": any; } - interface TypingInstallerRequest { + interface TypingInstallerRequestWithProjectName { readonly projectName: string; - readonly kind: "discover" | "closeProject"; } - interface DiscoverTypings extends TypingInstallerRequest { + interface DiscoverTypings extends TypingInstallerRequestWithProjectName { readonly fileNames: string[]; readonly projectRootPath: Path; readonly compilerOptions: CompilerOptions; @@ -4646,16 +4662,27 @@ declare namespace ts.server { readonly cachePath?: string; readonly kind: "discover"; } - interface CloseProject extends TypingInstallerRequest { + interface CloseProject extends TypingInstallerRequestWithProjectName { readonly kind: "closeProject"; } + interface TypesRegistryRequest { + readonly kind: "typesRegistry"; + } + interface InstallPackageRequest { + readonly kind: "installPackage"; + readonly fileName: Path; + readonly packageName: string; + readonly projectRootPath: Path; + } type ActionSet = "action::set"; type ActionInvalidate = "action::invalidate"; + type EventTypesRegistry = "event::typesRegistry"; + type EventPackageInstalled = "event::packageInstalled"; type EventBeginInstallTypes = "event::beginInstallTypes"; type EventEndInstallTypes = "event::endInstallTypes"; type EventInitializationFailed = "event::initializationFailed"; interface TypingInstallerResponse { - readonly kind: ActionSet | ActionInvalidate | EventBeginInstallTypes | EventEndInstallTypes | EventInitializationFailed; + readonly kind: ActionSet | ActionInvalidate | EventTypesRegistry | EventPackageInstalled | EventBeginInstallTypes | EventEndInstallTypes | EventInitializationFailed; } interface InitializationFailedResponse extends TypingInstallerResponse { readonly kind: EventInitializationFailed; @@ -4691,6 +4718,8 @@ declare namespace ts.server { declare namespace ts.server { const ActionSet: ActionSet; const ActionInvalidate: ActionInvalidate; + const EventTypesRegistry: EventTypesRegistry; + const EventPackageInstalled: EventPackageInstalled; const EventBeginInstallTypes: EventBeginInstallTypes; const EventEndInstallTypes: EventEndInstallTypes; const EventInitializationFailed: EventInitializationFailed; @@ -4828,6 +4857,7 @@ declare namespace ts.server.protocol { DocCommentTemplate = "docCommentTemplate", CompilerOptionsForInferredProjects = "compilerOptionsForInferredProjects", GetCodeFixes = "getCodeFixes", + ApplyCodeActionCommand = "applyCodeActionCommand", GetSupportedCodeFixes = "getSupportedCodeFixes", GetApplicableRefactors = "getApplicableRefactors", GetEditsForRefactor = "getEditsForRefactor", @@ -4849,6 +4879,7 @@ declare namespace ts.server.protocol { * Client-initiated request message */ interface Request extends Message { + type: "request"; /** * The command to execute */ @@ -4868,6 +4899,7 @@ declare namespace ts.server.protocol { * Server-initiated event message */ interface Event extends Message { + type: "event"; /** * Name of event */ @@ -4881,6 +4913,7 @@ declare namespace ts.server.protocol { * Response by server to client request message. */ interface Response extends Message { + type: "response"; /** * Sequence number of the request message. */ @@ -4894,7 +4927,8 @@ declare namespace ts.server.protocol { */ command: string; /** - * Contains error message if success === false. + * If success === false, this should always be provided. + * Otherwise, may (or may not) contain a success message. */ message?: string; /** @@ -5170,6 +5204,12 @@ declare namespace ts.server.protocol { command: CommandTypes.GetCodeFixes; arguments: CodeFixRequestArgs; } + interface ApplyCodeActionCommandRequest extends Request { + command: CommandTypes.ApplyCodeActionCommand; + arguments: ApplyCodeActionCommandRequestArgs; + } + interface ApplyCodeActionCommandResponse extends Response { + } interface FileRangeRequestArgs extends FileRequestArgs { /** * The line number for the request (1-based). @@ -5197,6 +5237,9 @@ declare namespace ts.server.protocol { */ errorCodes?: number[]; } + interface ApplyCodeActionCommandRequestArgs extends FileRequestArgs { + command: {}; + } /** * Response for GetCodeFixes request. */ @@ -5935,6 +5978,8 @@ declare namespace ts.server.protocol { description: string; /** Text changes to apply to each file as part of the code action */ changes: FileCodeEdits[]; + /** A command is an opaque object that should be passed to `ApplyCodeActionCommandRequestArgs` without modification. */ + commands?: {}[]; } /** * Format and format on key response message. @@ -6852,6 +6897,7 @@ declare namespace ts.server { send(msg: protocol.Message): void; event(info: T, eventName: string): void; output(info: any, cmdName: string, reqSeq?: number, errorMsg?: string): void; + private doOutput(info, cmdName, reqSeq, success, message?); private semanticCheck(file, project); private syntacticCheck(file, project); private updateErrorCheck(next, checkList, ms, requireOpen?); @@ -6927,8 +6973,9 @@ declare namespace ts.server { private getApplicableRefactors(args); private getEditsForRefactor(args, simplifiedResult); private getCodeFixes(args, simplifiedResult); + private applyCodeActionCommand(commandName, requestSeq, args); private getStartAndEndPosition(args, scriptInfo); - private mapCodeAction(codeAction, scriptInfo); + private mapCodeAction({description, changes: unmappedChanges, commands}, scriptInfo); private mapTextChangesToCodeEdits(project, textChanges); private convertTextChangeToCodeEdit(change, scriptInfo); private getBraceMatching(args, simplifiedResult); @@ -6938,19 +6985,17 @@ declare namespace ts.server { private notRequired(); private requiredResponse(response); private handlers; - addProtocolHandler(command: string, handler: (request: protocol.Request) => { - response?: any; - responseRequired: boolean; - }): void; + addProtocolHandler(command: string, handler: (request: protocol.Request) => HandlerResponse): void; private setCurrentRequest(requestId); private resetCurrentRequest(requestId); executeWithRequestId(requestId: number, f: () => T): T; - executeCommand(request: protocol.Request): { - response?: any; - responseRequired?: boolean; - }; + executeCommand(request: protocol.Request): HandlerResponse; onMessage(message: string): void; } + interface HandlerResponse { + response?: {}; + responseRequired?: boolean; + } } declare namespace ts.server { class ScriptInfo { @@ -7015,7 +7060,12 @@ declare namespace ts { function updateWatchingWildcardDirectories(existingWatchedForWildcards: Map, wildcardDirectories: Map, watchDirectory: (directory: string, flags: WatchDirectoryFlags) => FileWatcher): void; } declare namespace ts.server { + interface InstallPackageOptionsWithProjectRootPath extends InstallPackageOptions { + projectRootPath: Path; + } interface ITypingsInstaller { + isKnownTypesPackageName(name: string): boolean; + installPackage(options: InstallPackageOptionsWithProjectRootPath): PromiseLike; enqueueInstallTypingsRequest(p: Project, typeAcquisition: TypeAcquisition, unresolvedImports: SortedReadonlyArray): void; attach(projectService: ProjectService): void; onProjectClosed(p: Project): void; @@ -7026,6 +7076,8 @@ declare namespace ts.server { private readonly installer; private readonly perProjectCache; constructor(installer: ITypingsInstaller); + isKnownTypesPackageName(name: string): boolean; + installPackage(options: InstallPackageOptionsWithProjectRootPath): PromiseLike; getTypingsForProject(project: Project, unresolvedImports: SortedReadonlyArray, forceRefresh: boolean): SortedReadonlyArray; updateTypingsForProject(projectName: string, compilerOptions: CompilerOptions, typeAcquisition: TypeAcquisition, unresolvedImports: SortedReadonlyArray, newTypings: string[]): void; deleteTypingsForProject(projectName: string): void; @@ -7120,6 +7172,9 @@ declare namespace ts.server { isJsOnlyProject(): boolean; getCachedUnresolvedImportsPerFile_TestOnly(): UnresolvedImportsMap; static resolveModule(moduleName: string, initialDir: string, host: ServerHost, log: (message: string) => void): {}; + isKnownTypesPackageName(name: string): boolean; + installPackage(options: InstallPackageOptions): PromiseLike; + private readonly typingsCache; getCompilationSettings(): CompilerOptions; getCompilerOptions(): CompilerOptions; getNewLine(): string; diff --git a/tests/baselines/reference/api/typescript.d.ts b/tests/baselines/reference/api/typescript.d.ts index 2668a25bcfcd2..f3180b9d1069c 100644 --- a/tests/baselines/reference/api/typescript.d.ts +++ b/tests/baselines/reference/api/typescript.d.ts @@ -3869,7 +3869,11 @@ declare namespace ts { interface HostCancellationToken { isCancellationRequested(): boolean; } - interface LanguageServiceHost { + interface InstallPackageOptions { + fileName: Path; + packageName: string; + } + interface LanguageServiceHost extends GetEffectiveTypeRootsHost { getCompilationSettings(): CompilerOptions; getNewLine?(): string; getProjectVersion?(): string; @@ -3879,7 +3883,6 @@ declare namespace ts { getScriptSnapshot(fileName: string): IScriptSnapshot | undefined; getLocalizedDiagnosticMessages?(): any; getCancellationToken?(): HostCancellationToken; - getCurrentDirectory(): string; getDefaultLibFileName(options: CompilerOptions): string; log?(s: string): void; trace?(s: string): void; @@ -3891,12 +3894,13 @@ declare namespace ts { getTypeRootsVersion?(): number; resolveModuleNames?(moduleNames: string[], containingFile: string, reusedNames?: string[]): ResolvedModule[]; resolveTypeReferenceDirectives?(typeDirectiveNames: string[], containingFile: string): ResolvedTypeReferenceDirective[]; - directoryExists?(directoryName: string): boolean; getDirectories?(directoryName: string): string[]; /** * Gets a set of custom transformers to use during emit. */ getCustomTransformers?(): CustomTransformers | undefined; + isKnownTypesPackageName?(name: string): boolean; + installPackage?(options: InstallPackageOptions): PromiseLike; } interface LanguageService { cleanupSemanticCache(): void; @@ -3944,6 +3948,7 @@ declare namespace ts { isValidBraceCompletionAtPosition(fileName: string, position: number, openingBrace: number): boolean; getSpanOfEnclosingComment(fileName: string, position: number, onlyMultiLine: boolean): TextSpan; getCodeFixesAtPosition(fileName: string, start: number, end: number, errorCodes: number[], formatOptions: FormatCodeSettings): CodeAction[]; + applyCodeActionCommand(fileName: string, action: CodeActionCommand): PromiseLike; 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; @@ -3951,6 +3956,9 @@ declare namespace ts { getProgram(): Program; dispose(): void; } + interface ApplyCodeActionCommandResult { + successMessage: string; + } interface Classifications { spans: number[]; endOfLineState: EndOfLineState; @@ -4015,6 +4023,14 @@ declare namespace ts { description: string; /** Text changes to apply to each file as part of the code action */ changes: FileTextChanges[]; + /** + * If the user accepts the code fix, the editor should send the action back in a `applyAction` request. + * This allows the language service to have side effects (e.g. installing dependencies) upon a code fix. + */ + commands?: CodeActionCommand[]; + } + type CodeActionCommand = InstallPackageAction; + interface InstallPackageAction { } /** * A set of one or more available refactoring actions, grouped under a parent refactoring. @@ -4063,6 +4079,7 @@ declare namespace ts { edits: FileTextChanges[]; renameFilename: string | undefined; renameLocation: number | undefined; + commands?: CodeActionCommand[]; } interface TextInsertion { newText: string; diff --git a/tests/cases/fourslash/codeFixCannotFindModule.ts b/tests/cases/fourslash/codeFixCannotFindModule.ts new file mode 100644 index 0000000000000..96e5c201644d1 --- /dev/null +++ b/tests/cases/fourslash/codeFixCannotFindModule.ts @@ -0,0 +1,15 @@ +/// + +////import * as abs from "abs"; + +test.setTypesRegistry({ + "abs": undefined, +}); + +verify.codeFixAvailable([{ + description: "Install '@types/abs'", + commands: [{ + type: "install package", + packageName: "@types/abs", + }], +}]); diff --git a/tests/cases/fourslash/fourslash.ts b/tests/cases/fourslash/fourslash.ts index e1d9607de8aaf..a7c54b5dda649 100644 --- a/tests/cases/fourslash/fourslash.ts +++ b/tests/cases/fourslash/fourslash.ts @@ -118,6 +118,7 @@ declare namespace FourSlashInterface { rangesByText(): ts.Map; markerByName(s: string): Marker; symbolsInScope(range: Range): any[]; + setTypesRegistry(map: { [key: string]: void }): void; } class goTo { marker(name?: string | Marker): void; @@ -169,12 +170,17 @@ declare namespace FourSlashInterface { errorCode?: number, index?: number, }); - codeFixAvailable(): void; + codeFixAvailable(options: Array<{ description: string, actions: Array<{ type: string, data: {} }> }>): void; applicableRefactorAvailableAtMarker(markerName: string): void; codeFixDiagnosticsAvailableAtMarkers(markerNames: string[], diagnosticCode?: number): void; applicableRefactorAvailableForRange(): void; - refactorAvailable(name: string, actionName?: string); + refactorAvailable(name: string, actionName?: string): void; + refactor(options: { + name: string; + actionName: string; + refactors: any[]; + }): void; } class verify extends verifyNegatable { assertHasRanges(ranges: Range[]): void; @@ -277,6 +283,7 @@ declare namespace FourSlashInterface { fileAfterApplyingRefactorAtMarker(markerName: string, expectedContent: string, refactorNameToApply: string, actionName: string, formattingOptions?: FormatCodeOptions): void; rangeIs(expectedText: string, includeWhiteSpace?: boolean): void; fileAfterApplyingRefactorAtMarker(markerName: string, expectedContent: string, refactorNameToApply: string, formattingOptions?: FormatCodeOptions): void; + getAndApplyCodeFix(errorCode?: number, index?: number): void; importFixAtPosition(expectedTextArray: string[], errorCode?: number): void; navigationBar(json: any, options?: { checkSpans?: boolean }): void; diff --git a/tests/cases/fourslash/refactorInstallTypesForPackage.ts b/tests/cases/fourslash/refactorInstallTypesForPackage.ts new file mode 100644 index 0000000000000..edcdc2ba6e0c9 --- /dev/null +++ b/tests/cases/fourslash/refactorInstallTypesForPackage.ts @@ -0,0 +1,25 @@ +/// + +////import * as abs from "/*a*/abs/subModule/*b*/"; + +test.setTypesRegistry({ + "abs": undefined, +}); + +goTo.select("a", "b"); +verify.refactor({ + name: "Install missing types package", + actionName: "install", + refactors: [ + { + name: "Install missing types package", + description: "Install missing types package", + actions: [ + { + description: "Install '@types/abs'", + name: "install", + } + ] + } + ], +}); diff --git a/tests/cases/fourslash/refactorInstallTypesForPackage_importEquals.ts b/tests/cases/fourslash/refactorInstallTypesForPackage_importEquals.ts new file mode 100644 index 0000000000000..18793e4b353dd --- /dev/null +++ b/tests/cases/fourslash/refactorInstallTypesForPackage_importEquals.ts @@ -0,0 +1,25 @@ +/// + +////import abs = require("/*a*/abs/subModule/*b*/"); + +test.setTypesRegistry({ + "abs": undefined, +}); + +goTo.select("a", "b"); +verify.refactor({ + name: "Install missing types package", + actionName: "install", + refactors: [ + { + name: "Install missing types package", + description: "Install missing types package", + actions: [ + { + description: "Install '@types/abs'", + name: "install", + } + ] + } + ], +}); From 2cc4f537afff23c1f7a9d226c93cb52c75f85a46 Mon Sep 17 00:00:00 2001 From: Wesley Wigham Date: Tue, 17 Oct 2017 15:41:16 -0700 Subject: [PATCH 15/51] This wasnt required before... (#19262) --- src/harness/compilerRunner.ts | 2 +- src/harness/projectsRunner.ts | 2 +- src/harness/rwcRunner.ts | 3 ++- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/harness/compilerRunner.ts b/src/harness/compilerRunner.ts index 79695639fd8d0..a567c1c9e384e 100644 --- a/src/harness/compilerRunner.ts +++ b/src/harness/compilerRunner.ts @@ -48,7 +48,7 @@ class CompilerBaselineRunner extends RunnerBase { } public checkTestCodeOutput(fileName: string) { - describe("compiler tests for " + fileName, () => { + describe(`${this.testSuiteName} tests for ${fileName}`, () => { // Mocha holds onto the closure environment of the describe callback even after the test is done. // Everything declared here should be cleared out in the "after" callback. let justName: string; diff --git a/src/harness/projectsRunner.ts b/src/harness/projectsRunner.ts index ceab3ae8f3b82..cc68f8625f43f 100644 --- a/src/harness/projectsRunner.ts +++ b/src/harness/projectsRunner.ts @@ -443,7 +443,7 @@ class ProjectRunner extends RunnerBase { const name = "Compiling project for " + testCase.scenario + ": testcase " + testCaseFileName; - describe("Projects tests", () => { + describe("projects tests", () => { describe(name, () => { function verifyCompilerResults(moduleKind: ts.ModuleKind) { let compilerResult: BatchCompileProjectTestCaseResult; diff --git a/src/harness/rwcRunner.ts b/src/harness/rwcRunner.ts index eba845d44ec12..8fe97bb7e1209 100644 --- a/src/harness/rwcRunner.ts +++ b/src/harness/rwcRunner.ts @@ -26,7 +26,7 @@ namespace RWC { } export function runRWCTest(jsonPath: string) { - describe("Testing a RWC project: " + jsonPath, () => { + describe("Testing a rwc project: " + jsonPath, () => { let inputFiles: Harness.Compiler.TestFile[] = []; let otherFiles: Harness.Compiler.TestFile[] = []; let tsconfigFiles: Harness.Compiler.TestFile[] = []; @@ -266,6 +266,7 @@ class RWCRunner extends RunnerBase { public initializeTests(): void { // Read in and evaluate the test list const testList = this.tests && this.tests.length ? this.tests : this.enumerateTestFiles(); + for (let i = 0; i < testList.length; i++) { this.runTest(testList[i]); } From 64fc495234a3b8b3f7897041dd69c35ad8983a62 Mon Sep 17 00:00:00 2001 From: Nathan Shively-Sanders Date: Tue, 17 Oct 2017 16:33:52 -0700 Subject: [PATCH 16/51] Collapse jsdoc annotation refactors to one Previously there were two, and two always fired. --- .../refactors/annotateWithTypeFromJSDoc.ts | 47 ++++++++++--------- 1 file changed, 25 insertions(+), 22 deletions(-) diff --git a/src/services/refactors/annotateWithTypeFromJSDoc.ts b/src/services/refactors/annotateWithTypeFromJSDoc.ts index 60c0517f68113..29da66f697441 100644 --- a/src/services/refactors/annotateWithTypeFromJSDoc.ts +++ b/src/services/refactors/annotateWithTypeFromJSDoc.ts @@ -5,16 +5,9 @@ namespace ts.refactor.annotateWithTypeFromJSDoc { const annotateTypeFromJSDoc: Refactor = { name: "Annotate with type from JSDoc", description: Diagnostics.Annotate_with_type_from_JSDoc.message, - getEditsForAction: getEditsForAnnotation, + getEditsForAction, getAvailableActions }; - const annotateFunctionFromJSDoc: Refactor = { - name: "Annotate with types from JSDoc", - description: Diagnostics.Annotate_with_types_from_JSDoc.message, - getEditsForAction: getEditsForFunctionAnnotation, - getAvailableActions - }; - type DeclarationWithType = | FunctionLikeDeclaration | VariableDeclaration @@ -23,7 +16,6 @@ namespace ts.refactor.annotateWithTypeFromJSDoc { | PropertyDeclaration; registerRefactor(annotateTypeFromJSDoc); - registerRefactor(annotateFunctionFromJSDoc); function getAvailableActions(context: RefactorContext): ApplicableRefactorInfo[] | undefined { if (isInJavaScriptFile(context.file)) { @@ -37,16 +29,13 @@ namespace ts.refactor.annotateWithTypeFromJSDoc { } const jsdocType = getJSDocType(decl); const isFunctionWithJSDoc = isFunctionLikeDeclaration(decl) && (getJSDocReturnType(decl) || decl.parameters.some(p => !!getJSDocType(p))); - const refactor = (isFunctionWithJSDoc || jsdocType && decl.kind === SyntaxKind.Parameter) ? annotateFunctionFromJSDoc : - jsdocType ? annotateTypeFromJSDoc : - undefined; - if (refactor) { + if (isFunctionWithJSDoc || jsdocType) { return [{ - name: refactor.name, - description: refactor.description, + name: annotateTypeFromJSDoc.name, + description: annotateTypeFromJSDoc.description, actions: [ { - description: refactor.description, + description: annotateTypeFromJSDoc.description, name: actionName } ] @@ -54,11 +43,29 @@ namespace ts.refactor.annotateWithTypeFromJSDoc { } } - function getEditsForAnnotation(context: RefactorContext, action: string): RefactorEditInfo | undefined { + function getEditsForAction(context: RefactorContext, action: string): RefactorEditInfo | undefined { if (actionName !== action) { return Debug.fail(`actionName !== action: ${actionName} !== ${action}`); } + const node = getTokenAtPosition(context.file, context.startPosition, /*includeJsDocComment*/ false); + const decl = findAncestor(node, isDeclarationWithType); + if (!decl || decl.type) { + return undefined; + } + const jsdocType = getJSDocType(decl); + const isFunctionWithJSDoc = isFunctionLikeDeclaration(decl) && (getJSDocReturnType(decl) || decl.parameters.some(p => !!getJSDocType(p))); + if (isFunctionWithJSDoc || jsdocType && decl.kind === SyntaxKind.Parameter) { + return getEditsForFunctionAnnotation(context); + } + else if (jsdocType) { + return getEditsForAnnotation(context); + } + else { + Debug.assert(!!refactor, "No applicable refactor found."); + } + } + function getEditsForAnnotation(context: RefactorContext): RefactorEditInfo | undefined { const sourceFile = context.file; const token = getTokenAtPosition(sourceFile, context.startPosition, /*includeJsDocComment*/ false); const decl = findAncestor(token, isDeclarationWithType); @@ -78,11 +85,7 @@ namespace ts.refactor.annotateWithTypeFromJSDoc { }; } - function getEditsForFunctionAnnotation(context: RefactorContext, action: string): RefactorEditInfo | undefined { - if (actionName !== action) { - return Debug.fail(`actionName !== action: ${actionName} !== ${action}`); - } - + function getEditsForFunctionAnnotation(context: RefactorContext): RefactorEditInfo | undefined { const sourceFile = context.file; const token = getTokenAtPosition(sourceFile, context.startPosition, /*includeJsDocComment*/ false); const decl = findAncestor(token, isFunctionLikeDeclaration); From e962e4abfbedd071945c7e5ec0c9e5a9305951df Mon Sep 17 00:00:00 2001 From: Nathan Shively-Sanders Date: Tue, 17 Oct 2017 16:35:28 -0700 Subject: [PATCH 17/51] Update baselines --- tests/cases/fourslash/annotateWithTypeFromJSDoc10.ts | 2 +- tests/cases/fourslash/annotateWithTypeFromJSDoc11.ts | 2 +- tests/cases/fourslash/annotateWithTypeFromJSDoc12.ts | 2 +- tests/cases/fourslash/annotateWithTypeFromJSDoc13.ts | 2 +- tests/cases/fourslash/annotateWithTypeFromJSDoc14.ts | 2 +- tests/cases/fourslash/annotateWithTypeFromJSDoc15.ts | 2 +- tests/cases/fourslash/annotateWithTypeFromJSDoc17.ts | 2 +- tests/cases/fourslash/annotateWithTypeFromJSDoc18.ts | 2 +- tests/cases/fourslash/annotateWithTypeFromJSDoc19.ts | 2 +- tests/cases/fourslash/annotateWithTypeFromJSDoc20.ts | 2 +- tests/cases/fourslash/annotateWithTypeFromJSDoc3.ts | 2 +- tests/cases/fourslash/annotateWithTypeFromJSDoc4.ts | 2 +- tests/cases/fourslash/annotateWithTypeFromJSDoc7.ts | 2 +- tests/cases/fourslash/annotateWithTypeFromJSDoc8.ts | 2 +- tests/cases/fourslash/annotateWithTypeFromJSDoc9.ts | 2 +- 15 files changed, 15 insertions(+), 15 deletions(-) diff --git a/tests/cases/fourslash/annotateWithTypeFromJSDoc10.ts b/tests/cases/fourslash/annotateWithTypeFromJSDoc10.ts index 0553a411c2e6f..f90c61c33d94d 100644 --- a/tests/cases/fourslash/annotateWithTypeFromJSDoc10.ts +++ b/tests/cases/fourslash/annotateWithTypeFromJSDoc10.ts @@ -12,4 +12,4 @@ verify.fileAfterApplyingRefactorAtMarker('1', * @param {?} x * @returns {number} */ -var f = (x: any): number => x`, 'Annotate with types from JSDoc', 'annotate'); +var f = (x: any): number => x`, 'Annotate with type from JSDoc', 'annotate'); diff --git a/tests/cases/fourslash/annotateWithTypeFromJSDoc11.ts b/tests/cases/fourslash/annotateWithTypeFromJSDoc11.ts index aaa7aaefa5b7e..f0ab5f3013e5b 100644 --- a/tests/cases/fourslash/annotateWithTypeFromJSDoc11.ts +++ b/tests/cases/fourslash/annotateWithTypeFromJSDoc11.ts @@ -12,4 +12,4 @@ verify.fileAfterApplyingRefactorAtMarker('2', * @param {?} x * @returns {number} */ -var f = (x: any): number => x`, 'Annotate with types from JSDoc', 'annotate'); +var f = (x: any): number => x`, 'Annotate with type from JSDoc', 'annotate'); diff --git a/tests/cases/fourslash/annotateWithTypeFromJSDoc12.ts b/tests/cases/fourslash/annotateWithTypeFromJSDoc12.ts index e541bad4e4f29..fc9b7c2a17eae 100644 --- a/tests/cases/fourslash/annotateWithTypeFromJSDoc12.ts +++ b/tests/cases/fourslash/annotateWithTypeFromJSDoc12.ts @@ -15,4 +15,4 @@ verify.fileAfterApplyingRefactorAtMarker('1', */ m(x): any[] { } -}`, 'Annotate with types from JSDoc', 'annotate'); +}`, 'Annotate with type from JSDoc', 'annotate'); diff --git a/tests/cases/fourslash/annotateWithTypeFromJSDoc13.ts b/tests/cases/fourslash/annotateWithTypeFromJSDoc13.ts index a47c090cd17e9..da147548b2775 100644 --- a/tests/cases/fourslash/annotateWithTypeFromJSDoc13.ts +++ b/tests/cases/fourslash/annotateWithTypeFromJSDoc13.ts @@ -8,4 +8,4 @@ verify.fileAfterApplyingRefactorAtMarker('1', `class C { /** @return {number} */ get c(): number { return 12; } -}`, 'Annotate with types from JSDoc', 'annotate'); +}`, 'Annotate with type from JSDoc', 'annotate'); diff --git a/tests/cases/fourslash/annotateWithTypeFromJSDoc14.ts b/tests/cases/fourslash/annotateWithTypeFromJSDoc14.ts index 13b4591de4764..5020d0f11cf1b 100644 --- a/tests/cases/fourslash/annotateWithTypeFromJSDoc14.ts +++ b/tests/cases/fourslash/annotateWithTypeFromJSDoc14.ts @@ -8,4 +8,4 @@ verify.fileAfterApplyingRefactorAtMarker('1', `/** @return {number} */ function f(): number { return 12; -}`, 'Annotate with types from JSDoc', 'annotate'); +}`, 'Annotate with type from JSDoc', 'annotate'); diff --git a/tests/cases/fourslash/annotateWithTypeFromJSDoc15.ts b/tests/cases/fourslash/annotateWithTypeFromJSDoc15.ts index c316430b71803..1389e800f5a71 100644 --- a/tests/cases/fourslash/annotateWithTypeFromJSDoc15.ts +++ b/tests/cases/fourslash/annotateWithTypeFromJSDoc15.ts @@ -27,4 +27,4 @@ verify.fileAfterApplyingRefactorAtMarker('9', * @param {promise} zeta */ function f(x: boolean, y: string, z: number, alpha: object, beta: Date, gamma: Promise, delta: Array, epsilon: Array, zeta: Promise) { -}`, 'Annotate with types from JSDoc', 'annotate'); +}`, 'Annotate with type from JSDoc', 'annotate'); diff --git a/tests/cases/fourslash/annotateWithTypeFromJSDoc17.ts b/tests/cases/fourslash/annotateWithTypeFromJSDoc17.ts index ed3180d3bd8be..ec26a9d1de7bb 100644 --- a/tests/cases/fourslash/annotateWithTypeFromJSDoc17.ts +++ b/tests/cases/fourslash/annotateWithTypeFromJSDoc17.ts @@ -14,5 +14,5 @@ verify.fileAfterApplyingRefactorAtMarker('1', */ constructor(x: number) { } -}`, 'Annotate with types from JSDoc', 'annotate'); +}`, 'Annotate with type from JSDoc', 'annotate'); diff --git a/tests/cases/fourslash/annotateWithTypeFromJSDoc18.ts b/tests/cases/fourslash/annotateWithTypeFromJSDoc18.ts index 9cfcb5c2471f4..10d3192583dac 100644 --- a/tests/cases/fourslash/annotateWithTypeFromJSDoc18.ts +++ b/tests/cases/fourslash/annotateWithTypeFromJSDoc18.ts @@ -8,4 +8,4 @@ verify.fileAfterApplyingRefactorAtMarker('1', `class C { /** @param {number} value */ set c(value: number) { return 12; } -}`, 'Annotate with types from JSDoc', 'annotate'); +}`, 'Annotate with type from JSDoc', 'annotate'); diff --git a/tests/cases/fourslash/annotateWithTypeFromJSDoc19.ts b/tests/cases/fourslash/annotateWithTypeFromJSDoc19.ts index 9e7ef6d25fd63..a47522fe03ca1 100644 --- a/tests/cases/fourslash/annotateWithTypeFromJSDoc19.ts +++ b/tests/cases/fourslash/annotateWithTypeFromJSDoc19.ts @@ -16,4 +16,4 @@ verify.fileAfterApplyingRefactorAtMarker('1', * @param {T} b */ function f(a: number, b: T) { -}`, 'Annotate with types from JSDoc', 'annotate'); +}`, 'Annotate with type from JSDoc', 'annotate'); diff --git a/tests/cases/fourslash/annotateWithTypeFromJSDoc20.ts b/tests/cases/fourslash/annotateWithTypeFromJSDoc20.ts index a45eb788c738c..093966231fdd2 100644 --- a/tests/cases/fourslash/annotateWithTypeFromJSDoc20.ts +++ b/tests/cases/fourslash/annotateWithTypeFromJSDoc20.ts @@ -14,4 +14,4 @@ verify.fileAfterApplyingRefactorAtMarker('1', * @param {T} b */ function f(a: number, b: T) { -}`, 'Annotate with types from JSDoc', 'annotate'); +}`, 'Annotate with type from JSDoc', 'annotate'); diff --git a/tests/cases/fourslash/annotateWithTypeFromJSDoc3.ts b/tests/cases/fourslash/annotateWithTypeFromJSDoc3.ts index ca5dbe312e3a0..57ae42b93a335 100644 --- a/tests/cases/fourslash/annotateWithTypeFromJSDoc3.ts +++ b/tests/cases/fourslash/annotateWithTypeFromJSDoc3.ts @@ -24,5 +24,5 @@ function f(x: number, y: { a: string; b: Date; }, z: string, alpha, beta: any) { -}`, 'Annotate with types from JSDoc', 'annotate'); +}`, 'Annotate with type from JSDoc', 'annotate'); diff --git a/tests/cases/fourslash/annotateWithTypeFromJSDoc4.ts b/tests/cases/fourslash/annotateWithTypeFromJSDoc4.ts index 2a2d7f943e2e3..795ac8b5fd36d 100644 --- a/tests/cases/fourslash/annotateWithTypeFromJSDoc4.ts +++ b/tests/cases/fourslash/annotateWithTypeFromJSDoc4.ts @@ -26,4 +26,4 @@ verify.fileAfterApplyingRefactorAtMarker('5', function f(x: any, y: any, z: number | undefined, alpha: number[], beta: (this: { a: string; }, arg1: string, arg2: number) => boolean, gamma: number | null, delta: number) { -}`, 'Annotate with types from JSDoc', 'annotate'); +}`, 'Annotate with type from JSDoc', 'annotate'); diff --git a/tests/cases/fourslash/annotateWithTypeFromJSDoc7.ts b/tests/cases/fourslash/annotateWithTypeFromJSDoc7.ts index a99ea8233894d..68df14c2b52a8 100644 --- a/tests/cases/fourslash/annotateWithTypeFromJSDoc7.ts +++ b/tests/cases/fourslash/annotateWithTypeFromJSDoc7.ts @@ -14,4 +14,4 @@ verify.fileAfterApplyingRefactorAtMarker('1', * @returns {number} */ function f(x: number): number { -}`, 'Annotate with types from JSDoc', 'annotate'); +}`, 'Annotate with type from JSDoc', 'annotate'); diff --git a/tests/cases/fourslash/annotateWithTypeFromJSDoc8.ts b/tests/cases/fourslash/annotateWithTypeFromJSDoc8.ts index 72ddb36988e0a..7381b2f288073 100644 --- a/tests/cases/fourslash/annotateWithTypeFromJSDoc8.ts +++ b/tests/cases/fourslash/annotateWithTypeFromJSDoc8.ts @@ -14,4 +14,4 @@ verify.fileAfterApplyingRefactorAtMarker('1', * @returns {number} */ var f = function(x: number): number { -}`, 'Annotate with types from JSDoc', 'annotate'); +}`, 'Annotate with type from JSDoc', 'annotate'); diff --git a/tests/cases/fourslash/annotateWithTypeFromJSDoc9.ts b/tests/cases/fourslash/annotateWithTypeFromJSDoc9.ts index 6b967e8e004ac..704d3681b3d3b 100644 --- a/tests/cases/fourslash/annotateWithTypeFromJSDoc9.ts +++ b/tests/cases/fourslash/annotateWithTypeFromJSDoc9.ts @@ -12,4 +12,4 @@ verify.fileAfterApplyingRefactorAtMarker('1', * @param {?} x * @returns {number} */ -var f = (x: any): number => x`, 'Annotate with types from JSDoc', 'annotate'); +var f = (x: any): number => x`, 'Annotate with type from JSDoc', 'annotate'); From 0c1730a21871762c392f8d6862adfc1005743edd Mon Sep 17 00:00:00 2001 From: Mohamed Hegazy Date: Tue, 17 Oct 2017 16:51:22 -0700 Subject: [PATCH 18/51] Fix #19257: Ensure a generated signature has a return type (#19264) * Fix #19257: Ensure a generated signature has a return type * Ensure generated properties have types * Use the same context for multiple inferences to the same property access --- src/services/codefixes/inferFromUsage.ts | 6 +++--- .../cases/fourslash/codeFixInferFromUsageCall.ts | 8 ++++++++ .../codeFixInferFromUsagePropertyAccess.ts | 15 +++++++++++++++ 3 files changed, 26 insertions(+), 3 deletions(-) create mode 100644 tests/cases/fourslash/codeFixInferFromUsageCall.ts create mode 100644 tests/cases/fourslash/codeFixInferFromUsagePropertyAccess.ts diff --git a/src/services/codefixes/inferFromUsage.ts b/src/services/codefixes/inferFromUsage.ts index 046eb915c1ef6..b952cc5a98db2 100644 --- a/src/services/codefixes/inferFromUsage.ts +++ b/src/services/codefixes/inferFromUsage.ts @@ -521,7 +521,7 @@ namespace ts.codefix { if (!usageContext.properties) { usageContext.properties = createUnderscoreEscapedMap(); } - const propertyUsageContext = {}; + const propertyUsageContext = usageContext.properties.get(name) || { }; inferTypeFromContext(parent, checker, propertyUsageContext); usageContext.properties.set(name, propertyUsageContext); } @@ -575,7 +575,7 @@ namespace ts.codefix { if (usageContext.properties) { usageContext.properties.forEach((context, name) => { const symbol = checker.createSymbol(SymbolFlags.Property, name); - symbol.type = getTypeFromUsageContext(context, checker); + symbol.type = getTypeFromUsageContext(context, checker) || checker.getAnyType(); members.set(name, symbol); }); } @@ -636,7 +636,7 @@ namespace ts.codefix { symbol.type = checker.getWidenedType(checker.getBaseTypeOfLiteralType(callContext.argumentTypes[i])); parameters.push(symbol); } - const returnType = getTypeFromUsageContext(callContext.returnType, checker); + const returnType = getTypeFromUsageContext(callContext.returnType, checker) || checker.getVoidType(); return checker.createSignature(/*declaration*/ undefined, /*typeParameters*/ undefined, /*thisParameter*/ undefined, parameters, returnType, /*typePredicate*/ undefined, callContext.argumentTypes.length, /*hasRestParameter*/ false, /*hasLiteralTypes*/ false); } diff --git a/tests/cases/fourslash/codeFixInferFromUsageCall.ts b/tests/cases/fourslash/codeFixInferFromUsageCall.ts new file mode 100644 index 0000000000000..e74b6faf7d3f9 --- /dev/null +++ b/tests/cases/fourslash/codeFixInferFromUsageCall.ts @@ -0,0 +1,8 @@ +/// + +// @noImplicitAny: true +////function wat([|b |]) { +//// b(); +////} + +verify.rangeAfterCodeFix("b: () => void"); \ No newline at end of file diff --git a/tests/cases/fourslash/codeFixInferFromUsagePropertyAccess.ts b/tests/cases/fourslash/codeFixInferFromUsagePropertyAccess.ts new file mode 100644 index 0000000000000..c8b85ee11e88d --- /dev/null +++ b/tests/cases/fourslash/codeFixInferFromUsagePropertyAccess.ts @@ -0,0 +1,15 @@ +/// + +// @noImplicitAny: true +////function foo([|a, m, x |]) { +//// a.b.c; +//// +//// var numeric = 0; +//// numeric = m.n(); +//// +//// x.y.z +//// x.y.z.push(0); +//// return x.y.z +////} + +verify.rangeAfterCodeFix("a: { b: { c: any; }; }, m: { n: () => number; }, x: { y: { z: number[]; }; }", /*includeWhiteSpace*/ undefined, /*errorCode*/ undefined, /*index*/0); \ No newline at end of file From f9df4e69e6138e2933fd3738edbf83a9e0d20ebe Mon Sep 17 00:00:00 2001 From: Wesley Wigham Date: Tue, 17 Oct 2017 18:45:21 -0700 Subject: [PATCH 19/51] Respect newLine compiler option in language service output (#19279) --- Jakefile.js | 1 + src/compiler/utilities.ts | 2 +- src/harness/tsconfig.json | 3 +- src/harness/unittests/hostNewLineSupport.ts | 67 +++++++++++++++++++++ src/services/services.ts | 2 +- 5 files changed, 72 insertions(+), 3 deletions(-) create mode 100644 src/harness/unittests/hostNewLineSupport.ts diff --git a/Jakefile.js b/Jakefile.js index 0b3659e205116..ba6943d30153d 100644 --- a/Jakefile.js +++ b/Jakefile.js @@ -154,6 +154,7 @@ var harnessSources = harnessCoreSources.concat([ "symbolWalker.ts", "languageService.ts", "publicApi.ts", + "hostNewLineSupport.ts", ].map(function (f) { return path.join(unittestsDirectory, f); })).concat([ diff --git a/src/compiler/utilities.ts b/src/compiler/utilities.ts index f907cce85b1f3..b08d8888d70eb 100644 --- a/src/compiler/utilities.ts +++ b/src/compiler/utilities.ts @@ -3199,7 +3199,7 @@ namespace ts { const carriageReturnLineFeed = "\r\n"; const lineFeed = "\n"; - export function getNewLineCharacter(options: CompilerOptions | PrinterOptions, system?: System): string { + export function getNewLineCharacter(options: CompilerOptions | PrinterOptions, system?: { newLine: string }): string { switch (options.newLine) { case NewLineKind.CarriageReturnLineFeed: return carriageReturnLineFeed; diff --git a/src/harness/tsconfig.json b/src/harness/tsconfig.json index a0c141f4f1760..18baeb67c823f 100644 --- a/src/harness/tsconfig.json +++ b/src/harness/tsconfig.json @@ -139,6 +139,7 @@ "./unittests/telemetry.ts", "./unittests/languageService.ts", "./unittests/programMissingFiles.ts", - "./unittests/publicApi.ts" + "./unittests/publicApi.ts", + "./unittests/hostNewLineSupport.ts" ] } diff --git a/src/harness/unittests/hostNewLineSupport.ts b/src/harness/unittests/hostNewLineSupport.ts new file mode 100644 index 0000000000000..9f6b09dfb72d0 --- /dev/null +++ b/src/harness/unittests/hostNewLineSupport.ts @@ -0,0 +1,67 @@ +/// +namespace ts { + describe("hostNewLineSupport", () => { + function testLSWithFiles(settings: CompilerOptions, files: Harness.Compiler.TestFile[]) { + function snapFor(path: string): IScriptSnapshot { + if (path === "lib.d.ts") { + return { + dispose() {}, + getChangeRange() { return undefined; }, + getLength() { return 0; }, + getText(_start, _end) { + return ""; + } + }; + } + const result = forEach(files, f => f.unitName === path ? f : undefined); + if (result) { + return { + dispose() {}, + getChangeRange() { return undefined; }, + getLength() { return result.content.length; }, + getText(start, end) { + return result.content.substring(start, end); + } + }; + } + return undefined; + } + const lshost: LanguageServiceHost = { + getCompilationSettings: () => settings, + getScriptFileNames: () => map(files, f => f.unitName), + getScriptVersion: () => "1", + getScriptSnapshot: name => snapFor(name), + getDefaultLibFileName: () => "lib.d.ts", + getCurrentDirectory: () => "", + }; + return ts.createLanguageService(lshost); + } + + function verifyNewLines(content: string, options: CompilerOptions) { + const ls = testLSWithFiles(options, [{ + content, + fileOptions: {}, + unitName: "input.ts" + }]); + const result = ls.getEmitOutput("input.ts"); + assert(!result.emitSkipped, "emit was skipped"); + assert(result.outputFiles.length === 1, "a number of files other than 1 was output"); + assert(result.outputFiles[0].name === "input.js", `Expected output file name input.js, but got ${result.outputFiles[0].name}`); + assert(result.outputFiles[0].text.match(options.newLine === NewLineKind.CarriageReturnLineFeed ? /\r\n/ : /[^\r]\n/), "expected to find appropriate newlines"); + assert(!result.outputFiles[0].text.match(options.newLine === NewLineKind.CarriageReturnLineFeed ? /[^\r]\n/ : /\r\n/), "expected not to find inappropriate newlines"); + } + + function verifyBothNewLines(content: string) { + verifyNewLines(content, { newLine: NewLineKind.CarriageReturnLineFeed }); + verifyNewLines(content, { newLine: NewLineKind.LineFeed }); + } + + it("should exist and respect provided compiler options", () => { + verifyBothNewLines(` + function foo() { + return 2 + 2; + } + `); + }); + }); +} \ No newline at end of file diff --git a/src/services/services.ts b/src/services/services.ts index a5c9c1d353a18..114692ebba2e5 100644 --- a/src/services/services.ts +++ b/src/services/services.ts @@ -1146,7 +1146,7 @@ namespace ts { getCancellationToken: () => cancellationToken, getCanonicalFileName, useCaseSensitiveFileNames: () => useCaseSensitivefileNames, - getNewLine: () => getNewLineOrDefaultFromHost(host), + getNewLine: () => getNewLineCharacter(newSettings, { newLine: getNewLineOrDefaultFromHost(host) }), getDefaultLibFileName: (options) => host.getDefaultLibFileName(options), writeFile: noop, getCurrentDirectory: () => currentDirectory, From b792daab984b3e04dcf379c8c64f7c137235ff8b Mon Sep 17 00:00:00 2001 From: csigs Date: Wed, 18 Oct 2017 04:10:04 +0000 Subject: [PATCH 20/51] LEGO: check in for master to temporary branch. --- .../diagnosticMessages.generated.json.lcl | 37 ++++++- .../diagnosticMessages.generated.json.lcl | 100 ++++++++++++++++-- 2 files changed, 127 insertions(+), 10 deletions(-) diff --git a/src/loc/lcl/fra/diagnosticMessages/diagnosticMessages.generated.json.lcl b/src/loc/lcl/fra/diagnosticMessages/diagnosticMessages.generated.json.lcl index 43073f7cab335..c23ea7cc378a5 100644 --- a/src/loc/lcl/fra/diagnosticMessages/diagnosticMessages.generated.json.lcl +++ b/src/loc/lcl/fra/diagnosticMessages/diagnosticMessages.generated.json.lcl @@ -804,6 +804,9 @@ + + + @@ -1347,12 +1350,18 @@ + + + + + + @@ -1506,6 +1515,9 @@ + + + @@ -1917,8 +1929,8 @@ - - + + @@ -2040,6 +2052,9 @@ + + + @@ -3828,12 +3843,18 @@ + + + + + + @@ -4032,12 +4053,18 @@ + + + + + + @@ -7302,6 +7329,9 @@ + + + @@ -7788,6 +7818,9 @@ + + + diff --git a/src/loc/lcl/ptb/diagnosticMessages/diagnosticMessages.generated.json.lcl b/src/loc/lcl/ptb/diagnosticMessages/diagnosticMessages.generated.json.lcl index 603345e63b223..014ca9ef8f6f7 100644 --- a/src/loc/lcl/ptb/diagnosticMessages/diagnosticMessages.generated.json.lcl +++ b/src/loc/lcl/ptb/diagnosticMessages/diagnosticMessages.generated.json.lcl @@ -782,6 +782,15 @@ + + + + + + + + + @@ -1313,6 +1322,24 @@ + + + + + + + + + + + + + + + + + + @@ -1460,6 +1487,15 @@ + + + + + + + + + @@ -1867,10 +1903,13 @@ - + - + + + + @@ -1982,6 +2021,15 @@ + + + + + + + + + @@ -3758,6 +3806,24 @@ + + + + + + + + + + + + + + + + + + @@ -3950,20 +4016,20 @@ - + - + - + - + - + - + @@ -7217,6 +7283,15 @@ + + + + + + + + + @@ -7697,6 +7772,15 @@ + + + + + + + + + From d08b58c7d15aa885d79778645158246d9a60939a Mon Sep 17 00:00:00 2001 From: csigs Date: Wed, 18 Oct 2017 10:10:12 +0000 Subject: [PATCH 21/51] LEGO: check in for master to temporary branch. --- .../diagnosticMessages.generated.json.lcl | 99 +++++++++++++++++-- 1 file changed, 90 insertions(+), 9 deletions(-) diff --git a/src/loc/lcl/cht/diagnosticMessages/diagnosticMessages.generated.json.lcl b/src/loc/lcl/cht/diagnosticMessages/diagnosticMessages.generated.json.lcl index 16310396458a1..59537bdde09d3 100644 --- a/src/loc/lcl/cht/diagnosticMessages/diagnosticMessages.generated.json.lcl +++ b/src/loc/lcl/cht/diagnosticMessages/diagnosticMessages.generated.json.lcl @@ -792,6 +792,15 @@ + + + + + + + + + @@ -1329,6 +1338,24 @@ + + + + + + + + + + + + + + + + + + @@ -1476,6 +1503,15 @@ + + + + + + + + + @@ -1883,12 +1919,12 @@ - + - + - + @@ -2004,6 +2040,15 @@ + + + + + + + + + @@ -3786,6 +3831,24 @@ + + + + + + + + + + + + + + + + + + @@ -3978,20 +4041,20 @@ - + - + - + - + - + - + @@ -7254,6 +7317,15 @@ + + + + + + + + + @@ -7734,6 +7806,15 @@ + + + + + + + + + From 2ca0df8844bf630b1f8521f2100e6f2d7d5c72bd Mon Sep 17 00:00:00 2001 From: csigs Date: Wed, 18 Oct 2017 16:10:11 +0000 Subject: [PATCH 22/51] LEGO: check in for master to temporary branch. --- .../diagnosticMessages.generated.json.lcl | 99 +++++++++++++++++-- .../diagnosticMessages.generated.json.lcl | 99 +++++++++++++++++-- .../diagnosticMessages.generated.json.lcl | 99 +++++++++++++++++-- .../diagnosticMessages.generated.json.lcl | 37 ++++++- 4 files changed, 305 insertions(+), 29 deletions(-) diff --git a/src/loc/lcl/chs/diagnosticMessages/diagnosticMessages.generated.json.lcl b/src/loc/lcl/chs/diagnosticMessages/diagnosticMessages.generated.json.lcl index 3c83950d4f32c..014459b186046 100644 --- a/src/loc/lcl/chs/diagnosticMessages/diagnosticMessages.generated.json.lcl +++ b/src/loc/lcl/chs/diagnosticMessages/diagnosticMessages.generated.json.lcl @@ -792,6 +792,15 @@ + + + + + + + + + @@ -1329,6 +1338,24 @@ + + + + + + + + + + + + + + + + + + @@ -1476,6 +1503,15 @@ + + + + + + + + + @@ -1883,12 +1919,12 @@ - + - + - + @@ -2004,6 +2040,15 @@ + + + + + + + + + @@ -3786,6 +3831,24 @@ + + + + + + + + + + + + + + + + + + @@ -3978,20 +4041,20 @@ - + - + - + - + - + - + @@ -7254,6 +7317,15 @@ + + + + + + + + + @@ -7734,6 +7806,15 @@ + + + + + + + + + diff --git a/src/loc/lcl/jpn/diagnosticMessages/diagnosticMessages.generated.json.lcl b/src/loc/lcl/jpn/diagnosticMessages/diagnosticMessages.generated.json.lcl index de906e32915dd..7bf155734c81a 100644 --- a/src/loc/lcl/jpn/diagnosticMessages/diagnosticMessages.generated.json.lcl +++ b/src/loc/lcl/jpn/diagnosticMessages/diagnosticMessages.generated.json.lcl @@ -792,6 +792,15 @@ + + + + + + + + + @@ -1329,6 +1338,24 @@ + + + + + + + + + + + + + + + + + + @@ -1476,6 +1503,15 @@ + + + + + + + + + @@ -1883,12 +1919,12 @@ - + - + - + @@ -2004,6 +2040,15 @@ + + + + + + + + + @@ -3786,6 +3831,24 @@ + + + + + + + + + + + + + + + + + + @@ -3978,20 +4041,20 @@ - + - + - + - + - + - + @@ -7254,6 +7317,15 @@ + + + + + + + + + @@ -7734,6 +7806,15 @@ + + + + + + + + + diff --git a/src/loc/lcl/kor/diagnosticMessages/diagnosticMessages.generated.json.lcl b/src/loc/lcl/kor/diagnosticMessages/diagnosticMessages.generated.json.lcl index c25043edfa592..b2f99224643f7 100644 --- a/src/loc/lcl/kor/diagnosticMessages/diagnosticMessages.generated.json.lcl +++ b/src/loc/lcl/kor/diagnosticMessages/diagnosticMessages.generated.json.lcl @@ -792,6 +792,15 @@ + + + + + + + + + @@ -1329,6 +1338,24 @@ + + + + + + + + + + + + + + + + + + @@ -1476,6 +1503,15 @@ + + + + + + + + + @@ -1883,12 +1919,12 @@ - + - + - + @@ -2004,6 +2040,15 @@ + + + + + + + + + @@ -3786,6 +3831,24 @@ + + + + + + + + + + + + + + + + + + @@ -3978,20 +4041,20 @@ - + - + - + - + - + - + @@ -7254,6 +7317,15 @@ + + + + + + + + + @@ -7734,6 +7806,15 @@ + + + + + + + + + diff --git a/src/loc/lcl/plk/diagnosticMessages/diagnosticMessages.generated.json.lcl b/src/loc/lcl/plk/diagnosticMessages/diagnosticMessages.generated.json.lcl index 4ae9e7d5e5697..990ca10226f34 100644 --- a/src/loc/lcl/plk/diagnosticMessages/diagnosticMessages.generated.json.lcl +++ b/src/loc/lcl/plk/diagnosticMessages/diagnosticMessages.generated.json.lcl @@ -785,6 +785,9 @@ + + + @@ -1322,12 +1325,18 @@ + + + + + + @@ -1481,6 +1490,9 @@ + + + @@ -1892,8 +1904,8 @@ - - + + @@ -2012,6 +2024,9 @@ + + + @@ -3794,12 +3809,18 @@ + + + + + + @@ -3998,12 +4019,18 @@ + + + + + + @@ -7259,6 +7286,9 @@ + + + @@ -7745,6 +7775,9 @@ + + + From 3220ebc182cf33465abb64666e6b8a258f830213 Mon Sep 17 00:00:00 2001 From: Andy Date: Wed, 18 Oct 2017 10:23:18 -0700 Subject: [PATCH 23/51] Disambiguate same-named refactors using description (#19267) Disambiguate same-named refactors using actionName --- src/harness/fourslash.ts | 8 ++++---- tests/cases/fourslash/extract-const1.ts | 14 ++++++++++++++ 2 files changed, 18 insertions(+), 4 deletions(-) create mode 100644 tests/cases/fourslash/extract-const1.ts diff --git a/src/harness/fourslash.ts b/src/harness/fourslash.ts index 9ac2aac764529..6724015107974 100644 --- a/src/harness/fourslash.ts +++ b/src/harness/fourslash.ts @@ -2871,14 +2871,14 @@ Actual: ${stringify(fullActual)}`); public applyRefactor({ refactorName, actionName, actionDescription, newContent: newContentWithRenameMarker }: FourSlashInterface.ApplyRefactorOptions) { const range = this.getSelection(); const refactors = this.languageService.getApplicableRefactors(this.activeFile.fileName, range); - const refactor = refactors.find(r => r.name === refactorName); - if (!refactor) { + const refactorsWithName = refactors.filter(r => r.name === refactorName); + if (refactorsWithName.length === 0) { this.raiseError(`The expected refactor: ${refactorName} is not available at the marker location.\nAvailable refactors: ${refactors.map(r => r.name)}`); } - const action = refactor.actions.find(a => a.name === actionName); + const action = ts.firstDefined(refactorsWithName, refactor => refactor.actions.find(a => a.name === actionName)); if (!action) { - this.raiseError(`The expected action: ${action} is not included in: ${refactor.actions.map(a => a.name)}`); + this.raiseError(`The expected action: ${actionName} is not included in: ${ts.flatMap(refactorsWithName, r => r.actions.map(a => a.name))}`); } if (action.description !== actionDescription) { this.raiseError(`Expected action description to be ${JSON.stringify(actionDescription)}, got: ${JSON.stringify(action.description)}`); diff --git a/tests/cases/fourslash/extract-const1.ts b/tests/cases/fourslash/extract-const1.ts new file mode 100644 index 0000000000000..3ed9373f2bae1 --- /dev/null +++ b/tests/cases/fourslash/extract-const1.ts @@ -0,0 +1,14 @@ +/// + +////const x = /*a*/0/*b*/; + +goTo.select("a", "b"); +edit.applyRefactor({ + refactorName: "Extract Symbol", + actionName: "constant_scope_0", + actionDescription: "Extract to constant in enclosing scope", + newContent: +`const newLocal = 0; + +const x = /*RENAME*/newLocal;` +}); From 45ba0ac3b91a1bc80cfa489b979599cef33f5392 Mon Sep 17 00:00:00 2001 From: Bill Ticehurst Date: Mon, 16 Oct 2017 13:38:28 -0700 Subject: [PATCH 24/51] Set the scriptKind from the host configuration if present --- src/compiler/core.ts | 10 ++++++++++ src/compiler/program.ts | 6 ++++-- src/compiler/types.ts | 1 + src/server/editorServices.ts | 21 ++++++++++++++++++--- 4 files changed, 33 insertions(+), 5 deletions(-) diff --git a/src/compiler/core.ts b/src/compiler/core.ts index 90dcc1ad5d4a0..d794b8177667a 100644 --- a/src/compiler/core.ts +++ b/src/compiler/core.ts @@ -2691,6 +2691,16 @@ namespace ts { return find(supportedTypescriptExtensionsForExtractExtension, e => fileExtensionIs(path, e)) || find(supportedJavascriptExtensions, e => fileExtensionIs(path, e)); } + // Retrieves any string from the final "." onwards from a base file name. + // Unlike extensionFromPath, which throws an exception on unrecognized extensions. + export function getAnyExtensionFromPath(path: string): string | undefined { + const baseFileName = getBaseFileName(path); + const extensionIndex = baseFileName.lastIndexOf("."); + if (extensionIndex >= 0) { + return baseFileName.substring(extensionIndex); + } + } + export function isCheckJsEnabledForFile(sourceFile: SourceFile, compilerOptions: CompilerOptions) { return sourceFile.checkJsDirective ? sourceFile.checkJsDirective.enabled : compilerOptions.checkJs; } diff --git a/src/compiler/program.ts b/src/compiler/program.ts index b8c402dbb7f86..1d325983f5fc6 100755 --- a/src/compiler/program.ts +++ b/src/compiler/program.ts @@ -1278,8 +1278,10 @@ namespace ts { const typeChecker = getDiagnosticsProducingTypeChecker(); Debug.assert(!!sourceFile.bindDiagnostics); - // For JavaScript files, we don't want to report semantic errors unless explicitly requested. - const includeBindAndCheckDiagnostics = !isSourceFileJavaScript(sourceFile) || isCheckJsEnabledForFile(sourceFile, options); + + // By default, only type-check .ts, .tsx, and 'External' files (external files are added by plugins) + const includeBindAndCheckDiagnostics = sourceFile.scriptKind === ScriptKind.TS || sourceFile.scriptKind === ScriptKind.TSX || + sourceFile.scriptKind === ScriptKind.External || isCheckJsEnabledForFile(sourceFile, options); const bindDiagnostics = includeBindAndCheckDiagnostics ? sourceFile.bindDiagnostics : emptyArray; const checkDiagnostics = includeBindAndCheckDiagnostics ? typeChecker.getDiagnostics(sourceFile, cancellationToken) : emptyArray; const fileProcessingDiagnosticsInFile = fileProcessingDiagnostics.getDiagnostics(sourceFile.fileName); diff --git a/src/compiler/types.ts b/src/compiler/types.ts index 6226e4f98c723..3f9b492f90342 100644 --- a/src/compiler/types.ts +++ b/src/compiler/types.ts @@ -3626,6 +3626,7 @@ namespace ts { export interface JsFileExtensionInfo { extension: string; isMixedContent: boolean; + scriptKind?: ScriptKind; } export interface DiagnosticMessage { diff --git a/src/server/editorServices.ts b/src/server/editorServices.ts index 70598313a7365..ad08cd63da6b7 100644 --- a/src/server/editorServices.ts +++ b/src/server/editorServices.ts @@ -220,13 +220,28 @@ namespace ts.server { interface FilePropertyReader { getFileName(f: T): string; - getScriptKind(f: T): ScriptKind; + getScriptKind(f: T, extraFileExtensions?: JsFileExtensionInfo[]): ScriptKind; hasMixedContent(f: T, extraFileExtensions: JsFileExtensionInfo[]): boolean; } const fileNamePropertyReader: FilePropertyReader = { getFileName: x => x, - getScriptKind: _ => undefined, + getScriptKind: (fileName, extraFileExtensions) => { + let result: ScriptKind; + if (extraFileExtensions) { + const fileExtension = getAnyExtensionFromPath(fileName); + if (fileExtension) { + some(extraFileExtensions, info => { + if (info.extension === fileExtension) { + result = info.scriptKind; + return true; + } + return false; + }); + } + } + return result; + }, hasMixedContent: (fileName, extraFileExtensions) => some(extraFileExtensions, ext => ext.isMixedContent && fileExtensionIs(fileName, ext.extension)), }; @@ -1504,7 +1519,7 @@ namespace ts.server { scriptInfo = normalizedPath; } else { - const scriptKind = propertyReader.getScriptKind(f); + const scriptKind = propertyReader.getScriptKind(f, this.hostConfiguration.extraFileExtensions); const hasMixedContent = propertyReader.hasMixedContent(f, this.hostConfiguration.extraFileExtensions); scriptInfo = this.getOrCreateScriptInfoNotOpenedByClientForNormalizedPath(normalizedPath, scriptKind, hasMixedContent, project.directoryStructureHost); path = scriptInfo.path; From 9da745d37ba757026f0847ef3d6942a259a87ee7 Mon Sep 17 00:00:00 2001 From: Bill Ticehurst Date: Tue, 17 Oct 2017 16:50:32 -0700 Subject: [PATCH 25/51] Update API baselines --- tests/baselines/reference/api/tsserverlibrary.d.ts | 1 + tests/baselines/reference/api/typescript.d.ts | 1 + 2 files changed, 2 insertions(+) diff --git a/tests/baselines/reference/api/tsserverlibrary.d.ts b/tests/baselines/reference/api/tsserverlibrary.d.ts index 15eca3da329f4..52132ae832f25 100644 --- a/tests/baselines/reference/api/tsserverlibrary.d.ts +++ b/tests/baselines/reference/api/tsserverlibrary.d.ts @@ -2158,6 +2158,7 @@ declare namespace ts { interface JsFileExtensionInfo { extension: string; isMixedContent: boolean; + scriptKind?: ScriptKind; } interface DiagnosticMessage { key: string; diff --git a/tests/baselines/reference/api/typescript.d.ts b/tests/baselines/reference/api/typescript.d.ts index f3180b9d1069c..976d50942ae1b 100644 --- a/tests/baselines/reference/api/typescript.d.ts +++ b/tests/baselines/reference/api/typescript.d.ts @@ -2158,6 +2158,7 @@ declare namespace ts { interface JsFileExtensionInfo { extension: string; isMixedContent: boolean; + scriptKind?: ScriptKind; } interface DiagnosticMessage { key: string; From f37411785809e63780f826699887f890a81cdba1 Mon Sep 17 00:00:00 2001 From: Nathan Shively-Sanders Date: Wed, 18 Oct 2017 11:32:48 -0700 Subject: [PATCH 26/51] Remove erroneous error for JSDoc object literals appears with checkJS. --- src/compiler/checker.ts | 19 +++++++++++++----- .../reference/jsdocIndexSignature.errors.txt | 18 +++++++++++++++++ .../reference/jsdocIndexSignature.symbols | 12 +++++++++++ .../reference/jsdocIndexSignature.types | 20 +++++++++++++++++++ 4 files changed, 64 insertions(+), 5 deletions(-) create mode 100644 tests/baselines/reference/jsdocIndexSignature.errors.txt diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts index 219f370fd53f5..992c609929f94 100644 --- a/src/compiler/checker.ts +++ b/src/compiler/checker.ts @@ -7030,13 +7030,11 @@ namespace ts { function getIntendedTypeFromJSDocTypeReference(node: TypeReferenceNode): Type { if (isIdentifier(node.typeName)) { if (node.typeName.escapedText === "Object") { - if (node.typeArguments && node.typeArguments.length === 2) { + if (isJSDocIndexSignature(node)) { const indexed = getTypeFromTypeNode(node.typeArguments[0]); const target = getTypeFromTypeNode(node.typeArguments[1]); const index = createIndexInfo(target, /*isReadonly*/ false); - if (indexed === stringType || indexed === numberType) { - return createAnonymousType(undefined, emptySymbols, emptyArray, emptyArray, indexed === stringType && index, indexed === numberType && index); - } + return createAnonymousType(undefined, emptySymbols, emptyArray, emptyArray, indexed === stringType && index, indexed === numberType && index); } return anyType; } @@ -7066,6 +7064,14 @@ namespace ts { } } + function isJSDocIndexSignature(node: TypeReferenceNode | ExpressionWithTypeArguments) { + return isTypeReferenceNode(node) && + isIdentifier(node.typeName) && + node.typeName.escapedText === "Object" && + node.typeArguments && node.typeArguments.length === 2 && + (node.typeArguments[0].kind === SyntaxKind.StringKeyword || node.typeArguments[0].kind === SyntaxKind.NumberKeyword); + } + function getTypeFromJSDocNullableTypeNode(node: JSDocNullableType) { const type = getTypeFromTypeNode(node.type); return strictNullChecks ? getUnionType([type, nullType]) : type; @@ -19179,7 +19185,10 @@ namespace ts { // There is no resolved symbol cached if the type resolved to a builtin // via JSDoc type reference resolution (eg, Boolean became boolean), none // of which are generic when they have no associated symbol - error(node, Diagnostics.Type_0_is_not_generic, typeToString(type)); + // (additionally, JSDoc's index signature syntax, Object actually uses generic syntax without being generic) + if (!isJSDocIndexSignature(node)) { + error(node, Diagnostics.Type_0_is_not_generic, typeToString(type)); + } return; } let typeParameters = symbol.flags & SymbolFlags.TypeAlias && getSymbolLinks(symbol).typeParameters; diff --git a/tests/baselines/reference/jsdocIndexSignature.errors.txt b/tests/baselines/reference/jsdocIndexSignature.errors.txt new file mode 100644 index 0000000000000..de336795b3e64 --- /dev/null +++ b/tests/baselines/reference/jsdocIndexSignature.errors.txt @@ -0,0 +1,18 @@ +tests/cases/conformance/jsdoc/indices.js(9,5): error TS2322: Type '1' is not assignable to type 'boolean'. + + +==== tests/cases/conformance/jsdoc/indices.js (1 errors) ==== + /** @type {Object.} */ + var o1; + /** @type {Object.} */ + var o2; + /** @type {Object.} */ + var o3; + /** @param {Object.} o */ + function f(o) { + o.foo = 1; // error + ~~~~~ +!!! error TS2322: Type '1' is not assignable to type 'boolean'. + o.bar = false; // ok + } + \ No newline at end of file diff --git a/tests/baselines/reference/jsdocIndexSignature.symbols b/tests/baselines/reference/jsdocIndexSignature.symbols index 8814fc18b6f1f..89288458f470f 100644 --- a/tests/baselines/reference/jsdocIndexSignature.symbols +++ b/tests/baselines/reference/jsdocIndexSignature.symbols @@ -11,3 +11,15 @@ var o2; var o3; >o3 : Symbol(o3, Decl(indices.js, 5, 3)) +/** @param {Object.} o */ +function f(o) { +>f : Symbol(f, Decl(indices.js, 5, 7)) +>o : Symbol(o, Decl(indices.js, 7, 11)) + + o.foo = 1; // error +>o : Symbol(o, Decl(indices.js, 7, 11)) + + o.bar = false; // ok +>o : Symbol(o, Decl(indices.js, 7, 11)) +} + diff --git a/tests/baselines/reference/jsdocIndexSignature.types b/tests/baselines/reference/jsdocIndexSignature.types index 4c1feaa2721f1..d9ac826309244 100644 --- a/tests/baselines/reference/jsdocIndexSignature.types +++ b/tests/baselines/reference/jsdocIndexSignature.types @@ -11,3 +11,23 @@ var o2; var o3; >o3 : any +/** @param {Object.} o */ +function f(o) { +>f : (o: { [x: string]: boolean; }) => void +>o : { [x: string]: boolean; } + + o.foo = 1; // error +>o.foo = 1 : 1 +>o.foo : boolean +>o : { [x: string]: boolean; } +>foo : boolean +>1 : 1 + + o.bar = false; // ok +>o.bar = false : false +>o.bar : boolean +>o : { [x: string]: boolean; } +>bar : boolean +>false : false +} + From c13506e70c57644f53baae37aee7b0d83f53eeb7 Mon Sep 17 00:00:00 2001 From: Nathan Shively-Sanders Date: Wed, 18 Oct 2017 13:04:13 -0700 Subject: [PATCH 27/51] Update annotateWithTypeFromJSDoc tests 1. Object literals are single-line now. 2. Index signatures transform to TS index signatures. 3. The refactoring is only available when it could add types. --- .../fourslash/annotateWithTypeFromJSDoc21.ts | 73 +++++++++++++++++++ .../fourslash/annotateWithTypeFromJSDoc22.ts | 14 ++++ .../fourslash/annotateWithTypeFromJSDoc3.ts | 5 +- .../fourslash/annotateWithTypeFromJSDoc4.ts | 4 +- 4 files changed, 89 insertions(+), 7 deletions(-) create mode 100644 tests/cases/fourslash/annotateWithTypeFromJSDoc21.ts create mode 100644 tests/cases/fourslash/annotateWithTypeFromJSDoc22.ts diff --git a/tests/cases/fourslash/annotateWithTypeFromJSDoc21.ts b/tests/cases/fourslash/annotateWithTypeFromJSDoc21.ts new file mode 100644 index 0000000000000..b54f83070c4ab --- /dev/null +++ b/tests/cases/fourslash/annotateWithTypeFromJSDoc21.ts @@ -0,0 +1,73 @@ +/// +// @strict: true +/////** +//// * @return {number} +//// */ +////function /*1*/f(x, y) { +////} +//// +/////** +//// * @return {number} +//// */ +////function /*2*/g(x, y): number { +//// return 0; +////} +/////** +//// * @param {number} x +//// */ +////function /*3*/h(x: number, y): number { +//// return 0; +////} +//// +/////** +//// * @param {number} x +//// * @param {string} y +//// */ +////function /*4*/i(x: number, y: string) { +////} +/////** +//// * @param {number} x +//// * @return {boolean} +//// */ +////function /*5*/j(x: number, y): boolean { +//// return true; +////} + +verify.not.applicableRefactorAvailableAtMarker('2'); +verify.not.applicableRefactorAvailableAtMarker('3'); +verify.not.applicableRefactorAvailableAtMarker('4'); +verify.not.applicableRefactorAvailableAtMarker('5'); +verify.applicableRefactorAvailableAtMarker('1'); +verify.fileAfterApplyingRefactorAtMarker('1', +`/** + * @return {number} + */ +function f(x, y): number { +} + +/** + * @return {number} + */ +function g(x, y): number { + return 0; +} +/** + * @param {number} x + */ +function h(x: number, y): number { + return 0; +} + +/** + * @param {number} x + * @param {string} y + */ +function i(x: number, y: string) { +} +/** + * @param {number} x + * @return {boolean} + */ +function j(x: number, y): boolean { + return true; +}`, 'Annotate with type from JSDoc', 'annotate'); diff --git a/tests/cases/fourslash/annotateWithTypeFromJSDoc22.ts b/tests/cases/fourslash/annotateWithTypeFromJSDoc22.ts new file mode 100644 index 0000000000000..d00705848c642 --- /dev/null +++ b/tests/cases/fourslash/annotateWithTypeFromJSDoc22.ts @@ -0,0 +1,14 @@ +/// +// @strict: true +//// +/////** @param {Object} sb +//// * @param {Object} ns */ +////function /*1*/f(sb, ns) { +////} +verify.applicableRefactorAvailableAtMarker('1'); +verify.fileAfterApplyingRefactorAtMarker('1', +` +/** @param {Object} sb + * @param {Object} ns */ +function f(sb: { [s: string]: boolean; }, ns: { [n: number]: string; }) { +}`, 'Annotate with type from JSDoc', 'annotate'); diff --git a/tests/cases/fourslash/annotateWithTypeFromJSDoc3.ts b/tests/cases/fourslash/annotateWithTypeFromJSDoc3.ts index 57ae42b93a335..9413e6046c9e4 100644 --- a/tests/cases/fourslash/annotateWithTypeFromJSDoc3.ts +++ b/tests/cases/fourslash/annotateWithTypeFromJSDoc3.ts @@ -20,9 +20,6 @@ verify.fileAfterApplyingRefactorAtMarker('1', * @param alpha - the other best parameter * @param {*} beta - I have no idea how this got here */ -function f(x: number, y: { - a: string; - b: Date; -}, z: string, alpha, beta: any) { +function f(x: number, y: { a: string; b: Date; }, z: string, alpha, beta: any) { }`, 'Annotate with type from JSDoc', 'annotate'); diff --git a/tests/cases/fourslash/annotateWithTypeFromJSDoc4.ts b/tests/cases/fourslash/annotateWithTypeFromJSDoc4.ts index 795ac8b5fd36d..ad06bbbc699f2 100644 --- a/tests/cases/fourslash/annotateWithTypeFromJSDoc4.ts +++ b/tests/cases/fourslash/annotateWithTypeFromJSDoc4.ts @@ -23,7 +23,5 @@ verify.fileAfterApplyingRefactorAtMarker('5', * @param {number?} gamma * @param {number!} delta */ -function f(x: any, y: any, z: number | undefined, alpha: number[], beta: (this: { - a: string; -}, arg1: string, arg2: number) => boolean, gamma: number | null, delta: number) { +function f(x: any, y: any, z: number | undefined, alpha: number[], beta: (this: { a: string; }, arg1: string, arg2: number) => boolean, gamma: number | null, delta: number) { }`, 'Annotate with type from JSDoc', 'annotate'); From 2473ffcaac533a7bd6a5b5fe3a2896a6cff9a726 Mon Sep 17 00:00:00 2001 From: Nathan Shively-Sanders Date: Wed, 18 Oct 2017 13:06:15 -0700 Subject: [PATCH 28/51] Add a better test for jsdoc index signatures. The test case shows that the errorenous error no longer appears. --- tests/cases/conformance/jsdoc/jsdocIndexSignature.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/tests/cases/conformance/jsdoc/jsdocIndexSignature.ts b/tests/cases/conformance/jsdoc/jsdocIndexSignature.ts index fdf9e06e61eb0..29365cb91be5b 100644 --- a/tests/cases/conformance/jsdoc/jsdocIndexSignature.ts +++ b/tests/cases/conformance/jsdoc/jsdocIndexSignature.ts @@ -8,3 +8,8 @@ var o1; var o2; /** @type {Object.} */ var o3; +/** @param {Object.} o */ +function f(o) { + o.foo = 1; // error + o.bar = false; // ok +} From aa73ed822618d1819cfd37e0bfb1ca7af0933898 Mon Sep 17 00:00:00 2001 From: Nathan Shively-Sanders Date: Wed, 18 Oct 2017 13:07:54 -0700 Subject: [PATCH 29/51] Fix bugs in jsdoc annotation refactor 1. Transform index signatures to TS index signatures. 2. Print object literals on a single line. 3. Only offer the refactor when it could add types. (There must not be a type annotation already, and there must be a JSDoc that applies.) --- .../refactors/annotateWithTypeFromJSDoc.ts | 53 +++++++++++++++---- 1 file changed, 42 insertions(+), 11 deletions(-) diff --git a/src/services/refactors/annotateWithTypeFromJSDoc.ts b/src/services/refactors/annotateWithTypeFromJSDoc.ts index 29da66f697441..98131d0623cb4 100644 --- a/src/services/refactors/annotateWithTypeFromJSDoc.ts +++ b/src/services/refactors/annotateWithTypeFromJSDoc.ts @@ -23,26 +23,30 @@ namespace ts.refactor.annotateWithTypeFromJSDoc { } const node = getTokenAtPosition(context.file, context.startPosition, /*includeJsDocComment*/ false); - const decl = findAncestor(node, isDeclarationWithType); - if (!decl || decl.type) { - return undefined; - } - const jsdocType = getJSDocType(decl); - const isFunctionWithJSDoc = isFunctionLikeDeclaration(decl) && (getJSDocReturnType(decl) || decl.parameters.some(p => !!getJSDocType(p))); - if (isFunctionWithJSDoc || jsdocType) { + if (hasUsableJSDoc(findAncestor(node, isDeclarationWithType))) { return [{ name: annotateTypeFromJSDoc.name, description: annotateTypeFromJSDoc.description, actions: [ { - description: annotateTypeFromJSDoc.description, - name: actionName - } + description: annotateTypeFromJSDoc.description, + name: actionName + } ] }]; } } + function hasUsableJSDoc(decl: DeclarationWithType): boolean { + if (!decl) { + return false; + } + if (isFunctionLikeDeclaration(decl)) { + return decl.parameters.some(hasUsableJSDoc) || (!decl.type && !!getJSDocReturnType(decl)); + } + return !decl.type && !!getJSDocType(decl); + } + function getEditsForAction(context: RefactorContext, action: string): RefactorEditInfo | undefined { if (actionName !== action) { return Debug.fail(`actionName !== action: ${actionName} !== ${action}`); @@ -169,7 +173,9 @@ namespace ts.refactor.annotateWithTypeFromJSDoc { case SyntaxKind.TypeReference: return transformJSDocTypeReference(node as TypeReferenceNode); default: - return visitEachChild(node, transformJSDocType, /*context*/ undefined) as TypeNode; + const visited = visitEachChild(node, transformJSDocType, /*context*/ undefined) as TypeNode; + setEmitFlags(visited, EmitFlags.SingleLine); + return visited; } } @@ -202,6 +208,9 @@ namespace ts.refactor.annotateWithTypeFromJSDoc { let name = node.typeName; let args = node.typeArguments; if (isIdentifier(node.typeName)) { + if (isJSDocIndexSignature(node)) { + return transformJSDocIndexSignature(node); + } let text = node.typeName.text; switch (node.typeName.text) { case "String": @@ -226,4 +235,26 @@ namespace ts.refactor.annotateWithTypeFromJSDoc { } return createTypeReferenceNode(name, args); } + + function transformJSDocIndexSignature(node: TypeReferenceNode) { + const index = createParameter( + /*decorators*/ undefined, + /*modifiers*/ undefined, + /*dotDotDotToken*/ undefined, + node.typeArguments[0].kind === SyntaxKind.NumberKeyword ? "n" : "s", + /*questionToken*/ undefined, + createTypeReferenceNode(node.typeArguments[0].kind === SyntaxKind.NumberKeyword ? "number" : "string", []), + /*initializer*/ undefined); + const indexSignature = createTypeLiteralNode([createIndexSignature(/*decorators*/ undefined, /*modifiers*/ undefined, [index], node.typeArguments[1])]); + setEmitFlags(indexSignature, EmitFlags.SingleLine); + return indexSignature; + } + + function isJSDocIndexSignature(node: TypeReferenceNode | ExpressionWithTypeArguments) { + return isTypeReferenceNode(node) && + isIdentifier(node.typeName) && + node.typeName.escapedText === "Object" && + node.typeArguments && node.typeArguments.length === 2 && + (node.typeArguments[0].kind === SyntaxKind.StringKeyword || node.typeArguments[0].kind === SyntaxKind.NumberKeyword); + } } From f82dd7b1dab62d6c3352032ad8c512bd47074bcc Mon Sep 17 00:00:00 2001 From: Nathan Shively-Sanders Date: Wed, 18 Oct 2017 13:16:22 -0700 Subject: [PATCH 30/51] Move isJSDocIndexSignature to utilities --- src/compiler/checker.ts | 8 -------- src/compiler/utilities.ts | 8 ++++++++ src/services/refactors/annotateWithTypeFromJSDoc.ts | 8 -------- 3 files changed, 8 insertions(+), 16 deletions(-) diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts index 992c609929f94..5c4db4b945195 100644 --- a/src/compiler/checker.ts +++ b/src/compiler/checker.ts @@ -7064,14 +7064,6 @@ namespace ts { } } - function isJSDocIndexSignature(node: TypeReferenceNode | ExpressionWithTypeArguments) { - return isTypeReferenceNode(node) && - isIdentifier(node.typeName) && - node.typeName.escapedText === "Object" && - node.typeArguments && node.typeArguments.length === 2 && - (node.typeArguments[0].kind === SyntaxKind.StringKeyword || node.typeArguments[0].kind === SyntaxKind.NumberKeyword); - } - function getTypeFromJSDocNullableTypeNode(node: JSDocNullableType) { const type = getTypeFromTypeNode(node.type); return strictNullChecks ? getUnionType([type, nullType]) : type; diff --git a/src/compiler/utilities.ts b/src/compiler/utilities.ts index f907cce85b1f3..b1173fe81cdb3 100644 --- a/src/compiler/utilities.ts +++ b/src/compiler/utilities.ts @@ -1360,6 +1360,14 @@ namespace ts { return node && !!(node.flags & NodeFlags.JSDoc); } + export function isJSDocIndexSignature(node: TypeReferenceNode | ExpressionWithTypeArguments) { + return isTypeReferenceNode(node) && + isIdentifier(node.typeName) && + node.typeName.escapedText === "Object" && + node.typeArguments && node.typeArguments.length === 2 && + (node.typeArguments[0].kind === SyntaxKind.StringKeyword || node.typeArguments[0].kind === SyntaxKind.NumberKeyword); + } + /** * Returns true if the node is a CallExpression to the identifier 'require' with * exactly one argument (of the form 'require("name")'). diff --git a/src/services/refactors/annotateWithTypeFromJSDoc.ts b/src/services/refactors/annotateWithTypeFromJSDoc.ts index 98131d0623cb4..d3bf59638b2be 100644 --- a/src/services/refactors/annotateWithTypeFromJSDoc.ts +++ b/src/services/refactors/annotateWithTypeFromJSDoc.ts @@ -249,12 +249,4 @@ namespace ts.refactor.annotateWithTypeFromJSDoc { setEmitFlags(indexSignature, EmitFlags.SingleLine); return indexSignature; } - - function isJSDocIndexSignature(node: TypeReferenceNode | ExpressionWithTypeArguments) { - return isTypeReferenceNode(node) && - isIdentifier(node.typeName) && - node.typeName.escapedText === "Object" && - node.typeArguments && node.typeArguments.length === 2 && - (node.typeArguments[0].kind === SyntaxKind.StringKeyword || node.typeArguments[0].kind === SyntaxKind.NumberKeyword); - } } From d3f954e0cc9ae75a3364e9518c19f0a53c7f3dae Mon Sep 17 00:00:00 2001 From: Sheetal Nandi Date: Tue, 17 Oct 2017 23:02:22 -0700 Subject: [PATCH 31/51] Add failing testcase where when d.ts file is in program, the files get emitted multiple times with --out setting --- src/harness/unittests/tscWatchMode.ts | 40 +++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/src/harness/unittests/tscWatchMode.ts b/src/harness/unittests/tscWatchMode.ts index 2ee86d4255867..fefbffa224927 100644 --- a/src/harness/unittests/tscWatchMode.ts +++ b/src/harness/unittests/tscWatchMode.ts @@ -1105,6 +1105,46 @@ namespace ts.tscWatch { const outJs = "/a/out.js"; createWatchForOut(/*out*/ undefined, outJs); }); + + it("with --outFile and multiple declaration files in the program", () => { + const file1: FileOrFolder = { + path: "/a/b/output/AnotherDependency/file1.d.ts", + content: "declare namespace Common.SomeComponent.DynamicMenu { enum Z { Full = 0, Min = 1, Average = 2, } }" + }; + const file2: FileOrFolder = { + path: "/a/b/dependencies/file2.d.ts", + content: "declare namespace Dependencies.SomeComponent { export class SomeClass { version: string; } }" + }; + const file3: FileOrFolder = { + path: "/a/b/project/src/main.ts", + content: "namespace Main { export function fooBar() {} }" + }; + const file4: FileOrFolder = { + path: "/a/b/project/src/main2.ts", + content: "namespace main.file4 { import DynamicMenu = Common.SomeComponent.DynamicMenu; export function foo(a: DynamicMenu.z) { } }" + }; + const configFile: FileOrFolder = { + path: "/a/b/project/tsconfig.json", + content: JSON.stringify({ + compilerOptions: { + outFile: "../output/common.js", + target: "es5" + }, + files: [file1.path, file2.path, file3.path, file4.path] + }) + }; + const files = [file1, file2, file3, file4]; + const allfiles = files.concat(configFile); + const host = createWatchedSystem(allfiles); + const originalWriteFile = host.writeFile.bind(host); + let numberOfTimesFileWritten = 0; + host.writeFile = (p: string, content: string) => { + numberOfTimesFileWritten++; + return originalWriteFile(p, content); + }; + createWatchModeWithConfigFile(configFile.path, host); + assert.equal(numberOfTimesFileWritten, 1); + }); }); describe("tsc-watch emit for configured projects", () => { From f9c901ada7148cbaf697c4b8bdd8905ba2cc219f Mon Sep 17 00:00:00 2001 From: Sheetal Nandi Date: Tue, 17 Oct 2017 21:18:27 -0700 Subject: [PATCH 32/51] Use get files affected by internally and hence use file paths as input --- src/compiler/builder.ts | 136 +++++++++++++++++----------------------- src/server/project.ts | 9 ++- 2 files changed, 66 insertions(+), 79 deletions(-) diff --git a/src/compiler/builder.ts b/src/compiler/builder.ts index 192f1e430278e..93b265f12ca60 100644 --- a/src/compiler/builder.ts +++ b/src/compiler/builder.ts @@ -47,11 +47,11 @@ namespace ts { /* @internal */ namespace ts { export interface Builder { - /** - * Call this to feed new program - */ + /** Called to inform builder about new program */ updateProgram(newProgram: Program): void; - getFilesAffectedBy(program: Program, path: Path): string[]; + /** Gets the files affected by the file path */ + getFilesAffectedBy(program: Program, path: Path): ReadonlyArray; + /** Emits the given file */ emitFile(program: Program, path: Path): EmitOutput; /** Emit the changed files and clear the cache of the changed files */ @@ -88,11 +88,10 @@ namespace ts { /** * Gets the files affected by the script info which has updated shape from the known one */ - getFilesAffectedByUpdatedShape(program: Program, sourceFile: SourceFile, singleFileResult: string[]): string[]; + getFilesAffectedByUpdatedShape(program: Program, sourceFile: SourceFile): ReadonlyArray; } interface FileInfo { - fileName: string; version: string; signature: string; } @@ -109,7 +108,7 @@ namespace ts { const fileInfos = createMap(); const semanticDiagnosticsPerFile = createMap>(); /** The map has key by source file's path that has been changed */ - const changedFileNames = createMap(); + const changedFileNames = createMap(); let emitHandler: EmitHandler; return { updateProgram, @@ -142,31 +141,31 @@ namespace ts { ); } - function registerChangedFile(path: Path, fileName: string) { - changedFileNames.set(path, fileName); + function registerChangedFile(path: Path) { + changedFileNames.set(path, true); // All changed files need to re-evaluate its semantic diagnostics semanticDiagnosticsPerFile.delete(path); } function addNewFileInfo(program: Program, sourceFile: SourceFile): FileInfo { - registerChangedFile(sourceFile.path, sourceFile.fileName); + registerChangedFile(sourceFile.path); emitHandler.onAddSourceFile(program, sourceFile); - return { fileName: sourceFile.fileName, version: sourceFile.version, signature: undefined }; + return { version: sourceFile.version, signature: undefined }; } - function removeExistingFileInfo(existingFileInfo: FileInfo, path: Path) { - registerChangedFile(path, existingFileInfo.fileName); + function removeExistingFileInfo(_existingFileInfo: FileInfo, path: Path) { + registerChangedFile(path); emitHandler.onRemoveSourceFile(path); } function updateExistingFileInfo(program: Program, existingInfo: FileInfo, sourceFile: SourceFile) { if (existingInfo.version !== sourceFile.version) { - registerChangedFile(sourceFile.path, sourceFile.fileName); + registerChangedFile(sourceFile.path); existingInfo.version = sourceFile.version; emitHandler.onUpdateSourceFile(program, sourceFile); } else if (emitHandler.onUpdateSourceFileWithSameVersion(program, sourceFile)) { - registerChangedFile(sourceFile.path, sourceFile.fileName); + registerChangedFile(sourceFile.path); } } @@ -182,23 +181,23 @@ namespace ts { } } - function getFilesAffectedBy(program: Program, path: Path): string[] { + function getFilesAffectedBy(program: Program, path: Path): ReadonlyArray { ensureProgramGraph(program); - const sourceFile = program.getSourceFile(path); - const singleFileResult = sourceFile && options.shouldEmitFile(sourceFile) ? [sourceFile.fileName] : []; + const sourceFile = program.getSourceFileByPath(path); const info = fileInfos.get(path); if (!info || !updateShapeSignature(program, sourceFile, info)) { - return singleFileResult; + return sourceFile && [sourceFile] || emptyArray; } Debug.assert(!!sourceFile); - return emitHandler.getFilesAffectedByUpdatedShape(program, sourceFile, singleFileResult); + return emitHandler.getFilesAffectedByUpdatedShape(program, sourceFile); } function emitFile(program: Program, path: Path) { ensureProgramGraph(program); - if (!fileInfos.has(path)) { + const sourceFile = program.getSourceFileByPath(path); + if (!fileInfos.has(path) || options.shouldEmitFile(sourceFile)) { return { outputFiles: [], emitSkipped: true }; } @@ -207,14 +206,12 @@ namespace ts { function enumerateChangedFilesSet( program: Program, - onChangedFile: (fileName: string, path: Path) => void, - onAffectedFile: (fileName: string, sourceFile: SourceFile) => void + onAffectedFile: (sourceFile: SourceFile) => void ) { - changedFileNames.forEach((fileName, path) => { - onChangedFile(fileName, path as Path); + changedFileNames.forEach((_true, path) => { const affectedFiles = getFilesAffectedBy(program, path as Path); for (const file of affectedFiles) { - onAffectedFile(file, program.getSourceFile(file)); + onAffectedFile(file); } }); } @@ -222,27 +219,25 @@ namespace ts { function enumerateChangedFilesEmitOutput( program: Program, emitOnlyDtsFiles: boolean, - onChangedFile: (fileName: string, path: Path) => void, - onEmitOutput: (emitOutput: EmitOutputDetailed, sourceFile: SourceFile) => void + onEmitOutput: (emitOutput: EmitOutputDetailed) => void ) { const seenFiles = createMap(); - enumerateChangedFilesSet(program, onChangedFile, (fileName, sourceFile) => { - if (!seenFiles.has(fileName)) { - seenFiles.set(fileName, true); - if (sourceFile) { - // Any affected file shouldnt have the cached diagnostics - semanticDiagnosticsPerFile.delete(sourceFile.path); - - const emitOutput = options.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, true); - } + enumerateChangedFilesSet(program, sourceFile => { + if (!seenFiles.has(sourceFile.path)) { + seenFiles.set(sourceFile.path, true); + // Any affected file shouldnt have the cached diagnostics + semanticDiagnosticsPerFile.delete(sourceFile.path); + + const emitOutput = options.getEmitOutput(program, sourceFile, emitOnlyDtsFiles, /*isDetailed*/ true) as EmitOutputDetailed; + + // mark all the emitted source files as seen + if (emitOutput.emittedSourceFiles) { + for (const file of emitOutput.emittedSourceFiles) { + seenFiles.set(file.path, true); } } + + onEmitOutput(emitOutput); } }); } @@ -250,8 +245,7 @@ namespace ts { function emitChangedFiles(program: Program): EmitOutputDetailed[] { ensureProgramGraph(program); const result: EmitOutputDetailed[] = []; - enumerateChangedFilesEmitOutput(program, /*emitOnlyDtsFiles*/ false, - /*onChangedFile*/ noop, emitOutput => result.push(emitOutput)); + enumerateChangedFilesEmitOutput(program, /*emitOnlyDtsFiles*/ false, emitOutput => result.push(emitOutput)); changedFileNames.clear(); return result; } @@ -260,11 +254,7 @@ namespace ts { ensureProgramGraph(program); // Ensure that changed files have cleared their respective - enumerateChangedFilesSet(program, /*onChangedFile*/ noop, (_affectedFileName, sourceFile) => { - if (sourceFile) { - semanticDiagnosticsPerFile.delete(sourceFile.path); - } - }); + enumerateChangedFilesSet(program, sourceFile => semanticDiagnosticsPerFile.delete(sourceFile.path)); let diagnostics: Diagnostic[]; for (const sourceFile of program.getSourceFiles()) { @@ -386,24 +376,20 @@ namespace ts { } /** - * Gets all the emittable files from the program. - * @param firstSourceFile This one will be emitted first. See https://github.com/Microsoft/TypeScript/issues/16888 + * Gets all files of the program excluding the default library file */ - function getAllEmittableFiles(program: Program, firstSourceFile: SourceFile): string[] { - const defaultLibraryFileName = getDefaultLibFileName(program.getCompilerOptions()); - const sourceFiles = program.getSourceFiles(); - const result: string[] = []; - add(firstSourceFile); - for (const sourceFile of sourceFiles) { + function getAllFilesExcludingDefaultLibraryFile(program: Program, firstSourceFile: SourceFile): ReadonlyArray { + let result: SourceFile[]; + for (const sourceFile of program.getSourceFiles()) { if (sourceFile !== firstSourceFile) { - add(sourceFile); + addSourceFile(sourceFile); } } - return result; + return result || emptyArray; - function add(sourceFile: SourceFile): void { - if (getBaseFileName(sourceFile.fileName) !== defaultLibraryFileName && options.shouldEmitFile(sourceFile)) { - result.push(sourceFile.fileName); + function addSourceFile(sourceFile: SourceFile) { + if (!program.isSourceFileDefaultLibrary(sourceFile)) { + (result || (result = [])).push(sourceFile); } } } @@ -417,14 +403,14 @@ namespace ts { getFilesAffectedByUpdatedShape }; - function getFilesAffectedByUpdatedShape(program: Program, sourceFile: SourceFile, singleFileResult: string[]): string[] { + function getFilesAffectedByUpdatedShape(program: Program, sourceFile: SourceFile): ReadonlyArray { 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 [sourceFile]; } - return getAllEmittableFiles(program, sourceFile); + return getAllFilesExcludingDefaultLibraryFile(program, sourceFile); } } @@ -481,7 +467,7 @@ namespace ts { // add files referencing the removedFilePath, as changed files too const referencedByInfo = fileInfos.get(filePath); if (referencedByInfo) { - registerChangedFile(filePath as Path, referencedByInfo.fileName); + registerChangedFile(filePath as Path); } } }); @@ -495,37 +481,33 @@ namespace ts { ); } - function getFilesAffectedByUpdatedShape(program: Program, sourceFile: SourceFile, singleFileResult: string[]): string[] { + function getFilesAffectedByUpdatedShape(program: Program, sourceFile: SourceFile): ReadonlyArray { if (!isExternalModule(sourceFile) && !containsOnlyAmbientModules(sourceFile)) { - return getAllEmittableFiles(program, sourceFile); + return getAllFilesExcludingDefaultLibraryFile(program, sourceFile); } const compilerOptions = program.getCompilerOptions(); if (compilerOptions && (compilerOptions.isolatedModules || compilerOptions.out || compilerOptions.outFile)) { - return singleFileResult; + return [sourceFile]; } // 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 seenFileNamesMap = createMap(); - const setSeenFileName = (path: Path, sourceFile: SourceFile) => { - seenFileNamesMap.set(path, sourceFile && options.shouldEmitFile(sourceFile) ? sourceFile.fileName : undefined); - }; + const seenFileNamesMap = createMap(); // Start with the paths this file was referenced by const path = sourceFile.path; - setSeenFileName(path, sourceFile); + seenFileNamesMap.set(path, sourceFile); const queue = getReferencedByPaths(path); while (queue.length > 0) { const currentPath = queue.pop(); if (!seenFileNamesMap.has(currentPath)) { const currentSourceFile = program.getSourceFileByPath(currentPath); + seenFileNamesMap.set(currentPath, currentSourceFile); if (currentSourceFile && updateShapeSignature(program, currentSourceFile, fileInfos.get(currentPath))) { queue.push(...getReferencedByPaths(currentPath)); } - setSeenFileName(currentPath, currentSourceFile); } } diff --git a/src/server/project.ts b/src/server/project.ts index 725ce725c3881..5e6e76db986a1 100644 --- a/src/server/project.ts +++ b/src/server/project.ts @@ -448,18 +448,23 @@ namespace ts.server { computeHash: data => this.projectService.host.createHash(data), shouldEmitFile: sourceFile => - !this.projectService.getScriptInfoForPath(sourceFile.path).isDynamicOrHasMixedContent() + !this.shouldEmitFile(sourceFile) }); } } + private shouldEmitFile(sourceFile: SourceFile) { + return !this.projectService.getScriptInfoForPath(sourceFile.path).isDynamicOrHasMixedContent(); + } + getCompileOnSaveAffectedFileList(scriptInfo: ScriptInfo): string[] { if (!this.languageServiceEnabled) { return []; } this.updateGraph(); this.ensureBuilder(); - return this.builder.getFilesAffectedBy(this.program, scriptInfo.path); + return mapDefined(this.builder.getFilesAffectedBy(this.program, scriptInfo.path), + sourceFile => this.shouldEmitFile(sourceFile) ? sourceFile.fileName : undefined); } /** From 8fbfb5ffc0dd930d95a4ed04317a70da16b43c6c Mon Sep 17 00:00:00 2001 From: Sheetal Nandi Date: Tue, 3 Oct 2017 15:06:56 -0700 Subject: [PATCH 33/51] Modify api to emit affected files using callback instead of generating in memory output Also marking few apis introduced during watch improvements changes that are suppose to be internal for now --- src/compiler/builder.ts | 191 ++++++++---------- src/compiler/watch.ts | 38 ++-- src/compiler/watchUtilities.ts | 4 +- src/harness/unittests/builder.ts | 16 +- src/harness/unittests/tscWatchMode.ts | 34 +++- src/server/project.ts | 26 +-- src/services/services.ts | 4 +- src/services/types.ts | 1 - .../reference/api/tsserverlibrary.d.ts | 26 +-- tests/baselines/reference/api/typescript.d.ts | 7 - 10 files changed, 141 insertions(+), 206 deletions(-) diff --git a/src/compiler/builder.ts b/src/compiler/builder.ts index 93b265f12ca60..cd678b6f361fd 100644 --- a/src/compiler/builder.ts +++ b/src/compiler/builder.ts @@ -6,56 +6,36 @@ 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 function getFileEmitOutput(program: Program, sourceFile: SourceFile, emitOnlyDtsFiles: boolean, isDetailed: boolean, - cancellationToken?: CancellationToken, customTransformers?: CustomTransformers): EmitOutput | EmitOutputDetailed { +/* @internal */ +namespace ts { + export function getFileEmitOutput(program: Program, sourceFile: SourceFile, emitOnlyDtsFiles: boolean, + cancellationToken?: CancellationToken, customTransformers?: CustomTransformers): EmitOutput { 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[]) { + function writeFile(fileName: string, text: string, writeByteOrderMark: boolean) { outputFiles.push({ name: fileName, writeByteOrderMark, text }); - if (isDetailed) { - emittedSourceFiles = addRange(emittedSourceFiles, sourceFiles); - } } } -} -/* @internal */ -namespace ts { export interface Builder { /** Called to inform builder about new program */ updateProgram(newProgram: Program): void; + /** Gets the files affected by the file path */ getFilesAffectedBy(program: Program, path: Path): ReadonlyArray; - /** Emits the given file */ - emitFile(program: Program, path: Path): EmitOutput; /** Emit the changed files and clear the cache of the changed files */ - emitChangedFiles(program: Program): EmitOutputDetailed[]; + emitChangedFiles(program: Program, writeFileCallback: WriteFileCallback): ReadonlyArray; + /** When called gets the semantic diagnostics for the program. It also caches the diagnostics and manage them */ getSemanticDiagnostics(program: Program, cancellationToken?: CancellationToken): Diagnostic[]; @@ -98,9 +78,7 @@ namespace ts { export interface BuilderOptions { getCanonicalFileName: (fileName: string) => string; - getEmitOutput: (program: Program, sourceFile: SourceFile, emitOnlyDtsFiles: boolean, isDetailed: boolean) => EmitOutput | EmitOutputDetailed; computeHash: (data: string) => string; - shouldEmitFile: (sourceFile: SourceFile) => boolean; } export function createBuilder(options: BuilderOptions): Builder { @@ -108,12 +86,13 @@ namespace ts { const fileInfos = createMap(); const semanticDiagnosticsPerFile = createMap>(); /** The map has key by source file's path that has been changed */ - const changedFileNames = createMap(); + const changedFilesSet = createMap(); + const hasShapeChanged = createMap(); + let allFilesExcludingDefaultLibraryFile: ReadonlyArray | undefined; let emitHandler: EmitHandler; return { updateProgram, getFilesAffectedBy, - emitFile, emitChangedFiles, getSemanticDiagnostics, clear @@ -127,6 +106,8 @@ namespace ts { fileInfos.clear(); semanticDiagnosticsPerFile.clear(); } + hasShapeChanged.clear(); + allFilesExcludingDefaultLibraryFile = undefined; mutateMap( fileInfos, arrayToMap(program.getSourceFiles(), sourceFile => sourceFile.path), @@ -142,7 +123,7 @@ namespace ts { } function registerChangedFile(path: Path) { - changedFileNames.set(path, true); + changedFilesSet.set(path, true); // All changed files need to re-evaluate its semantic diagnostics semanticDiagnosticsPerFile.delete(path); } @@ -185,101 +166,79 @@ namespace ts { ensureProgramGraph(program); const sourceFile = program.getSourceFileByPath(path); - const info = fileInfos.get(path); - if (!info || !updateShapeSignature(program, sourceFile, info)) { - return sourceFile && [sourceFile] || emptyArray; + if (!sourceFile) { + return emptyArray; } - Debug.assert(!!sourceFile); + if (!updateShapeSignature(program, sourceFile)) { + return [sourceFile]; + } return emitHandler.getFilesAffectedByUpdatedShape(program, sourceFile); } - function emitFile(program: Program, path: Path) { + function emitChangedFiles(program: Program, writeFileCallback: WriteFileCallback): ReadonlyArray { ensureProgramGraph(program); - const sourceFile = program.getSourceFileByPath(path); - if (!fileInfos.has(path) || options.shouldEmitFile(sourceFile)) { - return { outputFiles: [], emitSkipped: true }; - } - - return options.getEmitOutput(program, program.getSourceFileByPath(path), /*emitOnlyDtsFiles*/ false, /*isDetailed*/ false); - } - - function enumerateChangedFilesSet( - program: Program, - onAffectedFile: (sourceFile: SourceFile) => void - ) { - changedFileNames.forEach((_true, path) => { - const affectedFiles = getFilesAffectedBy(program, path as Path); - for (const file of affectedFiles) { - onAffectedFile(file); - } - }); - } - - function enumerateChangedFilesEmitOutput( - program: Program, - emitOnlyDtsFiles: boolean, - onEmitOutput: (emitOutput: EmitOutputDetailed) => void - ) { + const compilerOptions = program.getCompilerOptions(); + let result: EmitResult[] | undefined; const seenFiles = createMap(); - enumerateChangedFilesSet(program, sourceFile => { - if (!seenFiles.has(sourceFile.path)) { - seenFiles.set(sourceFile.path, true); - // Any affected file shouldnt have the cached diagnostics - semanticDiagnosticsPerFile.delete(sourceFile.path); - - const emitOutput = options.getEmitOutput(program, sourceFile, emitOnlyDtsFiles, /*isDetailed*/ true) as EmitOutputDetailed; - - // mark all the emitted source files as seen - if (emitOutput.emittedSourceFiles) { - for (const file of emitOutput.emittedSourceFiles) { - seenFiles.set(file.path, true); + changedFilesSet.forEach((_true, path) => { + const affectedFiles = getFilesAffectedBy(program, path as Path); + affectedFiles.forEach(affectedFile => { + // Affected files shouldnt have cached diagnostics + semanticDiagnosticsPerFile.delete(affectedFile.path); + + if (!seenFiles.has(affectedFile.path)) { + seenFiles.set(affectedFile.path, true); + + // With --out or --outFile all outputs go into single file, do it only once + if (compilerOptions.outFile || compilerOptions.out) { + if (!result) { + result = [program.emit(affectedFile, writeFileCallback)]; + } + } + else { + // Emit the affected file + (result || (result = [])).push(program.emit(affectedFile, writeFileCallback)); } } - - onEmitOutput(emitOutput); - } + }); }); - } - - function emitChangedFiles(program: Program): EmitOutputDetailed[] { - ensureProgramGraph(program); - const result: EmitOutputDetailed[] = []; - enumerateChangedFilesEmitOutput(program, /*emitOnlyDtsFiles*/ false, emitOutput => result.push(emitOutput)); - changedFileNames.clear(); - return result; + changedFilesSet.clear(); + return result || emptyArray; } function getSemanticDiagnostics(program: Program, cancellationToken?: CancellationToken): Diagnostic[] { ensureProgramGraph(program); - - // Ensure that changed files have cleared their respective - enumerateChangedFilesSet(program, sourceFile => semanticDiagnosticsPerFile.delete(sourceFile.path)); + Debug.assert(changedFilesSet.size === 0); 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 = addRange(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 = addRange(diagnostics, cachedDiagnostics); - } + diagnostics = addRange(diagnostics, getSemanticDiagnosticsOfFile(program, sourceFile, cancellationToken)); } return diagnostics || emptyArray; } + function getSemanticDiagnosticsOfFile(program: Program, sourceFile: SourceFile, cancellationToken?: CancellationToken): ReadonlyArray { + 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) { + cachedDiagnostics; + } + + // Diagnostics werent cached, get them from program, and cache the result + const diagnostics = program.getSemanticDiagnostics(sourceFile, cancellationToken); + semanticDiagnosticsPerFile.set(path, diagnostics); + return diagnostics; + } + function clear() { isModuleEmit = undefined; emitHandler = undefined; fileInfos.clear(); semanticDiagnosticsPerFile.clear(); - changedFileNames.clear(); + changedFilesSet.clear(); + hasShapeChanged.clear(); } /** @@ -300,7 +259,18 @@ namespace ts { /** * @return {boolean} indicates if the shape signature has changed since last update. */ - function updateShapeSignature(program: Program, sourceFile: SourceFile, info: FileInfo) { + function updateShapeSignature(program: Program, sourceFile: SourceFile) { + Debug.assert(!!sourceFile); + + // If we have cached the result for this file, that means hence forth we should assume file shape is uptodate + if (hasShapeChanged.has(sourceFile.path)) { + return false; + } + + hasShapeChanged.set(sourceFile.path, true); + const info = fileInfos.get(sourceFile.path); + Debug.assert(!!info); + const prevSignature = info.signature; let latestSignature: string; if (sourceFile.isDeclarationFile) { @@ -308,7 +278,7 @@ namespace ts { info.signature = latestSignature; } else { - const emitOutput = options.getEmitOutput(program, sourceFile, /*emitOnlyDtsFiles*/ true, /*isDetailed*/ false); + const emitOutput = getFileEmitOutput(program, sourceFile, /*emitOnlyDtsFiles*/ true); if (emitOutput.outputFiles && emitOutput.outputFiles.length > 0) { latestSignature = options.computeHash(emitOutput.outputFiles[0].text); info.signature = latestSignature; @@ -379,13 +349,20 @@ namespace ts { * Gets all files of the program excluding the default library file */ function getAllFilesExcludingDefaultLibraryFile(program: Program, firstSourceFile: SourceFile): ReadonlyArray { + // Use cached result + if (allFilesExcludingDefaultLibraryFile) { + return allFilesExcludingDefaultLibraryFile; + } + let result: SourceFile[]; + addSourceFile(firstSourceFile); for (const sourceFile of program.getSourceFiles()) { if (sourceFile !== firstSourceFile) { addSourceFile(sourceFile); } } - return result || emptyArray; + allFilesExcludingDefaultLibraryFile = result || emptyArray; + return allFilesExcludingDefaultLibraryFile; function addSourceFile(sourceFile: SourceFile) { if (!program.isSourceFileDefaultLibrary(sourceFile)) { @@ -505,7 +482,7 @@ namespace ts { if (!seenFileNamesMap.has(currentPath)) { const currentSourceFile = program.getSourceFileByPath(currentPath); seenFileNamesMap.set(currentPath, currentSourceFile); - if (currentSourceFile && updateShapeSignature(program, currentSourceFile, fileInfos.get(currentPath))) { + if (currentSourceFile && updateShapeSignature(program, currentSourceFile)) { queue.push(...getReferencedByPaths(currentPath)); } } diff --git a/src/compiler/watch.ts b/src/compiler/watch.ts index 6f61469197b21..53692fe1e0488 100644 --- a/src/compiler/watch.ts +++ b/src/compiler/watch.ts @@ -144,13 +144,14 @@ namespace ts { function compileWatchedProgram(host: DirectoryStructureHost, program: Program, builder: Builder) { // First get and report any syntactic errors. - let diagnostics = program.getSyntacticDiagnostics().slice(); + const diagnostics = program.getSyntacticDiagnostics().slice(); 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()); + addRange(diagnostics, program.getOptionsDiagnostics()); + addRange(diagnostics, program.getGlobalDiagnostics()); if (diagnostics.length === 0) { reportSemanticDiagnostics = true; @@ -162,7 +163,7 @@ namespace ts { let sourceMaps: SourceMapData[]; let emitSkipped: boolean; - const result = builder.emitChangedFiles(program); + const result = builder.emitChangedFiles(program, writeFile); if (result.length === 0) { emitSkipped = true; } @@ -171,14 +172,13 @@ namespace ts { if (emitOutput.emitSkipped) { emitSkipped = true; } - diagnostics = concatenate(diagnostics, emitOutput.diagnostics); + addRange(diagnostics, emitOutput.diagnostics); sourceMaps = concatenate(sourceMaps, emitOutput.sourceMaps); - writeOutputFiles(emitOutput.outputFiles); } } if (reportSemanticDiagnostics) { - diagnostics = diagnostics.concat(builder.getSemanticDiagnostics(program)); + addRange(diagnostics, builder.getSemanticDiagnostics(program)); } return handleEmitOutputAndReportErrors(host, program, emittedFiles, emitSkipped, diagnostics, reportDiagnostic); @@ -191,31 +191,23 @@ namespace ts { } } - function writeFile(fileName: string, data: string, writeByteOrderMark: boolean) { + function writeFile(fileName: string, text: string, writeByteOrderMark: boolean, onError: (message: string) => void) { try { performance.mark("beforeIOWrite"); ensureDirectoriesExist(getDirectoryPath(normalizePath(fileName))); - host.writeFile(fileName, data, writeByteOrderMark); + host.writeFile(fileName, text, writeByteOrderMark); performance.mark("afterIOWrite"); performance.measure("I/O Write", "beforeIOWrite", "afterIOWrite"); + + if (emittedFiles) { + emittedFiles.push(fileName); + } } 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); - } + if (onError) { + onError(e.message); } } } @@ -308,7 +300,7 @@ namespace ts { getCurrentDirectory() ); // There is no extra check needed since we can just rely on the program to decide emit - const builder = createBuilder({ getCanonicalFileName, getEmitOutput: getFileEmitOutput, computeHash, shouldEmitFile: () => true }); + const builder = createBuilder({ getCanonicalFileName, computeHash }); synchronizeProgram(); diff --git a/src/compiler/watchUtilities.ts b/src/compiler/watchUtilities.ts index 26bf689401a8c..f39024d5a7cfd 100644 --- a/src/compiler/watchUtilities.ts +++ b/src/compiler/watchUtilities.ts @@ -1,5 +1,6 @@ /// +/* @internal */ namespace ts { /** * Updates the existing missing file watches with the new set of missing files after new program is created @@ -72,10 +73,7 @@ namespace ts { existingWatchedForWildcards.set(directory, createWildcardDirectoryWatcher(directory, flags)); } } -} -/* @internal */ -namespace ts { export function addFileWatcher(host: System, file: string, cb: FileWatcherCallback): FileWatcher { return host.watchFile(file, cb); } diff --git a/src/harness/unittests/builder.ts b/src/harness/unittests/builder.ts index edd471e9ebe9e..bfdeb1a40678a 100644 --- a/src/harness/unittests/builder.ts +++ b/src/harness/unittests/builder.ts @@ -46,15 +46,14 @@ namespace ts { function makeAssertChanges(getProgram: () => Program): (fileNames: ReadonlyArray) => void { const builder = createBuilder({ getCanonicalFileName: identity, - getEmitOutput: getFileEmitOutput, - computeHash: identity, - shouldEmitFile: returnTrue, + computeHash: identity }); return fileNames => { const program = getProgram(); builder.updateProgram(program); - const changedFiles = builder.emitChangedFiles(program); - assert.deepEqual(changedFileNames(changedFiles), fileNames); + const outputFileNames: string[] = []; + builder.emitChangedFiles(program, fileName => outputFileNames.push(fileName)); + assert.deepEqual(outputFileNames, fileNames); }; } @@ -63,11 +62,4 @@ namespace ts { updateProgramText(files, fileName, fileContent); }); } - - function changedFileNames(changedFiles: ReadonlyArray): string[] { - return changedFiles.map(f => { - assert.lengthOf(f.outputFiles, 1); - return f.outputFiles[0].name; - }); - } } diff --git a/src/harness/unittests/tscWatchMode.ts b/src/harness/unittests/tscWatchMode.ts index fefbffa224927..308021f0b0a84 100644 --- a/src/harness/unittests/tscWatchMode.ts +++ b/src/harness/unittests/tscWatchMode.ts @@ -1106,7 +1106,7 @@ namespace ts.tscWatch { createWatchForOut(/*out*/ undefined, outJs); }); - it("with --outFile and multiple declaration files in the program", () => { + function verifyFilesEmittedOnce(useOutFile: boolean) { const file1: FileOrFolder = { path: "/a/b/output/AnotherDependency/file1.d.ts", content: "declare namespace Common.SomeComponent.DynamicMenu { enum Z { Full = 0, Min = 1, Average = 2, } }" @@ -1126,10 +1126,9 @@ namespace ts.tscWatch { const configFile: FileOrFolder = { path: "/a/b/project/tsconfig.json", content: JSON.stringify({ - compilerOptions: { - outFile: "../output/common.js", - target: "es5" - }, + compilerOptions: useOutFile ? + { outFile: "../output/common.js", target: "es5" } : + { outDir: "../output", target: "es5" }, files: [file1.path, file2.path, file3.path, file4.path] }) }; @@ -1137,13 +1136,32 @@ namespace ts.tscWatch { const allfiles = files.concat(configFile); const host = createWatchedSystem(allfiles); const originalWriteFile = host.writeFile.bind(host); - let numberOfTimesFileWritten = 0; + const mapOfFilesWritten = createMap(); host.writeFile = (p: string, content: string) => { - numberOfTimesFileWritten++; + const count = mapOfFilesWritten.get(p); + mapOfFilesWritten.set(p, count ? count + 1 : 1); return originalWriteFile(p, content); }; createWatchModeWithConfigFile(configFile.path, host); - assert.equal(numberOfTimesFileWritten, 1); + if (useOutFile) { + // Only out file + assert.equal(mapOfFilesWritten.size, 1); + } + else { + // main.js and main2.js + assert.equal(mapOfFilesWritten.size, 2); + } + mapOfFilesWritten.forEach((value, key) => { + assert.equal(value, 1, "Key: " + key); + }); + } + + it("with --outFile and multiple declaration files in the program", () => { + verifyFilesEmittedOnce(/*useOutFile*/ true); + }); + + it("without --outFile and multiple declaration files in the program", () => { + verifyFilesEmittedOnce(/*useOutFile*/ false); }); }); diff --git a/src/server/project.ts b/src/server/project.ts index 5e6e76db986a1..3cadfd6822499 100644 --- a/src/server/project.ts +++ b/src/server/project.ts @@ -443,18 +443,13 @@ namespace ts.server { if (!this.builder) { this.builder = createBuilder({ getCanonicalFileName: this.projectService.toCanonicalFileName, - getEmitOutput: (_program, sourceFile, emitOnlyDts, isDetailed) => - this.getFileEmitOutput(sourceFile, emitOnlyDts, isDetailed), - computeHash: data => - this.projectService.host.createHash(data), - shouldEmitFile: sourceFile => - !this.shouldEmitFile(sourceFile) + computeHash: data => this.projectService.host.createHash(data) }); } } - private shouldEmitFile(sourceFile: SourceFile) { - return !this.projectService.getScriptInfoForPath(sourceFile.path).isDynamicOrHasMixedContent(); + private shouldEmitFile(scriptInfo: ScriptInfo) { + return scriptInfo && !scriptInfo.isDynamicOrHasMixedContent(); } getCompileOnSaveAffectedFileList(scriptInfo: ScriptInfo): string[] { @@ -464,15 +459,17 @@ namespace ts.server { this.updateGraph(); this.ensureBuilder(); return mapDefined(this.builder.getFilesAffectedBy(this.program, scriptInfo.path), - sourceFile => this.shouldEmitFile(sourceFile) ? sourceFile.fileName : undefined); + sourceFile => this.shouldEmitFile(this.projectService.getScriptInfoForPath(sourceFile.path)) ? sourceFile.fileName : undefined); } /** * 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 (!this.languageServiceEnabled || !this.shouldEmitFile(scriptInfo)) { + return false; + } + const { emitSkipped, outputFiles } = this.getLanguageService(/*ensureSynchronized*/ false).getEmitOutput(scriptInfo.fileName); if (!emitSkipped) { for (const outputFile of outputFiles) { const outputFileAbsoluteFileName = getNormalizedAbsolutePath(outputFile.name, this.currentDirectory); @@ -598,13 +595,6 @@ namespace ts.server { }); } - private getFileEmitOutput(sourceFile: SourceFile, emitOnlyDtsFiles: boolean, isDetailed: boolean) { - if (!this.languageServiceEnabled) { - return undefined; - } - return this.getLanguageService(/*ensureSynchronized*/ false).getEmitOutput(sourceFile.fileName, emitOnlyDtsFiles, isDetailed); - } - getExcludedFiles(): ReadonlyArray { return emptyArray; } diff --git a/src/services/services.ts b/src/services/services.ts index 114692ebba2e5..2923acf404cef 100644 --- a/src/services/services.ts +++ b/src/services/services.ts @@ -1511,12 +1511,12 @@ namespace ts { return ts.NavigateTo.getNavigateToItems(sourceFiles, program.getTypeChecker(), cancellationToken, searchValue, maxResultCount, excludeDtsFiles); } - function getEmitOutput(fileName: string, emitOnlyDtsFiles?: boolean, isDetailed?: boolean) { + function getEmitOutput(fileName: string, emitOnlyDtsFiles?: boolean) { synchronizeHostData(); const sourceFile = getValidSourceFile(fileName); const customTransformers = host.getCustomTransformers && host.getCustomTransformers(); - return getFileEmitOutput(program, sourceFile, emitOnlyDtsFiles, isDetailed, cancellationToken, customTransformers); + return getFileEmitOutput(program, sourceFile, emitOnlyDtsFiles, cancellationToken, customTransformers); } // Signature help diff --git a/src/services/types.ts b/src/services/types.ts index 7ec559be92283..f9d725c2fa3e3 100644 --- a/src/services/types.ts +++ b/src/services/types.ts @@ -287,7 +287,6 @@ namespace ts { 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; diff --git a/tests/baselines/reference/api/tsserverlibrary.d.ts b/tests/baselines/reference/api/tsserverlibrary.d.ts index 52132ae832f25..5f3d90e5ad0e5 100644 --- a/tests/baselines/reference/api/tsserverlibrary.d.ts +++ b/tests/baselines/reference/api/tsserverlibrary.d.ts @@ -3731,17 +3731,11 @@ declare namespace ts { outputFiles: OutputFile[]; emitSkipped: boolean; } - interface EmitOutputDetailed extends EmitOutput { - diagnostics: Diagnostic[]; - sourceMaps: SourceMapData[]; - emittedSourceFiles: SourceFile[]; - } interface OutputFile { name: string; writeByteOrderMark: boolean; text: string; } - function getFileEmitOutput(program: Program, sourceFile: SourceFile, emitOnlyDtsFiles: boolean, isDetailed: boolean, cancellationToken?: CancellationToken, customTransformers?: CustomTransformers): EmitOutput | EmitOutputDetailed; } declare namespace ts { function findConfigFile(searchPath: string, fileExists: (fileName: string) => boolean, configName?: string): string; @@ -3953,7 +3947,6 @@ declare 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; dispose(): void; } @@ -7043,23 +7036,6 @@ declare namespace ts.server { isJavaScript(): boolean; } } -declare namespace ts { - /** - * Updates the existing missing file watches with the new set of missing files after new program is created - */ - function updateMissingFilePathsWatch(program: Program, missingFileWatches: Map, createMissingFileWatch: (missingFilePath: Path) => FileWatcher): void; - 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 - */ - function updateWatchingWildcardDirectories(existingWatchedForWildcards: Map, wildcardDirectories: Map, watchDirectory: (directory: string, flags: WatchDirectoryFlags) => FileWatcher): void; -} declare namespace ts.server { interface InstallPackageOptionsWithProjectRootPath extends InstallPackageOptions { projectRootPath: Path; @@ -7204,6 +7180,7 @@ declare namespace ts.server { getAllProjectErrors(): ReadonlyArray; getLanguageService(ensureSynchronized?: boolean): LanguageService; private ensureBuilder(); + private shouldEmitFile(scriptInfo); getCompileOnSaveAffectedFileList(scriptInfo: ScriptInfo): string[]; /** * Returns true if emit was conducted @@ -7222,7 +7199,6 @@ declare namespace ts.server { getRootFiles(): NormalizedPath[]; getRootScriptInfos(): ScriptInfo[]; getScriptInfos(): ScriptInfo[]; - private getFileEmitOutput(sourceFile, emitOnlyDtsFiles, isDetailed); getExcludedFiles(): ReadonlyArray; getFileNames(excludeFilesFromExternalLibraries?: boolean, excludeConfigFiles?: boolean): NormalizedPath[]; hasConfigFile(configFilePath: NormalizedPath): boolean; diff --git a/tests/baselines/reference/api/typescript.d.ts b/tests/baselines/reference/api/typescript.d.ts index 976d50942ae1b..d14def7d79156 100644 --- a/tests/baselines/reference/api/typescript.d.ts +++ b/tests/baselines/reference/api/typescript.d.ts @@ -3678,17 +3678,11 @@ declare namespace ts { outputFiles: OutputFile[]; emitSkipped: boolean; } - interface EmitOutputDetailed extends EmitOutput { - diagnostics: Diagnostic[]; - sourceMaps: SourceMapData[]; - emittedSourceFiles: SourceFile[]; - } interface OutputFile { name: string; writeByteOrderMark: boolean; text: string; } - function getFileEmitOutput(program: Program, sourceFile: SourceFile, emitOnlyDtsFiles: boolean, isDetailed: boolean, cancellationToken?: CancellationToken, customTransformers?: CustomTransformers): EmitOutput | EmitOutputDetailed; } declare namespace ts { function findConfigFile(searchPath: string, fileExists: (fileName: string) => boolean, configName?: string): string; @@ -3953,7 +3947,6 @@ declare 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; dispose(): void; } From 835d7cb910f3acd44b72b87b6606e5e08b386dce Mon Sep 17 00:00:00 2001 From: Sheetal Nandi Date: Wed, 18 Oct 2017 12:50:36 -0700 Subject: [PATCH 34/51] Simplify emit changed files further Also use source file version as the signature of declaration file instead of computing it from text --- src/compiler/builder.ts | 34 +++++++++++++++++++++------------- 1 file changed, 21 insertions(+), 13 deletions(-) diff --git a/src/compiler/builder.ts b/src/compiler/builder.ts index cd678b6f361fd..1b110704e7540 100644 --- a/src/compiler/builder.ts +++ b/src/compiler/builder.ts @@ -135,7 +135,10 @@ namespace ts { } function removeExistingFileInfo(_existingFileInfo: FileInfo, path: Path) { - registerChangedFile(path); + // Since we dont need to track removed file as changed file + // We can just remove its diagnostics + changedFilesSet.delete(path); + semanticDiagnosticsPerFile.delete(path); emitHandler.onRemoveSourceFile(path); } @@ -179,9 +182,22 @@ namespace ts { function emitChangedFiles(program: Program, writeFileCallback: WriteFileCallback): ReadonlyArray { ensureProgramGraph(program); const compilerOptions = program.getCompilerOptions(); - let result: EmitResult[] | undefined; + + if (!changedFilesSet.size) { + return emptyArray; + } + + // With --out or --outFile all outputs go into single file, do it only once + if (compilerOptions.outFile || compilerOptions.out) { + semanticDiagnosticsPerFile.clear(); + changedFilesSet.clear(); + return [program.emit(/*targetSourceFile*/ undefined, writeFileCallback)]; + } + const seenFiles = createMap(); + let result: EmitResult[] | undefined; changedFilesSet.forEach((_true, path) => { + // Get the affected Files by this program const affectedFiles = getFilesAffectedBy(program, path as Path); affectedFiles.forEach(affectedFile => { // Affected files shouldnt have cached diagnostics @@ -190,16 +206,8 @@ namespace ts { if (!seenFiles.has(affectedFile.path)) { seenFiles.set(affectedFile.path, true); - // With --out or --outFile all outputs go into single file, do it only once - if (compilerOptions.outFile || compilerOptions.out) { - if (!result) { - result = [program.emit(affectedFile, writeFileCallback)]; - } - } - else { - // Emit the affected file - (result || (result = [])).push(program.emit(affectedFile, writeFileCallback)); - } + // Emit the affected file + (result || (result = [])).push(program.emit(affectedFile, writeFileCallback)); } }); }); @@ -274,7 +282,7 @@ namespace ts { const prevSignature = info.signature; let latestSignature: string; if (sourceFile.isDeclarationFile) { - latestSignature = options.computeHash(sourceFile.text); + latestSignature = sourceFile.version; info.signature = latestSignature; } else { From 7e780c0a1a4cc881dac93f0eddead090aef719d7 Mon Sep 17 00:00:00 2001 From: Sheetal Nandi Date: Wed, 18 Oct 2017 13:43:20 -0700 Subject: [PATCH 35/51] Do not cache the semantic diagnostics when compiler options has --out since we would anyways get all fresh diagnostics --- src/compiler/builder.ts | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/src/compiler/builder.ts b/src/compiler/builder.ts index 1b110704e7540..49a5bfc8d974a 100644 --- a/src/compiler/builder.ts +++ b/src/compiler/builder.ts @@ -37,7 +37,7 @@ namespace ts { emitChangedFiles(program: Program, writeFileCallback: WriteFileCallback): ReadonlyArray; /** When called gets the semantic diagnostics for the program. It also caches the diagnostics and manage them */ - getSemanticDiagnostics(program: Program, cancellationToken?: CancellationToken): Diagnostic[]; + getSemanticDiagnostics(program: Program, cancellationToken?: CancellationToken): ReadonlyArray; /** Called to reset the status of the builder */ clear(): void; @@ -189,7 +189,7 @@ namespace ts { // With --out or --outFile all outputs go into single file, do it only once if (compilerOptions.outFile || compilerOptions.out) { - semanticDiagnosticsPerFile.clear(); + Debug.assert(semanticDiagnosticsPerFile.size === 0); changedFilesSet.clear(); return [program.emit(/*targetSourceFile*/ undefined, writeFileCallback)]; } @@ -215,10 +215,17 @@ namespace ts { return result || emptyArray; } - function getSemanticDiagnostics(program: Program, cancellationToken?: CancellationToken): Diagnostic[] { + function getSemanticDiagnostics(program: Program, cancellationToken?: CancellationToken): ReadonlyArray { ensureProgramGraph(program); Debug.assert(changedFilesSet.size === 0); + const compilerOptions = program.getCompilerOptions(); + if (compilerOptions.outFile || compilerOptions.out) { + Debug.assert(semanticDiagnosticsPerFile.size === 0); + // We dont need to cache the diagnostics just return them from program + return program.getSemanticDiagnostics(/*sourceFile*/ undefined, cancellationToken); + } + let diagnostics: Diagnostic[]; for (const sourceFile of program.getSourceFiles()) { diagnostics = addRange(diagnostics, getSemanticDiagnosticsOfFile(program, sourceFile, cancellationToken)); From 75a687b524f75c2ab5bec44271408c14f86386e9 Mon Sep 17 00:00:00 2001 From: Wesley Wigham Date: Wed, 18 Oct 2017 14:57:18 -0700 Subject: [PATCH 36/51] make getCurrentDirectory required (#19303) --- src/harness/unittests/extractTestHelpers.ts | 1 + src/services/types.ts | 1 + tests/baselines/reference/api/tsserverlibrary.d.ts | 1 + tests/baselines/reference/api/typescript.d.ts | 1 + 4 files changed, 4 insertions(+) diff --git a/src/harness/unittests/extractTestHelpers.ts b/src/harness/unittests/extractTestHelpers.ts index 49c2c1d327704..fb2a94c361a0b 100644 --- a/src/harness/unittests/extractTestHelpers.ts +++ b/src/harness/unittests/extractTestHelpers.ts @@ -103,6 +103,7 @@ namespace ts { getScriptVersion: notImplemented, getScriptSnapshot: notImplemented, getDefaultLibFileName: notImplemented, + getCurrentDirectory: notImplemented, }; export function testExtractSymbol(caption: string, text: string, baselineFolder: string, description: DiagnosticMessage) { diff --git a/src/services/types.ts b/src/services/types.ts index 7ec559be92283..31c05a45dc0d8 100644 --- a/src/services/types.ts +++ b/src/services/types.ts @@ -163,6 +163,7 @@ namespace ts { getScriptSnapshot(fileName: string): IScriptSnapshot | undefined; getLocalizedDiagnosticMessages?(): any; getCancellationToken?(): HostCancellationToken; + getCurrentDirectory(): string; getDefaultLibFileName(options: CompilerOptions): string; log?(s: string): void; trace?(s: string): void; diff --git a/tests/baselines/reference/api/tsserverlibrary.d.ts b/tests/baselines/reference/api/tsserverlibrary.d.ts index ab39d0a6d6ccc..72c3b665617ff 100644 --- a/tests/baselines/reference/api/tsserverlibrary.d.ts +++ b/tests/baselines/reference/api/tsserverlibrary.d.ts @@ -3884,6 +3884,7 @@ declare namespace ts { getScriptSnapshot(fileName: string): IScriptSnapshot | undefined; getLocalizedDiagnosticMessages?(): any; getCancellationToken?(): HostCancellationToken; + getCurrentDirectory(): string; getDefaultLibFileName(options: CompilerOptions): string; log?(s: string): void; trace?(s: string): void; diff --git a/tests/baselines/reference/api/typescript.d.ts b/tests/baselines/reference/api/typescript.d.ts index 976d50942ae1b..374a9f4120289 100644 --- a/tests/baselines/reference/api/typescript.d.ts +++ b/tests/baselines/reference/api/typescript.d.ts @@ -3884,6 +3884,7 @@ declare namespace ts { getScriptSnapshot(fileName: string): IScriptSnapshot | undefined; getLocalizedDiagnosticMessages?(): any; getCancellationToken?(): HostCancellationToken; + getCurrentDirectory(): string; getDefaultLibFileName(options: CompilerOptions): string; log?(s: string): void; trace?(s: string): void; From 7d23ed152b6d00224b06129c6985edf44fdc458d Mon Sep 17 00:00:00 2001 From: csigs Date: Wed, 18 Oct 2017 22:10:31 +0000 Subject: [PATCH 37/51] LEGO: check in for master to temporary branch. --- .../diagnosticMessages.generated.json.lcl | 35 +++++++++++++++++- .../diagnosticMessages.generated.json.lcl | 37 ++++++++++++++++++- .../diagnosticMessages.generated.json.lcl | 37 ++++++++++++++++++- 3 files changed, 104 insertions(+), 5 deletions(-) diff --git a/src/loc/lcl/csy/diagnosticMessages/diagnosticMessages.generated.json.lcl b/src/loc/lcl/csy/diagnosticMessages/diagnosticMessages.generated.json.lcl index d27b3a500b880..7f0200d6662dd 100644 --- a/src/loc/lcl/csy/diagnosticMessages/diagnosticMessages.generated.json.lcl +++ b/src/loc/lcl/csy/diagnosticMessages/diagnosticMessages.generated.json.lcl @@ -804,6 +804,9 @@ + + + @@ -1347,12 +1350,18 @@ + + + + + + @@ -1506,6 +1515,9 @@ + + + @@ -1917,7 +1929,7 @@ - + @@ -2040,6 +2052,9 @@ + + + @@ -3828,12 +3843,18 @@ + + + + + + @@ -4032,12 +4053,18 @@ + + + + + + @@ -7302,6 +7329,9 @@ + + + @@ -7788,6 +7818,9 @@ + + + diff --git a/src/loc/lcl/ita/diagnosticMessages/diagnosticMessages.generated.json.lcl b/src/loc/lcl/ita/diagnosticMessages/diagnosticMessages.generated.json.lcl index 727bf964abf21..51208134ad73e 100644 --- a/src/loc/lcl/ita/diagnosticMessages/diagnosticMessages.generated.json.lcl +++ b/src/loc/lcl/ita/diagnosticMessages/diagnosticMessages.generated.json.lcl @@ -795,6 +795,9 @@ + + + @@ -1338,12 +1341,18 @@ + + + + + + @@ -1497,6 +1506,9 @@ + + + @@ -1908,8 +1920,8 @@ - - + + @@ -2031,6 +2043,9 @@ + + + @@ -3819,12 +3834,18 @@ + + + + + + @@ -4023,12 +4044,18 @@ + + + + + + @@ -7293,6 +7320,9 @@ + + + @@ -7779,6 +7809,9 @@ + + + diff --git a/src/loc/lcl/rus/diagnosticMessages/diagnosticMessages.generated.json.lcl b/src/loc/lcl/rus/diagnosticMessages/diagnosticMessages.generated.json.lcl index b0e6c4b3495f0..7c2b517078cf7 100644 --- a/src/loc/lcl/rus/diagnosticMessages/diagnosticMessages.generated.json.lcl +++ b/src/loc/lcl/rus/diagnosticMessages/diagnosticMessages.generated.json.lcl @@ -794,6 +794,9 @@ + + + @@ -1337,12 +1340,18 @@ + + + + + + @@ -1496,6 +1505,9 @@ + + + @@ -1907,8 +1919,8 @@ - - + + @@ -2030,6 +2042,9 @@ + + + @@ -3818,12 +3833,18 @@ + + + + + + @@ -4022,12 +4043,18 @@ + + + + + + @@ -7292,6 +7319,9 @@ + + + @@ -7778,6 +7808,9 @@ + + + From 9bea0dbdc2ed6ab6ccd8ca005ca422eb28f58924 Mon Sep 17 00:00:00 2001 From: Sheetal Nandi Date: Wed, 18 Oct 2017 15:28:20 -0700 Subject: [PATCH 38/51] Actually use cached semantic diagnostics --- src/compiler/builder.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/compiler/builder.ts b/src/compiler/builder.ts index 49a5bfc8d974a..635855083c08d 100644 --- a/src/compiler/builder.ts +++ b/src/compiler/builder.ts @@ -238,7 +238,7 @@ namespace ts { const cachedDiagnostics = semanticDiagnosticsPerFile.get(path); // Report the semantic diagnostics from the cache if we already have those diagnostics present if (cachedDiagnostics) { - cachedDiagnostics; + return cachedDiagnostics; } // Diagnostics werent cached, get them from program, and cache the result From 93c2b10f6861b52a96444375a5025d9c81eb8209 Mon Sep 17 00:00:00 2001 From: Nathan Shively-Sanders Date: Wed, 18 Oct 2017 15:29:11 -0700 Subject: [PATCH 39/51] Fix tsc-instrumented 1. Make recursiveCreateDirectory correctly handle relative paths. 2. Remove dependency on Harness 3. Correctly increment iocapture0, iocapture1, ... iocaptureN. 4. Stop double-nesting baseline files. --- src/compiler/sys.ts | 3 +-- src/harness/loggedIO.ts | 21 +++++++++++++++------ 2 files changed, 16 insertions(+), 8 deletions(-) diff --git a/src/compiler/sys.ts b/src/compiler/sys.ts index fa3ec22f586cd..c68292078cdcc 100644 --- a/src/compiler/sys.ts +++ b/src/compiler/sys.ts @@ -570,10 +570,9 @@ namespace ts { realpath }; } - function recursiveCreateDirectory(directoryPath: string, sys: System) { const basePath = getDirectoryPath(directoryPath); - const shouldCreateParent = directoryPath !== basePath && !sys.directoryExists(basePath); + const shouldCreateParent = basePath !== "" && directoryPath !== basePath && !sys.directoryExists(basePath); if (shouldCreateParent) { recursiveCreateDirectory(basePath, sys); } diff --git a/src/harness/loggedIO.ts b/src/harness/loggedIO.ts index 3123024f41947..ecdcc1a740d98 100644 --- a/src/harness/loggedIO.ts +++ b/src/harness/loggedIO.ts @@ -157,11 +157,20 @@ namespace Playback { return log; } + const canonicalizeForHarness = ts.createGetCanonicalFileName(/*caseSensitive*/ false); // This is done so tests work on windows _and_ linux + function sanitizeTestFilePath(name: string) { + const path = ts.toPath(ts.normalizeSlashes(name.replace(/[\^<>:"|?*%]/g, "_")).replace(/\.\.\//g, "__dotdot/"), "", canonicalizeForHarness); + if (ts.startsWith(path, "/")) { + return path.substring(1); + } + return path; + } + export function oldStyleLogIntoNewStyleLog(log: IOLog, writeFile: typeof Harness.IO.writeFile, baseTestName: string) { if (log.filesAppended) { for (const file of log.filesAppended) { if (file.contents !== undefined) { - file.contentsPath = ts.combinePaths("appended", Harness.Compiler.sanitizeTestFilePath(file.path)); + file.contentsPath = ts.combinePaths("appended", sanitizeTestFilePath(file.path)); writeFile(ts.combinePaths(baseTestName, file.contentsPath), file.contents); delete file.contents; } @@ -170,7 +179,7 @@ namespace Playback { if (log.filesWritten) { for (const file of log.filesWritten) { if (file.contents !== undefined) { - file.contentsPath = ts.combinePaths("written", Harness.Compiler.sanitizeTestFilePath(file.path)); + file.contentsPath = ts.combinePaths("written", sanitizeTestFilePath(file.path)); writeFile(ts.combinePaths(baseTestName, file.contentsPath), file.contents); delete file.contents; } @@ -180,7 +189,7 @@ namespace Playback { for (const file of log.filesRead) { const { contents } = file.result; if (contents !== undefined) { - file.result.contentsPath = ts.combinePaths("read", Harness.Compiler.sanitizeTestFilePath(file.path)); + file.result.contentsPath = ts.combinePaths("read", sanitizeTestFilePath(file.path)); writeFile(ts.combinePaths(baseTestName, file.result.contentsPath), contents); const len = contents.length; if (len >= 2 && contents.charCodeAt(0) === 0xfeff) { @@ -235,8 +244,8 @@ namespace Playback { if (recordLog !== undefined) { let i = 0; const fn = () => recordLogFileNameBase + i; - while (underlying.fileExists(fn() + ".json")) i++; - underlying.writeFile(ts.combinePaths(fn(), "test.json"), JSON.stringify(oldStyleLogIntoNewStyleLog(recordLog, (path, string) => underlying.writeFile(ts.combinePaths(fn(), path), string), fn()), null, 4)); // tslint:disable-line:no-null-keyword + while (underlying.fileExists(ts.combinePaths(fn(), "test.json"))) i++; + underlying.writeFile(ts.combinePaths(fn(), "test.json"), JSON.stringify(oldStyleLogIntoNewStyleLog(recordLog, (path, string) => underlying.writeFile(path, string), fn()), null, 4)); // tslint:disable-line:no-null-keyword recordLog = undefined; } }; @@ -415,4 +424,4 @@ namespace Playback { initWrapper(wrapper, underlying); return wrapper; } -} \ No newline at end of file +} From 79c672424e3183bb0b8e45c4992a92dd3f55c1fa Mon Sep 17 00:00:00 2001 From: Nathan Shively-Sanders Date: Wed, 18 Oct 2017 15:32:24 -0700 Subject: [PATCH 40/51] Fix lint --- src/compiler/sys.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/compiler/sys.ts b/src/compiler/sys.ts index c68292078cdcc..3ed80fbad391e 100644 --- a/src/compiler/sys.ts +++ b/src/compiler/sys.ts @@ -570,6 +570,7 @@ namespace ts { realpath }; } + function recursiveCreateDirectory(directoryPath: string, sys: System) { const basePath = getDirectoryPath(directoryPath); const shouldCreateParent = basePath !== "" && directoryPath !== basePath && !sys.directoryExists(basePath); From 5fa8db55b23271ae1c1ce764152e127d1b0cf622 Mon Sep 17 00:00:00 2001 From: Mohamed Hegazy Date: Wed, 18 Oct 2017 15:46:09 -0700 Subject: [PATCH 41/51] Fix https://github.com/Microsoft/TypeScript/issues/19270: ensure output name is a valid locale name (#19308) * Fix https://github.com/Microsoft/TypeScript/issues/19270: ensure output name is a valid locale name * Use const instead of var * Add comment * Fix typo * Split the concat logic for generatedLCGFile --- Gulpfile.ts | 21 ++++++++-- Jakefile.js | 21 ++++++++-- .../generateLocalizedDiagnosticMessages.ts | 42 ++++++++++++++++++- 3 files changed, 76 insertions(+), 8 deletions(-) diff --git a/Gulpfile.ts b/Gulpfile.ts index 24ff1f3f3c9fe..200692b6ca42f 100644 --- a/Gulpfile.ts +++ b/Gulpfile.ts @@ -180,6 +180,23 @@ const libraryTargets = librarySourceMap.map(function(f) { return path.join(builtLocalDirectory, f.target); }); +/** + * .lcg file is what localization team uses to know what messages to localize. + * The file is always generated in 'enu\diagnosticMessages.generated.json.lcg' + */ +const generatedLCGFile = path.join(builtLocalDirectory, "enu", "diagnosticMessages.generated.json.lcg"); + +/** + * The localization target produces the two following transformations: + * 1. 'src\loc\lcl\\diagnosticMessages.generated.json.lcl' => 'built\local\\diagnosticMessages.generated.json' + * convert localized resources into a .json file the compiler can understand + * 2. 'src\compiler\diagnosticMessages.generated.json' => 'built\local\ENU\diagnosticMessages.generated.json.lcg' + * generate the lcg file (source of messages to localize) from the diagnosticMessages.generated.json + */ +const localizationTargets = ["cs", "de", "es", "fr", "it", "ja", "ko", "pl", "pt-BR", "ru", "tr", "zh-CN", "zh-TW"].map(function (f) { + return path.join(builtLocalDirectory, f, "diagnosticMessages.generated.json"); +}).concat(generatedLCGFile); + for (const i in libraryTargets) { const entry = librarySourceMap[i]; const target = libraryTargets[i]; @@ -398,7 +415,6 @@ gulp.task(generateLocalizedDiagnosticMessagesJs, /*help*/ false, [], () => { }); // Localize diagnostics -const generatedLCGFile = path.join(builtLocalDirectory, "enu", "diagnosticMessages.generated.json.lcg"); gulp.task(generatedLCGFile, [generateLocalizedDiagnosticMessagesJs, diagnosticInfoMapTs], (done) => { if (fs.existsSync(builtLocalDirectory) && needsUpdate(generatedDiagnosticMessagesJSON, generatedLCGFile)) { exec(host, [generateLocalizedDiagnosticMessagesJs, lclDirectory, builtLocalDirectory, generatedDiagnosticMessagesJSON], done, done); @@ -576,8 +592,7 @@ gulp.task("dontUseDebugMode", /*help*/ false, [], (done) => { useDebugMode = fal gulp.task("VerifyLKG", /*help*/ false, [], () => { const expectedFiles = [builtLocalCompiler, servicesFile, serverFile, nodePackageFile, nodeDefinitionsFile, standaloneDefinitionsFile, tsserverLibraryFile, tsserverLibraryDefinitionFile, typingsInstallerJs, cancellationTokenJs].concat(libraryTargets); const missingFiles = expectedFiles. - concat(fs.readdirSync(lclDirectory).map(function (d) { return path.join(builtLocalDirectory, d, "diagnosticMessages.generated.json"); })). - concat(generatedLCGFile). + concat(localizationTargets). filter(f => !fs.existsSync(f)); if (missingFiles.length > 0) { throw new Error("Cannot replace the LKG unless all built targets are present in directory " + builtLocalDirectory + diff --git a/Jakefile.js b/Jakefile.js index ba6943d30153d..06f13262a2841 100644 --- a/Jakefile.js +++ b/Jakefile.js @@ -239,6 +239,23 @@ var libraryTargets = librarySourceMap.map(function (f) { return path.join(builtLocalDirectory, f.target); }); +/** + * .lcg file is what localization team uses to know what messages to localize. + * The file is always generated in 'enu\diagnosticMessages.generated.json.lcg' + */ +var generatedLCGFile = path.join(builtLocalDirectory, "enu", "diagnosticMessages.generated.json.lcg"); + +/** + * The localization target produces the two following transformations: + * 1. 'src\loc\lcl\\diagnosticMessages.generated.json.lcl' => 'built\local\\diagnosticMessages.generated.json' + * convert localized resources into a .json file the compiler can understand + * 2. 'src\compiler\diagnosticMessages.generated.json' => 'built\local\ENU\diagnosticMessages.generated.json.lcg' + * generate the lcg file (source of messages to localize) from the diagnosticMessages.generated.json + */ +var localizationTargets = ["cs", "de", "es", "fr", "it", "ja", "ko", "pl", "pt-BR", "ru", "tr", "zh-CN", "zh-TW"].map(function (f) { + return path.join(builtLocalDirectory, f); +}).concat(path.dirname(generatedLCGFile)); + // Prepends the contents of prefixFile to destinationFile function prependFile(prefixFile, destinationFile) { if (!fs.existsSync(prefixFile)) { @@ -444,7 +461,6 @@ compileFile(generateLocalizedDiagnosticMessagesJs, /*useBuiltCompiler*/ false, { noOutFile: true, types: ["node", "xml2js"] }); // Localize diagnostics -var generatedLCGFile = path.join(builtLocalDirectory, "enu", "diagnosticMessages.generated.json.lcg"); file(generatedLCGFile, [generateLocalizedDiagnosticMessagesJs, diagnosticInfoMapTs, generatedDiagnosticMessagesJSON], function () { var cmd = host + " " + generateLocalizedDiagnosticMessagesJs + " " + lclDirectory + " " + builtLocalDirectory + " " + generatedDiagnosticMessagesJSON; console.log(cmd); @@ -736,8 +752,7 @@ desc("Makes a new LKG out of the built js files"); task("LKG", ["clean", "release", "local"].concat(libraryTargets), function () { var expectedFiles = [tscFile, servicesFile, serverFile, nodePackageFile, nodeDefinitionsFile, standaloneDefinitionsFile, tsserverLibraryFile, tsserverLibraryDefinitionFile, cancellationTokenFile, typingsInstallerFile, buildProtocolDts, watchGuardFile]. concat(libraryTargets). - concat(fs.readdirSync(lclDirectory).map(function (d) { return path.join(builtLocalDirectory, d) })). - concat(path.dirname(generatedLCGFile)); + concat(localizationTargets); var missingFiles = expectedFiles.filter(function (f) { return !fs.existsSync(f); }); diff --git a/scripts/generateLocalizedDiagnosticMessages.ts b/scripts/generateLocalizedDiagnosticMessages.ts index edfe957515516..d7d5f5c0e7969 100644 --- a/scripts/generateLocalizedDiagnosticMessages.ts +++ b/scripts/generateLocalizedDiagnosticMessages.ts @@ -27,16 +27,54 @@ function main(): void { function visitDirectory(name: string) { const inputFilePath = path.join(inputPath, name, "diagnosticMessages", "diagnosticMessages.generated.json.lcl"); - const outputFilePath = path.join(outputPath, name, "diagnosticMessages.generated.json"); + fs.readFile(inputFilePath, (err, data) => { handleError(err); xml2js.parseString(data.toString(), (err, result) => { handleError(err); - writeFile(outputFilePath, xmlObjectToString(result)); + if (!result || !result.LCX || !result.LCX.$ || !result.LCX.$.TgtCul) { + console.error("Unexpected XML file structure. Expected to find result.LCX.$.TgtCul."); + process.exit(1); + } + const outputDirectoryName = getPreferedLocaleName(result.LCX.$.TgtCul); + if (!outputDirectoryName) { + console.error(`Invalid output locale name for '${result.LCX.$.TgtCul}'.`); + process.exit(1); + } + writeFile(path.join(outputPath, outputDirectoryName, "diagnosticMessages.generated.json"), xmlObjectToString(result)); }); }); } + /** + * A locale name is based on the language tagging conventions of RFC 4646 (Windows Vista + * and later), and is represented by LOCALE_SNAME. + * Generally, the pattern - is used. Here, language is a lowercase ISO 639 + * language code. The codes from ISO 639-1 are used when available. Otherwise, codes from + * ISO 639-2/T are used. REGION specifies an uppercase ISO 3166-1 country/region identifier. + * For example, the locale name for English (United States) is "en-US" and the locale name + * for Divehi (Maldives) is "dv-MV". + * + * If the locale is a neutral locale (no region), the LOCALE_SNAME value follows the + * pattern . If it is a neutral locale for which the script is significant, the + * pattern is -