From 403b7d8604087173e82946fff65dc32ea94443bd Mon Sep 17 00:00:00 2001 From: Sheetal Nandi Date: Thu, 22 Feb 2018 09:17:00 -0800 Subject: [PATCH] Add tests for module resolution order and reuse --- .../unittests/tsserverProjectSystem.ts | 343 +++++++++++++++++- 1 file changed, 341 insertions(+), 2 deletions(-) diff --git a/src/harness/unittests/tsserverProjectSystem.ts b/src/harness/unittests/tsserverProjectSystem.ts index e09dff2a6d5..02776b4de07 100644 --- a/src/harness/unittests/tsserverProjectSystem.ts +++ b/src/harness/unittests/tsserverProjectSystem.ts @@ -145,6 +145,12 @@ namespace ts.projectSystem { return map; } + function createHostModuleResolutionTrace(host: TestServerHost & ModuleResolutionHost) { + const resolutionTrace: string[] = []; + host.trace = resolutionTrace.push.bind(resolutionTrace); + return resolutionTrace; + } + export function toExternalFile(fileName: string): protocol.ExternalFile { return { fileName }; } @@ -3201,8 +3207,7 @@ namespace ts.projectSystem { content: "export let x = 1" }; const host: TestServerHost & ModuleResolutionHost = createServerHost([file1, lib]); - const resolutionTrace: string[] = []; - host.trace = resolutionTrace.push.bind(resolutionTrace); + const resolutionTrace = createHostModuleResolutionTrace(host); const projectService = createProjectService(host, { typingsInstaller: new TestTypingsInstaller("/a/cache", /*throttleLimit*/5, host) }); projectService.setCompilerOptionsForInferredProjects({ traceResolution: true, allowJs: true }); @@ -6971,4 +6976,338 @@ namespace ts.projectSystem { assert.deepEqual(diagnostics, []); }); }); + + describe("tsserverProjectSystem module resolution caching", () => { + const projectLocation = "/user/username/projects/myproject"; + const configFile: FileOrFolder = { + path: `${projectLocation}/tsconfig.json`, + content: JSON.stringify({ compilerOptions: { traceResolution: true } }) + }; + + function getModules(module1Path: string, module2Path: string) { + const module1: FileOrFolder = { + path: module1Path, + content: `export function module1() {}` + }; + const module2: FileOrFolder = { + path: module2Path, + content: `export function module2() {}` + }; + return { module1, module2 }; + } + + function verifyTrace(resolutionTrace: string[], expected: string[]) { + assert.deepEqual(resolutionTrace, expected); + resolutionTrace.length = 0; + } + + function getExpectedFileDoesNotExistResolutionTrace(host: TestServerHost, expectedTrace: string[], foundModule: boolean, module: FileOrFolder, directory: string, file: string, ignoreIfParentMissing?: boolean) { + if (!foundModule) { + const path = combinePaths(directory, file); + if (!ignoreIfParentMissing || host.directoryExists(getDirectoryPath(path))) { + if (module.path === path) { + foundModule = true; + } + else { + expectedTrace.push(`File '${path}' does not exist.`); + } + } + } + return foundModule; + } + + function getExpectedMissedLocationResolutionTrace(host: TestServerHost, expectedTrace: string[], dirPath: string, module: FileOrFolder, moduleName: string, useNodeModules: boolean) { + let foundModule = false; + forEachAncestorDirectory(dirPath, dirPath => { + const directory = useNodeModules ? combinePaths(dirPath, nodeModules) : dirPath; + if (useNodeModules && !foundModule && !host.directoryExists(directory)) { + expectedTrace.push(`Directory '${directory}' does not exist, skipping all lookups in it.`); + return undefined; + } + foundModule = getExpectedFileDoesNotExistResolutionTrace(host, expectedTrace, foundModule, module, directory, `${moduleName}/package.json`, /*ignoreIfParentMissing*/ true); + foundModule = getExpectedFileDoesNotExistResolutionTrace(host, expectedTrace, foundModule, module, directory, `${moduleName}.ts`); + foundModule = getExpectedFileDoesNotExistResolutionTrace(host, expectedTrace, foundModule, module, directory, `${moduleName}.tsx`); + foundModule = getExpectedFileDoesNotExistResolutionTrace(host, expectedTrace, foundModule, module, directory, `${moduleName}.d.ts`); + foundModule = getExpectedFileDoesNotExistResolutionTrace(host, expectedTrace, foundModule, module, directory, `${moduleName}/index.ts`, /*ignoreIfParentMissing*/ true); + if (useNodeModules && !foundModule) { + expectedTrace.push(`Directory '${directory}/@types' does not exist, skipping all lookups in it.`); + } + return foundModule ? true : undefined; + }); + } + + function getExpectedResolutionTraceHeader(expectedTrace: string[], file: FileOrFolder, moduleName: string) { + expectedTrace.push( + `======== Resolving module '${moduleName}' from '${file.path}'. ========`, + `Module resolution kind is not specified, using 'NodeJs'.` + ); + } + + function getExpectedResolutionTraceFooter(expectedTrace: string[], module: FileOrFolder, moduleName: string, addRealPathTrace: boolean) { + expectedTrace.push(`File '${module.path}' exist - use it as a name resolution result.`); + if (addRealPathTrace) { + expectedTrace.push(`Resolving real path for '${module.path}', result '${module.path}'.`); + } + expectedTrace.push(`======== Module name '${moduleName}' was successfully resolved to '${module.path}'. ========`); + } + + function getExpectedRelativeModuleResolutionTrace(host: TestServerHost, file: FileOrFolder, module: FileOrFolder, moduleName: string, expectedTrace: string[] = []) { + getExpectedResolutionTraceHeader(expectedTrace, file, moduleName); + expectedTrace.push(`Loading module as file / folder, candidate module location '${removeFileExtension(module.path)}', target file type 'TypeScript'.`); + getExpectedMissedLocationResolutionTrace(host, expectedTrace, getDirectoryPath(normalizePath(combinePaths(getDirectoryPath(file.path), moduleName))), module, moduleName.substring(moduleName.lastIndexOf("/") + 1), /*useNodeModules*/ false); + getExpectedResolutionTraceFooter(expectedTrace, module, moduleName, /*addRealPathTrace*/ false); + return expectedTrace; + } + + function getExpectedNonRelativeModuleResolutionTrace(host: TestServerHost, file: FileOrFolder, module: FileOrFolder, moduleName: string, expectedTrace: string[] = []) { + getExpectedResolutionTraceHeader(expectedTrace, file, moduleName); + expectedTrace.push(`Loading module '${moduleName}' from 'node_modules' folder, target file type 'TypeScript'.`); + getExpectedMissedLocationResolutionTrace(host, expectedTrace, getDirectoryPath(file.path), module, moduleName, /*useNodeModules*/ true); + getExpectedResolutionTraceFooter(expectedTrace, module, moduleName, /*addRealPathTrace*/ true); + return expectedTrace; + } + + function getExpectedReusingResolutionFromOldProgram(file: FileOrFolder, moduleName: string) { + return `Reusing resolution of module '${moduleName}' to file '${file.path}' from old program.`; + } + + function verifyWatchesWithConfigFile(host: TestServerHost, files: FileOrFolder[], openFile: FileOrFolder) { + checkWatchedFiles(host, mapDefined(files, f => f === openFile ? undefined : f.path)); + checkWatchedDirectories(host, [], /*recursive*/ false); + const configDirectory = getDirectoryPath(configFile.path); + checkWatchedDirectories(host, [configDirectory, `${configDirectory}/${nodeModulesAtTypes}`], /*recursive*/ true); + } + + describe("from files in same folder", () => { + function getFiles(fileContent: string) { + const file1: FileOrFolder = { + path: `${projectLocation}/src/file1.ts`, + content: fileContent + }; + const file2: FileOrFolder = { + path: `${projectLocation}/src/file2.ts`, + content: fileContent + }; + return { file1, file2 }; + } + + it("relative module name", () => { + const module1Name = "./module1"; + const module2Name = "../module2"; + const fileContent = `import { module1 } from "${module1Name}";import { module2 } from "${module2Name}";`; + const { file1, file2 } = getFiles(fileContent); + const { module1, module2 } = getModules(`${projectLocation}/src/module1.ts`, `${projectLocation}/module2.ts`); + const files = [module1, module2, file1, file2, configFile, libFile]; + const host = createServerHost(files); + const resolutionTrace = createHostModuleResolutionTrace(host); + const service = createProjectService(host); + service.openClientFile(file1.path); + const expectedTrace = getExpectedRelativeModuleResolutionTrace(host, file1, module1, module1Name); + getExpectedRelativeModuleResolutionTrace(host, file1, module2, module2Name, expectedTrace); + verifyTrace(resolutionTrace, expectedTrace); + verifyWatchesWithConfigFile(host, files, file1); + + file1.content += fileContent; + file2.content += fileContent; + host.reloadFS(files); + host.runQueuedTimeoutCallbacks(); + verifyTrace(resolutionTrace, [ + getExpectedReusingResolutionFromOldProgram(file1, module1Name), + getExpectedReusingResolutionFromOldProgram(file1, module2Name) + ]); + verifyWatchesWithConfigFile(host, files, file1); + }); + + it("non relative module name", () => { + const module1Name = "module1"; + const module2Name = "module2"; + const fileContent = `import { module1 } from "${module1Name}";import { module2 } from "${module2Name}";`; + const { file1, file2 } = getFiles(fileContent); + const { module1, module2 } = getModules(`${projectLocation}/src/node_modules/module1/index.ts`, `${projectLocation}/node_modules/module2/index.ts`); + const files = [module1, module2, file1, file2, configFile, libFile]; + const host = createServerHost(files); + const resolutionTrace = createHostModuleResolutionTrace(host); + const service = createProjectService(host); + service.openClientFile(file1.path); + const expectedTrace = getExpectedNonRelativeModuleResolutionTrace(host, file1, module1, module1Name); + getExpectedNonRelativeModuleResolutionTrace(host, file1, module2, module2Name, expectedTrace); + verifyTrace(resolutionTrace, expectedTrace); + verifyWatchesWithConfigFile(host, files, file1); + + file1.content += fileContent; + file2.content += fileContent; + host.reloadFS(files); + host.runQueuedTimeoutCallbacks(); + verifyTrace(resolutionTrace, [ + getExpectedReusingResolutionFromOldProgram(file1, module1Name), + getExpectedReusingResolutionFromOldProgram(file1, module2Name) + ]); + verifyWatchesWithConfigFile(host, files, file1); + }); + }); + + describe("from files in different folders", () => { + function getFiles(fileContent1: string, fileContent2 = fileContent1, fileContent3 = fileContent1, fileContent4 = fileContent1) { + const file1: FileOrFolder = { + path: `${projectLocation}/product/src/file1.ts`, + content: fileContent1 + }; + const file2: FileOrFolder = { + path: `${projectLocation}/product/src/feature/file2.ts`, + content: fileContent2 + }; + const file3: FileOrFolder = { + path: `${projectLocation}/product/test/src/file3.ts`, + content: fileContent3 + }; + const file4: FileOrFolder = { + path: `${projectLocation}/product/test/file4.ts`, + content: fileContent4 + }; + return { file1, file2, file3, file4 }; + } + + it("relative module name", () => { + const module1Name = "./module1"; + const module2Name = "../module2"; + const module3Name = "../module1"; + const module4Name = "../../module2"; + const module5Name = "../../src/module1"; + const module6Name = "../src/module1"; + const fileContent1 = `import { module1 } from "${module1Name}";import { module2 } from "${module2Name}";`; + const fileContent2 = `import { module1 } from "${module3Name}";import { module2 } from "${module4Name}";`; + const fileContent3 = `import { module1 } from "${module5Name}";import { module2 } from "${module4Name}";`; + const fileContent4 = `import { module1 } from "${module6Name}";import { module2 } from "${module2Name}";`; + const { file1, file2, file3, file4 } = getFiles(fileContent1, fileContent2, fileContent3, fileContent4); + const { module1, module2 } = getModules(`${projectLocation}/product/src/module1.ts`, `${projectLocation}/product/module2.ts`); + const files = [module1, module2, file1, file2, file3, file4, configFile, libFile]; + const host = createServerHost(files); + const resolutionTrace = createHostModuleResolutionTrace(host); + const service = createProjectService(host); + service.openClientFile(file1.path); + const expectedTrace = getExpectedRelativeModuleResolutionTrace(host, file1, module1, module1Name); + getExpectedRelativeModuleResolutionTrace(host, file1, module2, module2Name, expectedTrace); + getExpectedRelativeModuleResolutionTrace(host, file2, module1, module3Name, expectedTrace); + getExpectedRelativeModuleResolutionTrace(host, file2, module2, module4Name, expectedTrace); + getExpectedRelativeModuleResolutionTrace(host, file4, module1, module6Name, expectedTrace); + getExpectedRelativeModuleResolutionTrace(host, file4, module2, module2Name, expectedTrace); + getExpectedRelativeModuleResolutionTrace(host, file3, module1, module5Name, expectedTrace); + getExpectedRelativeModuleResolutionTrace(host, file3, module2, module4Name, expectedTrace); + verifyTrace(resolutionTrace, expectedTrace); + verifyWatchesWithConfigFile(host, files, file1); + + file1.content += fileContent1; + file2.content += fileContent2; + file3.content += fileContent3; + file4.content += fileContent4; + host.reloadFS(files); + host.runQueuedTimeoutCallbacks(); + + verifyTrace(resolutionTrace, [ + getExpectedReusingResolutionFromOldProgram(file1, module1Name), + getExpectedReusingResolutionFromOldProgram(file1, module2Name) + ]); + verifyWatchesWithConfigFile(host, files, file1); + }); + + it("non relative module name", () => { + const module1Name = "module1"; + const module2Name = "module2"; + const fileContent = `import { module1 } from "${module1Name}";import { module2 } from "${module2Name}";`; + const { file1, file2, file3, file4 } = getFiles(fileContent); + const { module1, module2 } = getModules(`${projectLocation}/product/node_modules/module1/index.ts`, `${projectLocation}/node_modules/module2/index.ts`); + const files = [module1, module2, file1, file2, file3, file4, configFile, libFile]; + const host = createServerHost(files); + const resolutionTrace = createHostModuleResolutionTrace(host); + const service = createProjectService(host); + service.openClientFile(file1.path); + const expectedTrace = getExpectedNonRelativeModuleResolutionTrace(host, file1, module1, module1Name); + getExpectedNonRelativeModuleResolutionTrace(host, file1, module2, module2Name, expectedTrace); + getExpectedNonRelativeModuleResolutionTrace(host, file2, module1, module1Name, expectedTrace); + getExpectedNonRelativeModuleResolutionTrace(host, file2, module2, module2Name, expectedTrace); + getExpectedNonRelativeModuleResolutionTrace(host, file4, module1, module1Name, expectedTrace); + getExpectedNonRelativeModuleResolutionTrace(host, file4, module2, module2Name, expectedTrace); + getExpectedNonRelativeModuleResolutionTrace(host, file3, module1, module1Name, expectedTrace); + getExpectedNonRelativeModuleResolutionTrace(host, file3, module2, module2Name, expectedTrace); + verifyTrace(resolutionTrace, expectedTrace); + verifyWatchesWithConfigFile(host, files, file1); + + file1.content += fileContent; + file2.content += fileContent; + file3.content += fileContent; + file4.content += fileContent; + host.reloadFS(files); + host.runQueuedTimeoutCallbacks(); + + verifyTrace(resolutionTrace, [ + getExpectedReusingResolutionFromOldProgram(file1, module1Name), + getExpectedReusingResolutionFromOldProgram(file1, module2Name) + ]); + verifyWatchesWithConfigFile(host, files, file1); + }); + + it("non relative module name from inferred project", () => { + const module1Name = "module1"; + const module2Name = "module2"; + const file2Name = "./feature/file2"; + const file3Name = "../test/src/file3"; + const file4Name = "../test/file4"; + const importModuleContent = `import { module1 } from "${module1Name}";import { module2 } from "${module2Name}";`; + const { file1, file2, file3, file4 } = getFiles(`import "${file2Name}"; import "${file4Name}"; import "${file3Name}"; ${importModuleContent}`, importModuleContent, importModuleContent, importModuleContent); + const { module1, module2 } = getModules(`${projectLocation}/product/node_modules/module1/index.ts`, `${projectLocation}/node_modules/module2/index.ts`); + const files = [module1, module2, file1, file2, file3, file4, libFile]; + const host = createServerHost(files); + const resolutionTrace = createHostModuleResolutionTrace(host); + const service = createProjectService(host); + service.setCompilerOptionsForInferredProjects({ traceResolution: true }); + service.openClientFile(file1.path); + const expectedTrace = getExpectedRelativeModuleResolutionTrace(host, file1, file2, file2Name); + getExpectedRelativeModuleResolutionTrace(host, file1, file4, file4Name, expectedTrace); + getExpectedRelativeModuleResolutionTrace(host, file1, file3, file3Name, expectedTrace); + getExpectedNonRelativeModuleResolutionTrace(host, file1, module1, module1Name, expectedTrace); + getExpectedNonRelativeModuleResolutionTrace(host, file1, module2, module2Name, expectedTrace); + getExpectedNonRelativeModuleResolutionTrace(host, file2, module1, module1Name, expectedTrace); + getExpectedNonRelativeModuleResolutionTrace(host, file2, module2, module2Name, expectedTrace); + getExpectedNonRelativeModuleResolutionTrace(host, file4, module1, module1Name, expectedTrace); + getExpectedNonRelativeModuleResolutionTrace(host, file4, module2, module2Name, expectedTrace); + getExpectedNonRelativeModuleResolutionTrace(host, file3, module1, module1Name, expectedTrace); + getExpectedNonRelativeModuleResolutionTrace(host, file3, module2, module2Name, expectedTrace); + verifyTrace(resolutionTrace, expectedTrace); + + const currentDirectory = getDirectoryPath(file1.path); + const watchedFiles = mapDefined(files, f => f === file1 ? undefined : f.path); + forEachAncestorDirectory(currentDirectory, d => { + watchedFiles.push(combinePaths(d, "tsconfig.json"), combinePaths(d, "jsconfig.json")); + }); + const watchedRecursiveDirectories = getTypeRootsFromLocation(currentDirectory).concat([ + currentDirectory, `${projectLocation}/product/${nodeModules}`, + `${projectLocation}/${nodeModules}`, `${projectLocation}/product/test/${nodeModules}`, + `${projectLocation}/product/test/src/${nodeModules}` + ]); + checkWatches(); + + file1.content += importModuleContent; + file2.content += importModuleContent; + file3.content += importModuleContent; + file4.content += importModuleContent; + host.reloadFS(files); + host.runQueuedTimeoutCallbacks(); + + verifyTrace(resolutionTrace, [ + getExpectedReusingResolutionFromOldProgram(file1, file2Name), + getExpectedReusingResolutionFromOldProgram(file1, file4Name), + getExpectedReusingResolutionFromOldProgram(file1, file3Name), + getExpectedReusingResolutionFromOldProgram(file1, module1Name), + getExpectedReusingResolutionFromOldProgram(file1, module2Name) + ]); + checkWatches(); + + function checkWatches() { + checkWatchedFiles(host, watchedFiles); + checkWatchedDirectories(host, [], /*recursive*/ false); + checkWatchedDirectories(host, watchedRecursiveDirectories, /*recursive*/ true); + } + }); + }); + }); }