From 4d7c112ef7f5cd18f5563fff8158cc9e6eef6a98 Mon Sep 17 00:00:00 2001 From: Sheetal Nandi Date: Wed, 11 Oct 2017 14:21:09 -0700 Subject: [PATCH] Make sure project root paths of inferred projects are canonical when comparing --- .../unittests/tsserverProjectSystem.ts | 111 +++++++++++++++++- src/server/editorServices.ts | 15 +-- src/server/project.ts | 6 +- .../reference/api/tsserverlibrary.d.ts | 3 +- 4 files changed, 124 insertions(+), 11 deletions(-) diff --git a/src/harness/unittests/tsserverProjectSystem.ts b/src/harness/unittests/tsserverProjectSystem.ts index f7d082128cd..b52a6968774 100644 --- a/src/harness/unittests/tsserverProjectSystem.ts +++ b/src/harness/unittests/tsserverProjectSystem.ts @@ -220,11 +220,11 @@ namespace ts.projectSystem { checkNumberOfProjects(this, count); } } - export function createProjectService(host: server.ServerHost, parameters: CreateProjectServiceParameters = {}) { + export function createProjectService(host: server.ServerHost, parameters: CreateProjectServiceParameters = {}, options?: Partial) { const cancellationToken = parameters.cancellationToken || server.nullCancellationToken; const logger = parameters.logger || nullLogger; const useSingleInferredProject = parameters.useSingleInferredProject !== undefined ? parameters.useSingleInferredProject : false; - return new TestProjectService(host, logger, cancellationToken, useSingleInferredProject, parameters.typingsInstaller, parameters.eventHandler); + return new TestProjectService(host, logger, cancellationToken, useSingleInferredProject, parameters.typingsInstaller, parameters.eventHandler, options); } export function checkNumberOfConfiguredProjects(projectService: server.ProjectService, expected: number) { @@ -3703,6 +3703,113 @@ namespace ts.projectSystem { assert.equal(projectService.inferredProjects[1].getCompilationSettings().target, ScriptTarget.ESNext); assert.equal(projectService.inferredProjects[2].getCompilationSettings().target, ScriptTarget.ES2015); }); + + function checkInferredProject(inferredProject: server.InferredProject, actualFiles: FileOrFolder[], target: ScriptTarget) { + checkProjectActualFiles(inferredProject, actualFiles.map(f => f.path)); + assert.equal(inferredProject.getCompilationSettings().target, target); + } + + function verifyProjectRootWithCaseSensitivity(useCaseSensitiveFileNames: boolean) { + const files: [FileOrFolder, FileOrFolder, FileOrFolder, FileOrFolder] = [ + { path: "/a/file1.ts", content: "let x = 1;" }, + { path: "/A/file2.ts", content: "let y = 2;" }, + { path: "/b/file2.ts", content: "let x = 3;" }, + { path: "/c/file3.ts", content: "let z = 4;" } + ]; + const host = createServerHost(files, { useCaseSensitiveFileNames }); + const projectService = createProjectService(host, { useSingleInferredProject: true, }, { useInferredProjectPerProjectRoot: true }); + projectService.setCompilerOptionsForInferredProjects({ + allowJs: true, + target: ScriptTarget.ESNext + }); + projectService.setCompilerOptionsForInferredProjects({ + allowJs: true, + target: ScriptTarget.ES2015 + }, "/a"); + + openClientFiles(["/a", "/a", "/b", undefined]); + verifyInferredProjectsState([ + [[files[3]], ScriptTarget.ESNext], + [[files[0], files[1]], ScriptTarget.ES2015], + [[files[2]], ScriptTarget.ESNext] + ]); + closeClientFiles(); + + openClientFiles(["/a", "/A", "/b", undefined]); + if (useCaseSensitiveFileNames) { + verifyInferredProjectsState([ + [[files[3]], ScriptTarget.ESNext], + [[files[0]], ScriptTarget.ES2015], + [[files[1]], ScriptTarget.ESNext], + [[files[2]], ScriptTarget.ESNext] + ]); + } + else { + verifyInferredProjectsState([ + [[files[3]], ScriptTarget.ESNext], + [[files[0], files[1]], ScriptTarget.ES2015], + [[files[2]], ScriptTarget.ESNext] + ]); + } + closeClientFiles(); + + projectService.setCompilerOptionsForInferredProjects({ + allowJs: true, + target: ScriptTarget.ES2017 + }, "/A"); + + openClientFiles(["/a", "/a", "/b", undefined]); + verifyInferredProjectsState([ + [[files[3]], ScriptTarget.ESNext], + [[files[0], files[1]], useCaseSensitiveFileNames ? ScriptTarget.ES2015 : ScriptTarget.ES2017], + [[files[2]], ScriptTarget.ESNext] + ]); + closeClientFiles(); + + openClientFiles(["/a", "/A", "/b", undefined]); + if (useCaseSensitiveFileNames) { + verifyInferredProjectsState([ + [[files[3]], ScriptTarget.ESNext], + [[files[0]], ScriptTarget.ES2015], + [[files[1]], ScriptTarget.ES2017], + [[files[2]], ScriptTarget.ESNext] + ]); + } + else { + verifyInferredProjectsState([ + [[files[3]], ScriptTarget.ESNext], + [[files[0], files[1]], ScriptTarget.ES2017], + [[files[2]], ScriptTarget.ESNext] + ]); + } + closeClientFiles(); + + function openClientFiles(projectRoots: [string | undefined, string | undefined, string | undefined, string | undefined]) { + files.forEach((file, index) => { + projectService.openClientFile(file.path, file.content, ScriptKind.JS, projectRoots[index]); + }); + } + + function closeClientFiles() { + files.forEach(file => projectService.closeClientFile(file.path)); + } + + function verifyInferredProjectsState(expected: [FileOrFolder[], ScriptTarget][]) { + checkNumberOfProjects(projectService, { inferredProjects: expected.length }); + projectService.inferredProjects.forEach((p, index) => { + const [actualFiles, target] = expected[index]; + checkInferredProject(p, actualFiles, target); + }); + } + } + + it("inferred projects per project root with case sensitive system", () => { + verifyProjectRootWithCaseSensitivity(/*useCaseSensitiveFileNames*/ true); + }); + + it("inferred projects per project root with case insensitive system", () => { + verifyProjectRootWithCaseSensitivity(/*useCaseSensitiveFileNames*/ false); + }); }); describe("No overwrite emit error", () => { diff --git a/src/server/editorServices.ts b/src/server/editorServices.ts index 379a5fbfe7e..98112b3f24e 100644 --- a/src/server/editorServices.ts +++ b/src/server/editorServices.ts @@ -590,9 +590,9 @@ namespace ts.server { // always set 'allowNonTsExtensions' for inferred projects since user cannot configure it from the outside // previously we did not expose a way for user to change these settings and this option was enabled by default compilerOptions.allowNonTsExtensions = true; - - if (projectRootPath) { - this.compilerOptionsForInferredProjectsPerProjectRoot.set(projectRootPath, compilerOptions); + const canonicalProjectRootPath = projectRootPath && this.toCanonicalFileName(projectRootPath); + if (canonicalProjectRootPath) { + this.compilerOptionsForInferredProjectsPerProjectRoot.set(canonicalProjectRootPath, compilerOptions); } else { this.compilerOptionsForInferredProjects = compilerOptions; @@ -608,9 +608,9 @@ namespace ts.server { // root path // - Inferred projects with a projectRootPath, if the new options apply to that // project root path. - if (projectRootPath ? - project.projectRootPath === projectRootPath : - !project.projectRootPath || !this.compilerOptionsForInferredProjectsPerProjectRoot.has(project.projectRootPath)) { + if (canonicalProjectRootPath ? + project.projectRootPath === canonicalProjectRootPath : + !project.projectRootPath || !this.compilerOptionsForInferredProjectsPerProjectRoot.has(project.projectRootPath)) { project.setCompilerOptions(compilerOptions); project.compileOnSaveEnabled = compilerOptions.compileOnSave; project.markAsDirty(); @@ -1596,9 +1596,10 @@ namespace ts.server { } if (projectRootPath) { + const canonicalProjectRootPath = this.toCanonicalFileName(projectRootPath); // if we have an explicit project root path, find (or create) the matching inferred project. for (const project of this.inferredProjects) { - if (project.projectRootPath === projectRootPath) { + if (project.projectRootPath === canonicalProjectRootPath) { return project; } } diff --git a/src/server/project.ts b/src/server/project.ts index 9c66f8b4a6c..bf060b66a27 100644 --- a/src/server/project.ts +++ b/src/server/project.ts @@ -1047,12 +1047,15 @@ namespace ts.server { super.setCompilerOptions(newOptions); } + /** this is canonical project root path */ + readonly projectRootPath: string | undefined; + /*@internal*/ constructor( projectService: ProjectService, documentRegistry: DocumentRegistry, compilerOptions: CompilerOptions, - readonly projectRootPath: string | undefined, + projectRootPath: string | undefined, currentDirectory: string | undefined) { super(InferredProject.newName(), ProjectKind.Inferred, @@ -1064,6 +1067,7 @@ namespace ts.server { /*compileOnSaveEnabled*/ false, projectService.host, currentDirectory); + this.projectRootPath = projectRootPath && projectService.toCanonicalFileName(projectRootPath); } addRoot(info: ScriptInfo) { diff --git a/tests/baselines/reference/api/tsserverlibrary.d.ts b/tests/baselines/reference/api/tsserverlibrary.d.ts index d862513c293..61320be7eb7 100644 --- a/tests/baselines/reference/api/tsserverlibrary.d.ts +++ b/tests/baselines/reference/api/tsserverlibrary.d.ts @@ -7186,11 +7186,12 @@ declare namespace ts.server { * the file and its imports/references are put into an InferredProject. */ class InferredProject extends Project { - readonly projectRootPath: string | undefined; private static readonly newName; private _isJsInferredProject; toggleJsInferredProject(isJsInferredProject: boolean): void; setCompilerOptions(options?: CompilerOptions): void; + /** this is canonical project root path*/ + readonly projectRootPath: string | undefined; addRoot(info: ScriptInfo): void; removeRoot(info: ScriptInfo): void; isProjectWithSingleRoot(): boolean;